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('

'): + content = \ + content[:len(content) - len('

')] + \ + hashtagHtml + '

' + else: + content += hashtagHtml + blog['object']['content'] = content - # Save a file containing the time when the post arrived - # this can then later be used to construct the news timeline - # excluding items during the voting period - if moderated: - saveArrivedTime(baseDir, filename, blog['object']['arrived']) - else: - if os.path.isfile(filename + '.arrived'): - os.remove(filename + '.arrived') + # update the newswire tags if new ones have been found by + # newswireHashtagProcessing + for tag in hashtags: + if tag not in newswire[originalDateStr][6]: + newswire[originalDateStr][6].append(tag) - # set the url - newswire[originalDateStr][1] = \ - '/users/news/statuses/' + statusNumber - # set the filename - newswire[originalDateStr][3] = filename + storeHashTags(baseDir, 'news', blog) + + clearFromPostCaches(baseDir, recentPostsCache, postId) + if saveJson(blog, filename): + updateFeedsOutboxIndex(baseDir, domain, postId + '.json') + + # Save a file containing the time when the post arrived + # this can then later be used to construct the news timeline + # excluding items during the voting period + if moderated: + saveArrivedTime(baseDir, filename, + blog['object']['arrived']) + else: + if os.path.isfile(filename + '.arrived'): + os.remove(filename + '.arrived') + + # set the url + newswire[originalDateStr][1] = \ + '/users/news/statuses/' + statusNumber + # set the filename + newswire[originalDateStr][3] = filename def mergeWithPreviousNewswire(oldNewswire: {}, newNewswire: {}) -> None: @@ -226,9 +696,10 @@ def runNewswireDaemon(baseDir: str, httpd, newNewswire = None try: newNewswire = \ - getDictFromNewswire(httpd.session, baseDir, + getDictFromNewswire(httpd.session, baseDir, domain, httpd.maxNewswirePostsPerSource, - httpd.maxNewswireFeedSizeKb) + httpd.maxNewswireFeedSizeKb, + httpd.maxTags) except Exception as e: print('WARN: unable to update newswire ' + str(e)) time.sleep(120) @@ -251,9 +722,23 @@ def runNewswireDaemon(baseDir: str, httpd, httpd.maxRecentPosts, httpd.session, httpd.cachedWebfingers, - httpd.personCache) + httpd.personCache, + httpd.federationList, + httpd.sendThreads, + httpd.postLog, + httpd.maxMirroredArticles) print('Newswire feed converted to ActivityPub') + if httpd.maxNewsPosts > 0: + archiveDir = baseDir + '/archive' + archiveSubdir = \ + archiveDir + '/accounts/news@' + domain + '/outbox' + archivePostsForPerson(httpPrefix, 'news', + domain, baseDir, 'outbox', + archiveSubdir, + httpd.recentPostsCache, + httpd.maxNewsPosts) + # wait a while before the next feeds update time.sleep(1200) diff --git a/newswire.py b/newswire.py index bd964580b..cc2eb538e 100644 --- a/newswire.py +++ b/newswire.py @@ -12,12 +12,16 @@ from socket import error as SocketError import errno from datetime import datetime from collections import OrderedDict +from utils import isPublicPost from utils import locatePost from utils import loadJson from utils import saveJson from utils import isSuspended from utils import containsInvalidChars +from utils import removeHtml from blocking import isBlockedDomain +from blocking import isBlockedHashtag +from filters import isFiltered def rss2Header(httpPrefix: str, @@ -52,7 +56,76 @@ def rss2Footer() -> str: return rssStr -def xml2StrToDict(baseDir: str, xmlStr: str, moderated: bool, +def getNewswireTags(text: str, maxTags: int) -> []: + """Returns a list of hashtags found in the given text + """ + if '#' not in text: + return [] + if ' ' not in text: + return [] + textSimplified = \ + text.replace(',', ' ').replace(';', ' ').replace('- ', ' ') + textSimplified = textSimplified.replace('. ', ' ').strip() + if textSimplified.endswith('.'): + textSimplified = textSimplified[:len(textSimplified)-1] + words = textSimplified.split(' ') + tags = [] + for wrd in words: + if wrd.startswith('#'): + if len(wrd) > 1: + if wrd not in tags: + tags.append(wrd) + if len(tags) >= maxTags: + break + return tags + + +def addNewswireDictEntry(baseDir: str, domain: str, + newswire: {}, dateStr: str, + title: str, link: str, + votesStatus: str, postFilename: str, + description: str, moderated: bool, + mirrored: bool, + tags=[], maxTags=32) -> None: + """Update the newswire dictionary + """ + allText = removeHtml(title + ' ' + description) + + # check that none of the text is filtered against + if isFiltered(baseDir, 'news', domain, allText): + return + + if tags is None: + tags = [] + + # extract hashtags from the text of the feed post + postTags = getNewswireTags(allText, maxTags) + + # combine the tags into a single list + for tag in tags: + if tag not in postTags: + if len(postTags) < maxTags: + postTags.append(tag) + + # check that no tags are blocked + for tag in postTags: + if isBlockedHashtag(baseDir, tag.replace('#', '')): + return + + newswire[dateStr] = [ + title, + link, + votesStatus, + postFilename, + description, + moderated, + postTags, + mirrored + ] + + +def xml2StrToDict(baseDir: str, domain: str, xmlStr: str, + moderated: bool, mirrored: bool, maxPostsPerSource: int) -> {}: """Converts an xml 2.0 string to a dictionary """ @@ -84,10 +157,10 @@ def xml2StrToDict(baseDir: str, xmlStr: str, moderated: bool, link = link.split('')[0] if '://' not in link: continue - domain = link.split('://')[1] - if '/' in domain: - domain = domain.split('/')[0] - if isBlockedDomain(baseDir, domain): + itemDomain = link.split('://')[1] + if '/' in itemDomain: + itemDomain = itemDomain.split('/')[0] + if isBlockedDomain(baseDir, itemDomain): continue pubDate = rssItem.split('')[1] pubDate = pubDate.split('')[0] @@ -97,9 +170,11 @@ def xml2StrToDict(baseDir: str, xmlStr: str, moderated: bool, datetime.strptime(pubDate, "%a, %d %b %Y %H:%M:%S %z") postFilename = '' votesStatus = [] - result[str(publishedDate)] = [title, link, - votesStatus, postFilename, - description, moderated] + addNewswireDictEntry(baseDir, domain, + result, str(publishedDate), + title, link, + votesStatus, postFilename, + description, moderated, mirrored) postCtr += 1 if postCtr >= maxPostsPerSource: break @@ -112,10 +187,12 @@ def xml2StrToDict(baseDir: str, xmlStr: str, moderated: bool, datetime.strptime(pubDate, "%a, %d %b %Y %H:%M:%S UT") postFilename = '' votesStatus = [] - result[str(publishedDate) + '+00:00'] = \ - [title, link, - votesStatus, postFilename, - description, moderated] + addNewswireDictEntry(baseDir, domain, + result, + str(publishedDate) + '+00:00', + title, link, + votesStatus, postFilename, + description, moderated, mirrored) postCtr += 1 if postCtr >= maxPostsPerSource: break @@ -126,7 +203,8 @@ def xml2StrToDict(baseDir: str, xmlStr: str, moderated: bool, return result -def atomFeedToDict(baseDir: str, xmlStr: str, moderated: bool, +def atomFeedToDict(baseDir: str, domain: str, xmlStr: str, + moderated: bool, mirrored: bool, maxPostsPerSource: int) -> {}: """Converts an atom feed string to a dictionary """ @@ -158,10 +236,10 @@ def atomFeedToDict(baseDir: str, xmlStr: str, moderated: bool, link = link.split('')[0] if '://' not in link: continue - domain = link.split('://')[1] - if '/' in domain: - domain = domain.split('/')[0] - if isBlockedDomain(baseDir, domain): + itemDomain = link.split('://')[1] + if '/' in itemDomain: + itemDomain = itemDomain.split('/')[0] + if isBlockedDomain(baseDir, itemDomain): continue pubDate = rssItem.split('')[1] pubDate = pubDate.split('')[0] @@ -171,9 +249,11 @@ def atomFeedToDict(baseDir: str, xmlStr: str, moderated: bool, datetime.strptime(pubDate, "%Y-%m-%dT%H:%M:%SZ") postFilename = '' votesStatus = [] - result[str(publishedDate)] = [title, link, - votesStatus, postFilename, - description, moderated] + addNewswireDictEntry(baseDir, domain, + result, str(publishedDate), + title, link, + votesStatus, postFilename, + description, moderated, mirrored) postCtr += 1 if postCtr >= maxPostsPerSource: break @@ -186,10 +266,11 @@ def atomFeedToDict(baseDir: str, xmlStr: str, moderated: bool, datetime.strptime(pubDate, "%a, %d %b %Y %H:%M:%S UT") postFilename = '' votesStatus = [] - result[str(publishedDate) + '+00:00'] = \ - [title, link, - votesStatus, postFilename, - description, moderated] + addNewswireDictEntry(baseDir, domain, result, + str(publishedDate) + '+00:00', + title, link, + votesStatus, postFilename, + description, moderated, mirrored) postCtr += 1 if postCtr >= maxPostsPerSource: break @@ -200,20 +281,23 @@ def atomFeedToDict(baseDir: str, xmlStr: str, moderated: bool, return result -def xmlStrToDict(baseDir: str, xmlStr: str, moderated: bool, +def xmlStrToDict(baseDir: str, domain: str, xmlStr: str, + moderated: bool, mirrored: bool, maxPostsPerSource: int) -> {}: """Converts an xml string to a dictionary """ if 'rss version="2.0"' in xmlStr: - return xml2StrToDict(baseDir, xmlStr, moderated, maxPostsPerSource) + return xml2StrToDict(baseDir, domain, + xmlStr, moderated, mirrored, maxPostsPerSource) elif 'xmlns="http://www.w3.org/2005/Atom"' in xmlStr: - return atomFeedToDict(baseDir, xmlStr, moderated, maxPostsPerSource) + return atomFeedToDict(baseDir, domain, + xmlStr, moderated, mirrored, maxPostsPerSource) return {} -def getRSS(baseDir: str, session, url: str, moderated: bool, - maxPostsPerSource: int, - maxFeedSizeKb: int) -> {}: +def getRSS(baseDir: str, domain: str, session, url: str, + moderated: bool, mirrored: bool, + maxPostsPerSource: int, maxFeedSizeKb: int) -> {}: """Returns an RSS url as a dict """ if not isinstance(url, str): @@ -239,7 +323,8 @@ def getRSS(baseDir: str, session, url: str, moderated: bool, if result: if int(len(result.text) / 1024) < maxFeedSizeKb and \ not containsInvalidChars(result.text): - return xmlStrToDict(baseDir, result.text, moderated, + return xmlStrToDict(baseDir, domain, result.text, + moderated, mirrored, maxPostsPerSource) else: print('WARN: feed is too large: ' + url) @@ -270,11 +355,17 @@ def getRSSfromDict(baseDir: str, newswire: {}, None, domainFull, 'Newswire', translate) for published, fields in newswire.items(): - published = published.replace('+00:00', 'Z').strip() - published = published.replace(' ', 'T') + if '+00:00' in published: + published = published.replace('+00:00', 'Z').strip() + published = published.replace(' ', 'T') + else: + publishedWithOffset = \ + datetime.strptime(published, "%Y-%m-%d %H:%M:%S%z") + published = publishedWithOffset.strftime("%Y-%m-%dT%H:%M:%SZ") try: pubDate = datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ") - except BaseException: + except Exception as e: + print('WARN: Unable to convert date ' + published + ' ' + str(e)) continue rssStr += '\n' rssStr += ' ' + fields[0] + '\n' @@ -290,8 +381,11 @@ def getRSSfromDict(baseDir: str, newswire: {}, return rssStr -def isaBlogPost(postJsonObject: {}) -> bool: +def isNewswireBlogPost(postJsonObject: {}) -> bool: """Is the given object a blog post? + There isn't any difference between a blog post and a newswire blog post + but we may here need to check for different properties than + isBlogPost does """ if not postJsonObject: return False @@ -302,14 +396,41 @@ def isaBlogPost(postJsonObject: {}) -> bool: if postJsonObject['object'].get('summary') and \ postJsonObject['object'].get('url') and \ postJsonObject['object'].get('published'): - return True + return isPublicPost(postJsonObject) return False +def getHashtagsFromPost(postJsonObject: {}) -> []: + """Returns a list of any hashtags within a post + """ + if not postJsonObject.get('object'): + return [] + if not isinstance(postJsonObject['object'], dict): + return [] + if not postJsonObject['object'].get('tag'): + return [] + if not isinstance(postJsonObject['object']['tag'], list): + return [] + tags = [] + for tg in postJsonObject['object']['tag']: + if not isinstance(tg, dict): + continue + if not tg.get('name'): + continue + if not tg.get('type'): + continue + if tg['type'] != 'Hashtag': + continue + if tg['name'] not in tags: + tags.append(tg['name']) + return tags + + def addAccountBlogsToNewswire(baseDir: str, nickname: str, domain: str, newswire: {}, maxBlogsPerAccount: int, - indexFilename: str) -> None: + indexFilename: str, + maxTags: int) -> None: """Adds blogs for the given account to the newswire """ if not os.path.isfile(indexFilename): @@ -355,7 +476,7 @@ def addAccountBlogsToNewswire(baseDir: str, nickname: str, domain: str, postJsonObject = None if fullPostFilename: postJsonObject = loadJson(fullPostFilename) - if isaBlogPost(postJsonObject): + if isNewswireBlogPost(postJsonObject): published = postJsonObject['object']['published'] published = published.replace('T', ' ') published = published.replace('Z', '+00:00') @@ -363,18 +484,23 @@ def addAccountBlogsToNewswire(baseDir: str, nickname: str, domain: str, if os.path.isfile(fullPostFilename + '.votes'): votes = loadJson(fullPostFilename + '.votes') description = '' - newswire[published] = \ - [postJsonObject['object']['summary'], - postJsonObject['object']['url'], votes, - fullPostFilename, description, moderated] + addNewswireDictEntry(baseDir, domain, + newswire, published, + postJsonObject['object']['summary'], + postJsonObject['object']['url'], + votes, fullPostFilename, + description, moderated, False, + getHashtagsFromPost(postJsonObject), + maxTags) ctr += 1 if ctr >= maxBlogsPerAccount: break -def addBlogsToNewswire(baseDir: str, newswire: {}, - maxBlogsPerAccount: int) -> None: +def addBlogsToNewswire(baseDir: str, domain: str, newswire: {}, + maxBlogsPerAccount: int, + maxTags: int) -> None: """Adds blogs from each user account into the newswire """ moderationDict = {} @@ -404,7 +530,7 @@ def addBlogsToNewswire(baseDir: str, newswire: {}, domain = handle.split('@')[1] addAccountBlogsToNewswire(baseDir, nickname, domain, newswire, maxBlogsPerAccount, - blogsIndex) + blogsIndex, maxTags) # sort the moderation dict into chronological order, latest first sortedModerationDict = \ @@ -419,8 +545,9 @@ def addBlogsToNewswire(baseDir: str, newswire: {}, os.remove(newswireModerationFilename) -def getDictFromNewswire(session, baseDir: str, - maxPostsPerSource: int, maxFeedSizeKb: int) -> {}: +def getDictFromNewswire(session, baseDir: str, domain: str, + maxPostsPerSource: int, maxFeedSizeKb: int, + maxTags: int) -> {}: """Gets rss feeds as a dictionary from newswire file """ subscriptionsFilename = baseDir + '/accounts/newswire.txt' @@ -451,13 +578,21 @@ def getDictFromNewswire(session, baseDir: str, moderated = True url = url.replace('*', '').strip() - itemsList = getRSS(baseDir, session, url, moderated, + # should this feed content be mirrored? + mirrored = False + if '!' in url: + mirrored = True + url = url.replace('!', '').strip() + + itemsList = getRSS(baseDir, domain, session, url, + moderated, mirrored, maxPostsPerSource, maxFeedSizeKb) for dateStr, item in itemsList.items(): result[dateStr] = item # add blogs from each user account - addBlogsToNewswire(baseDir, result, maxPostsPerSource) + addBlogsToNewswire(baseDir, domain, result, + maxPostsPerSource, maxTags) # sort into chronological order, latest first sortedResult = OrderedDict(sorted(result.items(), reverse=True)) diff --git a/person.py b/person.py index f4539e180..76f3d31e5 100644 --- a/person.py +++ b/person.py @@ -233,6 +233,15 @@ def createPersonBase(baseDir: str, nickname: str, domain: str, port: int, personName = originalDomain approveFollowers = True personType = 'Application' + elif nickname == 'news': + # shared inbox + inboxStr = httpPrefix + '://' + domain + '/actor/news' + personId = httpPrefix + '://' + domain + '/actor' + personUrl = httpPrefix + '://' + domain + \ + '/about/more?news_actor=true' + personName = originalDomain + approveFollowers = True + personType = 'Application' # NOTE: these image files don't need to have # cryptographically secure names diff --git a/posts.py b/posts.py index cd85388d5..ed93f04e9 100644 --- a/posts.py +++ b/posts.py @@ -49,9 +49,9 @@ from utils import getConfigParam from utils import locateNewsVotes from utils import locateNewsArrival from utils import votesOnNewswireItem +from utils import removeHtml from media import attachMedia from media import replaceYouTube -from content import removeHtml from content import removeLongWords from content import addHtmlTags from content import replaceEmojiFromTags @@ -3217,11 +3217,21 @@ def archivePostsForPerson(httpPrefix: str, nickname: str, domain: str, if not os.path.isfile(filePath): continue if archiveDir: - repliesPath = filePath.replace('.json', '.replies') archivePath = os.path.join(archiveDir, postFilename) os.rename(filePath, archivePath) - if os.path.isfile(repliesPath): - os.rename(repliesPath, archivePath) + + extensions = ('replies', 'votes', 'arrived', 'muted') + for ext in extensions: + extPath = filePath.replace('.json', '.' + ext) + if os.path.isfile(extPath): + os.rename(extPath, + archivePath.replace('.json', '.' + ext)) + else: + extPath = filePath.replace('.json', + '.json.' + ext) + if os.path.isfile(extPath): + os.rename(extPath, + archivePath.replace('.json', '.json.' + ext)) else: deletePost(baseDir, httpPrefix, nickname, domain, filePath, False, recentPostsCache) diff --git a/principlesofunity.txt b/principlesofunity.txt new file mode 100644 index 000000000..b16eebdff --- /dev/null +++ b/principlesofunity.txt @@ -0,0 +1,23 @@ +If you are setting up a media instance then this could be used as the Terms of Service. It's the original Indymedia principles from 2001. + +PRINCIPLES OF UNITY + +1. The Independent Media Center Network (IMCN) is based upon principles of equality, decentralization and local autonomy. The IMCN is not derived from a centralized bureaucratic process, but from the self-organization of autonomous collectives that recognize the importance in developing a union of networks. + +2. All IMC's consider open exchange of and open access to information a prerequisite to the building of a more free and just society. + +3. All IMC's respect the right of activists who choose not to be photographed or filmed. + +4. All IMC's, based upon the trust of their contributors and readers, shall utilize open web based publishing, allowing individuals, groups and organizations to express their views, anonymously if desired. + +5. The IMC Network and all local IMC collectives shall be not-for-profit. + +6. All IMC's recognize the importance of process to social change and are committed to the development of non-hierarchical and anti-authoritarian relationships, from interpersonal relationships to group dynamics. Therefore, shall organize themselves collectively and be committed to the principle of consensus decision making and the development of a direct, participatory democratic process] that is transparent to its membership. + +7. All IMC's recognize that a prerequisite for participation in the decision making process of each local group is the contribution of an individual's labor to the group. + +8. All IMC's are committed to caring for one another and our respective communities both collectively and as individuals and will promote the sharing of resources including knowledge, skills and equipment. + +9. All IMC's shall be committed to the use of free source code, whenever possible, in order to develop the digital infrastructure, and to increase the independence of the network by not relying on proprietary software. + +10. All IMC's shall be committed to the principle of human equality, and shall not discriminate, including discrimination based upon race, gender, age, class or sexual orientation. Recognizing the vast cultural traditions within the network, we are committed to building diversity within our localities. diff --git a/scripts/clearnewswire b/scripts/clearnewswire index 39999724f..f2faf45e2 100755 --- a/scripts/clearnewswire +++ b/scripts/clearnewswire @@ -2,6 +2,9 @@ rm accounts/news@*/outbox/* rm accounts/news@*/postcache/* rm accounts/news@*/outbox.index +if [ -d accounts/newsmirror ]; then + rm -rf accounts/newsmirror +fi if [ -f accounts/.newswirestate.json ]; then rm accounts/.newswirestate.json fi diff --git a/tests.py b/tests.py index 082108b61..647a8376a 100644 --- a/tests.py +++ b/tests.py @@ -43,6 +43,7 @@ from utils import loadJson from utils import saveJson from utils import getStatusNumber from utils import getFollowersOfPerson +from utils import removeHtml from follow import followerOfPerson from follow import unfollowPerson from follow import unfollowerOfPerson @@ -71,7 +72,6 @@ from inbox import validInboxFilenames from content import htmlReplaceEmailQuote from content import htmlReplaceQuoteMarks from content import dangerousMarkup -from content import removeHtml from content import addWebLinks from content import replaceEmojiFromTags from content import addHtmlTags @@ -82,6 +82,9 @@ from content import removeHtmlTag from theme import setCSSparam from jsonldsig import testSignJsonld from jsonldsig import jsonldVerify +from newsdaemon import hashtagRuleTree +from newsdaemon import hashtagRuleResolve +from newswire import getNewswireTags testServerAliceRunning = False testServerBobRunning = False @@ -288,7 +291,9 @@ def createServerAlice(path: str, domain: str, port: int, onionDomain = None i2pDomain = None print('Server running: Alice') - runDaemon(1024, 5, False, 0, False, 1, False, False, False, + runDaemon(False, True, False, False, True, 10, False, + 0, 100, 1024, 5, False, + 0, False, 1, False, False, False, 5, True, True, 'en', __version__, "instanceId", False, path, domain, onionDomain, i2pDomain, None, port, port, @@ -351,7 +356,9 @@ def createServerBob(path: str, domain: str, port: int, onionDomain = None i2pDomain = None print('Server running: Bob') - runDaemon(1024, 5, False, 0, False, 1, False, False, False, + runDaemon(False, True, False, False, True, 10, False, + 0, 100, 1024, 5, False, 0, + False, 1, False, False, False, 5, True, True, 'en', __version__, "instanceId", False, path, domain, onionDomain, i2pDomain, None, port, port, @@ -388,7 +395,9 @@ def createServerEve(path: str, domain: str, port: int, federationList: [], onionDomain = None i2pDomain = None print('Server running: Eve') - runDaemon(1024, 5, False, 0, False, 1, False, False, False, + runDaemon(False, True, False, False, True, 10, False, + 0, 100, 1024, 5, False, 0, + False, 1, False, False, False, 5, True, True, 'en', __version__, "instanceId", False, path, domain, onionDomain, i2pDomain, None, port, port, @@ -753,7 +762,7 @@ def testFollowBetweenServers(): clientToServer, federationList, aliceSendThreads, alicePostLog, aliceCachedWebfingers, alicePersonCache, - True, __version__) + True, __version__, False) print('sendResult: ' + str(sendResult)) for t in range(10): @@ -1665,6 +1674,15 @@ def testWebLinks(): resultText = removeLongWords(exampleText, 40, []) assert resultText == exampleText + exampleText = \ + 'some.incredibly.long.and.annoying.word.which.should.be.removed: ' + \ + 'The remaining text' + resultText = removeLongWords(exampleText, 40, []) + print('resultText: ' + resultText) + assert resultText == \ + 'some.incredibly.long.and.annoying.word.w\n' + \ + 'hich.should.be.removed: The remaining text' + exampleText = \ '

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 = '#ExcitingHashtag' + \ + 'Compelling description with #ExcitingHashtag, which is ' + \ + 'being posted in #BoringForum' + tags = getNewswireTags(rssDescription, 10) + assert len(tags) == 2 + assert '#BoringForum' in tags + assert '#ExcitingHashtag' in tags + + def runAllTests(): print('Running tests...') + testGetNewswireTags() + testHashtagRuleTree() testRemoveHtmlTag() testReplaceEmailQuote() testConstantTimeStringCheck() diff --git a/theme.py b/theme.py index 8e5a59a10..a54bf0b13 100644 --- a/theme.py +++ b/theme.py @@ -25,8 +25,9 @@ def getThemesList() -> []: and to lookup function names """ return ('Default', 'Blue', 'Hacker', 'Henge', 'HighVis', - 'Indymedia', 'LCD', 'Light', 'Night', 'Purple', - 'Solidaric', 'Starlight', 'Zen') + 'IndymediaClassic', 'IndymediaModern', + 'LCD', 'Light', 'Night', 'Purple', 'Solidaric', + 'Starlight', 'Zen') def setThemeInConfig(baseDir: str, name: str) -> bool: @@ -40,6 +41,74 @@ def setThemeInConfig(baseDir: str, name: str) -> bool: return saveJson(configJson, configFilename) +def setNewswirePublishAsIcon(baseDir: str, useIcon: bool) -> bool: + """Shows the newswire publish action as an icon or a button + """ + configFilename = baseDir + '/config.json' + if not os.path.isfile(configFilename): + return False + configJson = loadJson(configFilename, 0) + if not configJson: + return False + configJson['showPublishAsIcon'] = useIcon + return saveJson(configJson, configFilename) + + +def setIconsAsButtons(baseDir: str, useButtons: bool) -> bool: + """Whether to show icons in the header (inbox, outbox, etc) + as buttons + """ + configFilename = baseDir + '/config.json' + if not os.path.isfile(configFilename): + return False + configJson = loadJson(configFilename, 0) + if not configJson: + return False + configJson['iconsAsButtons'] = useButtons + return saveJson(configJson, configFilename) + + +def setRssIconAtTop(baseDir: str, atTop: bool) -> bool: + """Whether to show RSS icon at the top of the timeline + """ + configFilename = baseDir + '/config.json' + if not os.path.isfile(configFilename): + return False + configJson = loadJson(configFilename, 0) + if not configJson: + return False + configJson['rssIconAtTop'] = atTop + return saveJson(configJson, configFilename) + + +def setPublishButtonAtTop(baseDir: str, atTop: bool) -> bool: + """Whether to show the publish button above the title image + in the newswire column + """ + configFilename = baseDir + '/config.json' + if not os.path.isfile(configFilename): + return False + configJson = loadJson(configFilename, 0) + if not configJson: + return False + configJson['publishButtonAtTop'] = atTop + return saveJson(configJson, configFilename) + + +def setFullWidthTimelineButtonHeader(baseDir: str, fullWidth: bool) -> bool: + """Shows the timeline button header containing inbox, outbox, + calendar, etc as full width + """ + configFilename = baseDir + '/config.json' + if not os.path.isfile(configFilename): + return False + configJson = loadJson(configFilename, 0) + if not configJson: + return False + configJson['fullWidthTimelineButtonHeader'] = fullWidth + return saveJson(configJson, configFilename) + + def getTheme(baseDir: str) -> str: configFilename = baseDir + '/config.json' if os.path.isfile(configFilename): @@ -231,6 +300,11 @@ def setThemeDefault(baseDir: str): name = 'default' removeTheme(baseDir) setThemeInConfig(baseDir, name) + setNewswirePublishAsIcon(baseDir, True) + setFullWidthTimelineButtonHeader(baseDir, False) + setIconsAsButtons(baseDir, False) + setRssIconAtTop(baseDir, True) + setPublishButtonAtTop(baseDir, False) bgParams = { "login": "jpg", "follow": "jpg", @@ -238,15 +312,21 @@ def setThemeDefault(baseDir: str): "search": "jpg" } themeParams = { - "dummy": "1234" + "banner-height": "20vh", + "banner-height-mobile": "10vh" } setThemeFromDict(baseDir, name, themeParams, bgParams) -def setThemeIndymedia(baseDir: str): - name = 'indymedia' +def setThemeIndymediaClassic(baseDir: str): + name = 'indymediaclassic' removeTheme(baseDir) setThemeInConfig(baseDir, name) + setNewswirePublishAsIcon(baseDir, True) + setFullWidthTimelineButtonHeader(baseDir, True) + setIconsAsButtons(baseDir, False) + setRssIconAtTop(baseDir, False) + setPublishButtonAtTop(baseDir, False) bgParams = { "login": "jpg", "follow": "jpg", @@ -254,8 +334,11 @@ def setThemeIndymedia(baseDir: str): "search": "jpg" } themeParams = { + "container-button-padding": "0px", + "hashtag-background-color": "darkred", "font-size-newswire": "18px", - "font-size-newswire-mobile": "48px", + "font-size-publish-button": "18px", + "font-size-newswire-mobile": "40px", "line-spacing-newswire": "100%", "newswire-item-moderated-color": "white", "newswire-date-moderated-color": "white", @@ -267,7 +350,7 @@ def setThemeIndymedia(baseDir: str): "button-corner-radius": "5px", "timeline-border-radius": "5px", "focus-color": "blue", - "font-size-button-mobile": "36px", + "font-size-button-mobile": "26px", "font-size": "32px", "font-size2": "26px", "font-size3": "40px", @@ -288,11 +371,13 @@ def setThemeIndymedia(baseDir: str): "main-bg-color-dm": "#0b0a0a", "border-color": "#003366", "border-width": "0", + "border-width-header": "0", "main-bg-color-reply": "#0f0d10", "main-bg-color-report": "#0f0d10", "hashtag-vertical-spacing3": "100px", "hashtag-vertical-spacing4": "150px", "button-background-hover": "darkblue", + "button-text-hover": "white", "publish-button-background": "#ff9900", "publish-button-text": "#003366", "button-background": "#003366", @@ -323,7 +408,14 @@ def setThemeBlue(baseDir: str): name = 'blue' removeTheme(baseDir) setThemeInConfig(baseDir, name) + setNewswirePublishAsIcon(baseDir, True) + setFullWidthTimelineButtonHeader(baseDir, False) + setIconsAsButtons(baseDir, False) + setRssIconAtTop(baseDir, True) + setPublishButtonAtTop(baseDir, False) themeParams = { + "banner-height": "20vh", + "banner-height-mobile": "10vh", "newswire-date-color": "blue", "font-size-header": "22px", "font-size-header-mobile": "32px", @@ -360,13 +452,21 @@ def setThemeNight(baseDir: str): name = 'night' removeTheme(baseDir) setThemeInConfig(baseDir, name) + setNewswirePublishAsIcon(baseDir, True) + setFullWidthTimelineButtonHeader(baseDir, False) + setIconsAsButtons(baseDir, False) + setRssIconAtTop(baseDir, True) + setPublishButtonAtTop(baseDir, False) fontStr = \ "url('./fonts/solidaric.woff2') format('woff2')" fontStrItalic = \ "url('./fonts/solidaric-italic.woff2') format('woff2')" themeParams = { + "column-left-header-background": "#07447c", + "banner-height": "15vh", + "banner-height-mobile": "10vh", "focus-color": "blue", - "font-size-button-mobile": "36px", + "font-size-button-mobile": "26px", "font-size": "32px", "font-size2": "26px", "font-size3": "40px", @@ -387,6 +487,7 @@ def setThemeNight(baseDir: str): "hashtag-vertical-spacing3": "100px", "hashtag-vertical-spacing4": "150px", "button-background-hover": "#0481f5", + "button-text-hover": "#0f0d10", "publish-button-background": "#07447c", "button-background": "#07447c", "button-selected": "#0481f5", @@ -395,6 +496,8 @@ def setThemeNight(baseDir: str): "day-number": "#a961ab", "day-number2": "#555", "time-color": "#a961ab", + "time-vertical-align": "-4px", + "time-vertical-align-mobile": "15px", "place-color": "#a961ab", "event-color": "#a961ab", "event-background": "#333", @@ -417,11 +520,17 @@ def setThemeStarlight(baseDir: str): name = 'starlight' removeTheme(baseDir) setThemeInConfig(baseDir, name) + setNewswirePublishAsIcon(baseDir, True) + setFullWidthTimelineButtonHeader(baseDir, False) + setIconsAsButtons(baseDir, False) + setRssIconAtTop(baseDir, True) + setPublishButtonAtTop(baseDir, False) themeParams = { + "column-left-header-background": "#69282c", "column-left-image-width-mobile": "40vw", "line-spacing-newswire": "120%", "focus-color": "darkred", - "font-size-button-mobile": "36px", + "font-size-button-mobile": "26px", "font-size": "32px", "font-size2": "26px", "font-size3": "40px", @@ -440,11 +549,13 @@ def setThemeStarlight(baseDir: str): "main-bg-color-dm": "#0b0a0a", "border-color": "#69282c", "border-width": "3px", + "border-width-header": "3px", "main-bg-color-reply": "#0f0d10", "main-bg-color-report": "#0f0d10", "hashtag-vertical-spacing3": "100px", "hashtag-vertical-spacing4": "150px", "button-background-hover": "#a9282c", + "button-text-hover": "#ffc4bc", "publish-button-background": "#69282c", "button-background": "#69282c", "button-small-background": "darkblue", @@ -482,10 +593,15 @@ def setThemeHenge(baseDir: str): name = 'henge' removeTheme(baseDir) setThemeInConfig(baseDir, name) + setNewswirePublishAsIcon(baseDir, True) + setFullWidthTimelineButtonHeader(baseDir, False) + setIconsAsButtons(baseDir, False) + setRssIconAtTop(baseDir, True) + setPublishButtonAtTop(baseDir, False) themeParams = { "column-left-image-width-mobile": "40vw", "column-right-image-width-mobile": "40vw", - "font-size-button-mobile": "36px", + "font-size-button-mobile": "26px", "font-size": "32px", "font-size2": "26px", "font-size3": "40px", @@ -504,11 +620,13 @@ def setThemeHenge(baseDir: str): "main-bg-color-dm": "#343335", "border-color": "#222", "border-width": "5px", + "border-width-header": "5px", "main-bg-color-reply": "#383335", "main-bg-color-report": "#383335", "hashtag-vertical-spacing3": "100px", "hashtag-vertical-spacing4": "150px", "button-background-hover": "#444", + "button-text-hover": "white", "publish-button-background": "#222", "button-background": "#222", "button-selected": "black", @@ -542,7 +660,15 @@ def setThemeZen(baseDir: str): name = 'zen' removeTheme(baseDir) setThemeInConfig(baseDir, name) + setNewswirePublishAsIcon(baseDir, True) + setFullWidthTimelineButtonHeader(baseDir, False) + setIconsAsButtons(baseDir, False) + setRssIconAtTop(baseDir, True) + setPublishButtonAtTop(baseDir, False) themeParams = { + "banner-height": "25vh", + "banner-height-mobile": "10vh", + "newswire-date-color": "yellow", "main-bg-color": "#5c4e41", "column-left-color": "#5c4e41", "text-entry-background": "#5c4e41", @@ -552,6 +678,7 @@ def setThemeZen(baseDir: str): "day-number2": "#5c4e41", "border-color": "#463b35", "border-width": "7px", + "border-width-header": "7px", "main-link-color": "#dddddd", "main-link-color-hover": "white", "title-color": "#dddddd", @@ -599,6 +726,11 @@ def setThemeHighVis(baseDir: str): "search": "jpg" } setThemeFromDict(baseDir, name, themeParams, bgParams) + setNewswirePublishAsIcon(baseDir, True) + setFullWidthTimelineButtonHeader(baseDir, False) + setIconsAsButtons(baseDir, False) + setRssIconAtTop(baseDir, True) + setPublishButtonAtTop(baseDir, False) def setThemeLCD(baseDir: str): @@ -620,6 +752,7 @@ def setThemeLCD(baseDir: str): "main-fg-color": "#33390d", "border-color": "#33390d", "border-width": "5px", + "border-width-header": "5px", "main-link-color": "#9fb42b", "main-link-color-hover": "#cfb42b", "title-color": "#9fb42b", @@ -627,10 +760,12 @@ def setThemeLCD(baseDir: str): "button-selected": "black", "button-highlighted": "green", "button-background-hover": "#a3390d", + "button-text-hover": "#33390d", "publish-button-background": "#33390d", "button-background": "#33390d", "button-small-background": "#33390d", "button-text": "#9fb42b", + "button-selected-text": "#9fb42b", "publish-button-text": "#9fb42b", "button-small-text": "#9fb42b", "color: #FFFFFE;": "color: #9fb42b;", @@ -674,6 +809,11 @@ def setThemeLCD(baseDir: str): "search": "jpg" } setThemeFromDict(baseDir, name, themeParams, bgParams) + setNewswirePublishAsIcon(baseDir, True) + setFullWidthTimelineButtonHeader(baseDir, False) + setIconsAsButtons(baseDir, False) + setRssIconAtTop(baseDir, True) + setPublishButtonAtTop(baseDir, False) def setThemePurple(baseDir: str): @@ -681,7 +821,7 @@ def setThemePurple(baseDir: str): fontStr = \ "url('./fonts/CheGuevaraTextSans-Regular.woff2') format('woff2')" themeParams = { - "font-size-button-mobile": "36px", + "font-size-button-mobile": "26px", "font-size": "32px", "font-size2": "26px", "font-size3": "40px", @@ -702,10 +842,12 @@ def setThemePurple(baseDir: str): "main-visited-color": "#f93bb0", "button-selected": "#c042a0", "button-background-hover": "#af42a0", + "button-text-hover": "#f98bb0", "publish-button-background": "#ff42a0", "button-background": "#ff42a0", "button-small-background": "#ff42a0", "button-text": "white", + "button-selected-text": "white", "publish-button-text": "white", "button-small-text": "white", "color: #FFFFFE;": "color: #1f152d;", @@ -733,6 +875,11 @@ def setThemePurple(baseDir: str): "search": "jpg" } setThemeFromDict(baseDir, name, themeParams, bgParams) + setNewswirePublishAsIcon(baseDir, True) + setFullWidthTimelineButtonHeader(baseDir, False) + setIconsAsButtons(baseDir, False) + setRssIconAtTop(baseDir, True) + setPublishButtonAtTop(baseDir, False) def setThemeHacker(baseDir: str): @@ -755,10 +902,12 @@ def setThemeHacker(baseDir: str): "main-visited-color": "#3c8234", "button-selected": "#063200", "button-background-hover": "#a62200", + "button-text-hover": "#00ff00", "publish-button-background": "#062200", "button-background": "#062200", "button-small-background": "#062200", "button-text": "#00ff00", + "button-selected-text": "#00ff00", "publish-button-text": "#00ff00", "button-small-text": "#00ff00", "button-corner-radius": "4px", @@ -789,13 +938,21 @@ def setThemeHacker(baseDir: str): "search": "jpg" } setThemeFromDict(baseDir, name, themeParams, bgParams) + setNewswirePublishAsIcon(baseDir, True) + setFullWidthTimelineButtonHeader(baseDir, False) + setIconsAsButtons(baseDir, False) + setRssIconAtTop(baseDir, True) + setPublishButtonAtTop(baseDir, False) def setThemeLight(baseDir: str): name = 'light' themeParams = { + "banner-height": "20vh", + "banner-height-mobile": "10vh", + "hashtag-background-color": "lightblue", "focus-color": "grey", - "font-size-button-mobile": "36px", + "font-size-button-mobile": "26px", "font-size": "32px", "font-size2": "26px", "font-size3": "40px", @@ -847,16 +1004,126 @@ def setThemeLight(baseDir: str): "search": "jpg" } setThemeFromDict(baseDir, name, themeParams, bgParams) + setNewswirePublishAsIcon(baseDir, True) + setFullWidthTimelineButtonHeader(baseDir, False) + setIconsAsButtons(baseDir, False) + setRssIconAtTop(baseDir, True) + setPublishButtonAtTop(baseDir, False) + + +def setThemeIndymediaModern(baseDir: str): + name = 'indymediamodern' + fontStr = \ + "url('./fonts/NimbusSanL.otf') format('opentype')" + fontStrItalic = \ + "url('./fonts/NimbusSanL-italic.otf') format('opentype')" + themeParams = { + "publish-button-vertical-offset": "10px", + "container-button-padding": "0px", + "container-button-margin": "0px", + "column-right-icon-size": "11%", + "button-height-padding": "5px", + "icon-brightness-change": "70%", + "border-width-header": "0px", + "tab-border-width": "3px", + "tab-border-color": "grey", + "button-corner-radius": "0px", + "login-button-color": "#25408f", + "login-button-fg-color": "white", + "column-left-width": "10vw", + "column-center-width": "70vw", + "column-right-width": "20vw", + "column-right-fg-color": "#25408f", + "column-right-fg-color-voted-on": "red", + "newswire-item-moderated-color": "red", + "newswire-date-moderated-color": "red", + "newswire-date-color": "grey", + "timeline-border-radius": "0px", + "button-background": "#767674", + "button-background-hover": "#555", + "button-text-hover": "white", + "button-selected": "white", + "button-selected-text": "black", + "button-text": "white", + "hashtag-fg-color": "white", + "publish-button-background": "#25408f", + "publish-button-text": "white", + "hashtag-background-color": "#b2b2b2", + "focus-color": "grey", + "font-size-button-mobile": "26px", + "font-size-publish-button": "26px", + "font-size": "32px", + "font-size2": "26px", + "font-size3": "40px", + "font-size4": "24px", + "font-size5": "22px", + "rgba(0, 0, 0, 0.5)": "rgba(0, 0, 0, 0.0)", + "column-left-color": "white", + "main-bg-color": "white", + "main-bg-color-dm": "white", + "link-bg-color": "white", + "main-bg-color-reply": "white", + "main-bg-color-report": "white", + "main-header-color-roles": "#ebebf0", + "main-fg-color": "black", + "column-left-fg-color": "#25408f", + "border-color": "#c0cdd9", + "main-link-color": "#25408f", + "main-link-color-hover": "#10408f", + "title-color": "#2a2c37", + "main-visited-color": "#25408f", + "text-entry-foreground": "#111", + "text-entry-background": "white", + "font-color-header": "black", + "dropdown-fg-color": "#222", + "dropdown-fg-color-hover": "#222", + "dropdown-bg-color": "#e6ebf0", + "dropdown-bg-color-hover": "lightblue", + "color: #FFFFFE;": "color: black;", + "calendar-bg-color": "#e6ebf0", + "lines-color": "darkblue", + "day-number": "black", + "day-number2": "#282c37", + "place-color": "black", + "event-color": "#282c37", + "today-foreground": "white", + "today-circle": "red", + "event-background": "lightblue", + "event-foreground": "white", + "title-text": "#282c37", + "title-background": "#ccc", + "gallery-text-color": "black", + "*font-family": "'NimbusSanL'", + "*src": fontStr, + "**src": fontStrItalic + } + bgParams = { + "login": "jpg", + "follow": "jpg", + "options": "jpg", + "search": "jpg" + } + setThemeFromDict(baseDir, name, themeParams, bgParams) + setNewswirePublishAsIcon(baseDir, False) + setFullWidthTimelineButtonHeader(baseDir, True) + setIconsAsButtons(baseDir, True) + setRssIconAtTop(baseDir, False) + setPublishButtonAtTop(baseDir, True) def setThemeSolidaric(baseDir: str): name = 'solidaric' themeParams = { + "banner-height": "35vh", + "banner-height-mobile": "15vh", + "time-vertical-align": "-4px", + "time-vertical-align-mobile": "15px", + "hashtag-background-color": "lightred", "button-highlighted": "darkred", "button-selected-highlighted": "darkred", "newswire-date-color": "grey", "focus-color": "grey", - "font-size-button-mobile": "36px", + "font-size-button-mobile": "26px", "font-size": "32px", "font-size2": "26px", "font-size3": "40px", @@ -911,6 +1178,11 @@ def setThemeSolidaric(baseDir: str): "search": "jpg" } setThemeFromDict(baseDir, name, themeParams, bgParams) + setNewswirePublishAsIcon(baseDir, True) + setFullWidthTimelineButtonHeader(baseDir, False) + setIconsAsButtons(baseDir, False) + setRssIconAtTop(baseDir, True) + setPublishButtonAtTop(baseDir, False) def setThemeImages(baseDir: str, name: str) -> None: diff --git a/translations/ar.json b/translations/ar.json index de49c1547..607b7ce40 100644 --- a/translations/ar.json +++ b/translations/ar.json @@ -287,6 +287,8 @@ "Autogenerated Hashtags": "علامات التجزئة المُنشأة تلقائيًا", "Autogenerated Content Warnings": "تحذيرات المحتوى المُنشأ تلقائيًا", "Indymedia": "Indymedia", + "IndymediaClassic": "Indymedia Classic", + "IndymediaModern": "Indymedia Modern", "Hashtag Blocked": "Hashtag محظور", "This is a blogging instance": "هذا مثال على المدونات", "Edit Links": "تحرير الارتباطات", @@ -311,5 +313,16 @@ "Site Editors": "محررو الموقع", "Allow news posts": "السماح بنشر الأخبار", "Publish": "ينشر", - "Publish a news article": "انشر مقالة إخبارية" + "Publish a news article": "انشر مقالة إخبارية", + "News tagging rules": "قواعد وسم الأخبار", + "See instructions": "انظر التعليمات", + "Search": "بحث", + "Expand": "وسعت", + "Newswire": "نيوزواير", + "Links": "الروابط", + "Post": "بريد", + "User": "المستعمل", + "Features" : "ميزات", + "Article": "مقال إخباري", + "Create an article": "قم بإنشاء مقال" } diff --git a/translations/ca.json b/translations/ca.json index 3499c9de7..0b0b702d1 100644 --- a/translations/ca.json +++ b/translations/ca.json @@ -287,6 +287,8 @@ "Autogenerated Hashtags": "Hashtags autogenerats", "Autogenerated Content Warnings": "Advertiments de contingut autogenerats", "Indymedia": "Indymedia", + "IndymediaClassic": "Indymedia Classic", + "IndymediaModern": "Indymedia Modern", "Hashtag Blocked": "Hashtag bloquejat", "This is a blogging instance": "Aquesta és una instància de blocs", "Edit Links": "Edita els enllaços", @@ -311,5 +313,16 @@ "Site Editors": "Editors de llocs", "Allow news posts": "Permet publicacions de notícies", "Publish": "Publica", - "Publish a news article": "Publicar un article de notícies" + "Publish a news article": "Publicar un article de notícies", + "News tagging rules": "Regles d'etiquetatge de notícies", + "See instructions": "Consulteu les instruccions", + "Search": "Cerca", + "Expand": "Amplia", + "Newswire": "Newswire", + "Links": "Enllaços", + "Post": "Publicació", + "User": "Usuari", + "Features" : "Article", + "Article": "Reportatge", + "Create an article": "Creeu un article" } diff --git a/translations/cy.json b/translations/cy.json index 2ab1795cc..e4f548916 100644 --- a/translations/cy.json +++ b/translations/cy.json @@ -287,6 +287,8 @@ "Autogenerated Hashtags": "Hashtags awtogeneiddiedig", "Autogenerated Content Warnings": "Rhybuddion Cynnwys Autogenerated", "Indymedia": "Indymedia", + "IndymediaClassic": "Indymedia Classic", + "IndymediaModern": "Indymedia Modern", "Hashtag Blocked": "Hashtag wedi'i Blocio", "This is a blogging instance": "Dyma enghraifft blogio", "Edit Links": "Golygu Dolenni", @@ -311,5 +313,16 @@ "Site Editors": "Golygyddion Safle", "Allow news posts": "Caniatáu swyddi newyddion", "Publish": "Cyhoeddi", - "Publish a news article": "Cyhoeddi erthygl newyddion" + "Publish a news article": "Cyhoeddi erthygl newyddion", + "News tagging rules": "Rheolau tagio newyddion", + "See instructions": "Gweler y cyfarwyddiadau", + "Search": "Chwilio", + "Expand": "Ehangu", + "Newswire": "Newswire", + "Links": "Dolenni", + "Post": "Post", + "User": "Defnyddiwr", + "Features" : "Nodweddion", + "Article": "Erthygl", + "Create an article": "Creu erthygl" } diff --git a/translations/de.json b/translations/de.json index ceb6179e6..c486d8444 100644 --- a/translations/de.json +++ b/translations/de.json @@ -287,6 +287,8 @@ "Autogenerated Hashtags": "Automatisch generierte Hashtags", "Autogenerated Content Warnings": "Warnungen vor automatisch generierten Inhalten", "Indymedia": "Indymedia", + "IndymediaClassic": "Indymedia Classic", + "IndymediaModern": "Indymedia Modern", "Hashtag Blocked": "Hashtag blockiert", "This is a blogging instance": "Dies ist eine Blogging-Instanz", "Edit Links": "Links bearbeiten", @@ -311,5 +313,16 @@ "Site Editors": "Site-Editoren", "Allow news posts": "Nachrichtenbeiträge zulassen", "Publish": "Veröffentlichen", - "Publish a news article": "Veröffentlichen Sie einen Nachrichtenartikel" + "Publish a news article": "Veröffentlichen Sie einen Nachrichtenartikel", + "News tagging rules": "Regeln für das Markieren von Nachrichten", + "See instructions": "Siehe Anweisungen", + "Search": "Suche", + "Expand": "Erweitern", + "Newswire": "Newswire", + "Links": "Links", + "Post": "Post", + "User": "Nutzerin", + "Features" : "Eigenschaften", + "Article": "Artikel", + "Create an article": "Erstellen Sie einen Artikel" } diff --git a/translations/en.json b/translations/en.json index e5a65517c..2dcd54fda 100644 --- a/translations/en.json +++ b/translations/en.json @@ -287,6 +287,8 @@ "Autogenerated Hashtags": "Autogenerated Hashtags", "Autogenerated Content Warnings": "Autogenerated Content Warnings", "Indymedia": "Indymedia", + "IndymediaClassic": "Indymedia Classic", + "IndymediaModern": "Indymedia Modern", "Hashtag Blocked": "Hashtag Blocked", "This is a blogging instance": "This is a blogging instance", "Edit Links": "Edit Links", @@ -295,7 +297,7 @@ "Right column image": "Right column image", "RSS feed for this site": "RSS feed for this site", "Edit newswire": "Edit newswire", - "Add RSS feed links below.": "RSS feed links below. Add a * at the beginning or end to indicate that a feed should be moderated.", + "Add RSS feed links below.": "RSS feed links below. Add a * at the beginning or end to indicate that a feed should be moderated. Add a ! at the beginning or end to indicate that the feed content should be mirrored.", "Newswire RSS Feed": "Newswire RSS Feed", "Nicknames whose blog entries appear on the newswire.": "Nicknames whose blog entries appear on the newswire.", "Posts to be approved": "Posts to be approved", @@ -311,5 +313,16 @@ "Site Editors": "Site Editors", "Allow news posts": "Allow news posts", "Publish": "Publish", - "Publish a news article": "Publish a news article" + "Publish a news article": "Publish a news article", + "News tagging rules": "News tagging rules", + "See instructions": "See instructions", + "Search": "Search", + "Expand": "Expand", + "Newswire": "Newswire", + "Links": "Links", + "Post": "Post", + "User": "User", + "Features" : "Features", + "Article": "Article", + "Create an article": "Create an article" } diff --git a/translations/es.json b/translations/es.json index 32c7ac5f8..c3f84bda0 100644 --- a/translations/es.json +++ b/translations/es.json @@ -287,6 +287,8 @@ "Autogenerated Hashtags": "Hashtags autogenerados", "Autogenerated Content Warnings": "Advertencias de contenido generado automáticamente", "Indymedia": "Indymedia", + "IndymediaClassic": "Indymedia Classic", + "IndymediaModern": "Indymedia Modern", "Hashtag Blocked": "Hashtag bloqueada", "This is a blogging instance": "Esta es una instancia de blogs", "Edit Links": "Editar enlaces", @@ -311,5 +313,16 @@ "Site Editors": "Editores del sitio", "Allow news posts": "Permitir publicaciones de noticias", "Publish": "Publicar", - "Publish a news article": "Publica un artículo de noticias" + "Publish a news article": "Publica un artículo de noticias", + "News tagging rules": "Reglas de etiquetado de noticias", + "See instructions": "Vea las instrucciones", + "Search": "Buscar", + "Expand": "Expandir", + "Newswire": "Newswire", + "Links": "Enlaces", + "Post": "Enviar", + "User": "Usuaria", + "Features" : "Caracteristicas", + "Article": "Artículo", + "Create an article": "Crea un articulo" } diff --git a/translations/fr.json b/translations/fr.json index 068971bde..bb897925f 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -287,6 +287,8 @@ "Autogenerated Hashtags": "Hashtags générés automatiquement", "Autogenerated Content Warnings": "Avertissements de contenu générés automatiquement", "Indymedia": "Indymedia", + "IndymediaClassic": "Indymedia Classic", + "IndymediaModern": "Indymedia Modern", "Hashtag Blocked": "Hashtag bloqué", "This is a blogging instance": "Ceci est une instance de blog", "Edit Links": "Modifier les liens", @@ -311,5 +313,16 @@ "Site Editors": "Éditeurs du site", "Allow news posts": "Autoriser les articles d'actualité", "Publish": "Publier", - "Publish a news article": "Publier un article de presse" + "Publish a news article": "Publier un article de presse", + "News tagging rules": "Règles de marquage des actualités", + "See instructions": "Voir les instructions", + "Search": "Chercher", + "Expand": "Développer", + "Newswire": "Newswire", + "Links": "Liens", + "Post": "Publier", + "User": "Utilisatrice", + "Features" : "Traits", + "Article": "Article", + "Create an article": "Créer un article" } diff --git a/translations/ga.json b/translations/ga.json index 547f6b56a..5cd34c5f8 100644 --- a/translations/ga.json +++ b/translations/ga.json @@ -287,6 +287,8 @@ "Autogenerated Hashtags": "Hashtags uathghinte", "Autogenerated Content Warnings": "Rabhaidh Ábhar Uathghinte", "Indymedia": "Indymedia", + "IndymediaClassic": "Indymedia Classic", + "IndymediaModern": "Indymedia Modern", "Hashtag Blocked": "Hashtag Blocáilte", "This is a blogging instance": "Seo sampla blagála", "Edit Links": "Cuir Naisc in eagar", @@ -311,5 +313,16 @@ "Site Editors": "Eagarthóirí Suímh", "Allow news posts": "Ceadaigh poist nuachta", "Publish": "Fhoilsiú", - "Publish a news article": "Foilsigh alt nuachta" + "Publish a news article": "Foilsigh alt nuachta", + "News tagging rules": "Rialacha clibeála nuachta", + "See instructions": "Féach na treoracha", + "Search": "Cuardaigh", + "Expand": "Leathnaigh", + "Newswire": "Newswire", + "Links": "Naisc", + "Post": "Post", + "User": "Úsáideoir", + "Features" : "Gnéithe", + "Article": "Airteagal", + "Create an article": "Cruthaigh alt" } diff --git a/translations/hi.json b/translations/hi.json index 758a933ab..a8736813f 100644 --- a/translations/hi.json +++ b/translations/hi.json @@ -287,6 +287,8 @@ "Autogenerated Hashtags": "ऑटोजेनरेटेड हैशटैग", "Autogenerated Content Warnings": "स्वतः प्राप्त सामग्री चेतावनी", "Indymedia": "Indymedia", + "IndymediaClassic": "Indymedia Classic", + "IndymediaModern": "Indymedia Modern", "Hashtag Blocked": "हैशटैग अवरुद्ध", "This is a blogging instance": "यह एक ब्लॉगिंग उदाहरण है", "Edit Links": "लिंक संपादित करें", @@ -311,5 +313,16 @@ "Site Editors": "साइट संपादकों", "Allow news posts": "समाचार पोस्ट की अनुमति दें", "Publish": "प्रकाशित करना", - "Publish a news article": "एक समाचार लेख प्रकाशित करें" + "Publish a news article": "एक समाचार लेख प्रकाशित करें", + "News tagging rules": "समाचार टैगिंग नियम", + "See instructions": "निर्देश देखें", + "Search": "खोज", + "Expand": "विस्तार", + "Newswire": "न्यूज़वायर", + "Links": "लिंक", + "Post": "पद", + "User": "उपयोगकर्ता", + "Features" : "विशेषताएं", + "Article": "लेख", + "Create an article": "एक लेख बनाएँ" } diff --git a/translations/it.json b/translations/it.json index e103f50c9..834628eec 100644 --- a/translations/it.json +++ b/translations/it.json @@ -287,6 +287,8 @@ "Autogenerated Hashtags": "Hashtag generati automaticamente", "Autogenerated Content Warnings": "Avvisi sui contenuti generati automaticamente", "Indymedia": "Indymedia", + "IndymediaClassic": "Indymedia Classic", + "IndymediaModern": "Indymedia Modern", "Hashtag Blocked": "Hashtag bloccato", "This is a blogging instance": "Questa è un'istanza di blog", "Edit Links": "Modifica collegamenti", @@ -311,5 +313,16 @@ "Site Editors": "Editori del sito", "Allow news posts": "Consenti post di notizie", "Publish": "Pubblicare", - "Publish a news article": "Pubblica un articolo di notizie" + "Publish a news article": "Pubblica un articolo di notizie", + "News tagging rules": "Regole di tagging delle notizie", + "See instructions": "Vedere le istruzioni", + "Search": "Ricerca", + "Expand": "Espandere", + "Newswire": "Newswire", + "Links": "Collegamenti", + "Post": "Inviare", + "User": "Utente", + "Features" : "Caratteristiche", + "Article": "Articolo", + "Create an article": "Crea un articolo" } diff --git a/translations/ja.json b/translations/ja.json index ac44cd3e0..1c85a5de3 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -287,6 +287,8 @@ "Autogenerated Hashtags": "自動生成されたハッシュタグ", "Autogenerated Content Warnings": "自動生成されたコンテンツの警告", "Indymedia": "Indymedia", + "IndymediaClassic": "Indymedia Classic", + "IndymediaModern": "Indymedia Modern", "Hashtag Blocked": "ハッシュタグがブロックされました", "This is a blogging instance": "これはブログのインスタンスです", "Edit Links": "リンクの編集", @@ -311,5 +313,16 @@ "Site Editors": "サイト編集者", "Allow news posts": "ニュース投稿を許可する", "Publish": "公開する", - "Publish a news article": "ニュース記事を公開する" + "Publish a news article": "ニュース記事を公開する", + "News tagging rules": "ニュースのタグ付けルール", + "See instructions": "手順を参照してください", + "Search": "探す", + "Expand": "展開", + "Newswire": "Newswire", + "Links": "リンク", + "Post": "役職", + "User": "ユーザー", + "Features" : "特徴", + "Article": "論文", + "Create an article": "記事を作成する" } diff --git a/translations/oc.json b/translations/oc.json index 3e7c8007c..0586b23cd 100644 --- a/translations/oc.json +++ b/translations/oc.json @@ -283,6 +283,8 @@ "Autogenerated Hashtags": "Autogenerated Hashtags", "Autogenerated Content Warnings": "Autogenerated Content Warnings", "Indymedia": "Indymedia", + "IndymediaClassic": "Indymedia Classic", + "IndymediaModern": "Indymedia Modern", "Hashtag Blocked": "Hashtag Blocked", "This is a blogging instance": "This is a blogging instance", "Edit Links": "Edit Links", @@ -291,7 +293,7 @@ "Right column image": "Right column image", "RSS feed for this site": "RSS feed for this site", "Edit newswire": "Edit newswire", - "Add RSS feed links below.": "RSS feed links below. Add a * at the beginning or end to indicate that a feed should be moderated.", + "Add RSS feed links below.": "RSS feed links below. Add a * at the beginning or end to indicate that a feed should be moderated. Add a ! at the beginning or end to indicate that the feed content should be mirrored.", "Newswire RSS Feed": "Newswire RSS Feed", "Nicknames whose blog entries appear on the newswire.": "Nicknames whose blog entries appear on the newswire.", "Posts to be approved": "Posts to be approved", @@ -307,5 +309,16 @@ "Site Editors": "Site Editors", "Allow news posts": "Allow news posts", "Publish": "Publish", - "Publish a news article": "Publish a news article" + "Publish a news article": "Publish a news article", + "News tagging rules": "News tagging rules", + "See instructions": "See instructions", + "Search": "Search", + "Expand": "Expand", + "Newswire": "Newswire", + "Links": "Links", + "Post": "Post", + "User": "User", + "Features" : "Features", + "Article": "Article", + "Create an article": "Create an article" } diff --git a/translations/pt.json b/translations/pt.json index a894c6c02..58e95a5bf 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -287,6 +287,8 @@ "Autogenerated Hashtags": "Hashtags autogeradas", "Autogenerated Content Warnings": "Avisos de conteúdo gerado automaticamente", "Indymedia": "Indymedia", + "IndymediaClassic": "Indymedia Classic", + "IndymediaModern": "Indymedia Modern", "Hashtag Blocked": "Hashtag bloqueada", "This is a blogging instance": "Esta é uma instância de blog", "Edit Links": "Editar Links", @@ -311,5 +313,16 @@ "Site Editors": "Editores do site", "Allow news posts": "Permitir postagens de notícias", "Publish": "Publicar", - "Publish a news article": "Publique um artigo de notícias" + "Publish a news article": "Publique um artigo de notícias", + "News tagging rules": "Regras de marcação de notícias", + "See instructions": "Veja as instruções", + "Search": "Pesquisa", + "Expand": "Expandir", + "Newswire": "Newswire", + "Links": "Links", + "Post": "Postar", + "User": "Do utilizador", + "Features" : "Recursos", + "Article": "Artigo", + "Create an article": "Crie um artigo" } diff --git a/translations/ru.json b/translations/ru.json index 6ae4b03f5..75d6c4bd8 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -287,6 +287,8 @@ "Autogenerated Hashtags": "Автоматически сгенерированные хештеги", "Autogenerated Content Warnings": "Автоматические предупреждения о содержании", "Indymedia": "Indymedia", + "IndymediaClassic": "Indymedia Classic", + "IndymediaModern": "Indymedia Modern", "Hashtag Blocked": "Хештег заблокирован", "This is a blogging instance": "Это экземпляр блога", "Edit Links": "Редактировать ссылки", @@ -311,5 +313,16 @@ "Site Editors": "Редакторы сайта", "Allow news posts": "Разрешить публикации новостей", "Publish": "Публиковать", - "Publish a news article": "Опубликовать новостную статью" + "Publish a news article": "Опубликовать новостную статью", + "News tagging rules": "Правила тегирования новостей", + "See instructions": "См. Инструкции", + "Search": "Поиск", + "Expand": "Развернуть", + "Newswire": "Лента новостей", + "Links": "Ссылки", + "Post": "После", + "User": "Пользователь", + "Features" : "особенности", + "Article": "Статья", + "Create an article": "Создать статью" } diff --git a/translations/zh.json b/translations/zh.json index d2a33c46b..b848bd20a 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -287,6 +287,8 @@ "Autogenerated Hashtags": "自动生成的标签", "Autogenerated Content Warnings": "自动生成的内容警告", "Indymedia": "Indymedia", + "IndymediaClassic": "Indymedia Classic", + "IndymediaModern": "Indymedia Modern", "Hashtag Blocked": "标签被阻止", "This is a blogging instance": "这是一个博客实例", "Edit Links": "编辑连结", @@ -311,5 +313,16 @@ "Site Editors": "网站编辑", "Allow news posts": "允许新闻发布", "Publish": "发布", - "Publish a news article": "发布新闻文章" + "Publish a news article": "发布新闻文章", + "News tagging rules": "新闻标记规则", + "See instructions": "见说明", + "Search": "搜索", + "Expand": "扩大", + "Newswire": "新闻专线", + "Links": "链接", + "Post": "发布", + "User": "用户", + "Features" : "特征", + "Article": "文章", + "Create an article": "建立文章" } diff --git a/utils.py b/utils.py index 6ed6c1c62..08d21f159 100644 --- a/utils.py +++ b/utils.py @@ -19,6 +19,25 @@ from calendar import monthrange from followingCalendar import addPersonToCalendar +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 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 += \ + '
\n' + editNewswireForm += '
' + editNewswireForm += ' \n' + + hashtagRulesStr = '' + hashtagRulesFilename = \ + baseDir + '/accounts/hashtagrules.txt' + if os.path.isfile(hashtagRulesFilename): + with open(hashtagRulesFilename, 'r') as rulesfile: + hashtagRulesStr = rulesfile.read() + + editNewswireForm += \ + '
\n' + editNewswireForm += '
\n' + editNewswireForm += \ + ' ' + translate['See instructions'] + '\n' + editNewswireForm += ' \n' + editNewswireForm += \ '' @@ -1352,7 +1401,7 @@ def htmlEditNewswire(translate: {}, baseDir: str, path: str, return editNewswireForm -def htmlEditNewsPost(translate: {}, baseDir: str, path: str, +def htmlEditNewsPost(cssCache: {}, translate: {}, baseDir: str, path: str, domain: str, port: int, httpPrefix: str, postUrl: str) -> str: """Edits a news post @@ -1380,8 +1429,9 @@ def htmlEditNewsPost(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 + '://') @@ -1429,7 +1479,7 @@ def htmlEditNewsPost(translate: {}, baseDir: str, path: str, return editNewsPostForm -def htmlEditProfile(translate: {}, baseDir: str, path: str, +def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str, domain: str, port: int, httpPrefix: str) -> str: """Shows the edit profile screen """ @@ -1616,8 +1666,9 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str, cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' - with open(cssFilename, 'r') as cssFile: - editProfileCSS = cssFile.read() + + editProfileCSS = getCSS(baseDir, cssFilename, cssCache) + if editProfileCSS: if httpPrefix != 'https': editProfileCSS = \ editProfileCSS.replace('https://', httpPrefix + '://') @@ -2059,7 +2110,8 @@ def htmlGetLoginCredentials(loginParams: str, return nickname, password, register -def htmlLogin(translate: {}, baseDir: str, autocomplete=True) -> str: +def htmlLogin(cssCache: {}, translate: {}, + baseDir: str, autocomplete=True) -> str: """Shows the login screen """ accounts = noOfAccounts(baseDir) @@ -2114,8 +2166,11 @@ def htmlLogin(translate: {}, baseDir: str, autocomplete=True) -> str: cssFilename = baseDir + '/epicyon-login.css' if os.path.isfile(baseDir + '/login.css'): cssFilename = baseDir + '/login.css' - with open(cssFilename, 'r') as cssFile: - loginCSS = cssFile.read() + + loginCSS = getCSS(baseDir, cssFilename, cssCache) + if not loginCSS: + print('ERROR: login css file missing ' + cssFilename) + return None # show the register button registerButtonStr = '' @@ -2182,7 +2237,8 @@ def htmlLogin(translate: {}, baseDir: str, autocomplete=True) -> str: return loginForm -def htmlTermsOfService(baseDir: str, httpPrefix: str, domainFull: str) -> str: +def htmlTermsOfService(cssCache: {}, baseDir: str, + httpPrefix: str, domainFull: str) -> str: """Show the terms of service screen """ adminNickname = getConfigParam(baseDir, 'admin') @@ -2204,8 +2260,9 @@ def htmlTermsOfService(baseDir: str, httpPrefix: str, domainFull: str) -> str: cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' - with open(cssFilename, 'r') as cssFile: - termsCSS = cssFile.read() + + termsCSS = getCSS(baseDir, cssFilename, cssCache) + if termsCSS: if httpPrefix != 'https': termsCSS = termsCSS.replace('https://', httpPrefix+'://') @@ -2223,7 +2280,7 @@ def htmlTermsOfService(baseDir: str, httpPrefix: str, domainFull: str) -> str: return TOSForm -def htmlAbout(baseDir: str, httpPrefix: str, +def htmlAbout(cssCache: {}, baseDir: str, httpPrefix: str, domainFull: str, onionDomain: str) -> str: """Show the about screen """ @@ -2246,8 +2303,9 @@ def htmlAbout(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: - aboutCSS = cssFile.read() + + aboutCSS = getCSS(baseDir, cssFilename, cssCache) + if aboutCSS: if httpPrefix != 'http': aboutCSS = aboutCSS.replace('https://', httpPrefix + '://') @@ -2270,15 +2328,16 @@ def htmlAbout(baseDir: str, httpPrefix: str, return aboutForm -def htmlHashtagBlocked(baseDir: str, translate: {}) -> str: +def htmlHashtagBlocked(cssCache: {}, baseDir: str, translate: {}) -> str: """Show the screen for a blocked hashtag """ blockedHashtagForm = '' cssFilename = baseDir + '/epicyon-suspended.css' if os.path.isfile(baseDir + '/suspended.css'): cssFilename = baseDir + '/suspended.css' - with open(cssFilename, 'r') as cssFile: - blockedHashtagCSS = cssFile.read() + + blockedHashtagCSS = getCSS(baseDir, cssFilename, cssCache) + if blockedHashtagCSS: blockedHashtagForm = htmlHeader(cssFilename, blockedHashtagCSS) blockedHashtagForm += '
\n' blockedHashtagForm += \ @@ -2292,15 +2351,16 @@ def htmlHashtagBlocked(baseDir: str, translate: {}) -> str: return blockedHashtagForm -def htmlSuspended(baseDir: str) -> str: +def htmlSuspended(cssCache: {}, baseDir: str) -> str: """Show the screen for suspended accounts """ suspendedForm = '' cssFilename = baseDir + '/epicyon-suspended.css' if os.path.isfile(baseDir + '/suspended.css'): cssFilename = baseDir + '/suspended.css' - with open(cssFilename, 'r') as cssFile: - suspendedCSS = cssFile.read() + + suspendedCSS = getCSS(baseDir, cssFilename, cssCache) + if suspendedCSS: suspendedForm = htmlHeader(cssFilename, suspendedCSS) suspendedForm += '
\n' suspendedForm += '

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'] + '
' + \ translate['Visible to anyone'] + '\n' - dropDownContent += " " \ - '
  • ' + \ - translate['Blog'] + '
    ' + \ - translate['Publicly visible post'] + '
  • \n' + if defaultTimeline == 'tlnews': + dropDownContent += " " \ + '
  • ' + \ + translate['Article'] + '
    ' + \ + translate['Create an article'] + '
  • \n' + else: + dropDownContent += " " \ + '
  • ' + \ + translate['Blog'] + '
    ' + \ + translate['Publicly visible post'] + '
  • \n' dropDownContent += " " \ '
  • \n' newPostForm += \ ' \n' - newPostForm += '
    \n' + newPostForm += '
    \n' newPostForm += ' \n' newPostForm += '\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 = \ - '
    ' + loginButton = headerButtonsFrontScreen(translate, nickname, + 'features', authorized, + iconsAsButtons, iconsDir) else: editProfileStr = \ - '' + '' + \ + '| ' + translate['Edit'] + '\n' + logoutStr = \ - '' + '' + \ + '| ' + translate['Logout'] + \
+            '\n' + linkToTimelineStart = \ '
    ' + dropDownContent + '
    \n' profileHeaderStr += ' \n' @@ -3436,7 +3521,7 @@ def htmlProfile(defaultTimeline: str, getLeftColumnContent(baseDir, 'news', domainFull, httpPrefix, translate, iconsDir, False, - False, None) + False, None, rssIconAtTop, True) profileHeaderStr += ' \n' profileHeaderStr += ' \n' profileFooterStr += ' \n' profileFooterStr += ' \n' @@ -4485,11 +4573,6 @@ def individualPostAsHtml(allowDownloads: bool, '  \n' - else: - avatarLink += \ - '  \n' if showAvatarOptions and \ fullDomain + '/users/' + nickname not in postActor: @@ -5453,7 +5536,8 @@ def htmlHighlightLabel(label: str, highlight: bool) -> str: def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str, httpPrefix: str, translate: {}, iconsDir: str, editor: bool, - showBackButton: bool, timelinePath: str) -> str: + showBackButton: bool, timelinePath: str, + rssIconAtTop: bool, showHeaderImage: bool) -> str: """Returns html content for the left column """ htmlStr = '' @@ -5462,29 +5546,33 @@ def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str, if ':' in domain: domain = domain.split(':') - leftColumnImageFilename = \ - baseDir + '/accounts/' + nickname + '@' + domain + \ - '/left_col_image.png' - if not os.path.isfile(leftColumnImageFilename): - theme = getConfigParam(baseDir, 'theme').lower() - if theme == 'default': - theme = '' - else: - theme = '_' + theme - themeLeftColumnImageFilename = \ - baseDir + '/img/left_col_image' + theme + '.png' - if os.path.isfile(themeLeftColumnImageFilename): - copyfile(themeLeftColumnImageFilename, leftColumnImageFilename) + editImageClass = '' + if showHeaderImage: + leftColumnImageFilename = \ + baseDir + '/accounts/' + nickname + '@' + domain + \ + '/left_col_image.png' + if not os.path.isfile(leftColumnImageFilename): + theme = getConfigParam(baseDir, 'theme').lower() + if theme == 'default': + theme = '' + else: + theme = '_' + theme + themeLeftColumnImageFilename = \ + baseDir + '/img/left_col_image' + theme + '.png' + if os.path.isfile(themeLeftColumnImageFilename): + copyfile(themeLeftColumnImageFilename, + leftColumnImageFilename) - # show the image at the top of the column - editImageClass = 'leftColEdit' - if os.path.isfile(leftColumnImageFilename): - editImageClass = 'leftColEditImage' - htmlStr += \ - '\n
    \n' + \ - ' \n' + \ - '
    \n' + # show the image at the top of the column + editImageClass = 'leftColEdit' + if os.path.isfile(leftColumnImageFilename): + editImageClass = 'leftColEditImage' + htmlStr += \ + '\n
    \n' + \ + ' \n' + \ + '
    \n' if showBackButton: htmlStr += \ @@ -5493,6 +5581,9 @@ def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str, '\n' + if (editor or rssIconAtTop) and not showHeaderImage: + htmlStr += '
    ' + if editImageClass == 'leftColEdit': htmlStr += '\n
    \n' @@ -5515,20 +5606,27 @@ def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str, else: # rss feed for all accounts on the instance rssUrl = httpPrefix + '://' + domainFull + '/blog/rss.xml' - htmlStr += \ + rssIconStr = \ ' ' + \ '' + \
         translate['RSS feed for this site'] + \
         '\n' + if rssIconAtTop: + htmlStr += rssIconStr if editImageClass == 'leftColEdit': htmlStr += '
    \n' - else: - htmlStr += '
    \n' + + if (editor or rssIconAtTop) and not showHeaderImage: + htmlStr += '

    ' + + if showHeaderImage: + htmlStr += '
    ' linksFilename = baseDir + '/accounts/links.txt' + linksFileContainsEntries = False if os.path.isfile(linksFilename): linksList = None with open(linksFilename, "r") as f: @@ -5562,6 +5660,7 @@ def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str, htmlStr += \ '

    ' + \ lineStr + '

    \n' + linksFileContainsEntries = True else: if lineStr.startswith('#') or lineStr.startswith('*'): lineStr = lineStr[1:].strip() @@ -5571,7 +5670,10 @@ def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str, else: htmlStr += \ '

    ' + lineStr + '

    \n' + linksFileContainsEntries = True + if linksFileContainsEntries and not rssIconAtTop: + htmlStr += '
    ' + rssIconStr + '
    ' return htmlStr @@ -5596,7 +5698,7 @@ def htmlNewswire(newswire: {}, nickname: str, moderator: bool, htmlStr = '' for dateStr, item in newswire.items(): publishedDate = \ - datetime.strptime(dateStr, "%Y-%m-%d %H:%M:%S+00:00") + datetime.strptime(dateStr, "%Y-%m-%d %H:%M:%S%z") dateShown = publishedDate.strftime("%Y-%m-%d %H:%M") dateStrLink = dateStr.replace('T', ' ') @@ -5610,9 +5712,10 @@ def htmlNewswire(newswire: {}, nickname: str, moderator: bool, totalVotesStr = \ votesIndicator(totalVotes, positiveVoting) + title = removeLongWords(item[0], 16, []).replace('\n', '
    ') htmlStr += '

    ' + \ '' + \ - '' + item[0] + \ + '' + title + \ '' + totalVotesStr if moderator: htmlStr += \ @@ -5620,10 +5723,10 @@ def htmlNewswire(newswire: {}, nickname: str, moderator: bool, '/newswireunvote=' + dateStrLink + '" ' + \ 'title="' + translate['Remove Vote'] + '">' htmlStr += '

    ' + iconsDir + '/vote.png" />

    \n' else: htmlStr += ' ' - htmlStr += dateShown + '

    ' + htmlStr += dateShown + '

    \n' else: totalVotesStr = '' totalVotes = 0 @@ -5635,24 +5738,25 @@ def htmlNewswire(newswire: {}, nickname: str, moderator: bool, totalVotesStr = \ votesIndicator(totalVotes, positiveVoting) + title = removeLongWords(item[0], 16, []).replace('\n', '
    ') if moderator and moderatedItem: htmlStr += '

    ' + \ '' + \ - item[0] + '' + totalVotesStr + title + '' + totalVotesStr htmlStr += ' ' + dateShown htmlStr += '' htmlStr += '' - htmlStr += '

    ' + htmlStr += '

    \n' else: htmlStr += '

    ' + \ '' + \ - item[0] + '' + \ + title + '' + \ totalVotesStr htmlStr += ' ' - htmlStr += dateShown + '

    ' + htmlStr += dateShown + '

    \n' return htmlStr @@ -5661,7 +5765,12 @@ def getRightColumnContent(baseDir: str, nickname: str, domainFull: str, iconsDir: str, moderator: bool, editor: bool, newswire: {}, positiveVoting: bool, showBackButton: bool, timelinePath: str, - showPublishButton: bool) -> str: + showPublishButton: bool, + showPublishAsIcon: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool, + showHeaderImage: bool) -> str: """Returns html content for the right column """ htmlStr = '' @@ -5670,48 +5779,74 @@ def getRightColumnContent(baseDir: str, nickname: str, domainFull: str, if ':' in domain: domain = domain.split(':') - rightColumnImageFilename = \ - baseDir + '/accounts/' + nickname + '@' + domain + \ - '/right_col_image.png' - if not os.path.isfile(rightColumnImageFilename): - theme = getConfigParam(baseDir, 'theme').lower() - if theme == 'default': - theme = '' - else: - theme = '_' + theme - themeRightColumnImageFilename = \ - baseDir + '/img/right_col_image' + theme + '.png' - if os.path.isfile(themeRightColumnImageFilename): - copyfile(themeRightColumnImageFilename, rightColumnImageFilename) + if authorized: + # only show the publish button if logged in, otherwise replace it with + # a login button + publishButtonStr = \ + ' ' + \ + '\n' + else: + # if not logged in then replace the publish button with + # a login button + publishButtonStr = \ + ' \n' - # show the image at the top of the column - editImageClass = 'rightColEdit' - if os.path.isfile(rightColumnImageFilename): - editImageClass = 'rightColEditImage' - htmlStr += \ - '\n
    \n' + \ - ' \n' + \ - '
    \n' + # show publish button at the top if needed + if publishButtonAtTop: + htmlStr += '
    ' + publishButtonStr + '
    ' + + # show a column header image, eg. title of the theme or newswire banner + editImageClass = '' + if showHeaderImage: + rightColumnImageFilename = \ + baseDir + '/accounts/' + nickname + '@' + domain + \ + '/right_col_image.png' + if not os.path.isfile(rightColumnImageFilename): + theme = getConfigParam(baseDir, 'theme').lower() + if theme == 'default': + theme = '' + else: + theme = '_' + theme + themeRightColumnImageFilename = \ + baseDir + '/img/right_col_image' + theme + '.png' + if os.path.isfile(themeRightColumnImageFilename): + copyfile(themeRightColumnImageFilename, + rightColumnImageFilename) + + # show the image at the top of the column + editImageClass = 'rightColEdit' + if os.path.isfile(rightColumnImageFilename): + editImageClass = 'rightColEditImage' + htmlStr += \ + '\n
    \n' + \ + ' \n' + \ + '
    \n' + + if (showPublishButton or editor or rssIconAtTop) and not showHeaderImage: + htmlStr += '
    ' if editImageClass == 'rightColEdit': htmlStr += '\n
    \n' + # whether to show a back icon + # This is probably going to be osolete soon if showBackButton: htmlStr += \ ' ' + \ '\n' - if showPublishButton: - htmlStr += \ - ' ' + \ - '\n' + if showPublishButton and not publishButtonAtTop: + if not showPublishAsIcon: + htmlStr += publishButtonStr + # show the edit icon if editor: if os.path.isfile(baseDir + '/accounts/newswiremoderation.txt'): # show the edit icon highlighted @@ -5734,27 +5869,57 @@ def getRightColumnContent(baseDir: str, nickname: str, domainFull: str, translate['Edit newswire'] + '" src="/' + \ iconsDir + '/edit.png" />\n' - htmlStr += \ + # show the RSS icon + rssIconStr = \ ' ' + \ '' + \
         translate['Newswire RSS Feed'] + '\n' + if rssIconAtTop: + htmlStr += rssIconStr + + # show publish icon at top + if showPublishButton: + if showPublishAsIcon: + htmlStr += \ + ' ' + \ + '' + \
+                translate['Publish a news article'] + '\n' if editImageClass == 'rightColEdit': htmlStr += '
    \n' else: - htmlStr += '
    \n' + if showHeaderImage: + htmlStr += '
    \n' - htmlStr += htmlNewswire(newswire, nickname, moderator, translate, - positiveVoting, iconsDir) + if (showPublishButton or editor or rssIconAtTop) and not showHeaderImage: + htmlStr += '

    ' + + # show the newswire lines + newswireContentStr = \ + htmlNewswire(newswire, nickname, moderator, translate, + positiveVoting, iconsDir) + htmlStr += newswireContentStr + + # show the rss icon at the bottom, typically on the right hand side + if newswireContentStr and not rssIconAtTop: + htmlStr += '
    ' + rssIconStr + '
    ' return htmlStr -def htmlLinksMobile(baseDir: str, nickname: str, domainFull: str, +def htmlLinksMobile(cssCache: {}, baseDir: str, + nickname: str, domainFull: str, httpPrefix: str, translate, - timelinePath: str) -> str: + timelinePath: str, authorized: bool, + rssIconAtTop: bool, + iconsAsButtons: bool, + defaultTimeline: str) -> str: """Show the left column links within mobile view """ htmlStr = '' @@ -5764,11 +5929,8 @@ def htmlLinksMobile(baseDir: str, nickname: str, domainFull: str, if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' - profileStyle = None - with open(cssFilename, 'r') as cssFile: - # load css - profileStyle = \ - cssFile.read() + profileStyle = getCSS(baseDir, cssFilename, cssCache) + if profileStyle: # replace any https within the css with whatever prefix is needed if httpPrefix != 'https': profileStyle = \ @@ -5777,24 +5939,42 @@ def htmlLinksMobile(baseDir: str, nickname: str, domainFull: str, iconsDir = getIconsDir(baseDir) # is the user a site editor? - editor = isEditor(baseDir, nickname) + if nickname == 'news': + editor = False + else: + editor = isEditor(baseDir, nickname) htmlStr = htmlHeader(cssFilename, profileStyle) + htmlStr += \ + '' + \ + '\n' + + htmlStr += '
    ' + \ + headerButtonsFrontScreen(translate, nickname, + 'links', authorized, + iconsAsButtons, iconsDir) + '
    ' htmlStr += \ getLeftColumnContent(baseDir, nickname, domainFull, httpPrefix, translate, iconsDir, editor, - True, timelinePath) + False, timelinePath, + rssIconAtTop, False) htmlStr += '\n' + htmlFooter() return htmlStr -def htmlNewswireMobile(baseDir: str, nickname: str, +def htmlNewswireMobile(cssCache: {}, baseDir: str, nickname: str, domain: str, domainFull: str, httpPrefix: str, translate: {}, newswire: {}, positiveVoting: bool, - timelinePath: str) -> str: + timelinePath: str, + showPublishAsIcon: bool, + authorized: bool, + rssIconAtTop: bool, + iconsAsButtons: bool, + defaultTimeline: str) -> str: """Shows the mobile version of the newswire right column """ htmlStr = '' @@ -5804,11 +5984,8 @@ def htmlNewswireMobile(baseDir: str, nickname: str, if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' - profileStyle = None - with open(cssFilename, 'r') as cssFile: - # load css - profileStyle = \ - cssFile.read() + profileStyle = getCSS(baseDir, cssFilename, cssCache) + if profileStyle: # replace any https within the css with whatever prefix is needed if httpPrefix != 'https': profileStyle = \ @@ -5817,19 +5994,36 @@ def htmlNewswireMobile(baseDir: str, nickname: str, iconsDir = getIconsDir(baseDir) - # is the user a moderator? - moderator = isModerator(baseDir, nickname) + if nickname == 'news': + editor = False + moderator = False + else: + # is the user a moderator? + moderator = isModerator(baseDir, nickname) - # is the user a site editor? - editor = isEditor(baseDir, nickname) + # is the user a site editor? + editor = isEditor(baseDir, nickname) + + showPublishButton = editor htmlStr = htmlHeader(cssFilename, profileStyle) + htmlStr += \ + '' + \ + '\n' + + htmlStr += '
    ' + \ + headerButtonsFrontScreen(translate, nickname, + 'newswire', authorized, + iconsAsButtons, iconsDir) + '
    ' htmlStr += \ getRightColumnContent(baseDir, nickname, domainFull, httpPrefix, translate, iconsDir, moderator, editor, newswire, positiveVoting, - True, timelinePath, True) + False, timelinePath, showPublishButton, + showPublishAsIcon, rssIconAtTop, False, + authorized, False) htmlStr += htmlFooter() return htmlStr @@ -5859,7 +6053,378 @@ def getBannerFile(baseDir: str, nickname: str, domain: str) -> (str, str): return bannerFile, bannerFilename -def htmlTimeline(defaultTimeline: str, +def headerButtonsFrontScreen(translate: {}, + nickname: str, boxName: str, + authorized: bool, + iconsAsButtons: bool, + iconsDir: bool) -> str: + """Returns the header buttons for the front page of a news instance + """ + headerStr = '' + if nickname == 'news': + buttonFeatures = 'buttonMobile' + buttonNewswire = 'buttonMobile' + buttonLinks = 'buttonMobile' + if boxName == 'features': + buttonFeatures = 'buttonselected' + elif boxName == 'newswire': + buttonNewswire = 'buttonselected' + elif boxName == 'links': + buttonLinks = 'buttonselected' + + headerStr += \ + ' ' + \ + '\n' + if not authorized: + headerStr += \ + ' ' + \ + '\n' + if iconsAsButtons: + headerStr += \ + ' ' + \ + '\n' + headerStr += \ + ' ' + \ + '\n' + else: + headerStr += \ + ' ' + \ + '| ' + translate['Newswire'] + '\n' + headerStr += \ + ' ' + \ + '| ' + translate['Links'] + '\n' + else: + if not authorized: + headerStr += \ + ' ' + \ + '\n' + + if headerStr: + headerStr = \ + '\n
    \n' + \ + headerStr + \ + '
    \n' + return headerStr + + +def headerButtonsTimeline(defaultTimeline: str, + boxName: str, + pageNumber: int, + translate: {}, + usersPath: str, + mediaButton: str, + blogsButton: str, + newsButton: str, + inboxButton: str, + dmButton: str, + newDM: str, + repliesButton: str, + newReply: str, + minimal: bool, + sentButton: str, + sharesButtonStr: str, + bookmarksButtonStr: str, + eventsButtonStr: str, + moderationButtonStr: str, + newPostButtonStr: str, + baseDir: str, + nickname: str, domain: str, + iconsDir: str, + timelineStartTime, + newCalendarEvent: bool, + calendarPath: str, + calendarImage: str, + followApprovals: str, + iconsAsButtons: bool) -> str: + """Returns the header at the top of the timeline, containing + buttons for inbox, outbox, search, calendar, etc + """ + # start of the button header with inbox, outbox, etc + tlStr = '
    \n' + # first button + if defaultTimeline == 'tlmedia': + tlStr += \ + ' \n' + elif defaultTimeline == 'tlblogs': + tlStr += \ + ' \n' + elif defaultTimeline == 'tlnews': + tlStr += \ + ' \n' + else: + tlStr += \ + ' \n' + + # if this is a news instance and we are viewing the news timeline + newsHeader = False + if defaultTimeline == 'tlnews' and boxName == 'tlnews': + newsHeader = True + + if not newsHeader: + tlStr += \ + ' \n' + + tlStr += \ + ' \n' + + # typically the media button + if defaultTimeline != 'tlmedia': + if not minimal and not newsHeader: + tlStr += \ + ' \n' + else: + if not minimal: + tlStr += \ + ' \n' + + isFeaturesTimeline = \ + defaultTimeline == 'tlnews' and boxName == 'tlnews' + + if not isFeaturesTimeline: + # typically the blogs button + # but may change if this is a blogging oriented instance + if defaultTimeline != 'tlblogs': + if not minimal and not isFeaturesTimeline: + titleStr = translate['Blogs'] + if defaultTimeline == 'tlnews': + titleStr = translate['Article'] + tlStr += \ + ' \n' + else: + if not minimal: + tlStr += \ + ' \n' + + # typically the news button + # but may change if this is a news oriented instance + if defaultTimeline != 'tlnews': + tlStr += \ + ' \n' + else: + if not newsHeader: + tlStr += \ + ' \n' + + if not newsHeader: + # button for the outbox + tlStr += \ + ' \n' + + # add other buttons + tlStr += \ + sharesButtonStr + bookmarksButtonStr + eventsButtonStr + \ + moderationButtonStr + newPostButtonStr + + # show todays events buttons on the first inbox page + if boxName == 'inbox' and pageNumber == 1: + if todaysEventsCheck(baseDir, nickname, domain): + now = datetime.now() + + # happening today button + if not iconsAsButtons: + tlStr += \ + ' ' + \ + '\n' + else: + tlStr += \ + ' ' + \ + '\n' + + # happening this week button + if thisWeeksEventsCheck(baseDir, nickname, domain): + if not iconsAsButtons: + tlStr += \ + ' \n' + else: + tlStr += \ + ' \n' + else: + # happening this week button + if thisWeeksEventsCheck(baseDir, nickname, domain): + if not iconsAsButtons: + tlStr += \ + ' \n' + else: + tlStr += \ + ' \n' + + if not newsHeader: + if not iconsAsButtons: + # the search button + tlStr += \ + ' | ' + \
+                translate['Search and follow'] + \
+                '\n' + else: + tlStr += \ + ' \n' + + # benchmark 5 + timeDiff = int((time.time() - timelineStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE TIMING ' + boxName + ' 5 = ' + str(timeDiff)) + + # the calendar button + if not isFeaturesTimeline: + calendarAltText = translate['Calendar'] + if newCalendarEvent: + # indicate that the calendar icon is highlighted + calendarAltText = '*' + calendarAltText + '*' + if not iconsAsButtons: + tlStr += \ + ' | ' + calendarAltText + \
+                '\n' + else: + tlStr += \ + ' \n' + + if not newsHeader: + # the show/hide button, for a simpler header appearance + if not iconsAsButtons: + tlStr += \ + ' | ' + translate['Show/Hide Buttons'] + \
+                '\n' + else: + tlStr += \ + ' \n' + + if newsHeader: + tlStr += \ + ' \n' + + # the newswire button to show right column links + if not iconsAsButtons: + tlStr += \ + ' ' + \ + '| ' + translate['News'] + \
+            '\n' + else: + tlStr += \ + ' \n' + + # the links button to show left column links + if not iconsAsButtons: + tlStr += \ + ' ' + \ + '| ' + translate['Edit Links'] + \
+            '\n' + else: + tlStr += \ + ' \n' + + if not newsHeader: + tlStr += followApprovals + # end of the button header with inbox, outbox, etc + tlStr += '
    \n' + return tlStr + + +def htmlTimeline(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, baseDir: str, @@ -5873,7 +6438,13 @@ def htmlTimeline(defaultTimeline: str, showPublishedDateOnly: bool, newswire: {}, moderator: bool, editor: bool, - positiveVoting: bool) -> str: + positiveVoting: bool, + showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: """Show the timeline as html """ timelineStartTime = time.time() @@ -5941,16 +6512,20 @@ def htmlTimeline(defaultTimeline: str, if timeDiff > 100: print('TIMELINE TIMING ' + boxName + ' 1 = ' + str(timeDiff)) - with open(cssFilename, 'r') as cssFile: - # load css + profileStyle = getCSS(baseDir, cssFilename, cssCache) + if not profileStyle: + print('ERROR: css file not found ' + cssFilename) + return None + + # load css + profileStyle = \ + profileStyle.replace('banner.png', + '/users/' + nickname + '/' + bannerFile) + # replace any https within the css with whatever prefix is needed + if httpPrefix != 'https': profileStyle = \ - cssFile.read().replace('banner.png', - '/users/' + nickname + '/' + bannerFile) - # replace any https within the css with whatever prefix is needed - if httpPrefix != 'https': - profileStyle = \ - profileStyle.replace('https://', - httpPrefix + '://') + profileStyle.replace('https://', + httpPrefix + '://') # is the user a moderator? if not moderator: @@ -6056,6 +6631,7 @@ def htmlTimeline(defaultTimeline: str, moderationButtonStr = '' if moderator and not minimal: moderationButtonStr = \ + ' ' + \ '\n' bookmarksButtonStr = \ + ' ' + \ '\n' eventsButtonStr = \ + ' ' + \ '\n' @@ -6092,60 +6671,105 @@ def htmlTimeline(defaultTimeline: str, # what screen to go to when a new post is created if boxName == 'dm': - newPostButtonStr = \ - ' | ' + translate['Create a new DM'] + \
-            '\n' + if not iconsAsButtons: + newPostButtonStr = \ + ' | ' + translate['Create a new DM'] + \
+                '\n' + else: + newPostButtonStr = \ + '' + \ + '\n' elif boxName == 'tlblogs' or boxName == 'tlnews': - newPostButtonStr = \ - ' | ' + \
-            translate['Create a new post'] + \
-            '\n' - elif boxName == 'tlevents': - newPostButtonStr = \ - ' | ' + \
-            translate['Create a new event'] + \
-            '\n' - else: - if not manuallyApproveFollowers: + if not iconsAsButtons: newPostButtonStr = \ ' | ' + \
                 translate['Create a new post'] + \
                 '\n' else: + newPostButtonStr = \ + '' + \ + '\n' + elif boxName == 'tlevents': + if not iconsAsButtons: newPostButtonStr = \ ' | ' + translate['Create a new post'] + \
+                translate['Create a new event'] + '\n' - + else: + newPostButtonStr = \ + '' + \ + '\n' + else: + if not manuallyApproveFollowers: + if not iconsAsButtons: + newPostButtonStr = \ + ' | ' + \
+                    translate['Create a new post'] + \
+                    '\n' + else: + newPostButtonStr = \ + '' + \ + '\n' + else: + if not iconsAsButtons: + newPostButtonStr = \ + ' | ' + translate['Create a new post'] + \
+                    '\n' + else: + newPostButtonStr = \ + '' + \ + '\n' # This creates a link to the profile page when viewed # in lynx, but should be invisible in a graphical web browser tlStr += \ - '\n' + '\n' # banner and row of buttons tlStr += \ '\n' - tlStr += '
    ' - tlStr += '
    \n
    \n' + tlStr += '\n' + + if fullWidthTimelineButtonHeader: + tlStr += \ + headerButtonsTimeline(defaultTimeline, boxName, pageNumber, + translate, usersPath, mediaButton, + blogsButton, newsButton, inboxButton, + dmButton, newDM, repliesButton, + newReply, minimal, sentButton, + sharesButtonStr, bookmarksButtonStr, + eventsButtonStr, moderationButtonStr, + newPostButtonStr, baseDir, nickname, + domain, iconsDir, timelineStartTime, + newCalendarEvent, calendarPath, + calendarImage, followApprovals, + iconsAsButtons) # start the timeline tlStr += '
    \n' else: @@ -3464,9 +3549,9 @@ def htmlProfile(defaultTimeline: str, profileStr = \ linkToTimelineStart + profileHeaderStr + \ linkToTimelineEnd + donateSection - profileStr += '
    \n' - profileStr += '
    ' if not isSystemAccount(nickname): + profileStr += '
    \n' + profileStr += '
    ' profileStr += \ ' ' - profileStr += editProfileStr + logoutStr - profileStr += '
    ' - profileStr += '
    ' + profileStr += logoutStr + editProfileStr + profileStr += '
    ' + profileStr += '
    ' profileStr += followApprovalsSection cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' - with open(cssFilename, 'r') as cssFile: + + profileStyle = getCSS(baseDir, cssFilename, cssCache) + if profileStyle: profileStyle = \ - cssFile.read().replace('image.png', - profileJson['image']['url']) + profileStyle.replace('image.png', + profileJson['image']['url']) if isSystemAccount(nickname): bannerFile, bannerFilename = \ getBannerFile(baseDir, nickname, domain) @@ -3568,7 +3655,8 @@ def htmlProfile(defaultTimeline: str, httpPrefix, translate, iconsDir, False, False, newswire, False, - False, None, False) + False, None, False, False, + False, True, authorized, True) profileFooterStr += '
    \n' @@ -6166,190 +6790,27 @@ def htmlTimeline(defaultTimeline: str, leftColumnStr = \ getLeftColumnContent(baseDir, nickname, domainFull, httpPrefix, translate, iconsDir, - editor, False, None) + editor, False, None, rssIconAtTop, + True) tlStr += ' \n' # center column containing posts tlStr += ' \n' tlStr += ' \n' @@ -6550,7 +7014,7 @@ def htmlTimeline(defaultTimeline: str, return tlStr -def htmlShares(defaultTimeline: str, +def htmlShares(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, baseDir: str, wfRequest: {}, personCache: {}, @@ -6559,13 +7023,20 @@ def htmlShares(defaultTimeline: str, httpPrefix: str, projectVersion: str, YTReplacementDomain: str, showPublishedDateOnly: bool, - newswire: {}, positiveVoting: bool) -> str: + newswire: {}, positiveVoting: bool, + showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: """Show the shares timeline as html """ manuallyApproveFollowers = \ followerApprovalActive(baseDir, nickname, domain) - return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, + return htmlTimeline(cssCache, defaultTimeline, + recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, None, @@ -6574,10 +7045,13 @@ def htmlShares(defaultTimeline: str, False, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, - positiveVoting) + positiveVoting, showPublishAsIcon, + fullWidthTimelineButtonHeader, + iconsAsButtons, rssIconAtTop, publishButtonAtTop, + authorized) -def htmlInbox(defaultTimeline: str, +def htmlInbox(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, baseDir: str, wfRequest: {}, personCache: {}, @@ -6586,13 +7060,20 @@ def htmlInbox(defaultTimeline: str, httpPrefix: str, projectVersion: str, minimal: bool, YTReplacementDomain: str, showPublishedDateOnly: bool, - newswire: {}, positiveVoting: bool) -> str: + newswire: {}, positiveVoting: bool, + showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: """Show the inbox as html """ manuallyApproveFollowers = \ followerApprovalActive(baseDir, nickname, domain) - return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, + return htmlTimeline(cssCache, defaultTimeline, + recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, @@ -6601,10 +7082,13 @@ def htmlInbox(defaultTimeline: str, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, - positiveVoting) + positiveVoting, showPublishAsIcon, + fullWidthTimelineButtonHeader, + iconsAsButtons, rssIconAtTop, publishButtonAtTop, + authorized) -def htmlBookmarks(defaultTimeline: str, +def htmlBookmarks(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, baseDir: str, wfRequest: {}, personCache: {}, @@ -6613,13 +7097,20 @@ def htmlBookmarks(defaultTimeline: str, httpPrefix: str, projectVersion: str, minimal: bool, YTReplacementDomain: str, showPublishedDateOnly: bool, - newswire: {}, positiveVoting: bool) -> str: + newswire: {}, positiveVoting: bool, + showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: """Show the bookmarks as html """ manuallyApproveFollowers = \ followerApprovalActive(baseDir, nickname, domain) - return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, + return htmlTimeline(cssCache, defaultTimeline, + recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, bookmarksJson, @@ -6628,10 +7119,13 @@ def htmlBookmarks(defaultTimeline: str, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, - positiveVoting) + positiveVoting, showPublishAsIcon, + fullWidthTimelineButtonHeader, + iconsAsButtons, rssIconAtTop, publishButtonAtTop, + authorized) -def htmlEvents(defaultTimeline: str, +def htmlEvents(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, baseDir: str, wfRequest: {}, personCache: {}, @@ -6640,13 +7134,20 @@ def htmlEvents(defaultTimeline: str, httpPrefix: str, projectVersion: str, minimal: bool, YTReplacementDomain: str, showPublishedDateOnly: bool, - newswire: {}, positiveVoting: bool) -> str: + newswire: {}, positiveVoting: bool, + showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: """Show the events as html """ manuallyApproveFollowers = \ followerApprovalActive(baseDir, nickname, domain) - return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, + return htmlTimeline(cssCache, defaultTimeline, + recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, bookmarksJson, @@ -6655,10 +7156,13 @@ def htmlEvents(defaultTimeline: str, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, - positiveVoting) + positiveVoting, showPublishAsIcon, + fullWidthTimelineButtonHeader, + iconsAsButtons, rssIconAtTop, publishButtonAtTop, + authorized) -def htmlInboxDMs(defaultTimeline: str, +def htmlInboxDMs(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, baseDir: str, wfRequest: {}, personCache: {}, @@ -6667,19 +7171,30 @@ def htmlInboxDMs(defaultTimeline: str, httpPrefix: str, projectVersion: str, minimal: bool, YTReplacementDomain: str, showPublishedDateOnly: bool, - newswire: {}, positiveVoting: bool) -> str: + newswire: {}, positiveVoting: bool, + showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: """Show the DM timeline as html """ - return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, + return htmlTimeline(cssCache, defaultTimeline, + recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'dm', allowDeletion, httpPrefix, projectVersion, False, minimal, YTReplacementDomain, showPublishedDateOnly, - newswire, False, False, positiveVoting) + newswire, False, False, positiveVoting, + showPublishAsIcon, + fullWidthTimelineButtonHeader, + iconsAsButtons, rssIconAtTop, publishButtonAtTop, + authorized) -def htmlInboxReplies(defaultTimeline: str, +def htmlInboxReplies(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, baseDir: str, wfRequest: {}, personCache: {}, @@ -6688,10 +7203,17 @@ def htmlInboxReplies(defaultTimeline: str, httpPrefix: str, projectVersion: str, minimal: bool, YTReplacementDomain: str, showPublishedDateOnly: bool, - newswire: {}, positiveVoting: bool) -> str: + newswire: {}, positiveVoting: bool, + showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: """Show the replies timeline as html """ - return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, + return htmlTimeline(cssCache, defaultTimeline, + recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'tlreplies', @@ -6699,10 +7221,13 @@ def htmlInboxReplies(defaultTimeline: str, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, - positiveVoting) + positiveVoting, showPublishAsIcon, + fullWidthTimelineButtonHeader, + iconsAsButtons, rssIconAtTop, publishButtonAtTop, + authorized) -def htmlInboxMedia(defaultTimeline: str, +def htmlInboxMedia(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, baseDir: str, wfRequest: {}, personCache: {}, @@ -6711,10 +7236,17 @@ def htmlInboxMedia(defaultTimeline: str, httpPrefix: str, projectVersion: str, minimal: bool, YTReplacementDomain: str, showPublishedDateOnly: bool, - newswire: {}, positiveVoting: bool) -> str: + newswire: {}, positiveVoting: bool, + showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: """Show the media timeline as html """ - return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, + return htmlTimeline(cssCache, defaultTimeline, + recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'tlmedia', @@ -6722,10 +7254,13 @@ def htmlInboxMedia(defaultTimeline: str, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, - positiveVoting) + positiveVoting, showPublishAsIcon, + fullWidthTimelineButtonHeader, + iconsAsButtons, rssIconAtTop, publishButtonAtTop, + authorized) -def htmlInboxBlogs(defaultTimeline: str, +def htmlInboxBlogs(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, baseDir: str, wfRequest: {}, personCache: {}, @@ -6734,10 +7269,17 @@ def htmlInboxBlogs(defaultTimeline: str, httpPrefix: str, projectVersion: str, minimal: bool, YTReplacementDomain: str, showPublishedDateOnly: bool, - newswire: {}, positiveVoting: bool) -> str: + newswire: {}, positiveVoting: bool, + showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: """Show the blogs timeline as html """ - return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, + return htmlTimeline(cssCache, defaultTimeline, + recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'tlblogs', @@ -6745,10 +7287,13 @@ def htmlInboxBlogs(defaultTimeline: str, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, - positiveVoting) + positiveVoting, showPublishAsIcon, + fullWidthTimelineButtonHeader, + iconsAsButtons, rssIconAtTop, publishButtonAtTop, + authorized) -def htmlInboxNews(defaultTimeline: str, +def htmlInboxNews(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, baseDir: str, wfRequest: {}, personCache: {}, @@ -6758,10 +7303,16 @@ def htmlInboxNews(defaultTimeline: str, minimal: bool, YTReplacementDomain: str, showPublishedDateOnly: bool, newswire: {}, moderator: bool, editor: bool, - positiveVoting: bool) -> str: + positiveVoting: bool, showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: """Show the news timeline as html """ - return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, + return htmlTimeline(cssCache, defaultTimeline, + recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'tlnews', @@ -6769,10 +7320,13 @@ def htmlInboxNews(defaultTimeline: str, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, moderator, editor, - positiveVoting) + positiveVoting, showPublishAsIcon, + fullWidthTimelineButtonHeader, + iconsAsButtons, rssIconAtTop, publishButtonAtTop, + authorized) -def htmlModeration(defaultTimeline: str, +def htmlModeration(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, baseDir: str, wfRequest: {}, personCache: {}, @@ -6781,19 +7335,29 @@ def htmlModeration(defaultTimeline: str, httpPrefix: str, projectVersion: str, YTReplacementDomain: str, showPublishedDateOnly: bool, - newswire: {}, positiveVoting: bool) -> str: + newswire: {}, positiveVoting: bool, + showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: """Show the moderation feed as html """ - return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, + return htmlTimeline(cssCache, defaultTimeline, + recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'moderation', allowDeletion, httpPrefix, projectVersion, True, False, YTReplacementDomain, showPublishedDateOnly, - newswire, False, False, positiveVoting) + newswire, False, False, positiveVoting, + showPublishAsIcon, fullWidthTimelineButtonHeader, + iconsAsButtons, rssIconAtTop, publishButtonAtTop, + authorized) -def htmlOutbox(defaultTimeline: str, +def htmlOutbox(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, baseDir: str, wfRequest: {}, personCache: {}, @@ -6802,22 +7366,33 @@ def htmlOutbox(defaultTimeline: str, httpPrefix: str, projectVersion: str, minimal: bool, YTReplacementDomain: str, showPublishedDateOnly: bool, - newswire: {}, positiveVoting: bool) -> str: + newswire: {}, positiveVoting: bool, + showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: """Show the Outbox as html """ manuallyApproveFollowers = \ followerApprovalActive(baseDir, nickname, domain) - return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, + return htmlTimeline(cssCache, defaultTimeline, + recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, outboxJson, 'outbox', allowDeletion, httpPrefix, projectVersion, manuallyApproveFollowers, minimal, YTReplacementDomain, showPublishedDateOnly, - newswire, False, False, positiveVoting) + newswire, False, False, positiveVoting, + showPublishAsIcon, fullWidthTimelineButtonHeader, + iconsAsButtons, rssIconAtTop, publishButtonAtTop, + authorized) -def htmlIndividualPost(recentPostsCache: {}, maxRecentPosts: int, +def htmlIndividualPost(cssCache: {}, + recentPostsCache: {}, maxRecentPosts: int, translate: {}, baseDir: str, session, wfRequest: {}, personCache: {}, nickname: str, domain: str, port: int, authorized: bool, @@ -6928,15 +7503,17 @@ def htmlIndividualPost(recentPostsCache: {}, maxRecentPosts: int, cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' - with open(cssFilename, 'r') as cssFile: - postsCSS = cssFile.read() + + postsCSS = getCSS(baseDir, cssFilename, cssCache) + if postsCSS: if httpPrefix != 'https': postsCSS = postsCSS.replace('https://', httpPrefix + '://') return htmlHeader(cssFilename, postsCSS) + postStr + htmlFooter() -def htmlPostReplies(recentPostsCache: {}, maxRecentPosts: int, +def htmlPostReplies(cssCache: {}, + recentPostsCache: {}, maxRecentPosts: int, translate: {}, baseDir: str, session, wfRequest: {}, personCache: {}, nickname: str, domain: str, port: int, repliesJson: {}, @@ -6964,15 +7541,16 @@ def htmlPostReplies(recentPostsCache: {}, maxRecentPosts: int, cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' - with open(cssFilename, 'r') as cssFile: - postsCSS = cssFile.read() + + postsCSS = getCSS(baseDir, cssFilename, cssCache) + if postsCSS: if httpPrefix != 'https': postsCSS = postsCSS.replace('https://', httpPrefix + '://') return htmlHeader(cssFilename, postsCSS) + repliesStr + htmlFooter() -def htmlRemoveSharedItem(translate: {}, baseDir: str, +def htmlRemoveSharedItem(cssCache: {}, translate: {}, baseDir: str, actor: str, shareName: str, callingDomain: str) -> str: """Shows a screen asking to confirm the removal of a shared item @@ -7009,8 +7587,8 @@ def htmlRemoveSharedItem(translate: {}, baseDir: str, cssFilename = baseDir + '/epicyon-follow.css' if os.path.isfile(baseDir + '/follow.css'): cssFilename = baseDir + '/follow.css' - with open(cssFilename, 'r') as cssFile: - profileStyle = cssFile.read() + + profileStyle = getCSS(baseDir, cssFilename, cssCache) sharesStr = htmlHeader(cssFilename, profileStyle) sharesStr += '
    ' + \ leftColumnStr + ' \n' - # start of the button header with inbox, outbox, etc - tlStr += '
    \n' - # first button - if defaultTimeline == 'tlmedia': + if not fullWidthTimelineButtonHeader: tlStr += \ - ' \n' - elif defaultTimeline == 'tlblogs': - tlStr += \ - ' \n' - elif defaultTimeline == 'tlnews': - tlStr += \ - ' \n' - else: - tlStr += \ - ' \n' - - tlStr += \ - ' \n' - - tlStr += \ - ' \n' - - # typically the media button - if defaultTimeline != 'tlmedia': - if not minimal: - tlStr += \ - ' \n' - else: - if not minimal: - tlStr += \ - ' \n' - - # typically the blogs button - # but may change if this is a blogging oriented instance - if defaultTimeline != 'tlblogs': - if not minimal or defaultTimeline == 'tlnews': - tlStr += \ - ' \n' - else: - if not minimal: - tlStr += \ - ' \n' - - # typically the news button - # but may change if this is a news oriented instance - if defaultTimeline != 'tlnews': - tlStr += \ - ' \n' - else: - tlStr += \ - ' \n' - - # button for the outbox - tlStr += \ - ' \n' - - # add other buttons - tlStr += \ - sharesButtonStr + bookmarksButtonStr + eventsButtonStr + \ - moderationButtonStr + newPostButtonStr - - # show todays events buttons on the first inbox page - if boxName == 'inbox' and pageNumber == 1: - if todaysEventsCheck(baseDir, nickname, domain): - now = datetime.now() - - # happening today button - tlStr += \ - ' \n' - - # happening this week button - if thisWeeksEventsCheck(baseDir, nickname, domain): - tlStr += \ - ' \n' - else: - # happening this week button - if thisWeeksEventsCheck(baseDir, nickname, domain): - tlStr += \ - ' \n' - - # the search button - tlStr += \ - ' | ' + \
-        translate['Search and follow'] + '\n' - - # benchmark 5 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 5 = ' + str(timeDiff)) - - # the calendar button - calendarAltText = translate['Calendar'] - if newCalendarEvent: - # indicate that the calendar icon is highlighted - calendarAltText = '*' + calendarAltText + '*' - tlStr += \ - ' | ' + calendarAltText + '\n' - - # the show/hide button, for a simpler header appearance - tlStr += \ - ' | ' + translate['Show/Hide Buttons'] + \
-        '\n' - - # the newswire button to show right column links - tlStr += \ - ' ' + \ - '| ' + translate['News'] + \
-        '\n' - - # the links button to show left column links - tlStr += \ - ' ' + \ - '| ' + translate['Edit Links'] + \
-        '\n' - - tlStr += followApprovals - # end of the button header with inbox, outbox, etc - tlStr += '
    \n' + headerButtonsTimeline(defaultTimeline, boxName, pageNumber, + translate, usersPath, mediaButton, + blogsButton, newsButton, inboxButton, + dmButton, newDM, repliesButton, + newReply, minimal, sentButton, + sharesButtonStr, bookmarksButtonStr, + eventsButtonStr, moderationButtonStr, + newPostButtonStr, baseDir, nickname, + domain, iconsDir, timelineStartTime, + newCalendarEvent, calendarPath, + calendarImage, followApprovals, + iconsAsButtons) # second row of buttons for moderator actions if moderator and boxName == 'moderation': @@ -6516,7 +6977,10 @@ def htmlTimeline(defaultTimeline: str, httpPrefix, translate, iconsDir, moderator, editor, newswire, positiveVoting, - False, None, True) + False, None, True, + showPublishAsIcon, + rssIconAtTop, publishButtonAtTop, + authorized, True) tlStr += '
    ' + \ rightColumnStr + '