diff --git a/README.md b/README.md index ab8f6494d..52d90d0b6 100644 --- a/README.md +++ b/README.md @@ -41,12 +41,12 @@ sudo apt install -y \ tor python3-socks imagemagick \ python3-numpy python3-setuptools \ python3-crypto python3-pycryptodome \ - python3-dateutil python3-pil.imagetk + python3-dateutil python3-pil.imagetk \ python3-idna python3-requests \ python3-django-timezone-field \ libimage-exiftool-perl python3-flake8 \ python3-pyqrcode python3-png python3-bandit \ - certbot nginx + certbot nginx wget ``` ## Installation @@ -61,6 +61,14 @@ Add a dedicated user so that we don't have to run as root. adduser --system --home=/opt/epicyon --group epicyon ``` +Link news mirrors: + +``` bash +mkdir /var/www/YOUR_DOMAIN +mkdir -p /opt/epicyon/accounts/newsmirror +ln -s /opt/epicyon/accounts/newsmirror /var/www/YOUR_DOMAIN/newsmirror +``` + Edit */etc/systemd/system/epicyon.service* and add the following: ``` systemd @@ -151,7 +159,12 @@ server { error_log /dev/null; index index.html; - + + location /newsmirror { + root /var/www/YOUR_DOMAIN; + try_files $uri =404; + } + location / { proxy_http_version 1.1; client_max_body_size 31M; @@ -259,4 +272,3 @@ To run the network tests. These simulate instances exchanging messages. ``` bash python3 epicyon.py --testsnetwork ``` - diff --git a/blocking.py b/blocking.py index b82cf3e06..395faf234 100644 --- a/blocking.py +++ b/blocking.py @@ -28,8 +28,9 @@ def addGlobalBlock(baseDir: str, return False # block an account handle or domain blockFile = open(blockingFilename, "a+") - blockFile.write(blockHandle + '\n') - blockFile.close() + if blockFile: + blockFile.write(blockHandle + '\n') + blockFile.close() else: blockHashtag = blockNickname # is the hashtag already blocked? @@ -38,8 +39,9 @@ def addGlobalBlock(baseDir: str, return False # block a hashtag blockFile = open(blockingFilename, "a+") - blockFile.write(blockHashtag + '\n') - blockFile.close() + if blockFile: + blockFile.write(blockHashtag + '\n') + blockFile.close() return True @@ -147,20 +149,37 @@ def getDomainBlocklist(baseDir: str) -> str: globalBlockingFilename = baseDir + '/accounts/blocking.txt' if not os.path.isfile(globalBlockingFilename): return blockedStr - with open(globalBlockingFilename, 'r') as file: - blockedStr += file.read() + with open(globalBlockingFilename, 'r') as fpBlocked: + blockedStr += fpBlocked.read() return blockedStr def isBlockedDomain(baseDir: str, domain: str) -> bool: """Is the given domain blocked? """ + if '.' not in domain: + return False + if isEvil(domain): return True + + # by checking a shorter version we can thwart adversaries + # who constantly change their subdomain + sections = domain.split('.') + noOfSections = len(sections) + shortDomain = None + if noOfSections > 2: + shortDomain = domain[noOfSections-2] + '.' + domain[noOfSections-1] + globalBlockingFilename = baseDir + '/accounts/blocking.txt' if os.path.isfile(globalBlockingFilename): - if '*@' + domain in open(globalBlockingFilename).read(): - return True + with open(globalBlockingFilename, 'r') as fpBlocked: + blockedStr = fpBlocked.read() + if '*@' + domain in blockedStr: + return True + if shortDomain: + if '*@' + shortDomain in blockedStr: + return True return False diff --git a/blog.py b/blog.py index 1a4c98753..a1efa5e2b 100644 --- a/blog.py +++ b/blog.py @@ -279,6 +279,7 @@ def htmlBlogPostRSS2(authorized: bool, handle: str, restrictToDomain: bool) -> str: """Returns the RSS version 2 feed for a single blog post """ + rssStr = '' messageLink = '' if postJsonObject['object'].get('id'): messageLink = postJsonObject['object']['id'].replace('/statuses/', '/') @@ -305,6 +306,7 @@ def htmlBlogPostRSS3(authorized: bool, handle: str, restrictToDomain: bool) -> str: """Returns the RSS version 3 feed for a single blog post """ + rssStr = '' messageLink = '' if postJsonObject['object'].get('id'): messageLink = postJsonObject['object']['id'].replace('/statuses/', '/') diff --git a/content.py b/content.py index 8b140c1f4..95181a865 100644 --- a/content.py +++ b/content.py @@ -63,12 +63,14 @@ def htmlReplaceEmailQuote(content: str) -> str: # replace quote paragraph if '
"' in content: if '"
' in content: - content = content.replace('"', '
') - content = content.replace('"', '') + if content.count('
"') == content.count('"
'): + content = content.replace('"', '
') + content = content.replace('"', '') if '>\u201c' in content: if '\u201d<' in content: - content = content.replace('>\u201c', '>
') - content = content.replace('\u201d<', '<') + if content.count('>\u201c') == content.count('\u201d<'): + content = content.replace('>\u201c', '>
') + content = content.replace('\u201d<', '<') # replace email style quote if '>> ' not in content: return content @@ -103,6 +105,12 @@ def htmlReplaceQuoteMarks(content: str) -> str: if '"' not in content: return content + # only if there are a few quote marks + if content.count('"') > 4: + return content + if content.count('"') > 4: + return content + newContent = content if '"' in content: sections = content.split('"') @@ -353,6 +361,7 @@ def validHashTag(hashtag: str) -> bool: # long hashtags are not valid if len(hashtag) >= 32: return False + # TODO: this may need to be an international character set validChars = set('0123456789' + 'abcdefghijklmnopqrstuvwxyz' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') @@ -374,7 +383,7 @@ def addHashTags(wordStr: str, httpPrefix: str, domain: str, hashtagUrl = httpPrefix + "://" + domain + "/tags/" + hashtag postHashtags[hashtag] = { 'href': hashtagUrl, - 'name': '#'+hashtag, + 'name': '#' + hashtag, 'type': 'Hashtag' } replaceHashTags[wordStr] = " str: return content -def removeHtml(content: str) -> str: - """Removes html links from the given content. - Used to ensure that profile descriptions don't contain dubious content - """ - if '<' not in content: - return content - removing = False - content = content.replace('
', '"').replace('', '"') - result = '' - for ch in content: - if ch == '<': - removing = True - elif ch == '>': - removing = False - elif not removing: - result += ch - return result - - def removeLongWords(content: str, maxWordLength: int, longWordsList: []) -> str: """Breaks up long words so that on mobile screens this doesn't @@ -649,7 +639,7 @@ def removeLongWords(content: str, maxWordLength: int, wordStr[:maxWordLength]) if content.startswith('
'): if not content.endswith('
'): - content = content.strip()+'' + content = content.strip() + '' return content @@ -701,7 +691,12 @@ def addHtmlTags(baseDir: str, httpPrefix: str, content = content.replace('\r', '') content = content.replace('\n', ' --linebreak-- ') content = addMusicTag(content, 'nowplaying') - words = content.replace(',', ' ').replace(';', ' ').split(' ') + contentSimplified = \ + content.replace(',', ' ').replace(';', ' ').replace('- ', ' ') + contentSimplified = contentSimplified.replace('. ', ' ').strip() + if contentSimplified.endswith('.'): + contentSimplified = contentSimplified[:len(contentSimplified)-1] + words = contentSimplified.split(' ') # remove . for words which are not mentions newWords = [] diff --git a/daemon.py b/daemon.py index 86cb9be8f..f48106971 100644 --- a/daemon.py +++ b/daemon.py @@ -164,6 +164,7 @@ from shares import getSharesFeedForPerson from shares import addShare from shares import removeShare from shares import expireShares +from utils import clearFromPostCaches from utils import containsInvalidChars from utils import isSystemAccount from utils import setConfigParam @@ -1146,7 +1147,7 @@ class PubServer(BaseHTTPRequestHandler): self.headers['Authorization']) return False - def _clearLoginDetails(self, nickname: str, callingDomain: str): + def _clearLoginDetails(self, nickname: str, callingDomain: str) -> None: """Clears login details for the given account """ # remove any token @@ -1159,7 +1160,8 @@ class PubServer(BaseHTTPRequestHandler): callingDomain) def _benchmarkGETtimings(self, GETstartTime, GETtimings: {}, - prevGetId: str, currGetId: str): + prevGetId: str, + currGetId: str) -> None: """Updates a dictionary containing how long each segment of GET takes """ timeDiff = int((time.time() - GETstartTime) * 1000) @@ -1174,7 +1176,7 @@ class PubServer(BaseHTTPRequestHandler): print('GET TIMING ' + currGetId + ' = ' + str(timeDiff)) def _benchmarkPOSTtimings(self, POSTstartTime, POSTtimings: [], - postID: int): + postID: int) -> None: """Updates a list containing how long each segment of POST takes """ if self.server.debug: @@ -1225,7 +1227,7 @@ class PubServer(BaseHTTPRequestHandler): baseDir: str, httpPrefix: str, domain: str, domainFull: str, port: int, onionDomain: str, i2pDomain: str, - debug: bool): + debug: bool) -> None: """Shows the login screen """ # get the contents of POST containing login credentials @@ -1297,7 +1299,8 @@ class PubServer(BaseHTTPRequestHandler): else: if isSuspended(baseDir, loginNickname): msg = \ - htmlSuspended(baseDir).encode('utf-8') + htmlSuspended(self.server.cssCache, + baseDir).encode('utf-8') self._login_headers('text/html', len(msg), callingDomain) self._write(msg) @@ -1379,7 +1382,7 @@ class PubServer(BaseHTTPRequestHandler): baseDir: str, httpPrefix: str, domain: str, domainFull: str, port: int, onionDomain: str, i2pDomain: str, - debug: bool): + debug: bool) -> None: """Actions on the moderator screeen """ usersPath = path.replace('/moderationaction', '') @@ -1419,7 +1422,8 @@ class PubServer(BaseHTTPRequestHandler): moderationText = \ urllib.parse.unquote_plus(modText.strip()) elif moderationStr.startswith('submitInfo'): - msg = htmlModerationInfo(self.server.translate, + msg = htmlModerationInfo(self.server.cssCache, + self.server.translate, baseDir, httpPrefix) msg = msg.encode('utf-8') self._login_headers('text/html', @@ -1531,7 +1535,7 @@ class PubServer(BaseHTTPRequestHandler): baseDir: str, httpPrefix: str, domain: str, domainFull: str, port: int, onionDomain: str, i2pDomain: str, - debug: bool): + debug: bool) -> None: """Receive POST from person options screen """ pageNumber = 1 @@ -1762,7 +1766,8 @@ class PubServer(BaseHTTPRequestHandler): if debug: print('Unblocking ' + optionsActor) msg = \ - htmlUnblockConfirm(self.server.translate, + htmlUnblockConfirm(self.server.cssCache, + self.server.translate, baseDir, usersPath, optionsActor, @@ -1779,7 +1784,8 @@ class PubServer(BaseHTTPRequestHandler): if debug: print('Following ' + optionsActor) msg = \ - htmlFollowConfirm(self.server.translate, + htmlFollowConfirm(self.server.cssCache, + self.server.translate, baseDir, usersPath, optionsActor, @@ -1796,7 +1802,8 @@ class PubServer(BaseHTTPRequestHandler): if debug: print('Unfollowing ' + optionsActor) msg = \ - htmlUnfollowConfirm(self.server.translate, + htmlUnfollowConfirm(self.server.cssCache, + self.server.translate, baseDir, usersPath, optionsActor, @@ -1813,7 +1820,8 @@ class PubServer(BaseHTTPRequestHandler): if debug: print('Sending DM to ' + optionsActor) reportPath = path.replace('/personoptions', '') + '/newdm' - msg = htmlNewPost(False, self.server.translate, + msg = htmlNewPost(self.server.cssCache, + False, self.server.translate, baseDir, httpPrefix, reportPath, None, @@ -1821,7 +1829,8 @@ class PubServer(BaseHTTPRequestHandler): pageNumber, chooserNickname, domain, - domainFull).encode('utf-8') + domainFull, + self.server.defaultTimeline).encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) self._write(msg) @@ -1879,14 +1888,16 @@ class PubServer(BaseHTTPRequestHandler): print('Reporting ' + optionsActor) reportPath = \ path.replace('/personoptions', '') + '/newreport' - msg = htmlNewPost(False, self.server.translate, + msg = htmlNewPost(self.server.cssCache, + False, self.server.translate, baseDir, httpPrefix, reportPath, None, [], postUrl, pageNumber, chooserNickname, domain, - domainFull).encode('utf-8') + domainFull, + self.server.defaultTimeline).encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) self._write(msg) @@ -1906,7 +1917,8 @@ class PubServer(BaseHTTPRequestHandler): authorized: bool, path: str, baseDir: str, httpPrefix: str, domain: str, domainFull: str, port: int, - onionDomain: str, i2pDomain: str, debug: bool): + onionDomain: str, i2pDomain: str, + debug: bool) -> None: """Confirm to unfollow """ usersPath = path.split('/unfollowconfirm')[0] @@ -1985,7 +1997,8 @@ class PubServer(BaseHTTPRequestHandler): authorized: bool, path: str, baseDir: str, httpPrefix: str, domain: str, domainFull: str, port: int, - onionDomain: str, i2pDomain: str, debug: bool): + onionDomain: str, i2pDomain: str, + debug: bool) -> None: """Confirm to follow """ usersPath = path.split('/followconfirm')[0] @@ -2055,7 +2068,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.cachedWebfingers, self.server.personCache, debug, - self.server.projectVersion) + self.server.projectVersion, + self.server.allowNewsFollowers) if callingDomain.endswith('.onion') and onionDomain: originPathStr = 'http://' + onionDomain + usersPath elif (callingDomain.endswith('.i2p') and i2pDomain): @@ -2067,7 +2081,8 @@ class PubServer(BaseHTTPRequestHandler): authorized: bool, path: str, baseDir: str, httpPrefix: str, domain: str, domainFull: str, port: int, - onionDomain: str, i2pDomain: str, debug: bool): + onionDomain: str, i2pDomain: str, + debug: bool) -> None: """Confirms a block """ usersPath = path.split('/blockconfirm')[0] @@ -2155,7 +2170,8 @@ class PubServer(BaseHTTPRequestHandler): authorized: bool, path: str, baseDir: str, httpPrefix: str, domain: str, domainFull: str, port: int, - onionDomain: str, i2pDomain: str, debug: bool): + onionDomain: str, i2pDomain: str, + debug: bool) -> None: """Confirms a unblock """ usersPath = path.split('/unblockconfirm')[0] @@ -2244,7 +2260,8 @@ class PubServer(BaseHTTPRequestHandler): baseDir: str, httpPrefix: str, domain: str, domainFull: str, port: int, searchForEmoji: bool, - onionDomain: str, i2pDomain: str, debug: bool): + onionDomain: str, i2pDomain: str, + debug: bool) -> None: """Receive a search query """ # get the page number @@ -2303,9 +2320,8 @@ class PubServer(BaseHTTPRequestHandler): nickname = getNicknameFromActor(actorStr) # hashtag search hashtagStr = \ - htmlHashtagSearch(nickname, - domain, - port, + htmlHashtagSearch(self.server.cssCache, + nickname, domain, port, self.server.recentPostsCache, self.server.maxRecentPosts, self.server.translate, @@ -2330,7 +2346,8 @@ class PubServer(BaseHTTPRequestHandler): # skill search searchStr = searchStr.replace('*', '').strip() skillStr = \ - htmlSkillsSearch(self.server.translate, + htmlSkillsSearch(self.server.cssCache, + self.server.translate, baseDir, httpPrefix, searchStr, @@ -2348,7 +2365,8 @@ class PubServer(BaseHTTPRequestHandler): nickname = getNicknameFromActor(actorStr) searchStr = searchStr.replace('!', '').strip() historyStr = \ - htmlHistorySearch(self.server.translate, + htmlHistorySearch(self.server.cssCache, + self.server.translate, baseDir, httpPrefix, nickname, @@ -2392,7 +2410,8 @@ class PubServer(BaseHTTPRequestHandler): return profilePathStr = path.replace('/searchhandle', '') profileStr = \ - htmlProfileAfterSearch(self.server.recentPostsCache, + htmlProfileAfterSearch(self.server.cssCache, + self.server.recentPostsCache, self.server.maxRecentPosts, self.server.translate, baseDir, @@ -2433,7 +2452,8 @@ class PubServer(BaseHTTPRequestHandler): searchStr.replace(' emoji', '') # emoji search emojiStr = \ - htmlSearchEmoji(self.server.translate, + htmlSearchEmoji(self.server.cssCache, + self.server.translate, baseDir, httpPrefix, searchStr) @@ -2447,7 +2467,8 @@ class PubServer(BaseHTTPRequestHandler): else: # shared items search sharedItemsStr = \ - htmlSearchSharedItems(self.server.translate, + htmlSearchSharedItems(self.server.cssCache, + self.server.translate, baseDir, searchStr, pageNumber, maxPostsInFeed, @@ -2474,7 +2495,8 @@ class PubServer(BaseHTTPRequestHandler): authorized: bool, path: str, baseDir: str, httpPrefix: str, domain: str, domainFull: str, - onionDomain: str, i2pDomain: str, debug: bool): + onionDomain: str, i2pDomain: str, + debug: bool) -> None: """Receive a vote via POST """ pageNumber = 1 @@ -2559,7 +2581,8 @@ class PubServer(BaseHTTPRequestHandler): authorized: bool, path: str, baseDir: str, httpPrefix: str, domain: str, domainFull: str, - onionDomain: str, i2pDomain: str, debug: bool): + onionDomain: str, i2pDomain: str, + debug: bool) -> None: """Receives an image via POST """ if not self.outboxAuthenticated: @@ -2626,7 +2649,8 @@ class PubServer(BaseHTTPRequestHandler): authorized: bool, path: str, baseDir: str, httpPrefix: str, domain: str, domainFull: str, - onionDomain: str, i2pDomain: str, debug: bool): + onionDomain: str, i2pDomain: str, + debug: bool) -> None: """Removes a shared item """ usersPath = path.split('/rmshare')[0] @@ -2684,7 +2708,8 @@ class PubServer(BaseHTTPRequestHandler): authorized: bool, path: str, baseDir: str, httpPrefix: str, domain: str, domainFull: str, - onionDomain: str, i2pDomain: str, debug: bool): + onionDomain: str, i2pDomain: str, + debug: bool) -> None: """Endpoint for removing posts """ pageNumber = 1 @@ -2781,7 +2806,7 @@ class PubServer(BaseHTTPRequestHandler): baseDir: str, httpPrefix: str, domain: str, domainFull: str, onionDomain: str, i2pDomain: str, debug: bool, - defaultTimeline: str): + defaultTimeline: str) -> None: """Updates the left links column of the timeline """ usersPath = path.replace('/linksdata', '') @@ -2886,7 +2911,7 @@ class PubServer(BaseHTTPRequestHandler): baseDir: str, httpPrefix: str, domain: str, domainFull: str, onionDomain: str, i2pDomain: str, debug: bool, - defaultTimeline: str): + defaultTimeline: str) -> None: """Updates the right newswire column of the timeline """ usersPath = path.replace('/newswiredata', '') @@ -2973,6 +2998,27 @@ class PubServer(BaseHTTPRequestHandler): if os.path.isfile(newswireFilename): os.remove(newswireFilename) + # save filtered words list for the newswire + filterNewswireFilename = \ + baseDir + '/accounts/' + \ + 'news@' + domain + '/filters.txt' + if fields.get('filteredWordsNewswire'): + with open(filterNewswireFilename, 'w+') as filterfile: + filterfile.write(fields['filteredWordsNewswire']) + else: + if os.path.isfile(filterNewswireFilename): + os.remove(filterNewswireFilename) + + # save news tagging rules + hashtagRulesFilename = \ + baseDir + '/accounts/hashtagrules.txt' + if fields.get('hashtagRulesList'): + with open(hashtagRulesFilename, 'w+') as rulesfile: + rulesfile.write(fields['hashtagRulesList']) + else: + if os.path.isfile(hashtagRulesFilename): + os.remove(hashtagRulesFilename) + newswireTrustedFilename = baseDir + '/accounts/newswiretrusted.txt' if fields.get('trustedNewswire'): newswireTrusted = fields['trustedNewswire'] @@ -3004,7 +3050,7 @@ class PubServer(BaseHTTPRequestHandler): baseDir: str, httpPrefix: str, domain: str, domainFull: str, onionDomain: str, i2pDomain: str, debug: bool, - defaultTimeline: str): + defaultTimeline: str) -> None: """edits a news post """ usersPath = path.replace('/newseditdata', '') @@ -3103,13 +3149,6 @@ class PubServer(BaseHTTPRequestHandler): newsPostTitle postJsonObject['object']['content'] = \ newsPostContent - # remove the html from post cache - cachedPost = \ - baseDir + '/accounts/' + \ - nickname + '@' + domain + \ - '/postcache/' + newsPostUrl + '.html' - if os.path.isfile(cachedPost): - os.remove(cachedPost) # update newswire pubDate = postJsonObject['object']['published'] publishedDate = \ @@ -3128,6 +3167,13 @@ class PubServer(BaseHTTPRequestHandler): newswireStateFilename) except Exception as e: print('ERROR saving newswire state, ' + str(e)) + + # remove any previous cached news posts + newsId = \ + postJsonObject['object']['id'].replace('/', '#') + clearFromPostCaches(baseDir, self.server.recentPostsCache, + newsId) + # save the news post saveJson(postJsonObject, postFilename) @@ -3148,7 +3194,8 @@ class PubServer(BaseHTTPRequestHandler): authorized: bool, path: str, baseDir: str, httpPrefix: str, domain: str, domainFull: str, - onionDomain: str, i2pDomain: str, debug: bool): + onionDomain: str, i2pDomain: str, + debug: bool) -> None: """Updates your user profile after editing via the Edit button on the profile screen """ @@ -3453,6 +3500,21 @@ class PubServer(BaseHTTPRequestHandler): setTheme(baseDir, fields['themeDropdown'], domain) + self.server.showPublishAsIcon = \ + getConfigParam(self.server.baseDir, + 'showPublishAsIcon') + self.server.fullWidthTimelineButtonHeader = \ + getConfigParam(self.server.baseDir, + 'fullWidthTimelineButtonHeader') + self.server.iconsAsButtons = \ + getConfigParam(self.server.baseDir, + 'iconsAsButtons') + self.server.rssIconAtTop = \ + getConfigParam(self.server.baseDir, + 'rssIconAtTop') + self.server.publishButtonAtTop = \ + getConfigParam(self.server.baseDir, + 'publishButtonAtTop') setNewsAvatar(baseDir, fields['themeDropdown'], httpPrefix, @@ -3797,6 +3859,22 @@ class PubServer(BaseHTTPRequestHandler): currTheme = getTheme(baseDir) if currTheme: setTheme(baseDir, currTheme, domain) + self.server.showPublishAsIcon = \ + getConfigParam(self.server.baseDir, + 'showPublishAsIcon') + self.server.fullWidthTimelineButtonHeader = \ + getConfigParam(self.server.baseDir, + 'fullWidthTimeline' + + 'ButtonHeader') + self.server.iconsAsButtons = \ + getConfigParam(self.server.baseDir, + 'iconsAsButtons') + self.server.rssIconAtTop = \ + getConfigParam(self.server.baseDir, + 'rssIconAtTop') + self.server.publishButtonAtTop = \ + getConfigParam(self.server.baseDir, + 'publishButtonAtTop') # only receive DMs from accounts you follow followDMsFilename = \ @@ -4040,7 +4118,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.POSTbusy = False def _progressiveWebAppManifest(self, callingDomain: str, - GETstartTime, GETtimings: {}): + GETstartTime, + GETtimings: {}) -> None: """gets the PWA manifest """ app1 = "https://f-droid.org/en/packages/eu.siacs.conversations" @@ -4135,7 +4214,7 @@ class PubServer(BaseHTTPRequestHandler): 'show logout', 'send manifest') def _getFavicon(self, callingDomain: str, - baseDir: str, debug: bool): + baseDir: str, debug: bool) -> None: """Return the favicon """ favType = 'image/x-icon' @@ -4188,7 +4267,7 @@ class PubServer(BaseHTTPRequestHandler): def _getFonts(self, callingDomain: str, path: str, baseDir: str, debug: bool, - GETstartTime, GETtimings: {}): + GETstartTime, GETtimings: {}) -> None: """Returns a font """ fontStr = path.split('/fonts/')[1] @@ -4250,7 +4329,7 @@ class PubServer(BaseHTTPRequestHandler): baseDir: str, httpPrefix: str, domain: str, port: int, proxyType: str, GETstartTime, GETtimings: {}, - debug: bool): + debug: bool) -> None: """Returns an RSS2 feed for the blog """ nickname = path.split('/blog/')[1] @@ -4303,7 +4382,7 @@ class PubServer(BaseHTTPRequestHandler): domainFull: str, port: int, proxyType: str, translate: {}, GETstartTime, GETtimings: {}, - debug: bool): + debug: bool) -> None: """Returns an RSS2 feed for all blogs on this instance """ if not self.server.session: @@ -4362,7 +4441,7 @@ class PubServer(BaseHTTPRequestHandler): baseDir: str, httpPrefix: str, domain: str, port: int, proxyType: str, GETstartTime, GETtimings: {}, - debug: bool): + debug: bool) -> None: """Returns the newswire feed """ if not self.server.session: @@ -4398,7 +4477,7 @@ class PubServer(BaseHTTPRequestHandler): baseDir: str, httpPrefix: str, domain: str, port: int, proxyType: str, GETstartTime, GETtimings: {}, - debug: bool): + debug: bool) -> None: """Returns an RSS3 feed """ nickname = path.split('/blog/')[1] @@ -4445,12 +4524,12 @@ class PubServer(BaseHTTPRequestHandler): domain: str, domainFull: str, GETstartTime, GETtimings: {}, onionDomain: str, i2pDomain: str, - cookie: str, debug: bool): + cookie: str, debug: bool) -> None: """Show person options screen """ optionsStr = path.split('?options=')[1] originPathStr = path.split('?options=')[0] - if ';' in optionsStr: + if ';' in optionsStr and '/users/news/' not in path: pageNumber = 1 optionsList = optionsStr.split(';') optionsActor = optionsList[0] @@ -4484,7 +4563,8 @@ class PubServer(BaseHTTPRequestHandler): emailAddress = getEmailAddress(actorJson) PGPpubKey = getPGPpubKey(actorJson) PGPfingerprint = getPGPfingerprint(actorJson) - msg = htmlPersonOptions(self.server.translate, + msg = htmlPersonOptions(self.server.cssCache, + self.server.translate, baseDir, domain, domainFull, originPathStr, @@ -4504,6 +4584,12 @@ class PubServer(BaseHTTPRequestHandler): 'registered devices done', 'person options') return + + if '/users/news/' in path: + self._redirect_headers(originPathStr + '/tlnews', + cookie, callingDomain) + return + if callingDomain.endswith('.onion') and onionDomain: originPathStrAbsolute = \ 'http://' + onionDomain + originPathStr @@ -4518,7 +4604,7 @@ class PubServer(BaseHTTPRequestHandler): def _showMedia(self, callingDomain: str, path: str, baseDir: str, - GETstartTime, GETtimings: {}): + GETstartTime, GETtimings: {}) -> None: """Returns a media file """ if self._pathIsImage(path) or \ @@ -4566,7 +4652,7 @@ class PubServer(BaseHTTPRequestHandler): def _showEmoji(self, callingDomain: str, path: str, baseDir: str, - GETstartTime, GETtimings: {}): + GETstartTime, GETtimings: {}) -> None: """Returns an emoji image """ if self._pathIsImage(path): @@ -4604,7 +4690,7 @@ class PubServer(BaseHTTPRequestHandler): def _showIcon(self, callingDomain: str, path: str, baseDir: str, - GETstartTime, GETtimings: {}): + GETstartTime, GETtimings: {}) -> None: """Shows an icon """ if path.endswith('.png'): @@ -4640,7 +4726,7 @@ class PubServer(BaseHTTPRequestHandler): def _showCachedAvatar(self, callingDomain: str, path: str, baseDir: str, - GETstartTime, GETtimings: {}): + GETstartTime, GETtimings: {}) -> None: """Shows an avatar image obtained from the cache """ mediaFilename = baseDir + '/cache' + path @@ -4696,7 +4782,7 @@ class PubServer(BaseHTTPRequestHandler): baseDir: str, httpPrefix: str, domain: str, domainFull: str, port: int, onionDomain: str, i2pDomain: str, - GETstartTime, GETtimings: {}): + GETstartTime, GETtimings: {}) -> None: """Return the result of a hashtag search """ pageNumber = 1 @@ -4710,7 +4796,7 @@ class PubServer(BaseHTTPRequestHandler): if '?page=' in hashtag: hashtag = hashtag.split('?page=')[0] if isBlockedHashtag(baseDir, hashtag): - msg = htmlHashtagBlocked(baseDir, + msg = htmlHashtagBlocked(self.server.cssCache, baseDir, self.server.translate).encode('utf-8') self._login_headers('text/html', len(msg), callingDomain) self._write(msg) @@ -4723,8 +4809,8 @@ class PubServer(BaseHTTPRequestHandler): nickname = \ getNicknameFromActor(actor) hashtagStr = \ - htmlHashtagSearch(nickname, - domain, port, + htmlHashtagSearch(self.server.cssCache, + nickname, domain, port, self.server.recentPostsCache, self.server.maxRecentPosts, self.server.translate, @@ -4763,7 +4849,7 @@ class PubServer(BaseHTTPRequestHandler): baseDir: str, httpPrefix: str, domain: str, domainFull: str, port: int, onionDomain: str, i2pDomain: str, - GETstartTime, GETtimings: {}): + GETstartTime, GETtimings: {}) -> None: """Return an RSS 2 feed for a hashtag """ hashtag = path.split('/tags/rss2/')[1] @@ -4819,7 +4905,8 @@ class PubServer(BaseHTTPRequestHandler): domain: str, domainFull: str, port: int, onionDomain: str, i2pDomain: str, GETstartTime, GETtimings: {}, - repeatPrivate: bool, debug: bool): + repeatPrivate: bool, + debug: bool) -> None: """The announce/repeat button was pressed on a post """ pageNumber = 1 @@ -5634,7 +5721,8 @@ class PubServer(BaseHTTPRequestHandler): return deleteStr = \ - htmlDeletePost(self.server.recentPostsCache, + htmlDeletePost(self.server.cssCache, + self.server.recentPostsCache, self.server.maxRecentPosts, self.server.translate, pageNumber, self.server.session, baseDir, @@ -5828,7 +5916,8 @@ class PubServer(BaseHTTPRequestHandler): projectVersion = self.server.projectVersion ytDomain = self.server.YTReplacementDomain msg = \ - htmlPostReplies(recentPostsCache, + htmlPostReplies(self.server.cssCache, + recentPostsCache, maxRecentPosts, translate, baseDir, @@ -5909,7 +5998,8 @@ class PubServer(BaseHTTPRequestHandler): projectVersion = self.server.projectVersion ytDomain = self.server.YTReplacementDomain msg = \ - htmlPostReplies(recentPostsCache, + htmlPostReplies(self.server.cssCache, + recentPostsCache, maxRecentPosts, translate, baseDir, @@ -5986,8 +6076,13 @@ class PubServer(BaseHTTPRequestHandler): self.server.cachedWebfingers YTReplacementDomain = \ self.server.YTReplacementDomain + iconsAsButtons = \ + self.server.iconsAsButtons msg = \ - htmlProfile(defaultTimeline, + htmlProfile(self.server.rssIconAtTop, + self.server.cssCache, + iconsAsButtons, + defaultTimeline, recentPostsCache, self.server.maxRecentPosts, self.server.translate, @@ -6060,8 +6155,13 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain showPublishedDateOnly = \ self.server.showPublishedDateOnly + iconsAsButtons = \ + self.server.iconsAsButtons msg = \ - htmlProfile(defaultTimeline, + htmlProfile(self.server.rssIconAtTop, + self.server.cssCache, + iconsAsButtons, + defaultTimeline, recentPostsCache, self.server.maxRecentPosts, self.server.translate, @@ -6176,8 +6276,10 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain showPublishedDateOnly = \ self.server.showPublishedDateOnly + cssCache = self.server.cssCache msg = \ - htmlIndividualPost(recentPostsCache, + htmlIndividualPost(cssCache, + recentPostsCache, maxRecentPosts, translate, self.server.session, @@ -6287,7 +6389,8 @@ class PubServer(BaseHTTPRequestHandler): showPublishedDateOnly = \ self.server.showPublishedDateOnly msg = \ - htmlIndividualPost(recentPostsCache, + htmlIndividualPost(self.server.cssCache, + recentPostsCache, maxRecentPosts, translate, baseDir, @@ -6402,7 +6505,10 @@ class PubServer(BaseHTTPRequestHandler): GETtimings, 'show status done', 'show inbox page') - msg = htmlInbox(defaultTimeline, + fullWidthTimelineButtonHeader = \ + self.server.fullWidthTimelineButtonHeader + msg = htmlInbox(self.server.cssCache, + defaultTimeline, recentPostsCache, maxRecentPosts, translate, @@ -6422,7 +6528,13 @@ class PubServer(BaseHTTPRequestHandler): YTReplacementDomain, self.server.showPublishedDateOnly, self.server.newswire, - self.server.positiveVoting) + self.server.positiveVoting, + self.server.showPublishAsIcon, + fullWidthTimelineButtonHeader, + self.server.iconsAsButtons, + self.server.rssIconAtTop, + self.server.publishButtonAtTop, + authorized) if GETstartTime: self._benchmarkGETtimings(GETstartTime, GETtimings, 'show status done', @@ -6515,8 +6627,11 @@ class PubServer(BaseHTTPRequestHandler): 0, self.server.positiveVoting, self.server.votingTimeMins) + fullWidthTimelineButtonHeader = \ + self.server.fullWidthTimelineButtonHeader msg = \ - htmlInboxDMs(self.server.defaultTimeline, + htmlInboxDMs(self.server.cssCache, + self.server.defaultTimeline, self.server.recentPostsCache, self.server.maxRecentPosts, self.server.translate, @@ -6536,7 +6651,13 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain, self.server.showPublishedDateOnly, self.server.newswire, - self.server.positiveVoting) + self.server.positiveVoting, + self.server.showPublishAsIcon, + fullWidthTimelineButtonHeader, + self.server.iconsAsButtons, + self.server.rssIconAtTop, + self.server.publishButtonAtTop, + authorized) msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) @@ -6622,8 +6743,11 @@ class PubServer(BaseHTTPRequestHandler): True, 0, self.server.positiveVoting, self.server.votingTimeMins) + fullWidthTimelineButtonHeader = \ + self.server.fullWidthTimelineButtonHeader msg = \ - htmlInboxReplies(self.server.defaultTimeline, + htmlInboxReplies(self.server.cssCache, + self.server.defaultTimeline, self.server.recentPostsCache, self.server.maxRecentPosts, self.server.translate, @@ -6643,7 +6767,13 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain, self.server.showPublishedDateOnly, self.server.newswire, - self.server.positiveVoting) + self.server.positiveVoting, + self.server.showPublishAsIcon, + fullWidthTimelineButtonHeader, + self.server.iconsAsButtons, + self.server.rssIconAtTop, + self.server.publishButtonAtTop, + authorized) msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) @@ -6729,8 +6859,11 @@ class PubServer(BaseHTTPRequestHandler): True, 0, self.server.positiveVoting, self.server.votingTimeMins) + fullWidthTimelineButtonHeader = \ + self.server.fullWidthTimelineButtonHeader msg = \ - htmlInboxMedia(self.server.defaultTimeline, + htmlInboxMedia(self.server.cssCache, + self.server.defaultTimeline, self.server.recentPostsCache, self.server.maxRecentPosts, self.server.translate, @@ -6750,7 +6883,13 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain, self.server.showPublishedDateOnly, self.server.newswire, - self.server.positiveVoting) + self.server.positiveVoting, + self.server.showPublishAsIcon, + fullWidthTimelineButtonHeader, + self.server.iconsAsButtons, + self.server.rssIconAtTop, + self.server.publishButtonAtTop, + authorized) msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) @@ -6836,8 +6975,11 @@ class PubServer(BaseHTTPRequestHandler): True, 0, self.server.positiveVoting, self.server.votingTimeMins) + fullWidthTimelineButtonHeader = \ + self.server.fullWidthTimelineButtonHeader msg = \ - htmlInboxBlogs(self.server.defaultTimeline, + htmlInboxBlogs(self.server.cssCache, + self.server.defaultTimeline, self.server.recentPostsCache, self.server.maxRecentPosts, self.server.translate, @@ -6857,7 +6999,13 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain, self.server.showPublishedDateOnly, self.server.newswire, - self.server.positiveVoting) + self.server.positiveVoting, + self.server.showPublishAsIcon, + fullWidthTimelineButtonHeader, + self.server.iconsAsButtons, + self.server.rssIconAtTop, + self.server.publishButtonAtTop, + authorized) msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) @@ -6951,8 +7099,11 @@ class PubServer(BaseHTTPRequestHandler): currNickname = currNickname.split('/')[0] moderator = isModerator(baseDir, currNickname) editor = isEditor(baseDir, currNickname) + fullWidthTimelineButtonHeader = \ + self.server.fullWidthTimelineButtonHeader msg = \ - htmlInboxNews(self.server.defaultTimeline, + htmlInboxNews(self.server.cssCache, + self.server.defaultTimeline, self.server.recentPostsCache, self.server.maxRecentPosts, self.server.translate, @@ -6973,7 +7124,13 @@ class PubServer(BaseHTTPRequestHandler): self.server.showPublishedDateOnly, self.server.newswire, moderator, editor, - self.server.positiveVoting) + self.server.positiveVoting, + self.server.showPublishAsIcon, + fullWidthTimelineButtonHeader, + self.server.iconsAsButtons, + self.server.rssIconAtTop, + self.server.publishButtonAtTop, + authorized) msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) @@ -7032,7 +7189,8 @@ class PubServer(BaseHTTPRequestHandler): else: pageNumber = 1 msg = \ - htmlShares(self.server.defaultTimeline, + htmlShares(self.server.cssCache, + self.server.defaultTimeline, self.server.recentPostsCache, self.server.maxRecentPosts, self.server.translate, @@ -7050,7 +7208,13 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain, self.server.showPublishedDateOnly, self.server.newswire, - self.server.positiveVoting) + self.server.positiveVoting, + self.server.showPublishAsIcon, + self.server.fullWidthTimelineButtonHeader, + self.server.iconsAsButtons, + self.server.rssIconAtTop, + self.server.publishButtonAtTop, + authorized) msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) @@ -7120,8 +7284,11 @@ class PubServer(BaseHTTPRequestHandler): authorized, 0, self.server.positiveVoting, self.server.votingTimeMins) + fullWidthTimelineButtonHeader = \ + self.server.fullWidthTimelineButtonHeader msg = \ - htmlBookmarks(self.server.defaultTimeline, + htmlBookmarks(self.server.cssCache, + self.server.defaultTimeline, self.server.recentPostsCache, self.server.maxRecentPosts, self.server.translate, @@ -7141,7 +7308,13 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain, self.server.showPublishedDateOnly, self.server.newswire, - self.server.positiveVoting) + self.server.positiveVoting, + self.server.showPublishAsIcon, + fullWidthTimelineButtonHeader, + self.server.iconsAsButtons, + self.server.rssIconAtTop, + self.server.publishButtonAtTop, + authorized) msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) @@ -7230,8 +7403,11 @@ class PubServer(BaseHTTPRequestHandler): authorized, 0, self.server.positiveVoting, self.server.votingTimeMins) + fullWidthTimelineButtonHeader = \ + self.server.fullWidthTimelineButtonHeader msg = \ - htmlEvents(self.server.defaultTimeline, + htmlEvents(self.server.cssCache, + self.server.defaultTimeline, self.server.recentPostsCache, self.server.maxRecentPosts, self.server.translate, @@ -7251,7 +7427,13 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain, self.server.showPublishedDateOnly, self.server.newswire, - self.server.positiveVoting) + self.server.positiveVoting, + self.server.showPublishAsIcon, + fullWidthTimelineButtonHeader, + self.server.iconsAsButtons, + self.server.rssIconAtTop, + self.server.publishButtonAtTop, + authorized) msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) @@ -7332,8 +7514,11 @@ class PubServer(BaseHTTPRequestHandler): self.server.newswireVotesThreshold, self.server.positiveVoting, self.server.votingTimeMins) + fullWidthTimelineButtonHeader = \ + self.server.fullWidthTimelineButtonHeader msg = \ - htmlOutbox(self.server.defaultTimeline, + htmlOutbox(self.server.cssCache, + self.server.defaultTimeline, self.server.recentPostsCache, self.server.maxRecentPosts, self.server.translate, @@ -7353,7 +7538,13 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain, self.server.showPublishedDateOnly, self.server.newswire, - self.server.positiveVoting) + self.server.positiveVoting, + self.server.showPublishAsIcon, + fullWidthTimelineButtonHeader, + self.server.iconsAsButtons, + self.server.rssIconAtTop, + self.server.publishButtonAtTop, + authorized) msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) @@ -7425,8 +7616,11 @@ class PubServer(BaseHTTPRequestHandler): True, 0, self.server.positiveVoting, self.server.votingTimeMins) + fullWidthTimelineButtonHeader = \ + self.server.fullWidthTimelineButtonHeader msg = \ - htmlModeration(self.server.defaultTimeline, + htmlModeration(self.server.cssCache, + self.server.defaultTimeline, self.server.recentPostsCache, self.server.maxRecentPosts, self.server.translate, @@ -7445,7 +7639,13 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain, self.server.showPublishedDateOnly, self.server.newswire, - self.server.positiveVoting) + self.server.positiveVoting, + self.server.showPublishAsIcon, + fullWidthTimelineButtonHeader, + self.server.iconsAsButtons, + self.server.rssIconAtTop, + self.server.publishButtonAtTop, + authorized) msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) @@ -7523,7 +7723,10 @@ class PubServer(BaseHTTPRequestHandler): self.server.GETbusy = False return True msg = \ - htmlProfile(self.server.defaultTimeline, + htmlProfile(self.server.rssIconAtTop, + self.server.cssCache, + self.server.iconsAsButtons, + self.server.defaultTimeline, self.server.recentPostsCache, self.server.maxRecentPosts, self.server.translate, @@ -7611,7 +7814,10 @@ class PubServer(BaseHTTPRequestHandler): return True msg = \ - htmlProfile(self.server.defaultTimeline, + htmlProfile(self.server.rssIconAtTop, + self.server.cssCache, + self.server.iconsAsButtons, + self.server.defaultTimeline, self.server.recentPostsCache, self.server.maxRecentPosts, self.server.translate, @@ -7698,7 +7904,10 @@ class PubServer(BaseHTTPRequestHandler): self.server.GETbusy = False return True msg = \ - htmlProfile(self.server.defaultTimeline, + htmlProfile(self.server.rssIconAtTop, + self.server.cssCache, + self.server.iconsAsButtons, + self.server.defaultTimeline, self.server.recentPostsCache, self.server.maxRecentPosts, self.server.translate, @@ -7761,7 +7970,10 @@ class PubServer(BaseHTTPRequestHandler): self.server.GETbusy = False return True msg = \ - htmlProfile(self.server.defaultTimeline, + htmlProfile(self.server.rssIconAtTop, + self.server.cssCache, + self.server.iconsAsButtons, + self.server.defaultTimeline, self.server.recentPostsCache, self.server.maxRecentPosts, self.server.translate, @@ -8215,9 +8427,9 @@ class PubServer(BaseHTTPRequestHandler): if '?' in postDay: postDay = postDay.split('?')[0] # show the confirmation screen screen - msg = htmlCalendarDeleteConfirm(translate, - baseDir, - path, + msg = htmlCalendarDeleteConfirm(self.server.cssCache, + translate, + baseDir, path, httpPrefix, domainFull, postId, postTime, @@ -8271,7 +8483,8 @@ class PubServer(BaseHTTPRequestHandler): break if isNewPostEndpoint: nickname = getNicknameFromActor(path) - msg = htmlNewPost(mediaInstance, + msg = htmlNewPost(self.server.cssCache, + mediaInstance, translate, baseDir, httpPrefix, @@ -8280,7 +8493,8 @@ class PubServer(BaseHTTPRequestHandler): shareDescription, replyPageNumber, nickname, domain, - domainFull).encode('utf-8') + domainFull, + self.server.defaultTimeline).encode('utf-8') if not msg: print('Error replying to ' + inReplyToUrl) self._404() @@ -8303,7 +8517,8 @@ class PubServer(BaseHTTPRequestHandler): """Show the edit profile screen """ if '/users/' in path and path.endswith('/editprofile'): - msg = htmlEditProfile(translate, + msg = htmlEditProfile(self.server.cssCache, + translate, baseDir, path, domain, port, @@ -8325,7 +8540,8 @@ class PubServer(BaseHTTPRequestHandler): """Show the links from the left column """ if '/users/' in path and path.endswith('/editlinks'): - msg = htmlEditLinks(translate, + msg = htmlEditLinks(self.server.cssCache, + translate, baseDir, path, domain, port, @@ -8347,7 +8563,8 @@ class PubServer(BaseHTTPRequestHandler): """Show the newswire from the right column """ if '/users/' in path and path.endswith('/editnewswire'): - msg = htmlEditNewswire(translate, + msg = htmlEditNewswire(self.server.cssCache, + translate, baseDir, path, domain, port, @@ -8376,7 +8593,8 @@ class PubServer(BaseHTTPRequestHandler): postUrl = httpPrefix + '://' + domainFull + \ '/users/news/statuses/' + postId path = path.split('/editnewspost=')[0] - msg = htmlEditNewsPost(translate, baseDir, + msg = htmlEditNewsPost(self.server.cssCache, + translate, baseDir, path, domain, port, httpPrefix, postUrl).encode('utf-8') @@ -8470,7 +8688,8 @@ class PubServer(BaseHTTPRequestHandler): if self.path == '/logout': if not self.server.newsInstance: msg = \ - htmlLogin(self.server.translate, + htmlLogin(self.server.cssCache, + self.server.translate, self.server.baseDir, False).encode('utf-8') self._logout_headers('text/html', len(msg), callingDomain) self._write(msg) @@ -8809,7 +9028,8 @@ class PubServer(BaseHTTPRequestHandler): actor = \ self.server.httpPrefix + '://' + \ self.server.domainFull + usersPath - msg = htmlRemoveSharedItem(self.server.translate, + msg = htmlRemoveSharedItem(self.server.cssCache, + self.server.translate, self.server.baseDir, actor, shareName, callingDomain).encode('utf-8') @@ -8838,14 +9058,17 @@ class PubServer(BaseHTTPRequestHandler): if self.path.startswith('/terms'): if callingDomain.endswith('.onion') and \ self.server.onionDomain: - msg = htmlTermsOfService(self.server.baseDir, 'http', + msg = htmlTermsOfService(self.server.cssCache, + self.server.baseDir, 'http', self.server.onionDomain) elif (callingDomain.endswith('.i2p') and self.server.i2pDomain): - msg = htmlTermsOfService(self.server.baseDir, 'http', + msg = htmlTermsOfService(self.server.cssCache, + self.server.baseDir, 'http', self.server.i2pDomain) else: - msg = htmlTermsOfService(self.server.baseDir, + msg = htmlTermsOfService(self.server.cssCache, + self.server.baseDir, self.server.httpPrefix, self.server.domainFull) msg = msg.encode('utf-8') @@ -8870,7 +9093,8 @@ class PubServer(BaseHTTPRequestHandler): if not os.path.isfile(followingFilename): self._404() return - msg = htmlFollowingList(self.server.baseDir, followingFilename) + msg = htmlFollowingList(self.server.cssCache, + self.server.baseDir, followingFilename) self._login_headers('text/html', len(msg), callingDomain) self._write(msg.encode('utf-8')) self._benchmarkGETtimings(GETstartTime, GETtimings, @@ -8885,17 +9109,20 @@ class PubServer(BaseHTTPRequestHandler): if self.path.endswith('/about'): if callingDomain.endswith('.onion'): msg = \ - htmlAbout(self.server.baseDir, 'http', + htmlAbout(self.server.cssCache, + self.server.baseDir, 'http', self.server.onionDomain, None) elif callingDomain.endswith('.i2p'): msg = \ - htmlAbout(self.server.baseDir, 'http', + htmlAbout(self.server.cssCache, + self.server.baseDir, 'http', self.server.i2pDomain, None) else: msg = \ - htmlAbout(self.server.baseDir, + htmlAbout(self.server.cssCache, + self.server.baseDir, self.server.httpPrefix, self.server.domainFull, self.server.onionDomain) @@ -8921,7 +9148,10 @@ class PubServer(BaseHTTPRequestHandler): # if not authorized then show the login screen if htmlGET and self.path != '/login' and \ - not self._pathIsImage(self.path) and self.path != '/': + not self._pathIsImage(self.path) and \ + self.path != '/' and \ + self.path != '/users/news/linksmobile' and \ + self.path != '/users/news/newswiremobile': if self._redirectToLoginScreen(callingDomain, self.path, self.server.httpPrefix, self.server.domainFull, @@ -9245,7 +9475,8 @@ class PubServer(BaseHTTPRequestHandler): not authorized and not self.server.newsInstance)): # request basic auth - msg = htmlLogin(self.server.translate, + msg = htmlLogin(self.server.cssCache, + self.server.translate, self.server.baseDir).encode('utf-8') self._login_headers('text/html', len(msg), callingDomain) self._write(msg) @@ -9286,47 +9517,73 @@ class PubServer(BaseHTTPRequestHandler): 'permitted directory', 'login shown done') - if authorized and htmlGET and '/users/' in self.path and \ + if htmlGET and self.path.startswith('/users/') and \ self.path.endswith('/newswiremobile'): - nickname = getNicknameFromActor(self.path) - if not nickname: - self._404() + if (authorized or + (not authorized and + self.path.startswith('/users/news/') and + self.server.newsInstance)): + nickname = getNicknameFromActor(self.path) + if not nickname: + self._404() + self.server.GETbusy = False + return + timelinePath = \ + '/users/' + nickname + '/' + self.server.defaultTimeline + showPublishAsIcon = self.server.showPublishAsIcon + rssIconAtTop = self.server.rssIconAtTop + iconsAsButtons = self.server.iconsAsButtons + defaultTimeline = self.server.defaultTimeline + msg = htmlNewswireMobile(self.server.cssCache, + self.server.baseDir, + nickname, + self.server.domain, + self.server.domainFull, + self.server.httpPrefix, + self.server.translate, + self.server.newswire, + self.server.positiveVoting, + timelinePath, + showPublishAsIcon, + authorized, + rssIconAtTop, + iconsAsButtons, + defaultTimeline).encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) self.server.GETbusy = False return - timelinePath = \ - '/users/' + nickname + '/' + self.server.defaultTimeline - msg = htmlNewswireMobile(self.server.baseDir, - nickname, - self.server.domain, - self.server.domainFull, - self.server.httpPrefix, - self.server.translate, - self.server.newswire, - self.server.positiveVoting, - timelinePath).encode('utf-8') - self._set_headers('text/html', len(msg), cookie, callingDomain) - self._write(msg) - self.server.GETbusy = False - return - if authorized and htmlGET and '/users/' in self.path and \ + if htmlGET and self.path.startswith('/users/') and \ self.path.endswith('/linksmobile'): - nickname = getNicknameFromActor(self.path) - if not nickname: - self._404() + if (authorized or + (not authorized and + self.path.startswith('/users/news/') and + self.server.newsInstance)): + nickname = getNicknameFromActor(self.path) + if not nickname: + self._404() + self.server.GETbusy = False + return + timelinePath = \ + '/users/' + nickname + '/' + self.server.defaultTimeline + iconsAsButtons = self.server.iconsAsButtons + defaultTimeline = self.server.defaultTimeline + msg = htmlLinksMobile(self.server.cssCache, + self.server.baseDir, nickname, + self.server.domainFull, + self.server.httpPrefix, + self.server.translate, + timelinePath, + authorized, + self.server.rssIconAtTop, + iconsAsButtons, + defaultTimeline).encode('utf-8') + self._set_headers('text/html', len(msg), cookie, callingDomain) + self._write(msg) self.server.GETbusy = False return - timelinePath = \ - '/users/' + nickname + '/' + self.server.defaultTimeline - msg = htmlLinksMobile(self.server.baseDir, nickname, - self.server.domainFull, - self.server.httpPrefix, - self.server.translate, - timelinePath).encode('utf-8') - self._set_headers('text/html', len(msg), cookie, callingDomain) - self._write(msg) - self.server.GETbusy = False - return # hashtag search if self.path.startswith('/tags/') or \ @@ -9368,8 +9625,7 @@ class PubServer(BaseHTTPRequestHandler): nickname = nickname.split('/')[0] self._setMinimal(nickname, not self._isMinimal(nickname)) if not (self.server.mediaInstance or - self.server.blogsInstance or - self.server.newsInstance): + self.server.blogsInstance): self.path = '/users/' + nickname + '/inbox' else: if self.server.blogsInstance: @@ -9387,7 +9643,8 @@ class PubServer(BaseHTTPRequestHandler): if '?' in self.path: self.path = self.path.split('?')[0] # show the search screen - msg = htmlSearch(self.server.translate, + msg = htmlSearch(self.server.cssCache, + self.server.translate, self.server.baseDir, self.path, self.server.domain).encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) @@ -9406,7 +9663,8 @@ class PubServer(BaseHTTPRequestHandler): if htmlGET and '/users/' in self.path: if '/calendar' in self.path: # show the calendar screen - msg = htmlCalendar(self.server.translate, + msg = htmlCalendar(self.server.cssCache, + self.server.translate, self.server.baseDir, self.path, self.server.httpPrefix, self.server.domainFull).encode('utf-8') @@ -9446,7 +9704,8 @@ class PubServer(BaseHTTPRequestHandler): if htmlGET and '/users/' in self.path: if self.path.endswith('/searchemoji'): # show the search screen - msg = htmlSearchEmojiTextEntry(self.server.translate, + msg = htmlSearchEmojiTextEntry(self.server.cssCache, + self.server.translate, self.server.baseDir, self.path).encode('utf-8') self._set_headers('text/html', len(msg), @@ -11434,7 +11693,14 @@ class PubServer(BaseHTTPRequestHandler): self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 7) - if authorized: + if not authorized: + if self.path.endswith('/rmpost'): + print('ERROR: attempt to remove post was not authorized. ' + + self.path) + self._400() + self.server.POSTbusy = False + return + else: # a vote/question/poll is posted if self.path.endswith('/question') or \ '/question?page=' in self.path: @@ -11466,11 +11732,12 @@ class PubServer(BaseHTTPRequestHandler): # removes a post if self.path.endswith('/rmpost'): - print('ERROR: attempt to remove post was not authorized. ' + - self.path) - self._400() - self.server.POSTbusy = False - return + if '/users/' not in self.path: + print('ERROR: attempt to remove post ' + + 'was not authorized. ' + self.path) + self._400() + self.server.POSTbusy = False + return if self.path.endswith('/rmpost'): self._removePost(callingDomain, cookie, authorized, self.path, @@ -11951,7 +12218,16 @@ def loadTokens(baseDir: str, tokensDict: {}, tokensLookup: {}) -> None: tokensLookup[token] = nickname -def runDaemon(maxNewswireFeedSizeKb: int, +def runDaemon(publishButtonAtTop: bool, + rssIconAtTop: bool, + iconsAsButtons: bool, + fullWidthTimelineButtonHeader: bool, + showPublishAsIcon: bool, + maxFollowers: int, + allowNewsFollowers: bool, + maxNewsPosts: int, + maxMirroredArticles: int, + maxNewswireFeedSizeKb: int, maxNewswirePostsPerSource: int, showPublishedDateOnly: bool, votingTimeMins: int, @@ -12086,6 +12362,40 @@ def runDaemon(maxNewswireFeedSizeKb: int, # Show only the date at the bottom of posts, and not the time httpd.showPublishedDateOnly = showPublishedDateOnly + # maximum number of news articles to mirror + httpd.maxMirroredArticles = maxMirroredArticles + + # maximum number of posts in the news timeline/outbox + httpd.maxNewsPosts = maxNewsPosts + + # whether or not to allow followers of the news account + httpd.allowNewsFollowers = allowNewsFollowers + + # The maximum number of tags per post which can be + # attached to RSS feeds pulled in via the newswire + httpd.maxTags = 32 + + # maximum number of followers per account + httpd.maxFollowers = maxFollowers + + # whether to show an icon for publish on the + # newswire, or a 'Publish' button + httpd.showPublishAsIcon = showPublishAsIcon + + # Whether to show the timeline header containing inbox, outbox + # calendar, etc as the full width of the screen or not + httpd.fullWidthTimelineButtonHeader = fullWidthTimelineButtonHeader + + # whether to show icons in the header (eg calendar) as buttons + httpd.iconsAsButtons = iconsAsButtons + + # whether to show the RSS icon at the top or the bottom of the timeline + httpd.rssIconAtTop = rssIconAtTop + + # Whether to show the newswire publish button at the top, + # above the header image + httpd.publishButtonAtTop = publishButtonAtTop + if registration == 'open': httpd.registration = True else: @@ -12139,6 +12449,9 @@ def runDaemon(maxNewswireFeedSizeKb: int, # contains threads used to send posts to followers httpd.followersThreads = [] + # cache to store css files + httpd.cssCache = {} + if not os.path.isdir(baseDir + '/accounts/inbox@' + domain): print('Creating shared inbox: inbox@' + domain) createSharedInbox(baseDir, 'inbox', domain, port, httpPrefix) @@ -12227,7 +12540,9 @@ def runDaemon(maxNewswireFeedSizeKb: int, allowDeletion, debug, maxMentions, maxEmoji, httpd.translate, unitTest, httpd.YTReplacementDomain, - httpd.showPublishedDateOnly), daemon=True) + httpd.showPublishedDateOnly, + httpd.allowNewsFollowers, + httpd.maxFollowers), daemon=True) print('Creating scheduled post thread') httpd.thrPostSchedule = \ diff --git a/deploy/onion b/deploy/onion index c60df40b1..cf277b4e6 100755 --- a/deploy/onion +++ b/deploy/onion @@ -229,6 +229,9 @@ fi if [ ! -d ${web_dir}/cache ]; then mkdir ${web_dir}/cache fi +if [ ! -d /var/www/${ONION_DOMAIN}/htdocs ]; then + mkdir -p /var/www/${ONION_DOMAIN}/htdocs +fi echo "Creating nginx virtual host for ${ONION_DOMAIN}" { echo "proxy_cache_path ${web_dir}/cache levels=1:2 keys_zone=my_cache:10m max_size=10g"; @@ -252,6 +255,12 @@ echo "Creating nginx virtual host for ${ONION_DOMAIN}" echo ' error_log /dev/null;'; echo ''; echo ' index index.html;'; + echo ''; + echo ' location /newsmirror {'; + echo ' root /var/www/${ONION_DOMAIN}/htdocs;'; + echo ' try_files $uri =404;'; + echo ' }'; + echo ''; echo ' location / {'; echo ' proxy_http_version 1.1;'; echo ' client_max_body_size 31M;'; @@ -299,6 +308,13 @@ echo "Creating nginx virtual host for ${ONION_DOMAIN}" echo ' }'; echo '}'; } > "/etc/nginx/sites-available/${username}" +chown -R www-data:www-data /var/www/${ONION_DOMAIN}/htdocs +if [ ! -d ${install_destination}/accounts/newsmirror ]; then + mkdir -p ${install_destination}/accounts/newsmirror + chown -R ${username}:${username} ${install_destination} +fi +ln -s ${install_destination}/newsmirror /var/www/${ONION_DOMAIN}/htdocs/newsmirror + ln -s "/etc/nginx/sites-available/${username}" /etc/nginx/sites-enabled/ systemctl restart nginx diff --git a/emoji/default_emoji.json b/emoji/default_emoji.json index 2f4fc92d1..9524f45df 100644 --- a/emoji/default_emoji.json +++ b/emoji/default_emoji.json @@ -450,6 +450,7 @@ "turkey": "1F983", "turtle": "1F422", "twitter": "E040", + "birdsite": "E040", "two": "0032", "umbrellawithraindrops": "2614", "unamusedface": "1F612", diff --git a/epicyon-login.css b/epicyon-login.css index e0a5ebec0..8d55f72b9 100644 --- a/epicyon-login.css +++ b/epicyon-login.css @@ -10,8 +10,8 @@ --border-width: 2px; --font-size-header: 18px; --font-color-header: #ccc; - --font-size: 22px; - --font-size-mobile: 40px; + --login-font-size: 22px; + --login-font-size-mobile: 40px; --text-entry-foreground: #ccc; --text-entry-background: #111; --time-color: #aaa; @@ -21,6 +21,7 @@ --form-border-radius: 30px; --focus-color: white; --line-spacing: 130%; + --login-logo-width: 20%; } @font-face { @@ -53,7 +54,7 @@ body, html { max-width: 60%; min-width: 600px; margin: 0 auto; - font-size: var(--font-size); + font-size: var(--login-font-size); line-height: var(--line-spacing); } @@ -89,7 +90,7 @@ input[type=text], input[type=password] { display: inline-block; border: 1px solid #ccc; box-sizing: border-box; - font-size: var(--font-size); + font-size: var(--login-font-size); font-family: Arial, Helvetica, sans-serif; } @@ -101,12 +102,12 @@ button { border: none; cursor: pointer; width: 100%; - font-size: var(--font-size); + font-size: var(--login-font-size); font-family: Arial, Helvetica, sans-serif; } .login-text { - font-size: var(--font-size); + font-size: var(--login-font-size); font-family: Arial, Helvetica, sans-serif; } @@ -119,6 +120,10 @@ button:hover { margin: 24px 0 12px 0; } +.imgcontainer img { + width: var(--login-logo-width); +} + img.avatar { width: 40%; border-radius: 50%; @@ -148,12 +153,12 @@ span.psw { max-width: 60%; min-width: 600px; margin: 0 auto; - font-size: var(--font-size); + font-size: var(--login-font-size); font-family: Arial, Helvetica, sans-serif; position: relative; } .login-text { - font-size: var(--font-size); + font-size: var(--login-font-size); font-family: Arial, Helvetica, sans-serif; } input[type=text], input[type=password] { @@ -163,7 +168,7 @@ span.psw { display: inline-block; border: 1px solid #ccc; box-sizing: border-box; - font-size: var(--font-size); + font-size: var(--login-font-size); font-family: Arial, Helvetica, sans-serif; } button { @@ -174,7 +179,7 @@ span.psw { border: none; cursor: pointer; width: 100%; - font-size: var(--font-size); + font-size: var(--login-font-size); font-family: Arial, Helvetica, sans-serif; } } @@ -188,12 +193,12 @@ span.psw { max-width: 95%; min-width: 600px; margin: 0 auto; - font-size: var(--font-size-mobile); + font-size: var(--login-font-size-mobile); font-family: Arial, Helvetica, sans-serif; position: relative; } .login-text { - font-size: var(--font-size-mobile); + font-size: var(--login-font-size-mobile); font-family: Arial, Helvetica, sans-serif; } input[type=text], input[type=password] { @@ -203,7 +208,7 @@ span.psw { display: inline-block; border: 1px solid #ccc; box-sizing: border-box; - font-size: var(--font-size-mobile); + font-size: var(--login-font-size-mobile); font-family: Arial, Helvetica, sans-serif; } button { @@ -214,7 +219,7 @@ span.psw { border: none; cursor: pointer; width: 100%; - font-size: var(--font-size-mobile); + font-size: var(--login-font-size-mobile); font-family: Arial, Helvetica, sans-serif; } } diff --git a/epicyon-profile.css b/epicyon-profile.css index a73bf58b7..75572b813 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -21,13 +21,15 @@ --main-visited-color: #888; --border-color: #505050; --border-width: 2px; + --border-width-header: 2px; --font-size-header: 18px; --font-size-header-mobile: 32px; --font-color-header: #ccc; --font-size-button-mobile: 34px; --font-size-links: 18px; + --font-size-publish-button: 18px; --font-size-newswire: 18px; - --font-size-newswire-mobile: 48px; + --font-size-newswire-mobile: 40px; --font-size: 30px; --font-size2: 24px; --font-size3: 38px; @@ -41,11 +43,14 @@ --font-size-tox2: 8px; --time-color: #aaa; --time-vertical-align: 4px; + --time-vertical-align-mobile: 25px; --publish-button-text: #FFFFFF; --button-text: #FFFFFF; + --button-selected-text: #FFFFFF; --publish-button-background: #999; --button-background: #999; --button-background-hover: #777; + --button-text-hover: white; --button-selected: #666; --button-highlighted: green; --button-fg-highlighted: #FFFFFF; @@ -91,6 +96,28 @@ --newswire-voted-background-color: black; --login-button-color: #2965; --login-button-fg-color: black; + --button-event-corner-radius: 60px; + --button-event-background-color: green; + --button-event-fg-color: white; + --hashtag-background-color: black; + --hashtag-fg-color: white; + --tab-border-width: 0px; + --tab-border-color: grey; + --icon-brightness-change: 150%; + --container-button-padding: 20px; + --container-padding: 2%; + --container-padding-bottom: 1%; + --container-padding-bottom-mobile: 0%; + --vertical-between-posts: 10px; + --vertical-between-posts-header: 10px; + --containericons-horizontal-spacing: 1%; + --containericons-horizontal-spacing-mobile: 3%; + --containericons-horizontal-offset: -1%; + --likes-count-offset: 5px; + --likes-count-offset-mobile: 10px; + --publish-button-vertical-offset: 10px; + --banner-height: 15vh; + --banner-height-mobile: 10vh; } @font-face { @@ -184,7 +211,7 @@ a:visited:hover { } .buttonevent:hover { - filter: brightness(150%); + filter: brightness(var(----icon-brightness-change)); } a:focus { @@ -292,11 +319,16 @@ a:focus { } .container img.timelineicon:hover { - filter: brightness(150%); + filter: brightness(var(--icon-brightness-change)); +} + +.containerHeader img.timelineicon:hover { + filter: brightness(var(--icon-brightness-change)); } .buttonunfollow:hover { background-color: var(--button-background-hover); + color: var(--button-text-hover); } .followRequestHandle { @@ -323,10 +355,12 @@ a:focus { .button:hover { background-color: var(--button-background-hover); + color: var(--button-text-hover); } .donateButton:hover { background-color: var(--button-background-hover); + color: var(--button-text-hover); } .buttonselected span { @@ -349,14 +383,23 @@ a:focus { .buttonselected:hover { background-color: var(--button-background-hover); + color: var(--button-text-hover); } -.container { +.containerNewPost { border: var(--border-width) solid var(--border-color); background-color: var(--main-bg-color); border-radius: var(--timeline-border-radius); - padding: 20px; - margin: 10px; + padding: var(--container-padding); + margin: var(--vertical-between-posts); +} + +.containerHeader { + border: var(--border-width-header) solid var(--border-color); + background-color: var(--main-bg-color); + border-radius: var(--timeline-border-radius); + padding: var(--container-button-padding); + margin: var(--vertical-between-posts-header); } .media { @@ -367,8 +410,19 @@ a:focus { } .message { - margin-left: 7%; - width: 90%; + margin-left: 0%; + margin-right: 0%; + width: 100%; +} + +.addedHashtag:link { + background-color: var(--hashtag-background-color); + color: var(--hashtag-fg-color); +} + +.addedHashtag:visited { + background-color: var(--hashtag-background-color); + color: var(--hashtag-fg-color); } .message:focus{ @@ -384,12 +438,6 @@ a:focus { font-family: 'monospace'; } -.container::after { - content: ""; - clear: both; - display: table; -} - .searchEmoji { vertical-align: middle; float: none; @@ -421,7 +469,7 @@ a:focus { .containericons { padding: 0px 0px; - margin: 0px 0px; + margin: 0px var(--containericons-horizontal-offset); } .replyingto { @@ -467,11 +515,13 @@ a:focus { } .container img.attachment { - max-width: 120%; - margin-left: 5%; - width: 120%; + max-width: 140%; + margin-left: -2%; + margin-right: 2%; + width: 125%; padding-bottom: 3%; } + .container img.right { float: var(--icons-side); margin-left: 0px; @@ -479,6 +529,13 @@ a:focus { padding: 0 0; margin: 0 0; } +.containerHeader img.right { + float: var(--icons-side); + margin-left: 0px; + margin-right:0; + padding: 0 0; + margin: 0 0; +} .containericons img.right { float: var(--icons-side); margin-left: 20px; @@ -486,7 +543,7 @@ a:focus { } .containericons img:hover { - filter: brightness(150%); + filter: brightness(var(----icon-brightness-change)); } .post-title { @@ -542,10 +599,11 @@ input[type=number] { } .transparent { - color: rgba(0, 0, 0, 0.0); + color: transparent; + background: transparent; font-size: 0px; - font-family: Arial, Helvetica, sans-serif; - line-height: 0; + line-height: 0px; + height: 0px; } .labelsright { @@ -946,6 +1004,14 @@ aside .toggle-inside li { display: none; } +div.containerHeader { + overflow: auto; +} + +div.container { + overflow: hidden; +} + @media screen and (min-width: 400px) { body, html { background-color: var(--main-bg-color); @@ -957,13 +1023,23 @@ aside .toggle-inside li { font-size: var(--font-size); line-height: var(--line-spacing); } + .container { + border: var(--border-width) solid var(--border-color); + background-color: var(--main-bg-color); + border-radius: var(--timeline-border-radius); + padding-left: var(--container-padding); + padding-right: var(--container-padding); + padding-top: var(--container-padding); + padding-bottom: var(--container-padding-bottom); + margin: var(--vertical-between-posts); + } h3.linksHeader { - background-color: var(--column-left-header-background); - color: var(--column-left-header-color); - font-size: var(--column-left-header-size); - text-transform: uppercase; - padding: 4px; - border: none; + background-color: var(--column-left-header-background); + color: var(--column-left-header-color); + font-size: var(--column-left-header-size); + text-transform: uppercase; + padding: 4px; + border: none; } .newswireItem { font-size: var(--font-size-newswire); @@ -1001,20 +1077,16 @@ aside .toggle-inside li { color: var(--column-right-fg-color-voted-on); float: right; } - .imageAnchorMobile img{ + .imageAnchorMobile img { display: none; } .timeline-banner { - background-image: linear-gradient(rgba(0, 0, 0, 0.0), rgba(0, 0, 0, 0.5)), url("banner.png"); - height: 15%; - background-position: center; - background-repeat: no-repeat; - background-size: 100vw; - position: relative; + width: 98vw; + height: var(--banner-height); } .timeline { border: 0; - width: 100vw; + width: 98vw; } .col-left a:link { background: var(--column-left-color); @@ -1028,7 +1100,6 @@ aside .toggle-inside li { } .col-left { color: var(--column-left-fg-color); - padding: 10px 10px; font-size: var(--font-size-links); float: left; width: var(--column-left-width); @@ -1065,14 +1136,14 @@ aside .toggle-inside li { .column-right { background-color: var(--column-left-color); width: var(--column-right-width); + overflow: hidden; } .col-right { background-color: var(--column-left-color); color: var(--column-left-fg-color); - padding-left: 10px; - padding-right: 30px; font-size: var(--font-size-links); width: var(--column-right-width); + overflow: hidden; } .col-right img.rightColEdit { background: var(--column-left-color); @@ -1095,7 +1166,7 @@ aside .toggle-inside li { font-family: Arial, Helvetica, sans-serif; float: right; padding: 10px 0; - transform: translateX(-10px); + transform: translateX(var(--likes-count-offset)); font-weight: bold; } .container p.administeredby { @@ -1132,10 +1203,10 @@ aside .toggle-inside li { font-family: Arial, Helvetica, sans-serif; } div.gallery { - margin: 5px; + margin: 5px 1.5%; border: 1px solid var(--gallery-border); float: left; - width: 100%; + width: 95%; object-fit: scale-down; } div.imagedesc { @@ -1150,6 +1221,14 @@ aside .toggle-inside li { margin-right: 20px; border-radius: var(--image-corners); } + .containerHeader img { + float: left; + max-width: 400px; + width: 5%; + padding: 0px 7px; + margin-right: 20px; + border-radius: var(--image-corners); + } .container img.emojisearch { width: 15%; float: right; @@ -1162,6 +1241,14 @@ aside .toggle-inside li { transform: translateY(-25%); } .container img.timelineicon { + float: var(--icons-side); + margin-left: 0px; + margin-right: 0px; + padding: 0px 0px; + margin: 0px 0px; + width: 50px; + } + .containerHeader img.timelineicon { float: var(--icons-side); margin-left: 0px; margin-right:0; @@ -1182,7 +1269,8 @@ aside .toggle-inside li { float: var(--icons-side); max-width: 200px; width: 3%; - margin: 0px 1%; + margin: 0px var(--containericons-horizontal-spacing); + margin-right: 0px; border-radius: 0%; } div.mediaicons img { @@ -1208,10 +1296,10 @@ aside .toggle-inside li { transform: translateY(-10%); } .buttonevent { - border-radius: var(--button-corner-radius); - background-color: var(--button-highlighted); + border-radius: var(--button-event-corner-radius); + background-color: var(--button-event-background-color); border: none; - color: var(--button-fg-highlighted); + color: var(--button-event-fg-color); text-align: center; font-size: var(--font-size-header); font-family: Arial, Helvetica, sans-serif; @@ -1220,21 +1308,31 @@ aside .toggle-inside li { cursor: pointer; margin: 5px; } + .frontPageMobileButtons{ + display: none; + } + .buttonMobile { + background: transparent; + border: none !important; + font-size: 0; + } .button { border-radius: var(--button-corner-radius); background-color: var(--button-background); - border: none; color: var(--button-text); text-align: center; font-size: var(--font-size-header); font-family: Arial, Helvetica, sans-serif; padding: var(--button-height-padding); width: 10%; - max-width: 200px; + margin: 5px; min-width: var(--button-width-chars); transition: all 0.5s; cursor: pointer; - margin: 5px; + border-top: var(--tab-border-width) solid var(--tab-border-color); + border-bottom: none; + border-left: var(--tab-border-width) solid var(--tab-border-color); + border-right: var(--tab-border-width) solid var(--tab-border-color); } .publishbtn { border-radius: var(--button-corner-radius); @@ -1242,7 +1340,7 @@ aside .toggle-inside li { border: none; color: var(--publish-button-text); text-align: center; - font-size: var(--font-size-header); + font-size: var(--font-size-publish-button); font-family: Arial, Helvetica, sans-serif; padding: var(--button-height-padding); width: 10%; @@ -1250,7 +1348,8 @@ aside .toggle-inside li { min-width: var(--button-width-chars); transition: all 0.5s; cursor: pointer; - margin: -20px 0px; + margin: 0 0px; + margin-top: var(--publish-button-vertical-offset); } .buttonhighlighted { border-radius: var(--button-corner-radius); @@ -1271,18 +1370,20 @@ aside .toggle-inside li { .buttonselected { border-radius: var(--button-corner-radius); background-color: var(--button-selected); - border: none; - color: var(--button-text); + color: var(--button-selected-text); text-align: center; font-size: var(--font-size-header); font-family: Arial, Helvetica, sans-serif; padding: var(--button-height-padding); width: 10%; - max-width: 100px; + margin: 5px; min-width: var(--button-width-chars); transition: all 0.5s; cursor: pointer; - margin: 5px; + border-top: var(--tab-border-width) solid var(--tab-border-color); + border-bottom: none; + border-left: var(--tab-border-width) solid var(--tab-border-color); + border-right: var(--tab-border-width) solid var(--tab-border-color); } .buttonselectedhighlighted { border-radius: var(--button-corner-radius); @@ -1543,6 +1644,9 @@ aside .toggle-inside li { padding: 10px; margin: 20px 60px; } + .columnIcons img { + float: right; + } } @media screen and (min-width: 2200px) { @@ -1566,13 +1670,23 @@ aside .toggle-inside li { font-size: var(--font-size); line-height: var(--line-spacing); } + .container { + border: var(--border-width) solid var(--border-color); + background-color: var(--main-bg-color); + border-radius: var(--timeline-border-radius); + padding-left: var(--container-padding); + padding-right: var(--container-padding); + padding-top: var(--container-padding); + padding-bottom: var(--container-padding-bottom-mobile); + margin: var(--vertical-between-posts); + } h3.linksHeader { - background-color: var(--column-left-header-background); - color: var(--column-left-header-color); - font-size: var(--column-left-header-size-mobile); - text-transform: uppercase; - padding: 4px; - border: none; + background-color: var(--column-left-header-background); + color: var(--column-left-header-color); + font-size: var(--column-left-header-size-mobile); + text-transform: uppercase; + padding: 4px; + border: none; } .leftColEditImage { background: var(--main-bg-color); @@ -1636,20 +1750,18 @@ aside .toggle-inside li { color: var(--column-right-fg-color-voted-on); float: right; } - .imageAnchorMobile img{ + .imageAnchorMobile img { display: inline; } .timeline-banner { - background-image: linear-gradient(rgba(0, 0, 0, 0.0), rgba(0, 0, 0, 0.5)), url("banner.png"); - height: 6%; - background-position: center; - background-repeat: no-repeat; - background-size: 145vw; - position: relative; + width: 98vw; + height: var(--banner-height-mobile); } .timeline { border: 0; width: 100vw; + table-layout: fixed; + overflow: hidden; } .column-left { display: none; @@ -1680,7 +1792,7 @@ aside .toggle-inside li { font-family: Arial, Helvetica, sans-serif; float: right; padding: 32px 0; - transform: translateX(-20px); + transform: translateX(var(--likes-count-offset-mobile)); font-weight: bold; } .container p.administeredby { @@ -1716,10 +1828,10 @@ aside .toggle-inside li { background-color: var(--main-bg-color); } div.gallery { - margin: 5px; + margin: 5px 1.5%; border: 1px solid var(--gallery-border); float: left; - width: 100%; + width: 98%; } div.imagedesc { padding: 35px; @@ -1733,6 +1845,14 @@ aside .toggle-inside li { margin-right: 20px; border-radius: var(--image-corners); } + .containerHeader img { + float: left; + max-width: 400px; + width: 15%; + padding: 0px 7px; + margin-right: 20px; + border-radius: var(--image-corners); + } .container img.emojisearch { width: 25%; float: right; @@ -1745,6 +1865,14 @@ aside .toggle-inside li { transform: translateY(-25%); } .container img.timelineicon { + float: var(--icons-side); + margin-left: 0px; + margin-right: 0px; + padding: 0px 0px; + margin: 0px 0px; + width: 100px; + } + .containerHeader img.timelineicon { float: var(--icons-side); margin-left: 0px; margin-right:0; @@ -1779,7 +1907,8 @@ aside .toggle-inside li { float: var(--icons-side); max-width: 200px; width: 7%; - margin: 1% 3%; + margin: 1% var(--containericons-horizontal-spacing-mobile); + margin-right: 0px; border-radius: 0%; } .timeline-avatar img { @@ -1791,10 +1920,10 @@ aside .toggle-inside li { transform: translateY(-10%); } .buttonevent { - border-radius: var(--button-corner-radius); - background-color: var(--button-highlighted); + border-radius: var(--button-event-corner-radius); + background-color: var(--button-event-background-color); border: none; - color: var(--button-fg-highlighted); + color: var(--button-event-fg-color); text-align: center; font-size: var(--font-size-button-mobile); font-family: Arial, Helvetica, sans-serif; @@ -1806,18 +1935,53 @@ aside .toggle-inside li { .button { border-radius: var(--button-corner-radius); background-color: var(--button-background); - border: none; color: var(--button-text); text-align: center; font-size: var(--font-size-button-mobile); font-family: Arial, Helvetica, sans-serif; padding: var(--button-height-padding-mobile); width: 20%; - max-width: 400px; min-width: var(--button-width-chars); transition: all 0.5s; cursor: pointer; - margin: 15px; + margin: 5px; + border-top: var(--tab-border-width) solid var(--tab-border-color); + border-bottom: none; + border-left: var(--tab-border-width) solid var(--tab-border-color); + border-right: var(--tab-border-width) solid var(--tab-border-color); + } + .frontPageMobileButtons{ + display: block; + border: var(--border-width-header) solid var(--border-color); + background-color: var(--main-bg-color); + border-radius: var(--timeline-border-radius); + padding: var(--container-button-padding); + margin: var(--vertical-between-posts-header); + } + .frontPageMobileButtons img { + float: right; + max-width: 400px; + width: 10%; + padding: 0px 7px; + margin-right: 20px; + } + .buttonMobile { + border-radius: var(--button-corner-radius); + background-color: var(--button-background); + color: var(--button-text); + text-align: center; + font-size: var(--font-size-button-mobile); + font-family: Arial, Helvetica, sans-serif; + padding: var(--button-height-padding-mobile); + width: 20%; + min-width: var(--button-width-chars); + transition: all 0.5s; + cursor: pointer; + margin: 5px; + border-top: var(--tab-border-width) solid var(--tab-border-color); + border-bottom: none; + border-left: var(--tab-border-width) solid var(--tab-border-color); + border-right: var(--tab-border-width) solid var(--tab-border-color); } .publishbtn { border-radius: var(--button-corner-radius); @@ -1854,18 +2018,20 @@ aside .toggle-inside li { .buttonselected { border-radius: var(--button-corner-radius); background-color: var(--button-selected); - border: none; - color: var(--button-text); + color: var(--button-selected-text); text-align: center; font-size: var(--font-size-button-mobile); font-family: Arial, Helvetica, sans-serif; padding: var(--button-height-padding-mobile); width: 20%; - max-width: 400px; min-width: var(--button-width-chars); transition: all 0.5s; cursor: pointer; - margin: 15px; + margin: 5px; + border-top: var(--tab-border-width) solid var(--tab-border-color); + border-bottom: none; + border-left: var(--tab-border-width) solid var(--tab-border-color); + border-right: var(--tab-border-width) solid var(--tab-border-color); } .buttonselectedhighlighted { border-radius: var(--button-corner-radius); @@ -1921,7 +2087,7 @@ aside .toggle-inside li { .time-right { float: var(--icons-side); color: var(--time-color); - margin: 25px 20px; + margin: var(--time-vertical-align-mobile) 20px; } input[type=text], select, textarea { width: 100%; @@ -2127,4 +2293,9 @@ aside .toggle-inside li { padding: 10px; margin: 40px 80px; } + .columnIcons img { + width: 10%; + float: right; + margin-right: 1vw; + } } diff --git a/epicyon.py b/epicyon.py index 5b4787869..343b12447 100644 --- a/epicyon.py +++ b/epicyon.py @@ -120,6 +120,21 @@ parser.add_argument('--maxFeedSize', dest='maxNewswireFeedSizeKb', type=int, default=2048, help='Maximum newswire rss/atom feed size in K') +parser.add_argument('--maxMirroredArticles', + dest='maxMirroredArticles', type=int, + default=100, + help='Maximum number of news articles to mirror.' + + ' Set to zero for indefinite mirroring.') +parser.add_argument('--maxNewsPosts', + dest='maxNewsPosts', type=int, + default=0, + help='Maximum number of news timeline posts to keep. ' + + 'Zero for no expiry.') +parser.add_argument('--maxFollowers', + dest='maxFollowers', type=int, + default=2000, + help='Maximum number of followers per account. ' + + 'Zero for no limit.') parser.add_argument('--postcache', dest='maxRecentPosts', type=int, default=512, help='The maximum number of recent posts to store in RAM') @@ -194,6 +209,41 @@ parser.add_argument("--repliesEnabled", "--commentsEnabled", type=str2bool, nargs='?', const=True, default=True, help="Enable replies to a post") +parser.add_argument("--showPublishAsIcon", + dest='showPublishAsIcon', + type=str2bool, nargs='?', + const=True, default=True, + help="Whether to show newswire publish " + + "as an icon or a button") +parser.add_argument("--fullWidthTimelineButtonHeader", + dest='fullWidthTimelineButtonHeader', + type=str2bool, nargs='?', + const=True, default=False, + help="Whether to show the timeline " + + "button header containing inbox and outbox " + + "as the full width of the screen") +parser.add_argument("--allowNewsFollowers", + dest='allowNewsFollowers', + type=str2bool, nargs='?', + const=True, default=False, + help="Whether to allow the news account to be followed") +parser.add_argument("--iconsAsButtons", + dest='iconsAsButtons', + type=str2bool, nargs='?', + const=True, default=False, + help="Show header icons as buttons") +parser.add_argument("--rssIconAtTop", + dest='rssIconAtTop', + type=str2bool, nargs='?', + const=True, default=True, + help="Whether to show the rss icon at teh top or bottom" + + "of the timeline") +parser.add_argument("--publishButtonAtTop", + dest='publishButtonAtTop', + type=str2bool, nargs='?', + const=True, default=False, + help="Whether to show the publish button at the top of " + + "the newswire column") parser.add_argument("--noapproval", type=str2bool, nargs='?', const=True, default=False, help="Allow followers without approval") @@ -1937,15 +1987,58 @@ if dateonly: maxNewswirePostsPerSource = \ getConfigParam(baseDir, 'maxNewswirePostsPerSource') if maxNewswirePostsPerSource: - if maxNewswirePostsPerSource.isdigit(): - args.maxNewswirePostsPerSource = maxNewswirePostsPerSource + args.maxNewswirePostsPerSource = int(maxNewswirePostsPerSource) # set the maximum size of a newswire rss/atom feed in Kilobytes maxNewswireFeedSizeKb = \ getConfigParam(baseDir, 'maxNewswireFeedSizeKb') if maxNewswireFeedSizeKb: - if maxNewswireFeedSizeKb.isdigit(): - args.maxNewswireFeedSizeKb = maxNewswireFeedSizeKb + args.maxNewswireFeedSizeKb = int(maxNewswireFeedSizeKb) + +maxMirroredArticles = \ + getConfigParam(baseDir, 'maxMirroredArticles') +if maxMirroredArticles is not None: + args.maxMirroredArticles = int(maxMirroredArticles) + +maxNewsPosts = \ + getConfigParam(baseDir, 'maxNewsPosts') +if maxNewsPosts is not None: + args.maxNewsPosts = int(maxNewsPosts) + +maxFollowers = \ + getConfigParam(baseDir, 'maxFollowers') +if maxFollowers is not None: + args.maxFollowers = int(maxFollowers) + +allowNewsFollowers = \ + getConfigParam(baseDir, 'allowNewsFollowers') +if allowNewsFollowers is not None: + args.allowNewsFollowers = bool(allowNewsFollowers) + +showPublishAsIcon = \ + getConfigParam(baseDir, 'showPublishAsIcon') +if showPublishAsIcon is not None: + args.showPublishAsIcon = bool(showPublishAsIcon) + +iconsAsButtons = \ + getConfigParam(baseDir, 'iconsAsButtons') +if iconsAsButtons is not None: + args.iconsAsButtons = bool(iconsAsButtons) + +rssIconAtTop = \ + getConfigParam(baseDir, 'rssIconAtTop') +if rssIconAtTop is not None: + args.rssIconAtTop = bool(rssIconAtTop) + +publishButtonAtTop = \ + getConfigParam(baseDir, 'publishButtonAtTop') +if publishButtonAtTop is not None: + args.publishButtonAtTop = bool(publishButtonAtTop) + +fullWidthTimelineButtonHeader = \ + getConfigParam(baseDir, 'fullWidthTimelineButtonHeader') +if fullWidthTimelineButtonHeader is not None: + args.fullWidthTimelineButtonHeader = bool(fullWidthTimelineButtonHeader) YTDomain = getConfigParam(baseDir, 'youtubedomain') if YTDomain: @@ -1960,7 +2053,16 @@ if setTheme(baseDir, themeName, domain): print('Theme set to ' + themeName) if __name__ == "__main__": - runDaemon(args.maxNewswireFeedSizeKb, + runDaemon(args.publishButtonAtTop, + args.rssIconAtTop, + args.iconsAsButtons, + args.fullWidthTimelineButtonHeader, + args.showPublishAsIcon, + args.maxFollowers, + args.allowNewsFollowers, + args.maxNewsPosts, + args.maxMirroredArticles, + args.maxNewswireFeedSizeKb, args.maxNewswirePostsPerSource, args.dateonly, args.votingtime, diff --git a/follow.py b/follow.py index 1616c0fd5..84c7823da 100644 --- a/follow.py +++ b/follow.py @@ -8,6 +8,7 @@ __status__ = "Production" from pprint import pprint import os +from utils import isSystemAccount from utils import getFollowersList from utils import validNickname from utils import domainPermitted @@ -28,9 +29,14 @@ from session import postJson def preApprovedFollower(baseDir: str, nickname: str, domain: str, - approveHandle: str) -> bool: + approveHandle: str, + allowNewsFollowers: bool) -> bool: """Is the given handle an already manually approved follower? """ + # optionally allow the news account to be followed + if nickname == 'news' and allowNewsFollowers: + return True + handle = nickname + '@' + domain accountDir = baseDir + '/accounts/' + handle approvedFilename = accountDir + '/approved.txt' @@ -149,7 +155,26 @@ def isFollowerOfPerson(baseDir: str, nickname: str, domain: str, if not os.path.isfile(followersFile): return False handle = followerNickname + '@' + followerDomain - return handle in open(followersFile).read() + + alreadyFollowing = False + + followersStr = '' + with open(followersFile, 'r') as fpFollowers: + followersStr = fpFollowers.read() + + if handle in followersStr: + alreadyFollowing = True + elif '://' + followerDomain + \ + '/profile/' + followerNickname in followersStr: + alreadyFollowing = True + elif '://' + followerDomain + \ + '/channel/' + followerNickname in followersStr: + alreadyFollowing = True + elif '://' + followerDomain + \ + '/accounts/' + followerNickname in followersStr: + alreadyFollowing = True + + return alreadyFollowing def unfollowPerson(baseDir: str, nickname: str, domain: str, @@ -247,13 +272,19 @@ def getNoOfFollows(baseDir: str, nickname: str, domain: str, with open(filename, "r") as f: lines = f.readlines() for line in lines: - if '#' not in line: - if '@' in line and \ - '.' in line and \ - not line.startswith('http'): - ctr += 1 - elif line.startswith('http') and '/users/' in line: - ctr += 1 + if '#' in line: + continue + if '@' in line and \ + '.' in line and \ + not line.startswith('http'): + ctr += 1 + elif ((line.startswith('http') or + line.startswith('dat')) and + ('/users/' in line or + '/profile/' in line or + '/accounts/' in line or + '/channel/' in line)): + ctr += 1 return ctr @@ -269,7 +300,8 @@ def getFollowingFeed(baseDir: str, domain: str, port: int, path: str, httpPrefix: str, authenticated: bool, followsPerPage=12, followFile='following') -> {}: - """Returns the following and followers feeds from GET requests + """Returns the following and followers feeds from GET requests. + This accesses the following.txt or followers.txt and builds a collection. """ # Show a small number of follows to non-authenticated viewers if not authenticated: @@ -360,6 +392,7 @@ def getFollowingFeed(baseDir: str, domain: str, port: int, path: str, for line in lines: if '#' not in line: if '@' in line and not line.startswith('http'): + # nickname@domain pageCtr += 1 totalCtr += 1 if currPage == pageNumber: @@ -371,7 +404,12 @@ def getFollowingFeed(baseDir: str, domain: str, port: int, path: str, line2.split('@')[0] following['orderedItems'].append(url) elif ((line.startswith('http') or - line.startswith('dat')) and '/users/' in line): + line.startswith('dat')) and + ('/users/' in line or + '/profile/' in line or + '/accounts/' in line or + '/channel/' in line)): + # https://domain/users/nickname pageCtr += 1 totalCtr += 1 if currPage == pageNumber: @@ -394,12 +432,13 @@ def getFollowingFeed(baseDir: str, domain: str, port: int, path: str, def followApprovalRequired(baseDir: str, nicknameToFollow: str, domainToFollow: str, debug: bool, - followRequestHandle: str) -> bool: + followRequestHandle: str, + allowNewsFollowers: bool) -> bool: """ Returns the policy for follower approvals """ # has this handle already been manually approved? if preApprovedFollower(baseDir, nicknameToFollow, domainToFollow, - followRequestHandle): + followRequestHandle, allowNewsFollowers): return False manuallyApproveFollows = False @@ -453,7 +492,7 @@ def storeFollowRequest(baseDir: str, nicknameToFollow: str, domainToFollow: str, port: int, nickname: str, domain: str, fromPort: int, followJson: {}, - debug: bool) -> bool: + debug: bool, personUrl: str) -> bool: """Stores the follow request for later use """ accountsDir = baseDir + '/accounts/' + \ @@ -462,14 +501,31 @@ def storeFollowRequest(baseDir: str, return False approveHandle = nickname + '@' + domain + domainFull = domain if fromPort: if fromPort != 80 and fromPort != 443: if ':' not in domain: approveHandle = nickname + '@' + domain + ':' + str(fromPort) + domainFull = domain + ':' + str(fromPort) followersFilename = accountsDir + '/followers.txt' if os.path.isfile(followersFilename): - if approveHandle in open(followersFilename).read(): + alreadyFollowing = False + + followersStr = '' + with open(followersFilename, 'r') as fpFollowers: + followersStr = fpFollowers.read() + + if approveHandle in followersStr: + alreadyFollowing = True + elif '://' + domainFull + '/profile/' + nickname in followersStr: + alreadyFollowing = True + elif '://' + domainFull + '/channel/' + nickname in followersStr: + alreadyFollowing = True + elif '://' + domainFull + '/accounts/' + nickname in followersStr: + alreadyFollowing = True + + if alreadyFollowing: if debug: print('DEBUG: ' + nicknameToFollow + '@' + domainToFollow + @@ -488,17 +544,23 @@ def storeFollowRequest(baseDir: str, # add to a file which contains a list of requests approveFollowsFilename = accountsDir + '/followrequests.txt' + + # store either nick@domain or the full person/actor url + approveHandleStored = approveHandle + if '/users/' not in personUrl: + approveHandleStored = personUrl + if os.path.isfile(approveFollowsFilename): if approveHandle not in open(approveFollowsFilename).read(): with open(approveFollowsFilename, 'a+') as fp: - fp.write(approveHandle + '\n') + fp.write(approveHandleStored + '\n') else: if debug: - print('DEBUG: ' + approveHandle + + print('DEBUG: ' + approveHandleStored + ' is already awaiting approval') else: with open(approveFollowsFilename, "w+") as fp: - fp.write(approveHandle + '\n') + fp.write(approveHandleStored + '\n') # store the follow request in its own directory # We don't rely upon the inbox because items in there could expire @@ -513,7 +575,9 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str, port: int, sendThreads: [], postLog: [], cachedWebfingers: {}, personCache: {}, messageJson: {}, federationList: [], - debug: bool, projectVersion: str) -> bool: + debug: bool, projectVersion: str, + allowNewsFollowers: bool, + maxFollowers: int) -> bool: """Receives a follow request within the POST section of HTTPServer """ if not messageJson['type'].startswith('Follow'): @@ -528,7 +592,7 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str, '/channel/' not in messageJson['actor'] and \ '/profile/' not in messageJson['actor']: if debug: - print('DEBUG: "users" or "profile" missing from actor') + print('DEBUG: users/profile/accounts/channel missing from actor') return False domain, tempPort = getDomainFromActor(messageJson['actor']) fromPort = port @@ -556,7 +620,8 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str, '/channel/' not in messageJson['object'] and \ '/profile/' not in messageJson['object']: if debug: - print('DEBUG: "users" or "profile" not found within object') + print('DEBUG: users/profile/channel/accounts ' + + 'not found within object') return False domainToFollow, tempPort = getDomainFromActor(messageJson['object']) if not domainPermitted(domainToFollow, federationList): @@ -574,10 +639,19 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str, print('DEBUG: follow request does not contain a ' + 'nickname for the account followed') return True - if nicknameToFollow == 'news' or nicknameToFollow == 'inbox': - if debug: - print('DEBUG: Cannot follow the news or inbox accounts') - return True + if isSystemAccount(nicknameToFollow): + if not (nicknameToFollow == 'news' and allowNewsFollowers): + if debug: + print('DEBUG: Cannot follow system account - ' + + nicknameToFollow) + return True + if maxFollowers > 0: + if getNoOfFollowers(baseDir, + nicknameToFollow, domainToFollow, + True) > maxFollowers: + print('WARN: ' + nicknameToFollow + + ' has reached their maximum number of followers') + return True handleToFollow = nicknameToFollow + '@' + domainToFollow if domainToFollow == domain: if not os.path.isdir(baseDir + '/accounts/' + handleToFollow): @@ -598,7 +672,8 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str, # what is the followers policy? approveHandle = nickname + '@' + domainFull if followApprovalRequired(baseDir, nicknameToFollow, - domainToFollow, debug, approveHandle): + domainToFollow, debug, approveHandle, + allowNewsFollowers): print('Follow approval is required') if domain.endswith('.onion'): if noOfFollowRequests(baseDir, @@ -626,7 +701,7 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str, return storeFollowRequest(baseDir, nicknameToFollow, domainToFollow, port, nickname, domain, fromPort, - messageJson, debug) + messageJson, debug, messageJson['actor']) else: print('Follow request does not require approval') # update the followers @@ -635,6 +710,12 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str, followersFilename = \ baseDir + '/accounts/' + \ nicknameToFollow + '@' + domainToFollow + '/followers.txt' + + # for actors which don't follow the mastodon + # /users/ path convention store the full actor + if '/users/' not in messageJson['actor']: + approveHandle = messageJson['actor'] + print('Updating followers file: ' + followersFilename + ' adding ' + approveHandle) if os.path.isfile(followersFilename): @@ -788,7 +869,7 @@ def sendFollowRequest(session, baseDir: str, clientToServer: bool, federationList: [], sendThreads: [], postLog: [], cachedWebfingers: {}, personCache: {}, debug: bool, - projectVersion: str) -> {}: + projectVersion: str, allowNewsFollowers: bool) -> {}: """Gets the json object for sending a follow request """ if not domainPermitted(followDomain, federationList): @@ -830,7 +911,8 @@ def sendFollowRequest(session, baseDir: str, 'object': followedId } - if followApprovalRequired(baseDir, nickname, domain, debug, followHandle): + if followApprovalRequired(baseDir, nickname, domain, debug, + followHandle, allowNewsFollowers): # Remove any follow requests rejected for the account being followed. # It's assumed that if you are following someone then you are # ok with them following back. If this isn't the case then a rejected diff --git a/fonts/LICENSES b/fonts/LICENSES index 3213688bd..80bd7eedd 100644 --- a/fonts/LICENSES +++ b/fonts/LICENSES @@ -13,6 +13,7 @@ Judges is under GPL. See https://webfonts.ffonts.net/Judges.font LinBiolinum is under GPLv2. See https://www.1001fonts.com/linux-biolinum-font.html LcdSolid is public domain. See https://www.fontspace.com/lcd-solid-font-f11346 MarginaliaRegular is public domain. See https://www.fontspace.com/marginalia-font-f32466 +Nimbus Sans L is GPL. See https://www.fontsquirrel.com/fonts/nimbus-sans-l Octavius is created by Jack Oatley and described as "100% free to use, though credit is appreciated" https://www.dafont.com/octavius.font RailModel is GPL. See https://www.fontspace.com/rail-model-font-f10741 Solidaric by Bob Mottram is under AGPL diff --git a/fonts/NimbusSanL-italic.otf b/fonts/NimbusSanL-italic.otf new file mode 100644 index 000000000..eca167231 Binary files /dev/null and b/fonts/NimbusSanL-italic.otf differ diff --git a/fonts/NimbusSanL.otf b/fonts/NimbusSanL.otf new file mode 100644 index 000000000..9cedff5ea Binary files /dev/null and b/fonts/NimbusSanL.otf differ diff --git a/gemini/EN/install.gmi b/gemini/EN/install.gmi index 7f8f7f68b..06000e78b 100644 --- a/gemini/EN/install.gmi +++ b/gemini/EN/install.gmi @@ -4,7 +4,7 @@ You will need python version 3.7 or later. On a Debian based system: - sudo apt install -y tor python3-socks imagemagick python3-numpy python3-setuptools python3-crypto python3-pycryptodome python3-dateutil python3-pil.imagetk python3-idna python3-requests python3-flake8 python3-django-timezone-field python3-pyqrcode python3-png python3-bandit libimage-exiftool-perl certbot nginx + sudo apt install -y tor python3-socks imagemagick python3-numpy python3-setuptools python3-crypto python3-pycryptodome python3-dateutil python3-pil.imagetk python3-idna python3-requests python3-flake8 python3-django-timezone-field python3-pyqrcode python3-png python3-bandit libimage-exiftool-perl certbot nginx wget The following instructions install Epicyon to the /opt directory. It's not essential that it be installed there, and it could be in any other preferred directory. @@ -19,6 +19,12 @@ Create a user for the server to run as: adduser --system --home=/opt/epicyon --group epicyon chown -R epicyon:epicyon /opt/epicyon +Link news mirrors: + + mkdir /var/www/YOUR_DOMAIN + mkdir -p /opt/epicyon/accounts/newsmirror + ln -s /opt/epicyon/accounts/newsmirror /var/www/YOUR_DOMAIN/newsmirror + Create a daemon: nano /etc/systemd/system/epicyon.service @@ -104,6 +110,11 @@ And paste the following: index index.html; + location /newsmirror { + root /var/www/YOUR_DOMAIN; + try_files $uri =404; + } + location / { proxy_http_version 1.1; client_max_body_size 31M; diff --git a/hashtagrules.txt b/hashtagrules.txt new file mode 100644 index 000000000..4349a2d02 --- /dev/null +++ b/hashtagrules.txt @@ -0,0 +1,45 @@ +Epicyon news rules processing +============================= + +As news arrives via RSS or Atom feeds it can be processed to add or remove hashtags, in accordance to some rules which you can define. + +On the newswire edit screen, available to moderators, you can define the news processing rules. There is one rule per line. + +Syntax +------ + +if [conditions] then [action] + +Logical Operators +----------------- + +The following operators are available: + + not, and, or, xor, from, contains + +Examples +-------- + +A simple example is: + + if moderated and not #oxfordimc then block + +For moderated feeds this will only allow items through if they have the #oxfordimc hashtag. + +If you want to add hashtags an example is: + + if contains "garden" or contains "lawn" then add #gardening + +So if incoming news contains the word "garden" either in its title or description then it will automatically be assigned the hashtag #gardening. You can also add hashtags based upon other hashtags. + + if #garden or #lawn then add #gardening + +You can also remove hashtags. + + if #garden or #lawn then remove #gardening + +Which will remove #gardening if it exists as a hashtag within the news post. + +You can add tags based upon the RSS link, such as: + + if from "mycatsite.com" then add #cats diff --git a/img/banner_indymedia.png b/img/banner_indymediaclassic.png similarity index 100% rename from img/banner_indymedia.png rename to img/banner_indymediaclassic.png diff --git a/img/banner_indymediamodern.png b/img/banner_indymediamodern.png new file mode 100644 index 000000000..b7361d6c5 Binary files /dev/null and b/img/banner_indymediamodern.png differ diff --git a/img/banner_night.png b/img/banner_night.png index 3391f4b48..299d0e673 100644 Binary files a/img/banner_night.png and b/img/banner_night.png differ diff --git a/img/banner_zen.png b/img/banner_zen.png index 301886970..b8ab39fd1 100644 Binary files a/img/banner_zen.png and b/img/banner_zen.png differ diff --git a/img/icons/blue/logout.png b/img/icons/blue/logout.png new file mode 100644 index 000000000..6c52946df Binary files /dev/null and b/img/icons/blue/logout.png differ diff --git a/img/icons/blue/publish.png b/img/icons/blue/publish.png new file mode 100644 index 000000000..0fe148eea Binary files /dev/null and b/img/icons/blue/publish.png differ diff --git a/img/icons/blue/scope_blog.png b/img/icons/blue/scope_blog.png index 59713257c..0fe148eea 100644 Binary files a/img/icons/blue/scope_blog.png and b/img/icons/blue/scope_blog.png differ diff --git a/img/icons/hacker/logout.png b/img/icons/hacker/logout.png new file mode 100644 index 000000000..8ae5bee69 Binary files /dev/null and b/img/icons/hacker/logout.png differ diff --git a/img/icons/hacker/publish.png b/img/icons/hacker/publish.png new file mode 100644 index 000000000..6cd724fd8 Binary files /dev/null and b/img/icons/hacker/publish.png differ diff --git a/img/icons/hacker/scope_blog.png b/img/icons/hacker/scope_blog.png index b605e6786..6cd724fd8 100644 Binary files a/img/icons/hacker/scope_blog.png and b/img/icons/hacker/scope_blog.png differ diff --git a/img/icons/henge/logout.png b/img/icons/henge/logout.png new file mode 100644 index 000000000..ee37bde72 Binary files /dev/null and b/img/icons/henge/logout.png differ diff --git a/img/icons/henge/publish.png b/img/icons/henge/publish.png new file mode 100644 index 000000000..739ab9fa9 Binary files /dev/null and b/img/icons/henge/publish.png differ diff --git a/img/icons/henge/scope_blog.png b/img/icons/henge/scope_blog.png index c56c0096f..739ab9fa9 100644 Binary files a/img/icons/henge/scope_blog.png and b/img/icons/henge/scope_blog.png differ diff --git a/img/icons/indymedia/scope_blog.png b/img/icons/indymedia/scope_blog.png deleted file mode 100644 index c1a1b2596..000000000 Binary files a/img/icons/indymedia/scope_blog.png and /dev/null differ diff --git a/img/icons/indymedia/add.png b/img/icons/indymediaclassic/add.png similarity index 100% rename from img/icons/indymedia/add.png rename to img/icons/indymediaclassic/add.png diff --git a/img/icons/indymedia/avatar_news.png b/img/icons/indymediaclassic/avatar_news.png similarity index 100% rename from img/icons/indymedia/avatar_news.png rename to img/icons/indymediaclassic/avatar_news.png diff --git a/img/icons/indymedia/bookmark.png b/img/icons/indymediaclassic/bookmark.png similarity index 100% rename from img/icons/indymedia/bookmark.png rename to img/icons/indymediaclassic/bookmark.png diff --git a/img/icons/indymedia/bookmark_inactive.png b/img/icons/indymediaclassic/bookmark_inactive.png similarity index 100% rename from img/icons/indymedia/bookmark_inactive.png rename to img/icons/indymediaclassic/bookmark_inactive.png diff --git a/img/icons/indymedia/calendar.png b/img/icons/indymediaclassic/calendar.png similarity index 100% rename from img/icons/indymedia/calendar.png rename to img/icons/indymediaclassic/calendar.png diff --git a/img/icons/indymedia/calendar_notify.png b/img/icons/indymediaclassic/calendar_notify.png similarity index 100% rename from img/icons/indymedia/calendar_notify.png rename to img/icons/indymediaclassic/calendar_notify.png diff --git a/img/icons/indymedia/delete.png b/img/icons/indymediaclassic/delete.png similarity index 100% rename from img/icons/indymedia/delete.png rename to img/icons/indymediaclassic/delete.png diff --git a/img/icons/indymedia/dm.png b/img/icons/indymediaclassic/dm.png similarity index 100% rename from img/icons/indymedia/dm.png rename to img/icons/indymediaclassic/dm.png diff --git a/img/icons/indymedia/download.png b/img/icons/indymediaclassic/download.png similarity index 100% rename from img/icons/indymedia/download.png rename to img/icons/indymediaclassic/download.png diff --git a/img/icons/indymedia/edit.png b/img/icons/indymediaclassic/edit.png similarity index 100% rename from img/icons/indymedia/edit.png rename to img/icons/indymediaclassic/edit.png diff --git a/img/icons/indymedia/edit_notify.png b/img/icons/indymediaclassic/edit_notify.png similarity index 100% rename from img/icons/indymedia/edit_notify.png rename to img/icons/indymediaclassic/edit_notify.png diff --git a/img/icons/indymedia/like.png b/img/icons/indymediaclassic/like.png similarity index 100% rename from img/icons/indymedia/like.png rename to img/icons/indymediaclassic/like.png diff --git a/img/icons/indymedia/like_inactive.png b/img/icons/indymediaclassic/like_inactive.png similarity index 100% rename from img/icons/indymedia/like_inactive.png rename to img/icons/indymediaclassic/like_inactive.png diff --git a/img/icons/indymedia/links.png b/img/icons/indymediaclassic/links.png similarity index 100% rename from img/icons/indymedia/links.png rename to img/icons/indymediaclassic/links.png diff --git a/img/icons/indymedia/logorss.png b/img/icons/indymediaclassic/logorss.png similarity index 100% rename from img/icons/indymedia/logorss.png rename to img/icons/indymediaclassic/logorss.png diff --git a/img/icons/indymediaclassic/logout.png b/img/icons/indymediaclassic/logout.png new file mode 100644 index 000000000..968fb671e Binary files /dev/null and b/img/icons/indymediaclassic/logout.png differ diff --git a/img/icons/indymedia/mute.png b/img/icons/indymediaclassic/mute.png similarity index 100% rename from img/icons/indymedia/mute.png rename to img/icons/indymediaclassic/mute.png diff --git a/img/icons/indymedia/new.png b/img/icons/indymediaclassic/new.png similarity index 100% rename from img/icons/indymedia/new.png rename to img/icons/indymediaclassic/new.png diff --git a/img/icons/indymedia/newpost.png b/img/icons/indymediaclassic/newpost.png similarity index 100% rename from img/icons/indymedia/newpost.png rename to img/icons/indymediaclassic/newpost.png diff --git a/img/icons/indymedia/newswire.png b/img/icons/indymediaclassic/newswire.png similarity index 100% rename from img/icons/indymedia/newswire.png rename to img/icons/indymediaclassic/newswire.png diff --git a/img/icons/indymedia/pagedown.png b/img/icons/indymediaclassic/pagedown.png similarity index 100% rename from img/icons/indymedia/pagedown.png rename to img/icons/indymediaclassic/pagedown.png diff --git a/img/icons/indymedia/pageup.png b/img/icons/indymediaclassic/pageup.png similarity index 100% rename from img/icons/indymedia/pageup.png rename to img/icons/indymediaclassic/pageup.png diff --git a/img/icons/indymedia/person.png b/img/icons/indymediaclassic/person.png similarity index 100% rename from img/icons/indymedia/person.png rename to img/icons/indymediaclassic/person.png diff --git a/img/icons/indymedia/prev.png b/img/icons/indymediaclassic/prev.png similarity index 100% rename from img/icons/indymedia/prev.png rename to img/icons/indymediaclassic/prev.png diff --git a/img/icons/indymediaclassic/publish.png b/img/icons/indymediaclassic/publish.png new file mode 100644 index 000000000..17af02e86 Binary files /dev/null and b/img/icons/indymediaclassic/publish.png differ diff --git a/img/icons/indymedia/qrcode.png b/img/icons/indymediaclassic/qrcode.png similarity index 100% rename from img/icons/indymedia/qrcode.png rename to img/icons/indymediaclassic/qrcode.png diff --git a/img/icons/indymedia/repeat.png b/img/icons/indymediaclassic/repeat.png similarity index 100% rename from img/icons/indymedia/repeat.png rename to img/icons/indymediaclassic/repeat.png diff --git a/img/icons/indymedia/repeat_inactive.png b/img/icons/indymediaclassic/repeat_inactive.png similarity index 100% rename from img/icons/indymedia/repeat_inactive.png rename to img/icons/indymediaclassic/repeat_inactive.png diff --git a/img/icons/indymedia/reply.png b/img/icons/indymediaclassic/reply.png similarity index 100% rename from img/icons/indymedia/reply.png rename to img/icons/indymediaclassic/reply.png diff --git a/img/icons/indymedia/rss3.png b/img/icons/indymediaclassic/rss3.png similarity index 100% rename from img/icons/indymedia/rss3.png rename to img/icons/indymediaclassic/rss3.png diff --git a/img/icons/indymediaclassic/scope_blog.png b/img/icons/indymediaclassic/scope_blog.png new file mode 100644 index 000000000..17af02e86 Binary files /dev/null and b/img/icons/indymediaclassic/scope_blog.png differ diff --git a/img/icons/indymedia/scope_dm.png b/img/icons/indymediaclassic/scope_dm.png similarity index 100% rename from img/icons/indymedia/scope_dm.png rename to img/icons/indymediaclassic/scope_dm.png diff --git a/img/icons/indymedia/scope_event.png b/img/icons/indymediaclassic/scope_event.png similarity index 100% rename from img/icons/indymedia/scope_event.png rename to img/icons/indymediaclassic/scope_event.png diff --git a/img/icons/indymedia/scope_followers.png b/img/icons/indymediaclassic/scope_followers.png similarity index 100% rename from img/icons/indymedia/scope_followers.png rename to img/icons/indymediaclassic/scope_followers.png diff --git a/img/icons/indymedia/scope_public.png b/img/icons/indymediaclassic/scope_public.png similarity index 100% rename from img/icons/indymedia/scope_public.png rename to img/icons/indymediaclassic/scope_public.png diff --git a/img/icons/indymedia/scope_question.png b/img/icons/indymediaclassic/scope_question.png similarity index 100% rename from img/icons/indymedia/scope_question.png rename to img/icons/indymediaclassic/scope_question.png diff --git a/img/icons/indymedia/scope_reminder.png b/img/icons/indymediaclassic/scope_reminder.png similarity index 100% rename from img/icons/indymedia/scope_reminder.png rename to img/icons/indymediaclassic/scope_reminder.png diff --git a/img/icons/indymedia/scope_report.png b/img/icons/indymediaclassic/scope_report.png similarity index 100% rename from img/icons/indymedia/scope_report.png rename to img/icons/indymediaclassic/scope_report.png diff --git a/img/icons/indymedia/scope_share.png b/img/icons/indymediaclassic/scope_share.png similarity index 100% rename from img/icons/indymedia/scope_share.png rename to img/icons/indymediaclassic/scope_share.png diff --git a/img/icons/indymedia/scope_unlisted.png b/img/icons/indymediaclassic/scope_unlisted.png similarity index 100% rename from img/icons/indymedia/scope_unlisted.png rename to img/icons/indymediaclassic/scope_unlisted.png diff --git a/img/icons/indymedia/search.png b/img/icons/indymediaclassic/search.png similarity index 100% rename from img/icons/indymedia/search.png rename to img/icons/indymediaclassic/search.png diff --git a/img/icons/indymedia/showhide.png b/img/icons/indymediaclassic/showhide.png similarity index 100% rename from img/icons/indymedia/showhide.png rename to img/icons/indymediaclassic/showhide.png diff --git a/img/icons/indymedia/unmute.png b/img/icons/indymediaclassic/unmute.png similarity index 100% rename from img/icons/indymedia/unmute.png rename to img/icons/indymediaclassic/unmute.png diff --git a/img/icons/indymedia/vote.png b/img/icons/indymediaclassic/vote.png similarity index 100% rename from img/icons/indymedia/vote.png rename to img/icons/indymediaclassic/vote.png diff --git a/img/icons/indymediamodern/add.png b/img/icons/indymediamodern/add.png new file mode 100644 index 000000000..2825bc6d4 Binary files /dev/null and b/img/icons/indymediamodern/add.png differ diff --git a/img/icons/indymediamodern/avatar_news.png b/img/icons/indymediamodern/avatar_news.png new file mode 100644 index 000000000..87e0996ef Binary files /dev/null and b/img/icons/indymediamodern/avatar_news.png differ diff --git a/img/icons/indymediamodern/bookmark.png b/img/icons/indymediamodern/bookmark.png new file mode 100644 index 000000000..0f790bf07 Binary files /dev/null and b/img/icons/indymediamodern/bookmark.png differ diff --git a/img/icons/indymediamodern/bookmark_inactive.png b/img/icons/indymediamodern/bookmark_inactive.png new file mode 100644 index 000000000..49aeff58a Binary files /dev/null and b/img/icons/indymediamodern/bookmark_inactive.png differ diff --git a/img/icons/indymediamodern/calendar.png b/img/icons/indymediamodern/calendar.png new file mode 100644 index 000000000..fd51c4eb5 Binary files /dev/null and b/img/icons/indymediamodern/calendar.png differ diff --git a/img/icons/indymediamodern/calendar_notify.png b/img/icons/indymediamodern/calendar_notify.png new file mode 100644 index 000000000..c364c3109 Binary files /dev/null and b/img/icons/indymediamodern/calendar_notify.png differ diff --git a/img/icons/indymediamodern/delete.png b/img/icons/indymediamodern/delete.png new file mode 100644 index 000000000..80c16b1d2 Binary files /dev/null and b/img/icons/indymediamodern/delete.png differ diff --git a/img/icons/indymediamodern/dm.png b/img/icons/indymediamodern/dm.png new file mode 100644 index 000000000..92de16880 Binary files /dev/null and b/img/icons/indymediamodern/dm.png differ diff --git a/img/icons/indymediamodern/download.png b/img/icons/indymediamodern/download.png new file mode 100644 index 000000000..e915ed305 Binary files /dev/null and b/img/icons/indymediamodern/download.png differ diff --git a/img/icons/indymediamodern/edit.png b/img/icons/indymediamodern/edit.png new file mode 100644 index 000000000..fc78a5e83 Binary files /dev/null and b/img/icons/indymediamodern/edit.png differ diff --git a/img/icons/indymediamodern/edit_notify.png b/img/icons/indymediamodern/edit_notify.png new file mode 100644 index 000000000..a26dd4f04 Binary files /dev/null and b/img/icons/indymediamodern/edit_notify.png differ diff --git a/img/icons/indymediamodern/like.png b/img/icons/indymediamodern/like.png new file mode 100644 index 000000000..1f7f20156 Binary files /dev/null and b/img/icons/indymediamodern/like.png differ diff --git a/img/icons/indymediamodern/like_inactive.png b/img/icons/indymediamodern/like_inactive.png new file mode 100644 index 000000000..3a019c2bc Binary files /dev/null and b/img/icons/indymediamodern/like_inactive.png differ diff --git a/img/icons/indymediamodern/links.png b/img/icons/indymediamodern/links.png new file mode 100644 index 000000000..1050303be Binary files /dev/null and b/img/icons/indymediamodern/links.png differ diff --git a/img/icons/indymediamodern/logorss.png b/img/icons/indymediamodern/logorss.png new file mode 100644 index 000000000..df47fe87f Binary files /dev/null and b/img/icons/indymediamodern/logorss.png differ diff --git a/img/icons/indymediamodern/logout.png b/img/icons/indymediamodern/logout.png new file mode 100644 index 000000000..1fda9af43 Binary files /dev/null and b/img/icons/indymediamodern/logout.png differ diff --git a/img/icons/indymediamodern/mute.png b/img/icons/indymediamodern/mute.png new file mode 100644 index 000000000..7afde3692 Binary files /dev/null and b/img/icons/indymediamodern/mute.png differ diff --git a/img/icons/indymediamodern/new.png b/img/icons/indymediamodern/new.png new file mode 100644 index 000000000..c6c834eb8 Binary files /dev/null and b/img/icons/indymediamodern/new.png differ diff --git a/img/icons/indymediamodern/newpost.png b/img/icons/indymediamodern/newpost.png new file mode 100644 index 000000000..badf188e1 Binary files /dev/null and b/img/icons/indymediamodern/newpost.png differ diff --git a/img/icons/indymediamodern/newswire.png b/img/icons/indymediamodern/newswire.png new file mode 100644 index 000000000..ccc771bb3 Binary files /dev/null and b/img/icons/indymediamodern/newswire.png differ diff --git a/img/icons/indymediamodern/pagedown.png b/img/icons/indymediamodern/pagedown.png new file mode 100644 index 000000000..6c3a35c5f Binary files /dev/null and b/img/icons/indymediamodern/pagedown.png differ diff --git a/img/icons/indymediamodern/pageup.png b/img/icons/indymediamodern/pageup.png new file mode 100644 index 000000000..172b49295 Binary files /dev/null and b/img/icons/indymediamodern/pageup.png differ diff --git a/img/icons/indymediamodern/person.png b/img/icons/indymediamodern/person.png new file mode 100644 index 000000000..0241ae867 Binary files /dev/null and b/img/icons/indymediamodern/person.png differ diff --git a/img/icons/indymediamodern/prev.png b/img/icons/indymediamodern/prev.png new file mode 100644 index 000000000..8eac85532 Binary files /dev/null and b/img/icons/indymediamodern/prev.png differ diff --git a/img/icons/indymediamodern/publish.png b/img/icons/indymediamodern/publish.png new file mode 100644 index 000000000..2917ae876 Binary files /dev/null and b/img/icons/indymediamodern/publish.png differ diff --git a/img/icons/indymediamodern/qrcode.png b/img/icons/indymediamodern/qrcode.png new file mode 100644 index 000000000..933a2671c Binary files /dev/null and b/img/icons/indymediamodern/qrcode.png differ diff --git a/img/icons/indymediamodern/repeat.png b/img/icons/indymediamodern/repeat.png new file mode 100644 index 000000000..485822a9c Binary files /dev/null and b/img/icons/indymediamodern/repeat.png differ diff --git a/img/icons/indymediamodern/repeat_inactive.png b/img/icons/indymediamodern/repeat_inactive.png new file mode 100644 index 000000000..03e9ab250 Binary files /dev/null and b/img/icons/indymediamodern/repeat_inactive.png differ diff --git a/img/icons/indymediamodern/reply.png b/img/icons/indymediamodern/reply.png new file mode 100644 index 000000000..c4f0f7da6 Binary files /dev/null and b/img/icons/indymediamodern/reply.png differ diff --git a/img/icons/indymediamodern/rss3.png b/img/icons/indymediamodern/rss3.png new file mode 100644 index 000000000..83521cd1b Binary files /dev/null and b/img/icons/indymediamodern/rss3.png differ diff --git a/img/icons/indymediamodern/scope_blog.png b/img/icons/indymediamodern/scope_blog.png new file mode 100644 index 000000000..15d9fbc06 Binary files /dev/null and b/img/icons/indymediamodern/scope_blog.png differ diff --git a/img/icons/indymediamodern/scope_dm.png b/img/icons/indymediamodern/scope_dm.png new file mode 100644 index 000000000..19cbb72c8 Binary files /dev/null and b/img/icons/indymediamodern/scope_dm.png differ diff --git a/img/icons/indymediamodern/scope_event.png b/img/icons/indymediamodern/scope_event.png new file mode 100644 index 000000000..2ad0a0ce4 Binary files /dev/null and b/img/icons/indymediamodern/scope_event.png differ diff --git a/img/icons/indymediamodern/scope_followers.png b/img/icons/indymediamodern/scope_followers.png new file mode 100644 index 000000000..c51236bdf Binary files /dev/null and b/img/icons/indymediamodern/scope_followers.png differ diff --git a/img/icons/indymediamodern/scope_public.png b/img/icons/indymediamodern/scope_public.png new file mode 100644 index 000000000..2b9de67e2 Binary files /dev/null and b/img/icons/indymediamodern/scope_public.png differ diff --git a/img/icons/indymediamodern/scope_question.png b/img/icons/indymediamodern/scope_question.png new file mode 100644 index 000000000..b17c775af Binary files /dev/null and b/img/icons/indymediamodern/scope_question.png differ diff --git a/img/icons/indymediamodern/scope_reminder.png b/img/icons/indymediamodern/scope_reminder.png new file mode 100644 index 000000000..785f673b4 Binary files /dev/null and b/img/icons/indymediamodern/scope_reminder.png differ diff --git a/img/icons/indymediamodern/scope_report.png b/img/icons/indymediamodern/scope_report.png new file mode 100644 index 000000000..910f7046d Binary files /dev/null and b/img/icons/indymediamodern/scope_report.png differ diff --git a/img/icons/indymediamodern/scope_share.png b/img/icons/indymediamodern/scope_share.png new file mode 100644 index 000000000..b0cbe4216 Binary files /dev/null and b/img/icons/indymediamodern/scope_share.png differ diff --git a/img/icons/indymediamodern/scope_unlisted.png b/img/icons/indymediamodern/scope_unlisted.png new file mode 100644 index 000000000..aa315e363 Binary files /dev/null and b/img/icons/indymediamodern/scope_unlisted.png differ diff --git a/img/icons/indymediamodern/search.png b/img/icons/indymediamodern/search.png new file mode 100644 index 000000000..98504a8f5 Binary files /dev/null and b/img/icons/indymediamodern/search.png differ diff --git a/img/icons/indymediamodern/showhide.png b/img/icons/indymediamodern/showhide.png new file mode 100644 index 000000000..3b1376675 Binary files /dev/null and b/img/icons/indymediamodern/showhide.png differ diff --git a/img/icons/indymediamodern/unmute.png b/img/icons/indymediamodern/unmute.png new file mode 100644 index 000000000..eb9af71d2 Binary files /dev/null and b/img/icons/indymediamodern/unmute.png differ diff --git a/img/icons/indymediamodern/vote.png b/img/icons/indymediamodern/vote.png new file mode 100644 index 000000000..c810092b0 Binary files /dev/null and b/img/icons/indymediamodern/vote.png differ diff --git a/img/icons/lcd/logout.png b/img/icons/lcd/logout.png new file mode 100644 index 000000000..ae2f70b83 Binary files /dev/null and b/img/icons/lcd/logout.png differ diff --git a/img/icons/lcd/publish.png b/img/icons/lcd/publish.png new file mode 100644 index 000000000..b26e97d50 Binary files /dev/null and b/img/icons/lcd/publish.png differ diff --git a/img/icons/lcd/scope_blog.png b/img/icons/lcd/scope_blog.png index f11aa6a17..b26e97d50 100644 Binary files a/img/icons/lcd/scope_blog.png and b/img/icons/lcd/scope_blog.png differ diff --git a/img/icons/light/logout.png b/img/icons/light/logout.png new file mode 100644 index 000000000..6c52946df Binary files /dev/null and b/img/icons/light/logout.png differ diff --git a/img/icons/light/publish.png b/img/icons/light/publish.png new file mode 100644 index 000000000..0fe148eea Binary files /dev/null and b/img/icons/light/publish.png differ diff --git a/img/icons/light/scope_blog.png b/img/icons/light/scope_blog.png index 59713257c..e3cdb1b81 100644 Binary files a/img/icons/light/scope_blog.png and b/img/icons/light/scope_blog.png differ diff --git a/img/icons/logout.png b/img/icons/logout.png new file mode 100644 index 000000000..6c52946df Binary files /dev/null and b/img/icons/logout.png differ diff --git a/img/icons/night/logout.png b/img/icons/night/logout.png new file mode 100644 index 000000000..6c52946df Binary files /dev/null and b/img/icons/night/logout.png differ diff --git a/img/icons/night/publish.png b/img/icons/night/publish.png new file mode 100644 index 000000000..0fe148eea Binary files /dev/null and b/img/icons/night/publish.png differ diff --git a/img/icons/night/scope_blog.png b/img/icons/night/scope_blog.png index 59713257c..e3cdb1b81 100644 Binary files a/img/icons/night/scope_blog.png and b/img/icons/night/scope_blog.png differ diff --git a/img/icons/publish.png b/img/icons/publish.png new file mode 100644 index 000000000..0fe148eea Binary files /dev/null and b/img/icons/publish.png differ diff --git a/img/icons/purple/logout.png b/img/icons/purple/logout.png new file mode 100644 index 000000000..c8e134d15 Binary files /dev/null and b/img/icons/purple/logout.png differ diff --git a/img/icons/purple/publish.png b/img/icons/purple/publish.png new file mode 100644 index 000000000..d918e4f72 Binary files /dev/null and b/img/icons/purple/publish.png differ diff --git a/img/icons/purple/scope_blog.png b/img/icons/purple/scope_blog.png index 1332dca9a..d918e4f72 100644 Binary files a/img/icons/purple/scope_blog.png and b/img/icons/purple/scope_blog.png differ diff --git a/img/icons/scope_blog.png b/img/icons/scope_blog.png index 281fb795f..0fe148eea 100644 Binary files a/img/icons/scope_blog.png and b/img/icons/scope_blog.png differ diff --git a/img/icons/solidaric/logout.png b/img/icons/solidaric/logout.png new file mode 100644 index 000000000..1c1dad68a Binary files /dev/null and b/img/icons/solidaric/logout.png differ diff --git a/img/icons/solidaric/publish.png b/img/icons/solidaric/publish.png new file mode 100644 index 000000000..9d64f4b7b Binary files /dev/null and b/img/icons/solidaric/publish.png differ diff --git a/img/icons/solidaric/scope_blog.png b/img/icons/solidaric/scope_blog.png index d98c0e894..9d64f4b7b 100644 Binary files a/img/icons/solidaric/scope_blog.png and b/img/icons/solidaric/scope_blog.png differ diff --git a/img/icons/starlight/logout.png b/img/icons/starlight/logout.png new file mode 100644 index 000000000..199282adf Binary files /dev/null and b/img/icons/starlight/logout.png differ diff --git a/img/icons/starlight/publish.png b/img/icons/starlight/publish.png new file mode 100644 index 000000000..bfcfbb0b6 Binary files /dev/null and b/img/icons/starlight/publish.png differ diff --git a/img/icons/starlight/scope_blog.png b/img/icons/starlight/scope_blog.png index bb34e3bfb..bfcfbb0b6 100644 Binary files a/img/icons/starlight/scope_blog.png and b/img/icons/starlight/scope_blog.png differ diff --git a/img/icons/zen/logout.png b/img/icons/zen/logout.png new file mode 100644 index 000000000..4be063f93 Binary files /dev/null and b/img/icons/zen/logout.png differ diff --git a/img/icons/zen/newswire.png b/img/icons/zen/newswire.png new file mode 100644 index 000000000..52a890ceb Binary files /dev/null and b/img/icons/zen/newswire.png differ diff --git a/img/icons/zen/publish.png b/img/icons/zen/publish.png new file mode 100644 index 000000000..bac2b1219 Binary files /dev/null and b/img/icons/zen/publish.png differ diff --git a/img/icons/zen/scope_blog.png b/img/icons/zen/scope_blog.png index 4c3a74029..bac2b1219 100644 Binary files a/img/icons/zen/scope_blog.png and b/img/icons/zen/scope_blog.png differ diff --git a/img/image_indymedia.png b/img/image_indymediaclassic.png similarity index 100% rename from img/image_indymedia.png rename to img/image_indymediaclassic.png diff --git a/img/image_indymediamodern.png b/img/image_indymediamodern.png new file mode 100644 index 000000000..6c28873b4 Binary files /dev/null and b/img/image_indymediamodern.png differ diff --git a/img/left_col_image_indymedia.png b/img/left_col_image_indymediaclassic.png similarity index 100% rename from img/left_col_image_indymedia.png rename to img/left_col_image_indymediaclassic.png diff --git a/img/left_col_image_indymediamodern.png b/img/left_col_image_indymediamodern.png new file mode 100644 index 000000000..d663454b0 Binary files /dev/null and b/img/left_col_image_indymediamodern.png differ diff --git a/img/left_col_image_zen.png b/img/left_col_image_zen.png new file mode 100644 index 000000000..d683dd2f6 Binary files /dev/null and b/img/left_col_image_zen.png differ diff --git a/img/login_background_indymedia.jpg b/img/login_background_indymediaclassic.jpg similarity index 100% rename from img/login_background_indymedia.jpg rename to img/login_background_indymediaclassic.jpg diff --git a/img/options_background_indymedia.jpg b/img/options_background_indymediaclassic.jpg similarity index 100% rename from img/options_background_indymedia.jpg rename to img/options_background_indymediaclassic.jpg diff --git a/img/right_col_image_indymedia.png b/img/right_col_image_indymediaclassic.png similarity index 100% rename from img/right_col_image_indymedia.png rename to img/right_col_image_indymediaclassic.png diff --git a/img/right_col_image_indymediamodern.png b/img/right_col_image_indymediamodern.png new file mode 100644 index 000000000..9f3bf7525 Binary files /dev/null and b/img/right_col_image_indymediamodern.png differ diff --git a/img/right_col_image_zen.png b/img/right_col_image_zen.png new file mode 100644 index 000000000..88f56c6ca Binary files /dev/null and b/img/right_col_image_zen.png differ diff --git a/img/screenshot_indymedia.jpg b/img/screenshot_indymediaclassic.jpg similarity index 100% rename from img/screenshot_indymedia.jpg rename to img/screenshot_indymediaclassic.jpg diff --git a/img/search_banner_indymedia.png b/img/search_banner_indymediaclassic.png similarity index 100% rename from img/search_banner_indymedia.png rename to img/search_banner_indymediaclassic.png diff --git a/img/search_banner_indymediamodern.png b/img/search_banner_indymediamodern.png new file mode 100644 index 000000000..5a28e1f82 Binary files /dev/null and b/img/search_banner_indymediamodern.png differ diff --git a/img/search_banner_zen.png b/img/search_banner_zen.png index 301886970..cabb7e377 100644 Binary files a/img/search_banner_zen.png and b/img/search_banner_zen.png differ diff --git a/inbox.py b/inbox.py index 0fb51d0a6..a15b9f9c8 100644 --- a/inbox.py +++ b/inbox.py @@ -2436,7 +2436,9 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int, allowDeletion: bool, debug: bool, maxMentions: int, maxEmoji: int, translate: {}, unitTest: bool, YTReplacementDomain: str, - showPublishedDateOnly: bool) -> None: + showPublishedDateOnly: bool, + allowNewsFollowers: bool, + maxFollowers: int) -> None: """Processes received items and moves them to the appropriate directories """ @@ -2722,7 +2724,9 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int, personCache, queueJson['post'], federationList, - debug, projectVersion): + debug, projectVersion, + allowNewsFollowers, + maxFollowers): if os.path.isfile(queueFilename): os.remove(queueFilename) if len(queue) > 0: diff --git a/manualapprove.py b/manualapprove.py index b4252bc6a..e3648734e 100644 --- a/manualapprove.py +++ b/manualapprove.py @@ -98,10 +98,33 @@ def manualApproveFollowRequest(session, baseDir: str, print('Manual follow accept: follow requests file ' + approveFollowsFilename + ' not found') return + # is the handle in the requests file? - if approveHandle not in open(approveFollowsFilename).read(): - print('Manual follow accept: ' + approveHandle + - ' not in requests file ' + approveFollowsFilename) + approveFollowsStr = '' + with open(approveFollowsFilename, 'r') as fpFollowers: + approveFollowsStr = fpFollowers.read() + exists = False + approveHandleFull = approveHandle + if approveHandle in approveFollowsStr: + exists = True + elif '@' in approveHandle: + reqNick = approveHandle.split('@')[0] + reqDomain = approveHandle.split('@')[1].strip() + reqPrefix = httpPrefix + '://' + reqDomain + if reqPrefix + '/profile/' + reqNick in approveFollowsStr: + exists = True + approveHandleFull = reqPrefix + '/profile/' + reqNick + elif reqPrefix + '/channel/' + reqNick in approveFollowsStr: + exists = True + approveHandleFull = reqPrefix + '/channel/' + reqNick + elif reqPrefix + '/accounts/' + reqNick in approveFollowsStr: + exists = True + approveHandleFull = reqPrefix + '/accounts/' + reqNick + if not exists: + print('Manual follow accept: ' + approveHandleFull + + ' not in requests file "' + + approveFollowsStr.replace('\n', ' ') + + '" ' + approveFollowsFilename) return approvefilenew = open(approveFollowsFilename + '.new', 'w+') @@ -110,7 +133,7 @@ def manualApproveFollowRequest(session, baseDir: str, with open(approveFollowsFilename, 'r') as approvefile: for handleOfFollowRequester in approvefile: # is this the approved follow? - if handleOfFollowRequester.startswith(approveHandle): + if handleOfFollowRequester.startswith(approveHandleFull): handleOfFollowRequester = \ handleOfFollowRequester.replace('\n', '').replace('\r', '') port2 = port @@ -157,28 +180,28 @@ def manualApproveFollowRequest(session, baseDir: str, # update the followers print('Manual follow accept: updating ' + followersFilename) if os.path.isfile(followersFilename): - if approveHandle not in open(followersFilename).read(): + if approveHandleFull not in open(followersFilename).read(): try: with open(followersFilename, 'r+') as followersFile: content = followersFile.read() followersFile.seek(0, 0) - followersFile.write(approveHandle + '\n' + content) + followersFile.write(approveHandleFull + '\n' + content) except Exception as e: print('WARN: Manual follow accept. ' + 'Failed to write entry to followers file ' + str(e)) else: - print('WARN: Manual follow accept: ' + approveHandle + + print('WARN: Manual follow accept: ' + approveHandleFull + ' already exists in ' + followersFilename) else: print('Manual follow accept: first follower accepted for ' + - handle + ' is ' + approveHandle) + handle + ' is ' + approveHandleFull) followersFile = open(followersFilename, "w+") - followersFile.write(approveHandle + '\n') + followersFile.write(approveHandleFull + '\n') followersFile.close() # only update the follow requests file if the follow is confirmed to be # in followers.txt - if approveHandle in open(followersFilename).read(): + if approveHandleFull in open(followersFilename).read(): # mark this handle as approved for following approveFollowerHandle(accountDir, approveHandle) # update the follow requests with the handles not yet approved diff --git a/newsdaemon.py b/newsdaemon.py index 0e861bf06..9550212bc 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -6,17 +6,31 @@ __maintainer__ = "Bob Mottram" __email__ = "bob@freedombone.net" __status__ = "Production" +# Example hashtag logic: +# +# if moderated and not #imcoxford then block +# if #pol and contains "westminster" then add #britpol +# if #unwantedtag then block + import os import time import datetime +import html +from shutil import rmtree +from subprocess import Popen from collections import OrderedDict from newswire import getDictFromNewswire +# from posts import sendSignedJson from posts import createNewsPost +from posts import archivePostsForPerson from content import removeHtmlTag from content import dangerousMarkup +from content import validHashTag from utils import loadJson from utils import saveJson from utils import getStatusNumber +from utils import clearFromPostCaches +from inbox import storeHashTags def updateFeedsOutboxIndex(baseDir: str, domain: str, postId: str) -> None: @@ -53,30 +67,408 @@ def saveArrivedTime(baseDir: str, postFilename: str, arrived: str) -> None: def removeControlCharacters(content: str) -> str: - """TODO this is hacky and a better solution is needed - the unicode is messing up somehow + """Remove escaped html """ - lookups = { - "8211": "-", - "8230": "...", - "8216": "'", - "8217": "'", - "8220": '"', - "8221": '"' - } - for code, ch in lookups.items(): - content = content.replace('&' + code + ';', ch) - content = content.replace('' + code + ';', ch) + if '&' in content: + return html.unescape(content) return content +def hashtagRuleResolve(tree: [], hashtags: [], moderated: bool, + content: str, url: str) -> bool: + """Returns whether the tree for a hashtag rule evaluates to true or false + """ + if not tree: + return False + + if tree[0] == 'not': + if len(tree) == 2: + if isinstance(tree[1], str): + return tree[1] not in hashtags + elif isinstance(tree[1], list): + return not hashtagRuleResolve(tree[1], hashtags, moderated, + content, url) + elif tree[0] == 'contains': + if len(tree) == 2: + matchStr = None + if isinstance(tree[1], str): + matchStr = tree[1] + elif isinstance(tree[1], list): + matchStr = tree[1][0] + if matchStr: + if matchStr.startswith('"') and matchStr.endswith('"'): + matchStr = matchStr[1:] + matchStr = matchStr[:len(matchStr) - 1] + matchStrLower = matchStr.lower() + contentWithoutTags = content.replace('#' + matchStrLower, '') + return matchStrLower in contentWithoutTags + elif tree[0] == 'from': + if len(tree) == 2: + matchStr = None + if isinstance(tree[1], str): + matchStr = tree[1] + elif isinstance(tree[1], list): + matchStr = tree[1][0] + if matchStr: + if matchStr.startswith('"') and matchStr.endswith('"'): + matchStr = matchStr[1:] + matchStr = matchStr[:len(matchStr) - 1] + return matchStr.lower() in url + elif tree[0] == 'and': + if len(tree) >= 3: + for argIndex in range(1, len(tree)): + argValue = False + if isinstance(tree[argIndex], str): + argValue = (tree[argIndex] in hashtags) + elif isinstance(tree[argIndex], list): + argValue = hashtagRuleResolve(tree[argIndex], + hashtags, moderated, + content, url) + if not argValue: + return False + return True + elif tree[0] == 'or': + if len(tree) >= 3: + for argIndex in range(1, len(tree)): + argValue = False + if isinstance(tree[argIndex], str): + argValue = (tree[argIndex] in hashtags) + elif isinstance(tree[argIndex], list): + argValue = hashtagRuleResolve(tree[argIndex], + hashtags, moderated, + content, url) + if argValue: + return True + return False + elif tree[0] == 'xor': + if len(tree) >= 3: + trueCtr = 0 + for argIndex in range(1, len(tree)): + argValue = False + if isinstance(tree[argIndex], str): + argValue = (tree[argIndex] in hashtags) + elif isinstance(tree[argIndex], list): + argValue = hashtagRuleResolve(tree[argIndex], + hashtags, moderated, + content, url) + if argValue: + trueCtr += 1 + if trueCtr == 1: + return True + elif tree[0].startswith('#') and len(tree) == 1: + return tree[0] in hashtags + elif tree[0].startswith('moderated'): + return moderated + elif tree[0].startswith('"') and tree[0].endswith('"'): + return True + + return False + + +def hashtagRuleTree(operators: [], + conditionsStr: str, + tagsInConditions: [], + moderated: bool) -> []: + """Walks the tree + """ + if not operators and conditionsStr: + conditionsStr = conditionsStr.strip() + isStr = conditionsStr.startswith('"') and conditionsStr.endswith('"') + if conditionsStr.startswith('#') or isStr or \ + conditionsStr in operators or \ + conditionsStr == 'moderated' or \ + conditionsStr == 'contains': + if conditionsStr.startswith('#'): + if conditionsStr not in tagsInConditions: + if ' ' not in conditionsStr or \ + conditionsStr.startswith('"'): + tagsInConditions.append(conditionsStr) + return [conditionsStr.strip()] + else: + return None + if not operators or not conditionsStr: + return None + tree = None + conditionsStr = conditionsStr.strip() + isStr = conditionsStr.startswith('"') and conditionsStr.endswith('"') + if conditionsStr.startswith('#') or isStr or \ + conditionsStr in operators or \ + conditionsStr == 'moderated' or \ + conditionsStr == 'contains': + if conditionsStr.startswith('#'): + if conditionsStr not in tagsInConditions: + if ' ' not in conditionsStr or \ + conditionsStr.startswith('"'): + tagsInConditions.append(conditionsStr) + tree = [conditionsStr.strip()] + ctr = 0 + while ctr < len(operators): + op = operators[ctr] + opMatch = ' ' + op + ' ' + if opMatch not in conditionsStr and \ + not conditionsStr.startswith(op + ' '): + ctr += 1 + continue + else: + tree = [op] + if opMatch in conditionsStr: + sections = conditionsStr.split(opMatch) + else: + sections = conditionsStr.split(op + ' ', 1) + for subConditionStr in sections: + result = hashtagRuleTree(operators[ctr + 1:], + subConditionStr, + tagsInConditions, moderated) + if result: + tree.append(result) + break + return tree + + +def newswireHashtagProcessing(session, baseDir: str, postJsonObject: {}, + hashtags: [], httpPrefix: str, + domain: str, port: int, + personCache: {}, + cachedWebfingers: {}, + federationList: [], + sendThreads: [], postLog: [], + moderated: bool, url: str) -> bool: + """Applies hashtag rules to a news post. + Returns true if the post should be saved to the news timeline + of this instance + """ + rulesFilename = baseDir + '/accounts/hashtagrules.txt' + if not os.path.isfile(rulesFilename): + return True + rules = [] + with open(rulesFilename, "r") as f: + rules = f.readlines() + + domainFull = domain + if port: + if port != 80 and port != 443: + domainFull = domain + ':' + str(port) + + # get the full text content of the post + content = '' + if postJsonObject['object'].get('content'): + content += postJsonObject['object']['content'] + if postJsonObject['object'].get('summary'): + content += ' ' + postJsonObject['object']['summary'] + content = content.lower() + + # actionOccurred = False + operators = ('not', 'and', 'or', 'xor', 'from', 'contains') + for ruleStr in rules: + if not ruleStr: + continue + if not ruleStr.startswith('if '): + continue + if ' then ' not in ruleStr: + continue + conditionsStr = ruleStr.split('if ', 1)[1] + conditionsStr = conditionsStr.split(' then ')[0] + tagsInConditions = [] + tree = hashtagRuleTree(operators, conditionsStr, + tagsInConditions, moderated) + if not hashtagRuleResolve(tree, hashtags, moderated, content, url): + continue + # the condition matches, so do something + actionStr = ruleStr.split(' then ')[1].strip() + + # add a hashtag + if actionStr.startswith('add '): + addHashtag = actionStr.split('add ', 1)[1].strip() + if addHashtag.startswith('#'): + if addHashtag not in hashtags: + hashtags.append(addHashtag) + htId = addHashtag.replace('#', '') + if validHashTag(htId): + hashtagUrl = \ + httpPrefix + "://" + domainFull + "/tags/" + htId + newTag = { + 'href': hashtagUrl, + 'name': addHashtag, + 'type': 'Hashtag' + } + # does the tag already exist? + addTagObject = None + for t in postJsonObject['object']['tag']: + if t.get('type') and t.get('name'): + if t['type'] == 'Hashtag' and \ + t['name'] == addHashtag: + addTagObject = t + break + # append the tag if it wasn't found + if not addTagObject: + postJsonObject['object']['tag'].append(newTag) + # add corresponding html to the post content + hashtagHtml = \ + " #" + \ + htId + "" + content = postJsonObject['object']['content'] + if hashtagHtml not in content: + if content.endswith(''): + content = \ + content[:len(content) - len('')] + \ + hashtagHtml + '' + else: + content += hashtagHtml + postJsonObject['object']['content'] = content + storeHashTags(baseDir, 'news', postJsonObject) + # actionOccurred = True + + # remove a hashtag + if actionStr.startswith('remove '): + rmHashtag = actionStr.split('remove ', 1)[1].strip() + if rmHashtag.startswith('#'): + if rmHashtag in hashtags: + hashtags.remove(rmHashtag) + htId = rmHashtag.replace('#', '') + hashtagUrl = \ + httpPrefix + "://" + domainFull + "/tags/" + htId + # remove tag html from the post content + hashtagHtml = \ + "#" + \ + htId + "" + content = postJsonObject['object']['content'] + if hashtagHtml in content: + content = \ + content.replace(hashtagHtml, '').replace(' ', ' ') + postJsonObject['object']['content'] = content + rmTagObject = None + for t in postJsonObject['object']['tag']: + if t.get('type') and t.get('name'): + if t['type'] == 'Hashtag' and \ + t['name'] == rmHashtag: + rmTagObject = t + break + if rmTagObject: + postJsonObject['object']['tag'].remove(rmTagObject) + # actionOccurred = True + + # Block this item + if actionStr.startswith('block') or actionStr.startswith('drop'): + return False + + # TODO + # If routing to another instance + # sendSignedJson(postJsonObject: {}, session, baseDir: str, + # nickname: str, domain: str, port: int, + # toNickname: str, toDomain: str, toPort: int, cc: str, + # httpPrefix: str, False, False, + # federationList: [], + # sendThreads: [], postLog: [], cachedWebfingers: {}, + # personCache: {}, False, __version__) -> int: + # if actionOccurred: + # return True + return True + + +def createNewsMirror(baseDir: str, domain: str, + postIdNumber: str, url: str, + maxMirroredArticles: int) -> bool: + """Creates a local mirror of a news article + """ + if '|' in url or '>' in url: + return True + + mirrorDir = baseDir + '/accounts/newsmirror' + if not os.path.isdir(mirrorDir): + os.mkdir(mirrorDir) + + # count the directories + noOfDirs = 0 + for subdir, dirs, files in os.walk(mirrorDir): + noOfDirs = len(dirs) + + mirrorIndexFilename = baseDir + '/accounts/newsmirror.txt' + + if maxMirroredArticles > 0 and noOfDirs > maxMirroredArticles: + if not os.path.isfile(mirrorIndexFilename): + # no index for mirrors found + return True + removals = [] + with open(mirrorIndexFilename, 'r') as indexFile: + # remove the oldest directories + ctr = 0 + while noOfDirs > maxMirroredArticles: + ctr += 1 + if ctr > 5000: + # escape valve + break + + postId = indexFile.readline() + if not postId: + continue + postId = postId.strip() + mirrorArticleDir = mirrorDir + '/' + postId + if os.path.isdir(mirrorArticleDir): + rmtree(mirrorArticleDir) + removals.append(postId) + noOfDirs -= 1 + + # remove the corresponding index entries + if removals: + indexContent = '' + with open(mirrorIndexFilename, 'r') as indexFile: + indexContent = indexFile.read() + for removePostId in removals: + indexContent = \ + indexContent.replace(removePostId + '\n', '') + with open(mirrorIndexFilename, "w+") as indexFile: + indexFile.write(indexContent) + + mirrorArticleDir = mirrorDir + '/' + postIdNumber + if os.path.isdir(mirrorArticleDir): + # already mirrored + return True + + # for onion instances mirror via tor + prefixStr = '' + if domain.endswith('.onion'): + prefixStr = '/usr/bin/torsocks ' + + # download the files + commandStr = \ + prefixStr + '/usr/bin/wget -mkEpnp -e robots=off ' + url + \ + ' -P ' + mirrorArticleDir + p = Popen(commandStr, shell=True) + os.waitpid(p.pid, 0) + + if not os.path.isdir(mirrorArticleDir): + print('WARN: failed to mirror ' + url) + return True + + # append the post Id number to the index file + if os.path.isfile(mirrorIndexFilename): + indexFile = open(mirrorIndexFilename, "a+") + if indexFile: + indexFile.write(postIdNumber + '\n') + indexFile.close() + else: + indexFile = open(mirrorIndexFilename, "w+") + if indexFile: + indexFile.write(postIdNumber + '\n') + indexFile.close() + + return True + + def convertRSStoActivityPub(baseDir: str, httpPrefix: str, domain: str, port: int, newswire: {}, translate: {}, recentPostsCache: {}, maxRecentPosts: int, session, cachedWebfingers: {}, - personCache: {}) -> None: + personCache: {}, + federationList: [], + sendThreads: [], postLog: [], + maxMirroredArticles: int) -> None: """Converts rss items in a newswire into posts """ basePath = baseDir + '/accounts/news@' + domain + '/outbox' @@ -90,8 +482,13 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, for dateStr, item in newswireReverse.items(): originalDateStr = dateStr # convert the date to the format used by ActivityPub - dateStr = dateStr.replace(' ', 'T') - dateStr = dateStr.replace('+00:00', 'Z') + if '+00:00' in dateStr: + dateStr = dateStr.replace(' ', 'T') + dateStr = dateStr.replace('+00:00', 'Z') + else: + dateStrWithOffset = \ + datetime.datetime.strptime(dateStr, "%Y-%m-%d %H:%M:%S%z") + dateStr = dateStrWithOffset.strftime("%Y-%m-%dT%H:%M:%SZ") statusNumber, published = getStatusNumber(dateStr) newPostId = \ @@ -120,16 +517,28 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, if rssDescription.startswith('', '') + if '&' in rssDescription: + rssDescription = html.unescape(rssDescription) rssDescription = '' + rssDescription + '
'
+ mirrored = item[7]
+ postUrl = url
+ if mirrored and '://' in url:
+ postUrl = '/newsmirror/' + statusNumber + '/' + \
+ url.split('://')[1]
+ if postUrl.endswith('/'):
+ postUrl += 'index.html'
+ else:
+ postUrl += '/index.html'
+
# add the off-site link to the description
if rssDescription and not dangerousMarkup(rssDescription):
rssDescription += \
- '
' + \
+ '
' + \
translate['Read more...'] + ''
else:
rssDescription = \
- '' + \
+ '' + \
translate['Read more...'] + ''
# remove image dimensions
@@ -151,6 +560,11 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str,
if not blog:
continue
+ if mirrored:
+ if not createNewsMirror(baseDir, domain, statusNumber,
+ url, maxMirroredArticles):
+ continue
+
idStr = \
httpPrefix + '://' + domain + '/users/news' + \
'/statuses/' + statusNumber + '/replies'
@@ -171,28 +585,84 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str,
httpPrefix + '://' + domain + '/@news/' + statusNumber
blog['object']['published'] = dateStr
+ blog['object']['content'] = rssDescription
+ blog['object']['contentMap']['en'] = rssDescription
+
+ domainFull = domain
+ if port:
+ if port != 80 and port != 443:
+ domainFull = domain + ':' + str(port)
+
+ hashtags = item[6]
+
postId = newPostId.replace('/', '#')
moderated = item[5]
+ savePost = newswireHashtagProcessing(session, baseDir, blog, hashtags,
+ httpPrefix, domain, port,
+ personCache, cachedWebfingers,
+ federationList,
+ sendThreads, postLog,
+ moderated, url)
+
# save the post and update the index
- if saveJson(blog, filename):
- updateFeedsOutboxIndex(baseDir, domain, postId + '.json')
+ if savePost:
+ # ensure that all hashtags are stored in the json
+ # and appended to the content
+ blog['object']['tag'] = []
+ for tagName in hashtags:
+ htId = tagName.replace('#', '')
+ hashtagUrl = \
+ httpPrefix + "://" + domainFull + "/tags/" + htId
+ newTag = {
+ 'href': hashtagUrl,
+ 'name': tagName,
+ 'type': 'Hashtag'
+ }
+ blog['object']['tag'].append(newTag)
+ hashtagHtml = \
+ " #" + \
+ htId + ""
+ content = blog['object']['content']
+ if hashtagHtml not in content:
+ if content.endswith('
Tox address is 88AB9DED6F9FBEF43E105FB72060A2D89F9B93C74' + \ '4E8C45AB3C5E42C361C837155AFCFD9D448
' @@ -2092,7 +2110,7 @@ def testConstantTimeStringCheck(): avTime2 = ((end - start) * 1000000 / itterations) timeDiffMicroseconds = abs(avTime2 - avTime1) # time difference should be less than 10uS - assert timeDiffMicroseconds < 10 + assert int(timeDiffMicroseconds) < 10 # change multiple characters and observe timing difference start = time.time() @@ -2103,7 +2121,7 @@ def testConstantTimeStringCheck(): avTime2 = ((end - start) * 1000000 / itterations) timeDiffMicroseconds = abs(avTime2 - avTime1) # time difference should be less than 10uS - assert timeDiffMicroseconds < 10 + assert int(timeDiffMicroseconds) < 10 def testReplaceEmailQuote(): @@ -2173,8 +2191,156 @@ def testRemoveHtmlTag(): "src=\"https://somesiteorother.com/image.jpg\">" +def testHashtagRuleTree(): + print('testHashtagRuleTree') + operators = ('not', 'and', 'or', 'xor', 'from', 'contains') + + url = 'testsite.com' + moderated = True + conditionsStr = \ + 'contains "Cat" or contains "Corvid" or ' + \ + 'contains "Dormouse" or contains "Buzzard"' + tagsInConditions = [] + tree = hashtagRuleTree(operators, conditionsStr, + tagsInConditions, moderated) + assert str(tree) == str(['or', ['contains', ['"Cat"']], + ['contains', ['"Corvid"']], + ['contains', ['"Dormouse"']], + ['contains', ['"Buzzard"']]]) + + content = 'This is a test' + moderated = True + conditionsStr = '#foo or #bar' + tagsInConditions = [] + tree = hashtagRuleTree(operators, conditionsStr, + tagsInConditions, moderated) + assert str(tree) == str(['or', ['#foo'], ['#bar']]) + assert str(tagsInConditions) == str(['#foo', '#bar']) + hashtags = ['#foo'] + assert hashtagRuleResolve(tree, hashtags, moderated, content, url) + hashtags = ['#carrot', '#stick'] + assert not hashtagRuleResolve(tree, hashtags, moderated, content, url) + + content = 'This is a test' + url = 'https://testsite.com/something' + moderated = True + conditionsStr = '#foo and from "testsite.com"' + tagsInConditions = [] + tree = hashtagRuleTree(operators, conditionsStr, + tagsInConditions, moderated) + assert str(tree) == str(['and', ['#foo'], ['from', ['"testsite.com"']]]) + assert str(tagsInConditions) == str(['#foo']) + hashtags = ['#foo'] + assert hashtagRuleResolve(tree, hashtags, moderated, content, url) + assert not hashtagRuleResolve(tree, hashtags, moderated, content, + 'othersite.net') + + content = 'This is a test' + moderated = True + conditionsStr = 'contains "is a" and #foo or #bar' + tagsInConditions = [] + tree = hashtagRuleTree(operators, conditionsStr, + tagsInConditions, moderated) + assert str(tree) == \ + str(['and', ['contains', ['"is a"']], + ['or', ['#foo'], ['#bar']]]) + assert str(tagsInConditions) == str(['#foo', '#bar']) + hashtags = ['#foo'] + assert hashtagRuleResolve(tree, hashtags, moderated, content, url) + hashtags = ['#carrot', '#stick'] + assert not hashtagRuleResolve(tree, hashtags, moderated, content, url) + + moderated = False + conditionsStr = 'not moderated and #foo or #bar' + tagsInConditions = [] + tree = hashtagRuleTree(operators, conditionsStr, + tagsInConditions, moderated) + assert str(tree) == \ + str(['not', ['and', ['moderated'], ['or', ['#foo'], ['#bar']]]]) + assert str(tagsInConditions) == str(['#foo', '#bar']) + hashtags = ['#foo'] + assert hashtagRuleResolve(tree, hashtags, moderated, content, url) + hashtags = ['#carrot', '#stick'] + assert hashtagRuleResolve(tree, hashtags, moderated, content, url) + + moderated = True + conditionsStr = 'moderated and #foo or #bar' + tagsInConditions = [] + tree = hashtagRuleTree(operators, conditionsStr, + tagsInConditions, moderated) + assert str(tree) == \ + str(['and', ['moderated'], ['or', ['#foo'], ['#bar']]]) + assert str(tagsInConditions) == str(['#foo', '#bar']) + hashtags = ['#foo'] + assert hashtagRuleResolve(tree, hashtags, moderated, content, url) + hashtags = ['#carrot', '#stick'] + assert not hashtagRuleResolve(tree, hashtags, moderated, content, url) + + conditionsStr = 'x' + tagsInConditions = [] + tree = hashtagRuleTree(operators, conditionsStr, + tagsInConditions, moderated) + assert tree is None + assert tagsInConditions == [] + hashtags = ['#foo'] + assert not hashtagRuleResolve(tree, hashtags, moderated, content, url) + + conditionsStr = '#x' + tagsInConditions = [] + tree = hashtagRuleTree(operators, conditionsStr, + tagsInConditions, moderated) + assert str(tree) == str(['#x']) + assert str(tagsInConditions) == str(['#x']) + hashtags = ['#x'] + assert hashtagRuleResolve(tree, hashtags, moderated, content, url) + hashtags = ['#y', '#z'] + assert not hashtagRuleResolve(tree, hashtags, moderated, content, url) + + conditionsStr = 'not #b' + tagsInConditions = [] + tree = hashtagRuleTree(operators, conditionsStr, + tagsInConditions, moderated) + assert str(tree) == str(['not', ['#b']]) + assert str(tagsInConditions) == str(['#b']) + hashtags = ['#y', '#z'] + assert hashtagRuleResolve(tree, hashtags, moderated, content, url) + hashtags = ['#a', '#b', '#c'] + assert not hashtagRuleResolve(tree, hashtags, moderated, content, url) + + conditionsStr = '#foo or #bar and #a' + tagsInConditions = [] + tree = hashtagRuleTree(operators, conditionsStr, + tagsInConditions, moderated) + assert str(tree) == str(['and', ['or', ['#foo'], ['#bar']], ['#a']]) + assert str(tagsInConditions) == str(['#foo', '#bar', '#a']) + hashtags = ['#foo', '#bar', '#a'] + assert hashtagRuleResolve(tree, hashtags, moderated, content, url) + hashtags = ['#bar', '#a'] + assert hashtagRuleResolve(tree, hashtags, moderated, content, url) + hashtags = ['#foo', '#a'] + assert hashtagRuleResolve(tree, hashtags, moderated, content, url) + hashtags = ['#x', '#a'] + assert not hashtagRuleResolve(tree, hashtags, moderated, content, url) + + +def testGetNewswireTags(): + print('testGetNewswireTags') + rssDescription = '', '"').replace('', '"') + result = '' + for ch in content: + if ch == '<': + removing = True + elif ch == '>': + removing = False + elif not removing: + result += ch + return result + + def isSystemAccount(nickname: str) -> bool: """Returns true if the given nickname is a system account """ @@ -587,6 +606,39 @@ def locateNewsArrival(baseDir: str, domain: str, return None +def clearFromPostCaches(baseDir: str, recentPostsCache: {}, + postId: str) -> None: + """Clears cached html for the given post, so that edits + to news will appear + """ + filename = '/postcache/' + postId + '.html' + for subdir, dirs, files in os.walk(baseDir + '/accounts'): + for acct in dirs: + if '@' not in acct: + continue + if 'inbox@' in acct: + continue + cacheDir = os.path.join(baseDir + '/accounts', acct) + postFilename = cacheDir + filename + if os.path.isfile(postFilename): + try: + os.remove(postFilename) + except BaseException: + print('WARN: clearFromPostCaches file not removed ' + + postFilename) + pass + # if the post is in the recent posts cache then remove it + if recentPostsCache.get('index'): + if postId in recentPostsCache['index']: + recentPostsCache['index'].remove(postId) + if recentPostsCache.get('json'): + if recentPostsCache['json'].get(postId): + del recentPostsCache['json'][postId] + if recentPostsCache.get('html'): + if recentPostsCache['html'].get(postId): + del recentPostsCache['html'][postId] + + def locatePost(baseDir: str, nickname: str, domain: str, postUrl: str, replies=False) -> str: """Returns the filename for the given status post url @@ -719,26 +771,21 @@ def deletePost(baseDir: str, httpPrefix: str, if recentPostsCache.get('index'): if postId in recentPostsCache['index']: recentPostsCache['index'].remove(postId) - if recentPostsCache['json'].get(postId): - del recentPostsCache['json'][postId] + if recentPostsCache.get('json'): + if recentPostsCache['json'].get(postId): + del recentPostsCache['json'][postId] + if recentPostsCache.get('html'): + if recentPostsCache['html'].get(postId): + del recentPostsCache['html'][postId] # remove any attachment removeAttachment(baseDir, httpPrefix, domain, postJsonObject) - # remove any mute file - muteFilename = postFilename + '.muted' - if os.path.isfile(muteFilename): - os.remove(muteFilename) - - # remove any votes file - votesFilename = postFilename + '.votes' - if os.path.isfile(votesFilename): - os.remove(votesFilename) - - # remove any arrived file - arrivedFilename = postFilename + '.arrived' - if os.path.isfile(arrivedFilename): - os.remove(arrivedFilename) + extensions = ('votes', 'arrived', 'muted') + for ext in extensions: + extFilename = postFilename + '.' + ext + if os.path.isfile(extFilename): + os.remove(extFilename) # remove cached html version of the post cachedPostFilename = \ @@ -1006,6 +1053,35 @@ def fileLastModified(filename: str) -> str: return modifiedTime.strftime("%Y-%m-%dT%H:%M:%SZ") +def getCSS(baseDir: str, cssFilename: str, cssCache: {}) -> str: + """Retrieves the css for a given file, or from a cache + """ + # does the css file exist? + if not os.path.isfile(cssFilename): + return None + + lastModified = fileLastModified(cssFilename) + + # has this already been loaded into the cache? + if cssCache.get(cssFilename): + if cssCache[cssFilename][0] == lastModified: + # file hasn't changed, so return the version in the cache + return cssCache[cssFilename][1] + + with open(cssFilename, 'r') as fpCSS: + css = fpCSS.read() + if cssCache.get(cssFilename): + # alter the cache contents + cssCache[cssFilename][0] = lastModified + cssCache[cssFilename][1] = css + else: + # add entry to the cache + cssCache[cssFilename] = [lastModified, css] + return css + + return None + + def daysInMonth(year: int, monthNumber: int) -> int: """Returns the number of days in the month """ diff --git a/webinterface.py b/webinterface.py index 14b4aa121..7e20bf4ff 100644 --- a/webinterface.py +++ b/webinterface.py @@ -25,6 +25,7 @@ from ssb import getSSBAddress from tox import getToxAddress from matrix import getMatrixAddress from donate import getDonationUrl +from utils import getCSS from utils import isSystemAccount from utils import removeIdEnding from utils import getProtocolPrefixes @@ -45,6 +46,7 @@ from utils import getCachedPostFilename from utils import loadJson from utils import getConfigParam from utils import votesOnNewswireItem +from utils import removeHtml from follow import isFollowingActor from webfinger import webfingerHandle from posts import isDM @@ -71,7 +73,6 @@ from content import getMentionsFromHtml from content import addHtmlTags from content import replaceEmojiFromTags from content import removeLongWords -from content import removeHtml from skills import getSkills from cache import getPersonFromCache from cache import storePersonInCache @@ -327,7 +328,8 @@ def getPersonAvatarUrl(baseDir: str, personUrl: str, personCache: {}, return None -def htmlFollowingList(baseDir: str, followingFilename: str) -> str: +def htmlFollowingList(cssCache: {}, baseDir: str, + followingFilename: str) -> str: """Returns a list of handles being followed """ with open(followingFilename, 'r') as followingFile: @@ -338,8 +340,9 @@ def htmlFollowingList(baseDir: str, followingFilename: str) -> str: cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' - with open(cssFilename, 'r') as cssFile: - profileCSS = cssFile.read() + + profileCSS = getCSS(baseDir, cssFilename, cssCache) + if profileCSS: followingListHtml = htmlHeader(cssFilename, profileCSS) for followingAddress in followingList: if followingAddress: @@ -391,7 +394,8 @@ def htmlFollowingDataList(baseDir: str, nickname: str, return listStr -def htmlSearchEmoji(translate: {}, baseDir: str, httpPrefix: str, +def htmlSearchEmoji(cssCache: {}, translate: {}, + baseDir: str, httpPrefix: str, searchStr: str) -> str: """Search results for emoji """ @@ -405,8 +409,9 @@ def htmlSearchEmoji(translate: {}, baseDir: str, httpPrefix: str, cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' - with open(cssFilename, 'r') as cssFile: - emojiCSS = cssFile.read() + + emojiCSS = getCSS(baseDir, cssFilename, cssCache) + if emojiCSS: if httpPrefix != 'https': emojiCSS = emojiCSS.replace('https://', httpPrefix + '://') @@ -465,7 +470,7 @@ def getIconsDir(baseDir: str) -> str: return iconsDir -def htmlSearchSharedItems(translate: {}, +def htmlSearchSharedItems(cssCache: {}, translate: {}, baseDir: str, searchStr: str, pageNumber: int, resultsPerPage: int, @@ -485,8 +490,8 @@ def htmlSearchSharedItems(translate: {}, if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' - with open(cssFilename, 'r') as cssFile: - sharedItemsCSS = cssFile.read() + sharedItemsCSS = getCSS(baseDir, cssFilename, cssCache) + if sharedItemsCSS: if httpPrefix != 'https': sharedItemsCSS = \ sharedItemsCSS.replace('https://', @@ -640,7 +645,8 @@ def htmlSearchSharedItems(translate: {}, return sharedItemsForm -def htmlModerationInfo(translate: {}, baseDir: str, httpPrefix: str) -> str: +def htmlModerationInfo(cssCache: {}, translate: {}, + baseDir: str, httpPrefix: str) -> str: msgStr1 = \ 'These are globally blocked for all accounts on this instance' msgStr2 = \ @@ -649,8 +655,9 @@ def htmlModerationInfo(translate: {}, baseDir: str, httpPrefix: str) -> str: cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' - with open(cssFilename, 'r') as cssFile: - infoCSS = cssFile.read() + + infoCSS = getCSS(baseDir, cssFilename, cssCache) + if infoCSS: if httpPrefix != 'https': infoCSS = infoCSS.replace('https://', httpPrefix + '://') @@ -704,7 +711,8 @@ def htmlModerationInfo(translate: {}, baseDir: str, httpPrefix: str) -> str: return infoForm -def htmlHashtagSearch(nickname: str, domain: str, port: int, +def htmlHashtagSearch(cssCache: {}, + nickname: str, domain: str, port: int, recentPostsCache: {}, maxRecentPosts: int, translate: {}, baseDir: str, hashtag: str, pageNumber: int, @@ -743,8 +751,9 @@ def htmlHashtagSearch(nickname: str, domain: str, port: int, cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' - with open(cssFilename, 'r') as cssFile: - hashtagSearchCSS = cssFile.read() + + hashtagSearchCSS = getCSS(baseDir, cssFilename, cssCache) + if hashtagSearchCSS: if httpPrefix != 'https': hashtagSearchCSS = \ hashtagSearchCSS.replace('https://', @@ -979,7 +988,7 @@ def rssHashtagSearch(nickname: str, domain: str, port: int, return hashtagFeed + rss2TagFooter() -def htmlSkillsSearch(translate: {}, baseDir: str, +def htmlSkillsSearch(cssCache: {}, translate: {}, baseDir: str, httpPrefix: str, skillsearch: str, instanceOnly: bool, postsPerPage: int) -> str: @@ -1067,8 +1076,9 @@ def htmlSkillsSearch(translate: {}, baseDir: str, cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' - with open(cssFilename, 'r') as cssFile: - skillSearchCSS = cssFile.read() + + skillSearchCSS = getCSS(baseDir, cssFilename, cssCache) + if skillSearchCSS: if httpPrefix != 'https': skillSearchCSS = \ skillSearchCSS.replace('https://', @@ -1107,7 +1117,7 @@ def htmlSkillsSearch(translate: {}, baseDir: str, return skillSearchForm -def htmlHistorySearch(translate: {}, baseDir: str, +def htmlHistorySearch(cssCache: {}, translate: {}, baseDir: str, httpPrefix: str, nickname: str, domain: str, historysearch: str, @@ -1135,8 +1145,9 @@ def htmlHistorySearch(translate: {}, baseDir: str, cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' - with open(cssFilename, 'r') as cssFile: - historySearchCSS = cssFile.read() + + historySearchCSS = getCSS(baseDir, cssFilename, cssCache) + if historySearchCSS: if httpPrefix != 'https': historySearchCSS = \ historySearchCSS.replace('https://', @@ -1214,7 +1225,7 @@ def scheduledPostsExist(baseDir: str, nickname: str, domain: str) -> bool: return False -def htmlEditLinks(translate: {}, baseDir: str, path: str, +def htmlEditLinks(cssCache: {}, translate: {}, baseDir: str, path: str, domain: str, port: int, httpPrefix: str) -> str: """Shows the edit links screen """ @@ -1235,8 +1246,9 @@ def htmlEditLinks(translate: {}, baseDir: str, path: str, cssFilename = baseDir + '/epicyon-links.css' if os.path.isfile(baseDir + '/links.css'): cssFilename = baseDir + '/links.css' - with open(cssFilename, 'r') as cssFile: - editCSS = cssFile.read() + + editCSS = getCSS(baseDir, cssFilename, cssCache) + if editCSS: if httpPrefix != 'https': editCSS = \ editCSS.replace('https://', httpPrefix + '://') @@ -1282,7 +1294,7 @@ def htmlEditLinks(translate: {}, baseDir: str, path: str, return editLinksForm -def htmlEditNewswire(translate: {}, baseDir: str, path: str, +def htmlEditNewswire(cssCache: {}, translate: {}, baseDir: str, path: str, domain: str, port: int, httpPrefix: str) -> str: """Shows the edit newswire screen """ @@ -1303,8 +1315,9 @@ def htmlEditNewswire(translate: {}, baseDir: str, path: str, cssFilename = baseDir + '/epicyon-links.css' if os.path.isfile(baseDir + '/links.css'): cssFilename = baseDir + '/links.css' - with open(cssFilename, 'r') as cssFile: - editCSS = cssFile.read() + + editCSS = getCSS(baseDir, cssFilename, cssCache) + if editCSS: if httpPrefix != 'https': editCSS = \ editCSS.replace('https://', httpPrefix + '://') @@ -1345,6 +1358,42 @@ def htmlEditNewswire(translate: {}, baseDir: str, path: str, ' ' + filterStr = '' + filterFilename = \ + baseDir + '/accounts/news@' + domain + '/filters.txt' + if os.path.isfile(filterFilename): + with open(filterFilename, 'r') as filterfile: + filterStr = filterfile.read() + + editNewswireForm += \ + '
Account Suspended
\n' @@ -2310,13 +2370,14 @@ def htmlSuspended(baseDir: str) -> str: return suspendedForm -def htmlNewPost(mediaInstance: bool, translate: {}, +def htmlNewPost(cssCache: {}, mediaInstance: bool, translate: {}, baseDir: str, httpPrefix: str, path: str, inReplyTo: str, mentions: [], reportUrl: str, pageNumber: int, nickname: str, domain: str, - domainFull: str) -> str: + domainFull: str, + defaultTimeline: str) -> str: """New post screen """ iconsDir = getIconsDir(baseDir) @@ -2396,8 +2457,9 @@ def htmlNewPost(mediaInstance: bool, translate: {}, cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' - with open(cssFilename, 'r') as cssFile: - newPostCSS = cssFile.read() + + newPostCSS = getCSS(baseDir, cssFilename, cssCache) + if newPostCSS: if httpPrefix != 'https': newPostCSS = newPostCSS.replace('https://', httpPrefix + '://') @@ -2454,7 +2516,10 @@ def htmlNewPost(mediaInstance: bool, translate: {}, if path.endswith('/newblog'): placeholderSubject = translate['Title'] scopeIcon = 'scope_blog.png' - scopeDescription = translate['Blog'] + if defaultTimeline != 'tlnews': + scopeDescription = translate['Blog'] + else: + scopeDescription = translate['Article'] endpoint = 'newblog' elif path.endswith('/newunlisted'): scopeIcon = 'scope_unlisted.png' @@ -2767,12 +2832,20 @@ def htmlNewPost(mediaInstance: bool, translate: {}, iconsDir + '/scope_public.png"/>' + \ translate['Public'] + '' + dropDownContent + ' | \n' newPostForm += \ @@ -3225,7 +3298,9 @@ def htmlSharesTimeline(translate: {}, pageNumber: int, itemsPerPage: int, return timelineStr -def htmlProfile(defaultTimeline: str, +def htmlProfile(rssIconAtTop: bool, + cssCache: {}, iconsAsButtons: bool, + defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, projectVersion: str, baseDir: str, httpPrefix: str, authorized: bool, @@ -3331,18 +3406,25 @@ def htmlProfile(defaultTimeline: str, donateSection += ' \n' donateSection += '\n' + iconsDir = getIconsDir(baseDir) if not authorized: - loginButton = \ - '