diff --git a/auth.py b/auth.py index 021a17b61..f0124f550 100644 --- a/auth.py +++ b/auth.py @@ -11,6 +11,7 @@ import hashlib import binascii import os import secrets +from utils import isSystemAccount def hashPassword(password: str) -> str: @@ -85,7 +86,7 @@ def authorizeBasic(baseDir: str, path: str, authHeader: str, """ if ' ' not in authHeader: if debug: - print('DEBUG: Authorixation header does not ' + + print('DEBUG: basic auth - Authorixation header does not ' + 'contain a space character') return False if '/users/' not in path and \ @@ -93,23 +94,32 @@ def authorizeBasic(baseDir: str, path: str, authHeader: str, '/channel/' not in path and \ '/profile/' not in path: if debug: - print('DEBUG: Path for Authorization does not contain a user') + print('DEBUG: basic auth - ' + + 'path for Authorization does not contain a user') return False pathUsersSection = path.split('/users/')[1] if '/' not in pathUsersSection: if debug: - print('DEBUG: This is not a users endpoint') + print('DEBUG: basic auth - this is not a users endpoint') return False nicknameFromPath = pathUsersSection.split('/')[0] + if isSystemAccount(nicknameFromPath): + print('basic auth - attempted login using system account ' + + nicknameFromPath + ' in path') + return False base64Str = \ authHeader.split(' ')[1].replace('\n', '').replace('\r', '') plain = base64.b64decode(base64Str).decode('utf-8') if ':' not in plain: if debug: - print('DEBUG: Basic Auth header does not contain a ":" ' + + print('DEBUG: basic auth header does not contain a ":" ' + 'separator for username:password') return False nickname = plain.split(':')[0] + if isSystemAccount(nickname): + print('basic auth - attempted login using system account ' + nickname + + ' in Auth header') + return False if nickname != nicknameFromPath: if debug: print('DEBUG: Nickname given in the path (' + nicknameFromPath + diff --git a/blocking.py b/blocking.py index 395faf234..8cb23e5b8 100644 --- a/blocking.py +++ b/blocking.py @@ -131,6 +131,8 @@ def isBlockedHashtag(baseDir: str, hashtag: str) -> bool: globalBlockingFilename = baseDir + '/accounts/blocking.txt' if os.path.isfile(globalBlockingFilename): hashtag = hashtag.strip('\n').strip('\r') + if not hashtag.startswith('#'): + hashtag = '#' + hashtag if hashtag + '\n' in open(globalBlockingFilename).read(): return True return False diff --git a/blog.py b/blog.py index d14c72951..b0681a5e6 100644 --- a/blog.py +++ b/blog.py @@ -10,11 +10,11 @@ import os from datetime import datetime from content import replaceEmojiFromTags -from webapp import getIconsWebPath -from webapp import htmlHeaderWithExternalStyle -from webapp import htmlFooter -from webapp_media import addEmbeddedElements +from webapp_utils import getIconsWebPath +from webapp_utils import htmlHeaderWithExternalStyle +from webapp_utils import htmlFooter from webapp_utils import getPostAttachmentsAsHtml +from webapp_media import addEmbeddedElements from utils import getMediaFormats from utils import getNicknameFromActor from utils import getDomainFromActor diff --git a/content.py b/content.py index efac10f97..9e83b354e 100644 --- a/content.py +++ b/content.py @@ -8,6 +8,7 @@ __status__ = "Production" import os import email.parser +import urllib.parse from shutil import copyfile from utils import getImageExtensions from utils import loadJson @@ -991,5 +992,5 @@ def extractTextFieldsInPOST(postBytes, boundary, debug: bool) -> {}: if line > 2: postValue += '\n' postValue += postLines[line] - fields[postKey] = postValue + fields[postKey] = urllib.parse.unquote_plus(postValue) return fields diff --git a/daemon.py b/daemon.py index 7ffaba474..2ab60cb3c 100644 --- a/daemon.py +++ b/daemon.py @@ -40,6 +40,8 @@ from ssb import getSSBAddress from ssb import setSSBAddress from tox import getToxAddress from tox import setToxAddress +from jami import getJamiAddress +from jami import setJamiAddress from matrix import getMatrixAddress from matrix import setMatrixAddress from donate import getDonationUrl @@ -63,7 +65,6 @@ from person import canRemovePost from person import personSnooze from person import personUnsnooze from posts import isModerator -from posts import isEditor from posts import mutePost from posts import unmutePost from posts import createQuestionPost @@ -113,15 +114,16 @@ from blog import htmlBlogView from blog import htmlBlogPage from blog import htmlBlogPost from blog import htmlEditBlog +from webapp_utils import htmlHashtagBlocked +from webapp_utils import htmlFollowingList from webapp_utils import setBlogAddress from webapp_utils import getBlogAddress from webapp_calendar import htmlCalendarDeleteConfirm from webapp_calendar import htmlCalendar from webapp_about import htmlAbout -from webapp import htmlFollowingList -from webapp import htmlDeletePost -from webapp import htmlRemoveSharedItem -from webapp import htmlUnblockConfirm +from webapp_confirm import htmlConfirmDelete +from webapp_confirm import htmlConfirmRemoveSharedItem +from webapp_confirm import htmlConfirmUnblock from webapp_person_options import htmlPersonOptions from webapp_timeline import htmlShares from webapp_timeline import htmlInbox @@ -132,6 +134,7 @@ from webapp_timeline import htmlInboxReplies from webapp_timeline import htmlInboxMedia from webapp_timeline import htmlInboxBlogs from webapp_timeline import htmlInboxNews +from webapp_timeline import htmlInboxFeatures from webapp_timeline import htmlOutbox from webapp_moderation import htmlModeration from webapp_moderation import htmlModerationInfo @@ -140,9 +143,8 @@ from webapp_login import htmlLogin from webapp_login import htmlGetLoginCredentials from webapp_suspended import htmlSuspended from webapp_tos import htmlTermsOfService -from webapp import htmlFollowConfirm -from webapp import htmlUnfollowConfirm -from webapp import htmlHashtagBlocked +from webapp_confirm import htmlConfirmFollow +from webapp_confirm import htmlConfirmUnfollow from webapp_post import htmlPostReplies from webapp_post import htmlIndividualPost from webapp_profile import htmlEditProfile @@ -162,10 +164,14 @@ from webapp_search import htmlSearchEmoji from webapp_search import htmlSearchSharedItems from webapp_search import htmlSearchEmojiTextEntry from webapp_search import htmlSearch +from webapp_hashtagswarm import getHashtagCategoriesFeed +from webapp_hashtagswarm import htmlSearchHashtagCategory from shares import getSharesFeedForPerson from shares import addShare from shares import removeShare from shares import expireShares +from utils import setHashtagCategory +from utils import isEditor from utils import getImageExtensions from utils import mediaFileMimeType from utils import getCSS @@ -227,6 +233,7 @@ from newswire import rss2Header from newswire import rss2Footer from newsdaemon import runNewswireWatchdog from newsdaemon import runNewswireDaemon +from filters import isFiltered import os @@ -1109,6 +1116,7 @@ class PubServer(BaseHTTPRequestHandler): if self.path.startswith('/icons/') or \ self.path.startswith('/avatars/') or \ self.path.startswith('/favicon.ico') or \ + self.path.startswith('/categories.xml') or \ self.path.startswith('/newswire.xml'): return False @@ -1120,21 +1128,22 @@ class PubServer(BaseHTTPRequestHandler): tokenStr = tokenStr.split(';')[0].strip() if self.server.tokensLookup.get(tokenStr): nickname = self.server.tokensLookup[tokenStr] - self.authorizedNickname = nickname - # default to the inbox of the person - if self.path == '/': - self.path = '/users/' + nickname + '/inbox' - # check that the path contains the same nickname - # as the cookie otherwise it would be possible - # to be authorized to use an account you don't own - if '/' + nickname + '/' in self.path: - return True - elif '/' + nickname + '?' in self.path: - return True - elif self.path.endswith('/' + nickname): - return True - print('AUTH: nickname ' + nickname + - ' was not found in path ' + self.path) + if not isSystemAccount(nickname): + self.authorizedNickname = nickname + # default to the inbox of the person + if self.path == '/': + self.path = '/users/' + nickname + '/inbox' + # check that the path contains the same nickname + # as the cookie otherwise it would be possible + # to be authorized to use an account you don't own + if '/' + nickname + '/' in self.path: + return True + elif '/' + nickname + '?' in self.path: + return True + elif self.path.endswith('/' + nickname): + return True + print('AUTH: nickname ' + nickname + + ' was not found in path ' + self.path) return False print('AUTH: epicyon cookie ' + 'authorization failed, header=' + @@ -1144,13 +1153,13 @@ class PubServer(BaseHTTPRequestHandler): return False print('AUTH: Header cookie was not authorized') return False - # basic auth + # basic auth for c2s if self.headers.get('Authorization'): if authorize(self.server.baseDir, self.path, self.headers['Authorization'], self.server.debug): return True - print('AUTH: Basic auth did not authorize ' + + print('AUTH: C2S Basic auth did not authorize ' + self.headers['Authorization']) return False @@ -1518,9 +1527,7 @@ class PubServer(BaseHTTPRequestHandler): moderationText) if postFilename: if canRemovePost(baseDir, - nickname, - domain, - port, + nickname, domain, port, moderationText): deletePost(baseDir, httpPrefix, @@ -1528,6 +1535,23 @@ class PubServer(BaseHTTPRequestHandler): postFilename, debug, self.server.recentPostsCache) + if nickname != 'news': + # if this is a local blog post then also remove it + # from the news actor + postFilename = \ + locatePost(baseDir, 'news', domain, + moderationText) + if postFilename: + if canRemovePost(baseDir, + 'news', domain, port, + moderationText): + deletePost(baseDir, + httpPrefix, + 'news', domain, + postFilename, + debug, + self.server.recentPostsCache) + if callingDomain.endswith('.onion') and onionDomain: actorStr = 'http://' + onionDomain + usersPath elif (callingDomain.endswith('.i2p') and i2pDomain): @@ -1773,7 +1797,7 @@ class PubServer(BaseHTTPRequestHandler): if debug: print('Unblocking ' + optionsActor) msg = \ - htmlUnblockConfirm(self.server.cssCache, + htmlConfirmUnblock(self.server.cssCache, self.server.translate, baseDir, usersPath, @@ -1791,7 +1815,7 @@ class PubServer(BaseHTTPRequestHandler): if debug: print('Following ' + optionsActor) msg = \ - htmlFollowConfirm(self.server.cssCache, + htmlConfirmFollow(self.server.cssCache, self.server.translate, baseDir, usersPath, @@ -1808,7 +1832,7 @@ class PubServer(BaseHTTPRequestHandler): if '&submitUnfollow=' in optionsConfirmParams: print('Unfollowing ' + optionsActor) msg = \ - htmlUnfollowConfirm(self.server.cssCache, + htmlConfirmUnfollow(self.server.cssCache, self.server.translate, baseDir, usersPath, @@ -2732,7 +2756,7 @@ class PubServer(BaseHTTPRequestHandler): domain: str, domainFull: str, onionDomain: str, i2pDomain: str, debug: bool) -> None: - """Endpoint for removing posts + """Endpoint for removing posts after confirmation """ pageNumber = 1 usersPath = path.split('/rmpost')[0] @@ -2796,7 +2820,7 @@ class PubServer(BaseHTTPRequestHandler): 'actor': removePostActor, 'object': removeMessageId, 'to': toList, - 'cc': [removePostActor+'/followers'], + 'cc': [removePostActor + '/followers'], 'type': 'Delete' } self.postToNickname = getNicknameFromActor(removePostActor) @@ -2959,6 +2983,129 @@ class PubServer(BaseHTTPRequestHandler): cookie, callingDomain) self.server.POSTbusy = False + def _setHashtagCategory(self, callingDomain: str, cookie: str, + authorized: bool, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, + onionDomain: str, i2pDomain: str, debug: bool, + defaultTimeline: str, + allowLocalNetworkAccess: bool) -> None: + """On the screen after selecting a hashtag from the swarm, this sets + the category for that tag + """ + usersPath = path.replace('/sethashtagcategory', '') + hashtag = '' + if '/tags/' not in usersPath: + # no hashtag is specified within the path + self._404() + return + hashtag = usersPath.split('/tags/')[1].strip() + hashtag = urllib.parse.unquote_plus(hashtag) + if not hashtag: + # no hashtag was given in the path + self._404() + return + hashtagFilename = baseDir + '/tags/' + hashtag + '.txt' + if not os.path.isfile(hashtagFilename): + # the hashtag does not exist + self._404() + return + usersPath = usersPath.split('/tags/')[0] + actorStr = httpPrefix + '://' + domainFull + usersPath + tagScreenStr = actorStr + '/tags/' + hashtag + if ' boundary=' in self.headers['Content-type']: + boundary = self.headers['Content-type'].split('boundary=')[1] + if ';' in boundary: + boundary = boundary.split(';')[0] + + # get the nickname + nickname = getNicknameFromActor(actorStr) + editor = None + if nickname: + editor = isEditor(baseDir, nickname) + if not hashtag or not editor: + if callingDomain.endswith('.onion') and \ + onionDomain: + actorStr = \ + 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and + i2pDomain): + actorStr = \ + 'http://' + i2pDomain + usersPath + if not nickname: + print('WARN: nickname not found in ' + actorStr) + else: + print('WARN: nickname is not a moderator' + actorStr) + self._redirect_headers(tagScreenStr, cookie, callingDomain) + self.server.POSTbusy = False + return + + length = int(self.headers['Content-length']) + + # check that the POST isn't too large + if length > self.server.maxPostLength: + if callingDomain.endswith('.onion') and \ + onionDomain: + actorStr = \ + 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and + i2pDomain): + actorStr = \ + 'http://' + i2pDomain + usersPath + print('Maximum links data length exceeded ' + str(length)) + self._redirect_headers(tagScreenStr, cookie, callingDomain) + self.server.POSTbusy = False + return + + try: + # read the bytes of the http form POST + postBytes = self.rfile.read(length) + except SocketError as e: + if e.errno == errno.ECONNRESET: + print('WARN: connection was reset while ' + + 'reading bytes from http form POST') + else: + print('WARN: error while reading bytes ' + + 'from http form POST') + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + except ValueError as e: + print('ERROR: failed to read bytes for POST') + print(e) + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + + # extract all of the text fields into a dict + fields = \ + extractTextFieldsInPOST(postBytes, boundary, debug) + + if fields.get('hashtagCategory'): + categoryStr = fields['hashtagCategory'].lower() + if not isBlockedHashtag(baseDir, categoryStr) and \ + not isFiltered(baseDir, nickname, domain, categoryStr): + setHashtagCategory(baseDir, hashtag, categoryStr) + else: + categoryFilename = baseDir + '/tags/' + hashtag + '.category' + if os.path.isfile(categoryFilename): + os.remove(categoryFilename) + + # redirect back to the default timeline + if callingDomain.endswith('.onion') and \ + onionDomain: + actorStr = \ + 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and + i2pDomain): + actorStr = \ + 'http://' + i2pDomain + usersPath + self._redirect_headers(tagScreenStr, + cookie, callingDomain) + self.server.POSTbusy = False + def _newswireUpdate(self, callingDomain: str, cookie: str, authorized: bool, path: str, baseDir: str, httpPrefix: str, @@ -3207,7 +3354,7 @@ class PubServer(BaseHTTPRequestHandler): domain: str, domainFull: str, onionDomain: str, i2pDomain: str, debug: bool, defaultTimeline: str) -> None: - """edits a news post + """edits a news post after receiving POST """ usersPath = path.replace('/newseditdata', '') usersPath = usersPath.replace('/editnewspost', '') @@ -3235,8 +3382,12 @@ class PubServer(BaseHTTPRequestHandler): print('WARN: nickname not found in ' + actorStr) else: print('WARN: nickname is not an editor' + actorStr) - self._redirect_headers(actorStr + '/tlnews', - cookie, callingDomain) + if self.server.newsInstance: + self._redirect_headers(actorStr + '/tlfeatures', + cookie, callingDomain) + else: + self._redirect_headers(actorStr + '/tlnews', + cookie, callingDomain) self.server.POSTbusy = False return @@ -3253,8 +3404,12 @@ class PubServer(BaseHTTPRequestHandler): actorStr = \ 'http://' + i2pDomain + usersPath print('Maximum news data length exceeded ' + str(length)) - self._redirect_headers(actorStr + 'tlnews', - cookie, callingDomain) + if self.server.newsInstance: + self._redirect_headers(actorStr + '/tlfeatures', + cookie, callingDomain) + else: + self._redirect_headers(actorStr + '/tlnews', + cookie, callingDomain) self.server.POSTbusy = False return @@ -3342,8 +3497,12 @@ class PubServer(BaseHTTPRequestHandler): i2pDomain): actorStr = \ 'http://' + i2pDomain + usersPath - self._redirect_headers(actorStr + '/tlnews', - cookie, callingDomain) + if self.server.newsInstance: + self._redirect_headers(actorStr + '/tlfeatures', + cookie, callingDomain) + else: + self._redirect_headers(actorStr + '/tlnews', + cookie, callingDomain) self.server.POSTbusy = False def _profileUpdate(self, callingDomain: str, cookie: str, @@ -3616,7 +3775,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.newsInstance = True self.server.blogsInstance = False self.server.mediaInstance = False - self.server.defaultTimeline = 'tlnews' + self.server.defaultTimeline = 'tlfeatures' setConfigParam(baseDir, "mediaInstance", self.server.mediaInstance) @@ -3758,6 +3917,18 @@ class PubServer(BaseHTTPRequestHandler): setToxAddress(actorJson, '') actorChanged = True + # change jami address + currentJamiAddress = getJamiAddress(actorJson) + if fields.get('jamiAddress'): + if fields['jamiAddress'] != currentJamiAddress: + setJamiAddress(actorJson, + fields['jamiAddress']) + actorChanged = True + else: + if currentJamiAddress: + setJamiAddress(actorJson, '') + actorChanged = True + # change PGP public key currentPGPpubKey = getPGPpubKey(actorJson) if fields.get('pgp'): @@ -4643,6 +4814,41 @@ class PubServer(BaseHTTPRequestHandler): path + ' ' + callingDomain) self._404() + def _getHashtagCategoriesFeed(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, port: int, proxyType: str, + GETstartTime, GETtimings: {}, + debug: bool) -> None: + """Returns the hashtag categories feed + """ + if not self.server.session: + print('Starting new session during RSS categories request') + self.server.session = \ + createSession(proxyType) + if not self.server.session: + print('ERROR: GET failed to create session ' + + 'during RSS categories request') + self._404() + return + + hashtagCategories = None + msg = \ + getHashtagCategoriesFeed(baseDir, hashtagCategories) + if msg: + msg = msg.encode('utf-8') + self._set_headers('text/xml', len(msg), + None, callingDomain) + self._write(msg) + if debug: + print('Sent rss2 categories feed: ' + + path + ' ' + callingDomain) + return + if debug: + print('Failed to get rss2 categories feed: ' + + path + ' ' + callingDomain) + self._404() + def _getRSS3feed(self, authorized: bool, callingDomain: str, path: str, baseDir: str, httpPrefix: str, @@ -4718,6 +4924,7 @@ class PubServer(BaseHTTPRequestHandler): matrixAddress = None blogAddress = None toxAddress = None + jamiAddress = None ssbAddress = None emailAddress = None actorJson = getPersonFromCache(baseDir, @@ -4731,6 +4938,7 @@ class PubServer(BaseHTTPRequestHandler): ssbAddress = getSSBAddress(actorJson) blogAddress = getBlogAddress(actorJson) toxAddress = getToxAddress(actorJson) + jamiAddress = getJamiAddress(actorJson) emailAddress = getEmailAddress(actorJson) PGPpubKey = getPGPpubKey(actorJson) PGPfingerprint = getPGPfingerprint(actorJson) @@ -4746,7 +4954,7 @@ class PubServer(BaseHTTPRequestHandler): pageNumber, donateUrl, xmppAddress, matrixAddress, ssbAddress, blogAddress, - toxAddress, + toxAddress, jamiAddress, PGPpubKey, PGPfingerprint, emailAddress).encode('utf-8') self._set_headers('text/html', len(msg), @@ -4758,7 +4966,7 @@ class PubServer(BaseHTTPRequestHandler): return if '/users/news/' in path: - self._redirect_headers(originPathStr + '/tlnews', + self._redirect_headers(originPathStr + '/tlfeatures', cookie, callingDomain) return @@ -4929,7 +5137,9 @@ class PubServer(BaseHTTPRequestHandler): hashtag = path.split('/tags/')[1] if '?page=' in hashtag: hashtag = hashtag.split('?page=')[0] + hashtag = urllib.parse.unquote_plus(hashtag) if isBlockedHashtag(baseDir, hashtag): + print('BLOCK: hashtag #' + hashtag) msg = htmlHashtagBlocked(self.server.cssCache, baseDir, self.server.translate).encode('utf-8') self._login_headers('text/html', len(msg), callingDomain) @@ -5791,7 +6001,7 @@ class PubServer(BaseHTTPRequestHandler): GETstartTime, GETtimings: {}, proxyType: str, cookie: str, debug: str): - """Delete button is pressed + """Delete button is pressed on a post """ if not cookie: print('ERROR: no cookie given when deleting') @@ -5855,16 +6065,16 @@ class PubServer(BaseHTTPRequestHandler): return deleteStr = \ - htmlDeletePost(self.server.cssCache, - self.server.recentPostsCache, - self.server.maxRecentPosts, - self.server.translate, pageNumber, - self.server.session, baseDir, - deleteUrl, httpPrefix, - __version__, self.server.cachedWebfingers, - self.server.personCache, callingDomain, - self.server.YTReplacementDomain, - self.server.showPublishedDateOnly) + htmlConfirmDelete(self.server.cssCache, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, pageNumber, + self.server.session, baseDir, + deleteUrl, httpPrefix, + __version__, self.server.cachedWebfingers, + self.server.personCache, callingDomain, + self.server.YTReplacementDomain, + self.server.showPublishedDateOnly) if deleteStr: self._set_headers('text/html', len(deleteStr), cookie, callingDomain) @@ -7299,6 +7509,127 @@ class PubServer(BaseHTTPRequestHandler): return True return False + def _showFeaturesTimeline(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str) -> bool: + """Shows the features timeline (all local blogs) + """ + if '/users/' in path: + if authorized: + inboxFeaturesFeed = \ + personBoxJson(self.server.recentPostsCache, + self.server.session, + baseDir, + domain, + port, + path, + httpPrefix, + maxPostsInNewsFeed, 'tlfeatures', + True, + self.server.newswireVotesThreshold, + self.server.positiveVoting, + self.server.votingTimeMins) + if not inboxFeaturesFeed: + inboxFeaturesFeed = [] + if self._requestHTTP(): + nickname = path.replace('/users/', '') + nickname = nickname.replace('/tlfeatures', '') + pageNumber = 1 + if '?page=' in nickname: + pageNumber = nickname.split('?page=')[1] + nickname = nickname.split('?page=')[0] + if pageNumber.isdigit(): + pageNumber = int(pageNumber) + else: + pageNumber = 1 + if 'page=' not in path: + # if no page was specified then show the first + inboxFeaturesFeed = \ + personBoxJson(self.server.recentPostsCache, + self.server.session, + baseDir, + domain, + port, + path + '?page=1', + httpPrefix, + maxPostsInBlogsFeed, 'tlfeatures', + True, + self.server.newswireVotesThreshold, + self.server.positiveVoting, + self.server.votingTimeMins) + currNickname = path.split('/users/')[1] + if '/' in currNickname: + currNickname = currNickname.split('/')[0] + fullWidthTimelineButtonHeader = \ + self.server.fullWidthTimelineButtonHeader + msg = \ + htmlInboxFeatures(self.server.cssCache, + self.server.defaultTimeline, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + pageNumber, maxPostsInBlogsFeed, + self.server.session, + baseDir, + self.server.cachedWebfingers, + self.server.personCache, + nickname, + domain, + port, + inboxFeaturesFeed, + self.server.allowDeletion, + httpPrefix, + self.server.projectVersion, + self._isMinimal(nickname), + self.server.YTReplacementDomain, + self.server.showPublishedDateOnly, + self.server.newswire, + 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) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show blogs 2 done', + 'show news 2') + else: + # don't need authenticated fetch here because there is + # already the authorization check + msg = json.dumps(inboxFeaturesFeed, + ensure_ascii=False) + msg = msg.encode('utf-8') + self._set_headers('application/json', + len(msg), + None, callingDomain) + self._write(msg) + self.server.GETbusy = False + return True + else: + if debug: + nickname = 'news' + print('DEBUG: ' + nickname + + ' was not authorized to access ' + path) + if path != '/tlfeatures': + # not the features inbox + if debug: + print('DEBUG: GET access to features is unauthorized') + self.send_response(405) + self.end_headers() + self.server.GETbusy = False + return True + return False + def _showSharesTimeline(self, authorized: bool, callingDomain: str, path: str, baseDir: str, httpPrefix: str, @@ -8730,11 +9061,16 @@ class PubServer(BaseHTTPRequestHandler): """Show the edit screen for a news post """ if '/users/' in path and '/editnewspost=' in path: + postActor = 'news' + if '?actor=' in path: + postActor = path.split('?actor=')[1] + if '?' in postActor: + postActor = postActor.split('?')[0] postId = path.split('/editnewspost=')[1] if '?' in postId: postId = postId.split('?')[0] postUrl = httpPrefix + '://' + domainFull + \ - '/users/news/statuses/' + postId + '/users/' + postActor + '/statuses/' + postId path = path.split('/editnewspost=')[0] msg = htmlEditNewsPost(self.server.cssCache, translate, baseDir, @@ -8979,6 +9315,18 @@ class PubServer(BaseHTTPRequestHandler): self._benchmarkGETtimings(GETstartTime, GETtimings, 'fonts', 'sharedInbox enabled') + if self.path == '/categories.xml': + self._getHashtagCategoriesFeed(authorized, + callingDomain, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.port, + self.server.proxyType, + GETstartTime, GETtimings, + self.server.debug) + return + if self.path == '/newswire.xml': self._getNewswireFeed(authorized, callingDomain, self.path, @@ -9178,11 +9526,11 @@ class PubServer(BaseHTTPRequestHandler): actor = \ self.server.httpPrefix + '://' + \ self.server.domainFull + usersPath - msg = htmlRemoveSharedItem(self.server.cssCache, - self.server.translate, - self.server.baseDir, - actor, shareName, - callingDomain).encode('utf-8') + msg = htmlConfirmRemoveSharedItem(self.server.cssCache, + self.server.translate, + self.server.baseDir, + actor, shareName, + callingDomain).encode('utf-8') if not msg: if callingDomain.endswith('.onion') and \ self.server.onionDomain: @@ -9772,7 +10120,7 @@ class PubServer(BaseHTTPRequestHandler): elif self.server.mediaInstance: self.path = '/users/' + nickname + '/tlmedia' else: - self.path = '/users/' + nickname + '/tlnews' + self.path = '/users/' + nickname + '/tlfeatures' # search for a fediverse address, shared item or emoji # from the web interface by selecting search icon @@ -9795,6 +10143,22 @@ class PubServer(BaseHTTPRequestHandler): 'search screen shown') return + # show a hashtag category from the search screen + if htmlGET and '/category/' in self.path: + msg = htmlSearchHashtagCategory(self.server.cssCache, + self.server.translate, + self.server.baseDir, self.path, + self.server.domain) + if msg: + msg = msg.encode('utf-8') + self._set_headers('text/html', len(msg), cookie, callingDomain) + self._write(msg) + self.server.GETbusy = False + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'hashtag category done', + 'hashtag category screen shown') + return + self._benchmarkGETtimings(GETstartTime, GETtimings, 'hashtag search done', 'search screen shown done') @@ -10527,6 +10891,23 @@ class PubServer(BaseHTTPRequestHandler): cookie, self.server.debug): return + # get features (local blogs) for a given person + if self.path.endswith('/tlfeatures') or \ + '/tlfeatures?page=' in self.path: + if self._showFeaturesTimeline(authorized, + callingDomain, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + GETstartTime, GETtimings, + self.server.proxyType, + cookie, self.server.debug): + return + self._benchmarkGETtimings(GETstartTime, GETtimings, 'show blogs 2 done', 'show news 2 done') @@ -11111,6 +11492,13 @@ class PubServer(BaseHTTPRequestHandler): replaceYouTube(postJsonObject, self.server.YTReplacementDomain) saveJson(postJsonObject, postFilename) + # also save to the news actor + if nickname != 'news': + postFilename = \ + postFilename.replace('#users#' + + nickname + '#', + '#users#news#') + saveJson(postJsonObject, postFilename) print('Edited blog post, resaved ' + postFilename) return 1 else: @@ -11759,6 +12147,20 @@ class PubServer(BaseHTTPRequestHandler): self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 2) + if authorized and self.path.endswith('/sethashtagcategory'): + self._setHashtagCategory(callingDomain, cookie, + authorized, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.onionDomain, + self.server.i2pDomain, + self.server.debug, + self.server.defaultTimeline, + self.server.allowLocalNetworkAccess) + return + # update of profile/avatar from web interface, # after selecting Edit button then Submit if authorized and self.path.endswith('/profiledata'): @@ -12486,7 +12888,7 @@ def runDaemon(maxNewswirePosts: int, if blogsInstance: httpd.defaultTimeline = 'tlblogs' if newsInstance: - httpd.defaultTimeline = 'tlnews' + httpd.defaultTimeline = 'tlfeatures' # load translations dictionary httpd.translate = {} @@ -12579,6 +12981,9 @@ def runDaemon(maxNewswirePosts: int, # maximum size of individual RSS feed items, in K httpd.maxFeedItemSizeKb = maxFeedItemSizeKb + # maximum size of a hashtag category, in K + httpd.maxCategoriesFeedItemSizeKb = 256 + if registration == 'open': httpd.registration = True else: diff --git a/epicyon-blog.css b/epicyon-blog.css index 277a9710a..5adc38984 100644 --- a/epicyon-blog.css +++ b/epicyon-blog.css @@ -3,6 +3,7 @@ :root { --main-bg-color: #282c37; --link-bg-color: #282c37; + --title-color: #999; --dropdown-bg-color: #111; --dropdown-bg-color-hover: #333; --main-bg-color-reply: #212c37; @@ -16,6 +17,7 @@ --font-size-header: 18px; --font-color-header: #ccc; --font-size-button-mobile: 34px; + --font-size-mobile: 50px; --font-size: 30px; --font-size2: 24px; --font-size3: 38px; @@ -45,6 +47,7 @@ --focus-color: white; --line-spacing: 130%; --header-font: 'Bedstead'; + --main-link-color-hover: #bbb; } @font-face { @@ -78,27 +81,33 @@ body, html { a, u { color: var(--main-fg-color); } - + a:visited{ color: var(--main-visited-color); background: var(--link-bg-color); - font-weight: bold; + font-weight: normal; + text-decoration: none; } a:link { color: var(--main-link-color); background: var(--link-bg-color); - font-weight: bold; + font-weight: normal; + text-decoration: none; +} + +a:link:hover { + color: var(--main-link-color-hover); +} + +a:visited:hover { + color: var(--main-link-color-hover); } a:focus { border: 2px solid var(--focus-color); } -h1 { - font-family: var(--header-font); -} - .cwText { display: none; } @@ -689,6 +698,11 @@ div.gallery img { font-size: var(--font-size4); font-family: "Times New Roman", Roman, serif; } + h1 { + font-family: var(--header-font); + font-size: var(--font-size); + color: var(--title-color); + } .galleryContainer { display: grid; grid-template-columns: 50% 50%; @@ -1041,6 +1055,11 @@ div.gallery img { font-size: var(--font-size3); font-family: "Times New Roman", Roman, serif; } + h1 { + font-family: var(--header-font); + font-size: var(--font-size-mobile); + color: var(--title-color); + } div.gallerytext { color: var(--gallery-text-color); font-size: var(--gallery-font-size-mobile); diff --git a/epicyon-calendar.css b/epicyon-calendar.css index c51568bbe..076d5ad1f 100644 --- a/epicyon-calendar.css +++ b/epicyon-calendar.css @@ -23,6 +23,7 @@ --font-size-calendar-cell-mobile: 4rem; --calendar-header-font: 'Montserrat'; --calendar-header-font-style: italic; + --main-link-color-hover: #bbb; } @font-face { @@ -59,6 +60,7 @@ a:visited{ z-index: 1; padding: 1rem; margin: -1rem; + font-weight: normal; } a:link { @@ -67,6 +69,15 @@ a:link { z-index: 1; padding: 1rem; margin: -1rem; + font-weight: normal; +} + +a:link:hover { + color: var(--main-link-color-hover); +} + +a:visited:hover { + color: var(--main-link-color-hover); } a:focus { diff --git a/epicyon-follow.css b/epicyon-follow.css index 1e255985d..8007cbd11 100644 --- a/epicyon-follow.css +++ b/epicyon-follow.css @@ -31,6 +31,7 @@ --follow-text-size2: 40px; --follow-text-entry-width: 90%; --focus-color: white; + --main-link-color-hover: #bbb; } @font-face { @@ -66,13 +67,23 @@ a, u { a:visited{ color: var(--main-visited-color); background: var(--link-bg-color); - font-weight: bold; + font-weight: normal; + text-decoration: none; } a:link { color: var(--main-link-color); background: var(--link-bg-color); - font-weight: bold; + font-weight: normal; + text-decoration: none; +} + +a:link:hover { + color: var(--main-link-color-hover); +} + +a:visited:hover { + color: var(--main-link-color-hover); } a:focus { diff --git a/epicyon-links.css b/epicyon-links.css index 8cc051baa..3171925e2 100644 --- a/epicyon-links.css +++ b/epicyon-links.css @@ -154,13 +154,15 @@ a, u { a:visited{ color: var(--main-visited-color); background: var(--link-bg-color); - font-weight: bold; + font-weight: normal; + text-decoration: none; } a:link { color: var(--main-link-color); background: var(--link-bg-color); - font-weight: bold; + font-weight: normal; + text-decoration: none; } a:link:hover { @@ -213,6 +215,7 @@ a:focus { h1 { font-family: var(--header-font); + font-size: var(--font-size); color: var(--title-color); } diff --git a/epicyon-login.css b/epicyon-login.css index 8d55f72b9..6e8db72d5 100644 --- a/epicyon-login.css +++ b/epicyon-login.css @@ -1,9 +1,9 @@ @charset "UTF-8"; :root { - --main-bg-color: #282c37; + --login-bg-color: #282c37; --link-bg-color: #282c37; - --main-fg-color: #dddddd; + --login-fg-color: #dddddd; --main-link-color: #999; --main-visited-color: #888; --border-color: #505050; @@ -22,6 +22,7 @@ --focus-color: white; --line-spacing: 130%; --login-logo-width: 20%; + --main-link-color-hover: #bbb; } @font-face { @@ -40,8 +41,8 @@ } body, html { - background-color: var(--main-bg-color); - color: var(--main-fg-color); + background-color: var(--login-bg-color); + color: var(--login-fg-color); background-image: url("/login-background.jpg"); background-size: cover; @@ -59,19 +60,29 @@ body, html { } a, u { - color: var(--main-fg-color); + color: var(--login-fg-color); } a:visited{ color: var(--main-visited-color); background: var(--link-bg-color); - font-weight: bold; + font-weight: normal; + text-decoration: none; } a:link { color: var(--main-link-color); background: var(--link-bg-color); - font-weight: bold; + font-weight: normal; + text-decoration: none; +} + +a:link:hover { + color: var(--main-link-color-hover); +} + +a:visited:hover { + color: var(--main-link-color-hover); } a:focus { @@ -146,8 +157,8 @@ span.psw { @media screen and (min-width: 400px) { body, html { - background-color: var(--main-bg-color); - color: var(--main-fg-color); + background-color: var(--login-bg-color); + color: var(--login-fg-color); height: 100%; font-family: Arial, Helvetica, sans-serif; max-width: 60%; @@ -186,8 +197,8 @@ span.psw { @media screen and (max-width: 1000px) { body, html { - background-color: var(--main-bg-color); - color: var(--main-fg-color); + background-color: var(--login-bg-color); + color: var(--login-fg-color); height: 100%; font-family: Arial, Helvetica, sans-serif; max-width: 95%; diff --git a/epicyon-options.css b/epicyon-options.css index 330f108cf..1675d65da 100644 --- a/epicyon-options.css +++ b/epicyon-options.css @@ -1,9 +1,9 @@ @charset "UTF-8"; :root { - --main-bg-color: #282c37; - --link-bg-color: #282c37; - --main-fg-color: #dddddd; + --options-bg-color: #282c37; + --options-link-bg-color: transparent; + --options-fg-color: #dddddd; --main-link-color: #999; --main-visited-color: #888; --border-color: #505050; @@ -34,6 +34,7 @@ --follow-text-entry-width: 90%; --focus-color: white; --petname-width-chars: 16ch; + --main-link-color-hover: #bbb; } @font-face { @@ -58,8 +59,8 @@ body, html { -moz-background-size: cover; background-repeat: no-repeat; background-position: center; - background-color: var(--main-bg-color); - color: var(--main-fg-color); + background-color: var(--options-bg-color); + color: var(--options-fg-color); height: 100%; font-family: Arial, Helvetica, sans-serif; @@ -68,19 +69,29 @@ body, html { } a, u { - color: var(--main-fg-color); + color: var(--options-fg-color); } a:visited{ color: var(--main-visited-color); - background: var(--link-bg-color); - font-weight: bold; + background: var(--options-link-bg-color); + font-weight: normal; + text-decoration: none; } a:link { color: var(--main-link-color); - background: var(--link-bg-color); - font-weight: bold; + background: var(--options-link-bg-color); + font-weight: normal; + text-decoration: none; +} + +a:link:hover { + color: var(--main-link-color-hover); +} + +a:visited:hover { + color: var(--main-link-color-hover); } a:focus { @@ -90,7 +101,7 @@ a:focus { .follow { height: 100%; position: relative; - background-color: var(--main-bg-color); + background-color: var(--options-bg-color); } .followAvatar { @@ -111,7 +122,7 @@ a:focus { .pgp { font-size: var(--font-size5); color: var(--main-link-color); - background: var(--link-bg-color); + background: var(--options-link-bg-color); } .button:hover { @@ -123,6 +134,7 @@ a:focus { } .options img { + background-color: var(--options-bg-color); width: 15%; } @@ -132,7 +144,7 @@ a:focus { font-size: var(--font-size4); width: 90%; background-color: var(--text-entry-background); - color: white; + color: var(--text-entry-foreground); } .followText { font-size: var(--follow-text-size1); @@ -209,7 +221,7 @@ a:focus { font-size: var(--font-size); width: 90%; background-color: var(--text-entry-background); - color: white; + color: var(--text-entry-foreground); } .followText { font-size: var(--follow-text-size2); diff --git a/epicyon-profile.css b/epicyon-profile.css index caef0e4d0..94309a067 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -1,6 +1,8 @@ @charset "UTF-8"; :root { + --timeline-icon-width: 50px; + --timeline-icon-width-mobile: 100px; --header-bg-color: #282c37; --main-bg-color: #282c37; --post-bg-color: #282c37; @@ -31,8 +33,9 @@ --font-size-links: 18px; --font-size-publish-button: 18px; --font-size-newswire: 18px; - --font-size-newswire-mobile: 40px; + --font-size-newswire-mobile: 38px; --font-size-dropdown-header: 40px; + --font-size-mobile: 50px; --font-size: 30px; --font-size2: 24px; --font-size3: 38px; @@ -183,7 +186,7 @@ body, html { } .postSeparatorImage img { - background-color: var(--post-bg-color); + background-color: transparent; padding-top: var(--post-separator-margin-top); padding-bottom: var(--post-separator-margin-bottom); width: var(--post-separator-width); @@ -192,7 +195,7 @@ body, html { } .postSeparatorImageLeft img { - background-color: var(--column-left-color); + background-color: transparent; padding-top: var(--post-separator-margin-top); padding-bottom: var(--post-separator-margin-bottom); width: var(--separator-width-left); @@ -201,7 +204,7 @@ body, html { } .postSeparatorImageRight img { - background-color: var(--column-left-color); + background-color: transparent; padding-top: var(--post-separator-margin-top); padding-bottom: var(--post-separator-margin-bottom); width: var(--separator-width-right); @@ -254,25 +257,22 @@ blockquote p { border: 2px solid var(--focus-color); } -h1 { - font-family: var(--header-font); - color: var(--title-color); -} - a, u { color: var(--main-fg-color); } -a:visited{ +a:visited { color: var(--main-visited-color); background: var(--link-bg-color); - font-weight: bold; + font-weight: normal; + text-decoration: none; } a:link { color: var(--main-link-color); background: var(--link-bg-color); - font-weight: bold; + font-weight: normal; + text-decoration: none; } a:link:hover { @@ -284,7 +284,7 @@ a:visited:hover { } .buttonevent:hover { - filter: brightness(var(----icon-brightness-change)); + filter: brightness(var(--icon-brightness-change)); } a:focus { @@ -317,8 +317,6 @@ a:focus { .profileHeader * { -webkit-box-sizing: border-box; box-sizing: border-box; - -webkit-transition: all 0.25s ease; - transition: all 0.25s ease; } .profileHeader img.profileBackground { @@ -437,17 +435,7 @@ a:focus { cursor: pointer; display: inline-block; position: relative; - transition: 0.5s; -} - -.button span:after { - font-family: var(--header-font); - content: '\00bb'; - position: absolute; - opacity: 0; - top: 0; - right: -20px; - transition: 0.5s; + transition: 1.0s; } .button:hover { @@ -465,17 +453,7 @@ a:focus { cursor: pointer; display: inline-block; position: relative; - transition: 0.5s; -} - -.buttonselected span:after { - font-family: var(--header-font); - content: '\00bb'; - position: absolute; - opacity: 0; - top: 0; - right: -20px; - transition: 0.5s; + transition: 1.0s; } .buttonselected:hover { @@ -631,7 +609,7 @@ a:focus { } .containericons img:hover { - filter: brightness(var(----icon-brightness-change)); + filter: brightness(var(--icon-brightness-change)); } .post-title { @@ -940,14 +918,6 @@ div.gallery img { li { list-style:none;} /***********BUTTON CODE ******************************************************/ -a, button, input:focus, input[type='button'], input[type='reset'], input[type='submit'], textarea:focus, .button { - -webkit-transition: all 0.1s ease-in-out; - -moz-transition: all 0.1s ease-in-out; - -ms-transition: all 0.1s ease-in-out; - -o-transition: all 0.1s ease-in-out; - transition: all 0.1s ease-in-out; - text-decoration: none; -} .btn { margin: -3px 0 0 0; } @@ -971,6 +941,11 @@ div.container { font-size: var(--font-size); line-height: var(--line-spacing); } + h1 { + font-family: var(--header-font); + font-size: var(--font-size); + color: var(--title-color); + } .containerHeader { border: var(--border-width-header) solid var(--border-color); background-color: var(--header-bg-color); @@ -1067,12 +1042,14 @@ div.container { float: left; width: var(--column-left-width); } + .col-left img.leftColEditImage:hover { + filter: brightness(var(--icon-brightness-change)); + } .col-left img.leftColEdit { background: var(--column-left-color); width: var(--column-left-icon-size); } .col-left img.leftColEditImage { - background: var(--column-left-color); width: var(--column-left-icon-size); float: right; } @@ -1112,12 +1089,15 @@ div.container { width: var(--column-right-width); overflow: hidden; } + .col-right img.rightColEditImage:hover { + filter: brightness(var(--icon-brightness-change)); + } .col-right img.rightColEdit { - background: var(--column-left-color); + background: transparent; width: var(--column-right-icon-size); } .col-right img.rightColEditImage { - background: var(--column-left-color); + background: transparent; width: var(--column-right-icon-size); float: right; } @@ -1212,7 +1192,7 @@ div.container { margin-right: 0px; padding: 0px 0px; margin: 0px 0px; - width: 50px; + width: var(--timeline-icon-width); } .containerHeader img.timelineicon { float: var(--icons-side); @@ -1220,7 +1200,7 @@ div.container { margin-right:0; padding: 0 0; margin: 0 0; - width: 50px; + width: var(--timeline-icon-width); } .container img.emojiheader { float: none; @@ -1612,6 +1592,11 @@ div.container { font-size: var(--font-size); line-height: var(--line-spacing); } + h1 { + font-family: var(--header-font); + font-size: var(--font-size-mobile); + color: var(--title-color); + } .containerHeader { border: var(--border-width-header) solid var(--border-color); background-color: var(--header-bg-color); @@ -1832,7 +1817,7 @@ div.container { margin-right: 0px; padding: 0px 0px; margin: 0px 0px; - width: 100px; + width: var(--timeline-icon-width-mobile); } .containerHeader img.timelineicon { float: var(--icons-side); @@ -1840,7 +1825,7 @@ div.container { margin-right:0; padding: 0 0; margin: 0 0; - width: 100px; + width: var(--timeline-icon-width-mobile); } .container img.emojiheader { float: none; diff --git a/epicyon-search.css b/epicyon-search.css index 16ae3fb4b..65980b641 100644 --- a/epicyon-search.css +++ b/epicyon-search.css @@ -66,6 +66,7 @@ body, html { h1 { font-family: var(--header-font); + font-size: var(--font-size); color: var(--title-color); } @@ -76,13 +77,15 @@ a, u { a:visited{ color: var(--main-visited-color); background: var(--link-bg-color); - font-weight: bold; + font-weight: normal; + text-decoration: none; } a:link { color: var(--main-link-color); background: var(--link-bg-color); - font-weight: bold; + font-weight: normal; + text-decoration: none; } a:link:hover { @@ -206,6 +209,9 @@ input[type=text] { } @media screen and (min-width: 400px) { + details { + font-size: var(--hashtag-size1); + } .domainHistogram { border: 0; font-size: var(--hashtag-size1); @@ -262,6 +268,9 @@ input[type=text] { } @media screen and (max-width: 1000px) { + details { + font-size: var(--hashtag-size2); + } .domainHistogram { border: 0; font-size: var(--hashtag-size2); diff --git a/epicyon-suspended.css b/epicyon-suspended.css index 4a5d03304..716c3c42a 100644 --- a/epicyon-suspended.css +++ b/epicyon-suspended.css @@ -20,6 +20,7 @@ --button-background: #999; --button-selected: #666; --focus-color: white; + --main-link-color-hover: #bbb; } @font-face { @@ -56,13 +57,23 @@ a, u { a:visited{ color: var(--main-visited-color); background: var(--link-bg-color); - font-weight: bold; + font-weight: normal; + text-decoration: none; } a:link { color: var(--main-link-color); background: var(--link-bg-color); - font-weight: bold; + font-weight: normal; + text-decoration: none; +} + +a:link:hover { + color: var(--main-link-color-hover); +} + +a:visited:hover { + color: var(--main-link-color-hover); } a:focus { diff --git a/epicyon.py b/epicyon.py index ecc051bb6..96b87396e 100644 --- a/epicyon.py +++ b/epicyon.py @@ -689,12 +689,6 @@ if args.json: pprint(testJson) sys.exit() -if args.rss: - session = createSession(None) - testRSS = getRSS(session, args.rss) - pprint(testRSS) - sys.exit() - # create cache for actors if not os.path.isdir(baseDir + '/cache'): os.mkdir(baseDir + '/cache') @@ -756,6 +750,13 @@ if args.domain: domain = args.domain setConfigParam(baseDir, 'domain', domain) +if args.rss: + session = createSession(None) + testRSS = getRSS(baseDir, domain, session, args.rss, + False, False, 1000, 1000, 1000, 1000) + pprint(testRSS) + sys.exit() + if args.onion: if not args.onion.endswith('.onion'): print(args.onion + ' does not look like an onion domain') diff --git a/inbox.py b/inbox.py index 258e16e19..cf681eb4e 100644 --- a/inbox.py +++ b/inbox.py @@ -56,8 +56,8 @@ from posts import isMuted from posts import isImageMedia from posts import sendSignedJson from posts import sendToFollowersThread -from webapp import individualPostAsHtml -from webapp import getIconsWebPath +from webapp_utils import getIconsWebPath +from webapp_post import individualPostAsHtml from question import questionUpdateVotes from media import replaceYouTube from git import isGitPatch @@ -1251,18 +1251,31 @@ def receiveDelete(session, handle: str, isGroup: bool, baseDir: str, # if this post in the outbox of the person? messageId = removeIdEnding(messageJson['object']) removeModerationPostFromIndex(baseDir, messageId, debug) - postFilename = locatePost(baseDir, handle.split('@')[0], - handle.split('@')[1], messageId) + handleNickname = handle.split('@')[0] + handleDomain = handle.split('@')[1] + postFilename = locatePost(baseDir, handleNickname, + handleDomain, messageId) if not postFilename: if debug: print('DEBUG: delete post not found in inbox or outbox') print(messageId) return True - deletePost(baseDir, httpPrefix, handle.split('@')[0], - handle.split('@')[1], postFilename, debug, + deletePost(baseDir, httpPrefix, handleNickname, + handleDomain, postFilename, debug, recentPostsCache) if debug: print('DEBUG: post deleted - ' + postFilename) + + # also delete any local blogs saved to the news actor + if handleNickname != 'news' and handleDomain == domainFull: + postFilename = locatePost(baseDir, 'news', + handleDomain, messageId) + if postFilename: + deletePost(baseDir, httpPrefix, 'news', + handleDomain, postFilename, debug, + recentPostsCache) + if debug: + print('DEBUG: blog post deleted - ' + postFilename) return True diff --git a/jami.py b/jami.py new file mode 100644 index 000000000..6cc3fe3ca --- /dev/null +++ b/jami.py @@ -0,0 +1,93 @@ +__filename__ = "jami.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "1.1.0" +__maintainer__ = "Bob Mottram" +__email__ = "bob@freedombone.net" +__status__ = "Production" + + +def getJamiAddress(actorJson: {}) -> str: + """Returns jami address for the given actor + """ + if not actorJson.get('attachment'): + return '' + for propertyValue in actorJson['attachment']: + if not propertyValue.get('name'): + continue + if not propertyValue['name'].lower().startswith('jami'): + continue + if not propertyValue.get('type'): + continue + if not propertyValue.get('value'): + continue + if propertyValue['type'] != 'PropertyValue': + continue + propertyValue['value'] = propertyValue['value'].strip() + if len(propertyValue['value']) < 2: + continue + if '"' in propertyValue['value']: + continue + if ' ' in propertyValue['value']: + continue + if ',' in propertyValue['value']: + continue + if '.' in propertyValue['value']: + continue + return propertyValue['value'] + return '' + + +def setJamiAddress(actorJson: {}, jamiAddress: str) -> None: + """Sets an jami address for the given actor + """ + notJamiAddress = False + + if len(jamiAddress) < 2: + notJamiAddress = True + if '"' in jamiAddress: + notJamiAddress = True + if ' ' in jamiAddress: + notJamiAddress = True + if '.' in jamiAddress: + notJamiAddress = True + if ',' in jamiAddress: + notJamiAddress = True + + if not actorJson.get('attachment'): + actorJson['attachment'] = [] + + # remove any existing value + propertyFound = None + for propertyValue in actorJson['attachment']: + if not propertyValue.get('name'): + continue + if not propertyValue.get('type'): + continue + if not propertyValue['name'].lower().startswith('jami'): + continue + propertyFound = propertyValue + break + if propertyFound: + actorJson['attachment'].remove(propertyFound) + if notJamiAddress: + return + + for propertyValue in actorJson['attachment']: + if not propertyValue.get('name'): + continue + if not propertyValue.get('type'): + continue + if not propertyValue['name'].lower().startswith('jami'): + continue + if propertyValue['type'] != 'PropertyValue': + continue + propertyValue['value'] = jamiAddress + return + + newJamiAddress = { + "name": "Jami", + "type": "PropertyValue", + "value": jamiAddress + } + actorJson['attachment'].append(newJamiAddress) diff --git a/newsdaemon.py b/newsdaemon.py index a08f0e200..1e1a3068b 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -717,7 +717,8 @@ def runNewswireDaemon(baseDir: str, httpd, httpd.maxNewswireFeedSizeKb, httpd.maxTags, httpd.maxFeedItemSizeKb, - httpd.maxNewswirePosts) + httpd.maxNewswirePosts, + httpd.maxCategoriesFeedItemSizeKb) if not httpd.newswire: if os.path.isfile(newswireStateFilename): diff --git a/newswire.py b/newswire.py index 2e0ab8aaf..7ee8ef784 100644 --- a/newswire.py +++ b/newswire.py @@ -14,6 +14,7 @@ from datetime import datetime from datetime import timedelta from datetime import timezone from collections import OrderedDict +from utils import setHashtagCategory from utils import firstParagraphFromString from utils import isPublicPost from utils import locatePost @@ -122,7 +123,7 @@ def addNewswireDictEntry(baseDir: str, domain: str, # check that no tags are blocked for tag in postTags: - if isBlockedHashtag(baseDir, tag.replace('#', '')): + if isBlockedHashtag(baseDir, tag): return newswire[dateStr] = [ @@ -202,19 +203,64 @@ def parseFeedDate(pubDate: str) -> str: return pubDateStr +def xml2StrToHashtagCategories(baseDir: str, domain: str, xmlStr: str, + maxCategoriesFeedItemSizeKb: int) -> None: + """Updates hashtag categories based upon an rss feed + """ + rssItems = xmlStr.split('') + maxBytes = maxCategoriesFeedItemSizeKb * 1024 + for rssItem in rssItems: + if not rssItem: + continue + if len(rssItem) > maxBytes: + print('WARN: rss categories feed item is too big') + continue + if '' not in rssItem: + continue + if '' not in rssItem: + continue + if '' not in rssItem: + continue + if '' not in rssItem: + continue + categoryStr = rssItem.split('')[1] + categoryStr = categoryStr.split('')[0].strip() + if not categoryStr: + continue + if 'CDATA' in categoryStr: + continue + hashtagListStr = rssItem.split('')[1] + hashtagListStr = hashtagListStr.split('')[0].strip() + if not hashtagListStr: + continue + if 'CDATA' in hashtagListStr: + continue + hashtagList = hashtagListStr.split(' ') + if not isBlockedHashtag(baseDir, categoryStr): + for hashtag in hashtagList: + setHashtagCategory(baseDir, hashtag, categoryStr) + + def xml2StrToDict(baseDir: str, domain: str, xmlStr: str, moderated: bool, mirrored: bool, maxPostsPerSource: int, - maxFeedItemSizeKb: int) -> {}: + maxFeedItemSizeKb: int, + maxCategoriesFeedItemSizeKb: int) -> {}: """Converts an xml 2.0 string to a dictionary """ if '' not in xmlStr: return {} result = {} + if '#categories' in xmlStr: + xml2StrToHashtagCategories(baseDir, domain, xmlStr, + maxCategoriesFeedItemSizeKb) + return {} rssItems = xmlStr.split('') postCtr = 0 maxBytes = maxFeedItemSizeKb * 1024 for rssItem in rssItems: + if not rssItem: + continue if len(rssItem) > maxBytes: print('WARN: rss feed item is too big') continue @@ -266,6 +312,8 @@ def xml2StrToDict(baseDir: str, domain: str, xmlStr: str, postCtr += 1 if postCtr >= maxPostsPerSource: break + if postCtr > 0: + print('Added ' + str(postCtr) + ' rss feed items to newswire') return result @@ -282,6 +330,8 @@ def atomFeedToDict(baseDir: str, domain: str, xmlStr: str, postCtr = 0 maxBytes = maxFeedItemSizeKb * 1024 for atomItem in atomItems: + if not atomItem: + continue if len(atomItem) > maxBytes: print('WARN: atom feed item is too big') continue @@ -333,6 +383,8 @@ def atomFeedToDict(baseDir: str, domain: str, xmlStr: str, postCtr += 1 if postCtr >= maxPostsPerSource: break + if postCtr > 0: + print('Added ' + str(postCtr) + ' atom feed items to newswire') return result @@ -351,7 +403,10 @@ def atomFeedYTToDict(baseDir: str, domain: str, xmlStr: str, postCtr = 0 maxBytes = maxFeedItemSizeKb * 1024 for atomItem in atomItems: - print('YouTube feed item: ' + atomItem) + if not atomItem: + continue + if not atomItem.strip(): + continue if len(atomItem) > maxBytes: print('WARN: atom feed item is too big') continue @@ -359,9 +414,9 @@ def atomFeedYTToDict(baseDir: str, domain: str, xmlStr: str, continue if '' not in atomItem: continue - if '' not in atomItem: + if '' not in atomItem: continue - if '' not in atomItem: + if '' not in atomItem: continue if '' not in atomItem: continue @@ -382,8 +437,8 @@ def atomFeedYTToDict(baseDir: str, domain: str, xmlStr: str, link = atomItem.split('')[1] link = link.split('')[0] link = 'https://www.youtube.com/watch?v=' + link.strip() - pubDate = atomItem.split('')[1] - pubDate = pubDate.split('')[0] + pubDate = atomItem.split('')[1] + pubDate = pubDate.split('')[0] pubDateStr = parseFeedDate(pubDate) if pubDateStr: @@ -397,13 +452,16 @@ def atomFeedYTToDict(baseDir: str, domain: str, xmlStr: str, postCtr += 1 if postCtr >= maxPostsPerSource: break + if postCtr > 0: + print('Added ' + str(postCtr) + ' YouTube feed items to newswire') return result def xmlStrToDict(baseDir: str, domain: str, xmlStr: str, moderated: bool, mirrored: bool, maxPostsPerSource: int, - maxFeedItemSizeKb: int) -> {}: + maxFeedItemSizeKb: int, + maxCategoriesFeedItemSizeKb: int) -> {}: """Converts an xml string to a dictionary """ if '' in xmlStr and '' in xmlStr: @@ -414,7 +472,8 @@ def xmlStrToDict(baseDir: str, domain: str, xmlStr: str, elif 'rss version="2.0"' in xmlStr: return xml2StrToDict(baseDir, domain, xmlStr, moderated, mirrored, - maxPostsPerSource, maxFeedItemSizeKb) + maxPostsPerSource, maxFeedItemSizeKb, + maxCategoriesFeedItemSizeKb) elif 'xmlns="http://www.w3.org/2005/Atom"' in xmlStr: return atomFeedToDict(baseDir, domain, xmlStr, moderated, mirrored, @@ -437,7 +496,8 @@ def YTchannelToAtomFeed(url: str) -> str: def getRSS(baseDir: str, domain: str, session, url: str, moderated: bool, mirrored: bool, maxPostsPerSource: int, maxFeedSizeKb: int, - maxFeedItemSizeKb: int) -> {}: + maxFeedItemSizeKb: int, + maxCategoriesFeedItemSizeKb: int) -> {}: """Returns an RSS url as a dict """ if not isinstance(url, str): @@ -467,7 +527,8 @@ def getRSS(baseDir: str, domain: str, session, url: str, return xmlStrToDict(baseDir, domain, result.text, moderated, mirrored, maxPostsPerSource, - maxFeedItemSizeKb) + maxFeedItemSizeKb, + maxCategoriesFeedItemSizeKb) else: print('WARN: feed is too large, ' + 'or contains invalid characters: ' + url) @@ -701,7 +762,8 @@ def addBlogsToNewswire(baseDir: str, domain: str, newswire: {}, def getDictFromNewswire(session, baseDir: str, domain: str, maxPostsPerSource: int, maxFeedSizeKb: int, maxTags: int, maxFeedItemSizeKb: int, - maxNewswirePosts: int) -> {}: + maxNewswirePosts: int, + maxCategoriesFeedItemSizeKb: int) -> {}: """Gets rss feeds as a dictionary from newswire file """ subscriptionsFilename = baseDir + '/accounts/newswire.txt' @@ -741,7 +803,8 @@ def getDictFromNewswire(session, baseDir: str, domain: str, itemsList = getRSS(baseDir, domain, session, url, moderated, mirrored, maxPostsPerSource, maxFeedSizeKb, - maxFeedItemSizeKb) + maxFeedItemSizeKb, + maxCategoriesFeedItemSizeKb) if itemsList: for dateStr, item in itemsList.items(): result[dateStr] = item diff --git a/outbox.py b/outbox.py index 1329855a6..93d5c5cb9 100644 --- a/outbox.py +++ b/outbox.py @@ -7,6 +7,7 @@ __email__ = "bob@freedombone.net" __status__ = "Production" import os +from shutil import copyfile from session import createSession from auth import createPassword from posts import outboxMessageCreateWrap @@ -116,24 +117,23 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str, fileExtension = 'png' mediaTypeStr = \ attach['mediaType'] - if mediaTypeStr.endswith('jpeg'): - fileExtension = 'jpg' - elif mediaTypeStr.endswith('gif'): - fileExtension = 'gif' - elif mediaTypeStr.endswith('webp'): - fileExtension = 'webp' - elif mediaTypeStr.endswith('avif'): - fileExtension = 'avif' - elif mediaTypeStr.endswith('audio/mpeg'): - fileExtension = 'mp3' - elif mediaTypeStr.endswith('ogg'): - fileExtension = 'ogg' - elif mediaTypeStr.endswith('mp4'): - fileExtension = 'mp4' - elif mediaTypeStr.endswith('webm'): - fileExtension = 'webm' - elif mediaTypeStr.endswith('ogv'): - fileExtension = 'ogv' + + extensions = { + "jpeg": "jpg", + "gif": "gif", + "webp": "webp", + "avif": "avif", + "audio/mpeg": "mp3", + "ogg": "ogg", + "mp4": "mp4", + "webm": "webm", + "ogv": "ogv" + } + for matchExt, ext in extensions.items(): + if mediaTypeStr.endswith(matchExt): + fileExtension = ext + break + mediaDir = \ baseDir + '/accounts/' + \ postToNickname + '@' + domain @@ -188,11 +188,31 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str, savePostToBox(baseDir, httpPrefix, postId, - postToNickname, - domainFull, messageJson, outboxName) + postToNickname, domainFull, + messageJson, outboxName) if not savedFilename: print('WARN: post not saved to outbox ' + outboxName) return False + + # save all instance blogs to the news actor + if postToNickname != 'news' and outboxName == 'tlblogs': + if '/' in savedFilename: + savedPostId = savedFilename.split('/')[-1] + blogsDir = baseDir + '/accounts/news@' + domain + '/tlblogs' + if not os.path.isdir(blogsDir): + os.mkdir(blogsDir) + copyfile(savedFilename, blogsDir + '/' + savedPostId) + inboxUpdateIndex('tlblogs', baseDir, + 'news@' + domain, + savedFilename, debug) + + # clear the citations file if it exists + citationsFilename = \ + baseDir + '/accounts/' + \ + postToNickname + '@' + domain + '/.citations.txt' + if os.path.isfile(citationsFilename): + os.remove(citationsFilename) + if messageJson['type'] == 'Create' or \ messageJson['type'] == 'Question' or \ messageJson['type'] == 'Note' or \ diff --git a/person.py b/person.py index 9bd2ce695..86f0fe6e2 100644 --- a/person.py +++ b/person.py @@ -25,6 +25,7 @@ from posts import createRepliesTimeline from posts import createMediaTimeline from posts import createNewsTimeline from posts import createBlogsTimeline +from posts import createFeaturesTimeline from posts import createBookmarksTimeline from posts import createEventsTimeline from posts import createInbox @@ -236,7 +237,6 @@ def createPersonBase(baseDir: str, nickname: str, domain: str, port: int, elif nickname == 'news': personUrl = httpPrefix + '://' + domain + \ '/about/more?news_actor=true' - personName = originalDomain approveFollowers = True personType = 'Application' @@ -437,10 +437,16 @@ def createPerson(baseDir: str, nickname: str, domain: str, port: int, # If a config.json file doesn't exist then don't decrement # remaining registrations counter - remainingConfigExists = getConfigParam(baseDir, 'registrationsRemaining') - if remainingConfigExists: - registrationsRemaining = int(remainingConfigExists) - if registrationsRemaining <= 0: + if nickname != 'news': + remainingConfigExists = \ + getConfigParam(baseDir, 'registrationsRemaining') + if remainingConfigExists: + registrationsRemaining = int(remainingConfigExists) + if registrationsRemaining <= 0: + return None, None, None, None + else: + if os.path.isdir(baseDir + '/accounts/news@' + domain): + # news account already exists return None, None, None, None (privateKeyPem, publicKeyPem, @@ -451,12 +457,13 @@ def createPerson(baseDir: str, nickname: str, domain: str, port: int, manualFollowerApproval, password) if not getConfigParam(baseDir, 'admin'): - # print(nickname+' becomes the instance admin and a moderator') - setConfigParam(baseDir, 'admin', nickname) - setRole(baseDir, nickname, domain, 'instance', 'admin') - setRole(baseDir, nickname, domain, 'instance', 'moderator') - setRole(baseDir, nickname, domain, 'instance', 'editor') - setRole(baseDir, nickname, domain, 'instance', 'delegator') + if nickname != 'news': + # print(nickname+' becomes the instance admin and a moderator') + setConfigParam(baseDir, 'admin', nickname) + setRole(baseDir, nickname, domain, 'instance', 'admin') + setRole(baseDir, nickname, domain, 'instance', 'moderator') + setRole(baseDir, nickname, domain, 'instance', 'editor') + setRole(baseDir, nickname, domain, 'instance', 'delegator') if not os.path.isdir(baseDir + '/accounts'): os.mkdir(baseDir + '/accounts') @@ -470,22 +477,33 @@ def createPerson(baseDir: str, nickname: str, domain: str, port: int, fFile.write('\n') # notify when posts are liked - notifyLikesFilename = baseDir + '/accounts/' + \ - nickname + '@' + domain + '/.notifyLikes' - with open(notifyLikesFilename, 'w+') as nFile: - nFile.write('\n') + if nickname != 'news': + notifyLikesFilename = baseDir + '/accounts/' + \ + nickname + '@' + domain + '/.notifyLikes' + with open(notifyLikesFilename, 'w+') as nFile: + nFile.write('\n') - if os.path.isfile(baseDir + '/img/default-avatar.png'): - copyfile(baseDir + '/img/default-avatar.png', - baseDir + '/accounts/' + nickname + '@' + domain + - '/avatar.png') theme = getConfigParam(baseDir, 'theme') if not theme: theme = 'default' + + if nickname != 'news': + if os.path.isfile(baseDir + '/img/default-avatar.png'): + copyfile(baseDir + '/img/default-avatar.png', + baseDir + '/accounts/' + nickname + '@' + domain + + '/avatar.png') + else: + newsAvatar = baseDir + '/theme/' + theme + '/icons/avatar_news.png' + if os.path.isfile(newsAvatar): + copyfile(newsAvatar, + baseDir + '/accounts/' + nickname + '@' + domain + + '/avatar.png') + defaultProfileImageFilename = baseDir + '/theme/default/image.png' if theme: if os.path.isfile(baseDir + '/theme/' + theme + '/image.png'): - defaultBannerFilename = baseDir + '/theme/' + theme + '/image.png' + defaultProfileImageFilename = \ + baseDir + '/theme/' + theme + '/image.png' if os.path.isfile(defaultProfileImageFilename): copyfile(defaultProfileImageFilename, baseDir + '/accounts/' + nickname + '@' + domain + '/image.png') @@ -496,7 +514,7 @@ def createPerson(baseDir: str, nickname: str, domain: str, port: int, if os.path.isfile(defaultBannerFilename): copyfile(defaultBannerFilename, baseDir + '/accounts/' + nickname + '@' + domain + '/banner.png') - if remainingConfigExists: + if nickname != 'news' and remainingConfigExists: registrationsRemaining -= 1 setConfigParam(baseDir, 'registrationsRemaining', str(registrationsRemaining)) @@ -516,8 +534,8 @@ def createNewsInbox(baseDir: str, domain: str, port: int, httpPrefix: str) -> (str, str, {}, {}): """Generates the news inbox """ - return createPersonBase(baseDir, 'news', domain, port, httpPrefix, - True, True, None) + return createPerson(baseDir, 'news', domain, port, + httpPrefix, True, True, None) def personUpgradeActor(baseDir: str, personJson: {}, @@ -611,6 +629,7 @@ def personBoxJson(recentPostsCache: {}, if boxname != 'inbox' and boxname != 'dm' and \ boxname != 'tlreplies' and boxname != 'tlmedia' and \ boxname != 'tlblogs' and boxname != 'tlnews' and \ + boxname != 'tlfeatures' and \ boxname != 'outbox' and boxname != 'moderation' and \ boxname != 'tlbookmarks' and boxname != 'bookmarks' and \ boxname != 'tlevents': @@ -683,6 +702,10 @@ def personBoxJson(recentPostsCache: {}, httpPrefix, noOfItems, headerOnly, newswireVotesThreshold, positiveVoting, votingTimeMins, pageNumber) + elif boxname == 'tlfeatures': + return createFeaturesTimeline(session, baseDir, nickname, domain, port, + httpPrefix, noOfItems, headerOnly, + pageNumber) elif boxname == 'tlblogs': return createBlogsTimeline(session, baseDir, nickname, domain, port, httpPrefix, noOfItems, headerOnly, diff --git a/posts.py b/posts.py index 5e16b369b..67c5a56a5 100644 --- a/posts.py +++ b/posts.py @@ -92,34 +92,6 @@ def isModerator(baseDir: str, nickname: str) -> bool: return False -def isEditor(baseDir: str, nickname: str) -> bool: - """Returns true if the given nickname is an editor - """ - editorsFile = baseDir + '/accounts/editors.txt' - - if not os.path.isfile(editorsFile): - adminName = getConfigParam(baseDir, 'admin') - if not adminName: - return False - if adminName == nickname: - return True - return False - - with open(editorsFile, "r") as f: - lines = f.readlines() - if len(lines) == 0: - adminName = getConfigParam(baseDir, 'admin') - if not adminName: - return False - if adminName == nickname: - return True - for editor in lines: - editor = editor.strip('\n').strip('\r') - if editor == nickname: - return True - return False - - def noOfFollowersOnDomain(baseDir: str, handle: str, domain: str, followFile='followers.txt') -> int: """Returns the number of followers of the given handle from the given domain @@ -582,6 +554,7 @@ def savePostToBox(baseDir: str, httpPrefix: str, postId: str, boxDir = createPersonDir(nickname, domain, baseDir, boxname) filename = boxDir + '/' + postId.replace('/', '#') + '.json' + saveJson(postJsonObject, filename) return filename @@ -2587,6 +2560,15 @@ def createBlogsTimeline(session, baseDir: str, nickname: str, domain: str, 0, False, 0, pageNumber) +def createFeaturesTimeline(session, baseDir: str, nickname: str, domain: str, + port: int, httpPrefix: str, itemsPerPage: int, + headerOnly: bool, pageNumber=None) -> {}: + return createBoxIndexed({}, session, baseDir, 'tlfeatures', nickname, + domain, port, httpPrefix, + itemsPerPage, headerOnly, True, + 0, False, 0, pageNumber) + + def createMediaTimeline(session, baseDir: str, nickname: str, domain: str, port: int, httpPrefix: str, itemsPerPage: int, headerOnly: bool, pageNumber=None) -> {}: @@ -2879,7 +2861,9 @@ def addPostStringToTimeline(postStr: str, boxname: str, elif boxname == 'tlreplies': if boxActor not in postStr: return False - elif boxname == 'tlblogs' or boxname == 'tlnews': + elif (boxname == 'tlblogs' or + boxname == 'tlnews' or + boxname == 'tlfeatures'): if '"Create"' not in postStr: return False if '"Article"' not in postStr: @@ -2900,6 +2884,13 @@ def addPostToTimeline(filePath: str, boxname: str, """ with open(filePath, 'r') as postFile: postStr = postFile.read() + + if filePath.endswith('.json'): + repliesFilename = filePath.replace('.json', '.replies') + if os.path.isfile(repliesFilename): + # append a replies identifier, which will later be removed + postStr += '' + return addPostStringToTimeline(postStr, boxname, postsInBox, boxActor) return False @@ -2918,6 +2909,7 @@ def createBoxIndexed(recentPostsCache: {}, if boxname != 'inbox' and boxname != 'dm' and \ boxname != 'tlreplies' and boxname != 'tlmedia' and \ boxname != 'tlblogs' and boxname != 'tlnews' and \ + boxname != 'tlfeatures' and \ boxname != 'outbox' and boxname != 'tlbookmarks' and \ boxname != 'bookmarks' and \ boxname != 'tlevents': @@ -2926,9 +2918,14 @@ def createBoxIndexed(recentPostsCache: {}, # bookmarks and events timelines are like the inbox # but have their own separate index indexBoxName = boxname + timelineNickname = nickname if boxname == "tlbookmarks": boxname = "bookmarks" indexBoxName = boxname + elif boxname == "tlfeatures": + boxname = "tlblogs" + indexBoxName = boxname + timelineNickname = 'news' if port: if port != 80 and port != 443: @@ -2966,7 +2963,7 @@ def createBoxIndexed(recentPostsCache: {}, postsInBox = [] indexFilename = \ - baseDir + '/accounts/' + nickname + '@' + domain + \ + baseDir + '/accounts/' + timelineNickname + '@' + domain + \ '/' + indexBoxName + '.index' postsCtr = 0 if os.path.isfile(indexFilename): @@ -3051,7 +3048,18 @@ def createBoxIndexed(recentPostsCache: {}, addPostToTimeline(fullPostFilename, boxname, postsInBox, boxActor) else: - print('WARN: unable to locate post ' + postUrl) + # if this is the features timeline + if timelineNickname != nickname: + fullPostFilename = \ + locatePost(baseDir, timelineNickname, + domain, postUrl, False) + if fullPostFilename: + addPostToTimeline(fullPostFilename, boxname, + postsInBox, boxActor) + else: + print('WARN: unable to locate post ' + postUrl) + else: + print('WARN: unable to locate post ' + postUrl) postsCtr += 1 @@ -3080,12 +3088,24 @@ def createBoxIndexed(recentPostsCache: {}, return boxHeader for postStr in postsInBox: + # Check if the post has replies + hasReplies = False + if postStr.endswith(''): + hasReplies = True + # remove the replies identifier + postStr = postStr.replace('', '') + p = None try: p = json.loads(postStr) except BaseException: continue + # Does this post have replies? + # This will be used to indicate that replies exist within the html + # created by individualPostAsHtml + p['hasReplies'] = hasReplies + # Don't show likes, replies or shares (announces) to # unauthorized viewers if not authorized: diff --git a/roles.py b/roles.py index fe60b2ab3..22db327b3 100644 --- a/roles.py +++ b/roles.py @@ -46,15 +46,20 @@ def clearEditorStatus(baseDir: str) -> None: for f in os.scandir(directory): f = f.name filename = os.fsdecode(f) - if filename.endswith(".json") and '@' in filename: - filename = os.path.join(baseDir + '/accounts/', filename) - if '"editor"' in open(filename).read(): - actorJson = loadJson(filename) - if actorJson: - if actorJson['roles'].get('instance'): - if 'editor' in actorJson['roles']['instance']: - actorJson['roles']['instance'].remove('editor') - saveJson(actorJson, filename) + if '@' not in filename: + continue + if not filename.endswith(".json"): + continue + filename = os.path.join(baseDir + '/accounts/', filename) + if '"editor"' not in open(filename).read(): + continue + actorJson = loadJson(filename) + if not actorJson: + continue + if actorJson['roles'].get('instance'): + if 'editor' in actorJson['roles']['instance']: + actorJson['roles']['instance'].remove('editor') + saveJson(actorJson, filename) def addModerator(baseDir: str, nickname: str, domain: str) -> None: diff --git a/tests.py b/tests.py index 4b86d499d..aae2345b3 100644 --- a/tests.py +++ b/tests.py @@ -32,6 +32,7 @@ from follow import clearFollows from follow import clearFollowers from follow import sendFollowRequestViaServer from follow import sendUnfollowRequestViaServer +from utils import validNickname from utils import firstParagraphFromString from utils import removeIdEnding from utils import siteIsActive @@ -1387,6 +1388,8 @@ def testClientToServer(): httpPrefix, cachedWebfingers, personCache, True, __version__) + alicePetnamesFilename = aliceDir + '/accounts/' + \ + 'alice@' + aliceDomain + '/petnames.txt' aliceFollowingFilename = \ aliceDir + '/accounts/alice@' + aliceDomain + '/following.txt' bobFollowersFilename = \ @@ -1395,7 +1398,8 @@ def testClientToServer(): if os.path.isfile(bobFollowersFilename): if 'alice@' + aliceDomain + ':' + str(alicePort) in \ open(bobFollowersFilename).read(): - if os.path.isfile(aliceFollowingFilename): + if os.path.isfile(aliceFollowingFilename) and \ + os.path.isfile(alicePetnamesFilename): if 'bob@' + bobDomain + ':' + str(bobPort) in \ open(aliceFollowingFilename).read(): break @@ -1403,6 +1407,9 @@ def testClientToServer(): assert os.path.isfile(bobFollowersFilename) assert os.path.isfile(aliceFollowingFilename) + assert os.path.isfile(alicePetnamesFilename) + assert 'bob bob@' + bobDomain in \ + open(alicePetnamesFilename).read() print('alice@' + aliceDomain + ':' + str(alicePort) + ' in ' + bobFollowersFilename) assert 'alice@' + aliceDomain + ':' + str(alicePort) in \ @@ -2397,8 +2404,26 @@ def testParseFeedDate(): assert publishedDate == "2020-11-22 18:51:33+00:00" +def testValidNickname(): + print('testValidNickname') + domain = 'somedomain.net' + + nickname = 'myvalidnick' + assert validNickname(domain, nickname) + + nickname = 'my.invalid.nick' + assert not validNickname(domain, nickname) + + nickname = 'myinvalidnick?' + assert not validNickname(domain, nickname) + + nickname = 'my invalid nick?' + assert not validNickname(domain, nickname) + + def runAllTests(): print('Running tests...') + testValidNickname() testParseFeedDate() testFirstParagraphFromString() testGetNewswireTags() diff --git a/theme/blue/icons/categoriesrss.png b/theme/blue/icons/categoriesrss.png new file mode 100644 index 000000000..219adb9bc Binary files /dev/null and b/theme/blue/icons/categoriesrss.png differ diff --git a/theme/blue/theme.json b/theme/blue/theme.json index 8772270dc..2abaf6666 100644 --- a/theme/blue/theme.json +++ b/theme/blue/theme.json @@ -17,6 +17,8 @@ "gallery-font-size": "35px", "gallery-font-size-mobile": "55px", "main-bg-color": "#002365", + "login-bg-color": "#002365", + "options-bg-color": "#002365", "post-bg-color": "#002365", "timeline-posts-background-color": "#002365", "header-bg-color": "#002365", diff --git a/theme/debian/banner.png b/theme/debian/banner.png new file mode 100644 index 000000000..5a0794b27 Binary files /dev/null and b/theme/debian/banner.png differ diff --git a/theme/debian/icons/add.png b/theme/debian/icons/add.png new file mode 100644 index 000000000..02a632a51 Binary files /dev/null and b/theme/debian/icons/add.png differ diff --git a/theme/debian/icons/agpl.png b/theme/debian/icons/agpl.png new file mode 100644 index 000000000..c2d94b2e6 Binary files /dev/null and b/theme/debian/icons/agpl.png differ diff --git a/theme/debian/icons/avatar_news.png b/theme/debian/icons/avatar_news.png new file mode 100644 index 000000000..07599baeb Binary files /dev/null and b/theme/debian/icons/avatar_news.png differ diff --git a/theme/debian/icons/bookmark.png b/theme/debian/icons/bookmark.png new file mode 100644 index 000000000..6580755af Binary files /dev/null and b/theme/debian/icons/bookmark.png differ diff --git a/theme/debian/icons/bookmark_inactive.png b/theme/debian/icons/bookmark_inactive.png new file mode 100644 index 000000000..698025b0e Binary files /dev/null and b/theme/debian/icons/bookmark_inactive.png differ diff --git a/theme/debian/icons/calendar.png b/theme/debian/icons/calendar.png new file mode 100644 index 000000000..6d5789c3a Binary files /dev/null and b/theme/debian/icons/calendar.png differ diff --git a/theme/debian/icons/calendar_notify.png b/theme/debian/icons/calendar_notify.png new file mode 100644 index 000000000..635f715b0 Binary files /dev/null and b/theme/debian/icons/calendar_notify.png differ diff --git a/theme/debian/icons/categoriesrss.png b/theme/debian/icons/categoriesrss.png new file mode 100644 index 000000000..219adb9bc Binary files /dev/null and b/theme/debian/icons/categoriesrss.png differ diff --git a/theme/debian/icons/delete.png b/theme/debian/icons/delete.png new file mode 100644 index 000000000..9904774e3 Binary files /dev/null and b/theme/debian/icons/delete.png differ diff --git a/theme/debian/icons/dm.png b/theme/debian/icons/dm.png new file mode 100644 index 000000000..c0493f82e Binary files /dev/null and b/theme/debian/icons/dm.png differ diff --git a/theme/debian/icons/download.png b/theme/debian/icons/download.png new file mode 100644 index 000000000..3a9605ab7 Binary files /dev/null and b/theme/debian/icons/download.png differ diff --git a/theme/debian/icons/edit.png b/theme/debian/icons/edit.png new file mode 100644 index 000000000..c07dc2dec Binary files /dev/null and b/theme/debian/icons/edit.png differ diff --git a/theme/debian/icons/edit_notify.png b/theme/debian/icons/edit_notify.png new file mode 100644 index 000000000..09f95e3cd Binary files /dev/null and b/theme/debian/icons/edit_notify.png differ diff --git a/theme/debian/icons/favicon.ico b/theme/debian/icons/favicon.ico new file mode 100644 index 000000000..c7cb1bbbe Binary files /dev/null and b/theme/debian/icons/favicon.ico differ diff --git a/theme/debian/icons/like.png b/theme/debian/icons/like.png new file mode 100644 index 000000000..e536adf80 Binary files /dev/null and b/theme/debian/icons/like.png differ diff --git a/theme/debian/icons/like_inactive.png b/theme/debian/icons/like_inactive.png new file mode 100644 index 000000000..352e949b9 Binary files /dev/null and b/theme/debian/icons/like_inactive.png differ diff --git a/theme/debian/icons/links.png b/theme/debian/icons/links.png new file mode 100644 index 000000000..584d722f9 Binary files /dev/null and b/theme/debian/icons/links.png differ diff --git a/theme/debian/icons/logorss.png b/theme/debian/icons/logorss.png new file mode 100644 index 000000000..6bcef0273 Binary files /dev/null and b/theme/debian/icons/logorss.png differ diff --git a/theme/debian/icons/logout.png b/theme/debian/icons/logout.png new file mode 100644 index 000000000..6c52946df Binary files /dev/null and b/theme/debian/icons/logout.png differ diff --git a/theme/debian/icons/mute.png b/theme/debian/icons/mute.png new file mode 100644 index 000000000..5fe808e0f Binary files /dev/null and b/theme/debian/icons/mute.png differ diff --git a/theme/debian/icons/new.png b/theme/debian/icons/new.png new file mode 100644 index 000000000..9f7545394 Binary files /dev/null and b/theme/debian/icons/new.png differ diff --git a/theme/debian/icons/newpost.png b/theme/debian/icons/newpost.png new file mode 100644 index 000000000..3bc33deb8 Binary files /dev/null and b/theme/debian/icons/newpost.png differ diff --git a/theme/debian/icons/newswire.png b/theme/debian/icons/newswire.png new file mode 100644 index 000000000..f3521130d Binary files /dev/null and b/theme/debian/icons/newswire.png differ diff --git a/theme/debian/icons/pagedown.png b/theme/debian/icons/pagedown.png new file mode 100644 index 000000000..da7b236d8 Binary files /dev/null and b/theme/debian/icons/pagedown.png differ diff --git a/theme/debian/icons/pageup.png b/theme/debian/icons/pageup.png new file mode 100644 index 000000000..8adf6a8b8 Binary files /dev/null and b/theme/debian/icons/pageup.png differ diff --git a/theme/debian/icons/person.png b/theme/debian/icons/person.png new file mode 100644 index 000000000..0b823f7ef Binary files /dev/null and b/theme/debian/icons/person.png differ diff --git a/theme/debian/icons/prev.png b/theme/debian/icons/prev.png new file mode 100644 index 000000000..daa14c763 Binary files /dev/null and b/theme/debian/icons/prev.png differ diff --git a/theme/debian/icons/publish.png b/theme/debian/icons/publish.png new file mode 100644 index 000000000..0fe148eea Binary files /dev/null and b/theme/debian/icons/publish.png differ diff --git a/theme/debian/icons/qrcode.png b/theme/debian/icons/qrcode.png new file mode 100644 index 000000000..933a2671c Binary files /dev/null and b/theme/debian/icons/qrcode.png differ diff --git a/theme/debian/icons/repeat.png b/theme/debian/icons/repeat.png new file mode 100644 index 000000000..974659cb1 Binary files /dev/null and b/theme/debian/icons/repeat.png differ diff --git a/theme/debian/icons/repeat_inactive.png b/theme/debian/icons/repeat_inactive.png new file mode 100644 index 000000000..59ec9d794 Binary files /dev/null and b/theme/debian/icons/repeat_inactive.png differ diff --git a/theme/debian/icons/reply.png b/theme/debian/icons/reply.png new file mode 100644 index 000000000..f869a975e Binary files /dev/null and b/theme/debian/icons/reply.png differ diff --git a/theme/debian/icons/rss3.png b/theme/debian/icons/rss3.png new file mode 100644 index 000000000..83521cd1b Binary files /dev/null and b/theme/debian/icons/rss3.png differ diff --git a/theme/debian/icons/scope_blog.png b/theme/debian/icons/scope_blog.png new file mode 100644 index 000000000..e3cdb1b81 Binary files /dev/null and b/theme/debian/icons/scope_blog.png differ diff --git a/theme/debian/icons/scope_dm.png b/theme/debian/icons/scope_dm.png new file mode 100644 index 000000000..7c485959c Binary files /dev/null and b/theme/debian/icons/scope_dm.png differ diff --git a/theme/debian/icons/scope_event.png b/theme/debian/icons/scope_event.png new file mode 100644 index 000000000..6d5789c3a Binary files /dev/null and b/theme/debian/icons/scope_event.png differ diff --git a/theme/debian/icons/scope_followers.png b/theme/debian/icons/scope_followers.png new file mode 100644 index 000000000..2e420954c Binary files /dev/null and b/theme/debian/icons/scope_followers.png differ diff --git a/theme/debian/icons/scope_public.png b/theme/debian/icons/scope_public.png new file mode 100644 index 000000000..7f8633ff0 Binary files /dev/null and b/theme/debian/icons/scope_public.png differ diff --git a/theme/debian/icons/scope_question.png b/theme/debian/icons/scope_question.png new file mode 100644 index 000000000..a811b21c6 Binary files /dev/null and b/theme/debian/icons/scope_question.png differ diff --git a/theme/debian/icons/scope_reminder.png b/theme/debian/icons/scope_reminder.png new file mode 100644 index 000000000..809376840 Binary files /dev/null and b/theme/debian/icons/scope_reminder.png differ diff --git a/theme/debian/icons/scope_report.png b/theme/debian/icons/scope_report.png new file mode 100644 index 000000000..7fbd60b74 Binary files /dev/null and b/theme/debian/icons/scope_report.png differ diff --git a/theme/debian/icons/scope_share.png b/theme/debian/icons/scope_share.png new file mode 100644 index 000000000..07fe95502 Binary files /dev/null and b/theme/debian/icons/scope_share.png differ diff --git a/theme/debian/icons/scope_unlisted.png b/theme/debian/icons/scope_unlisted.png new file mode 100644 index 000000000..b3ce02e69 Binary files /dev/null and b/theme/debian/icons/scope_unlisted.png differ diff --git a/theme/debian/icons/search.png b/theme/debian/icons/search.png new file mode 100644 index 000000000..6d3b05c83 Binary files /dev/null and b/theme/debian/icons/search.png differ diff --git a/theme/debian/icons/separator_right.png b/theme/debian/icons/separator_right.png new file mode 100644 index 000000000..f3b5f0799 Binary files /dev/null and b/theme/debian/icons/separator_right.png differ diff --git a/theme/debian/icons/showhide.png b/theme/debian/icons/showhide.png new file mode 100644 index 000000000..32be2848c Binary files /dev/null and b/theme/debian/icons/showhide.png differ diff --git a/theme/debian/icons/unmute.png b/theme/debian/icons/unmute.png new file mode 100644 index 000000000..f91614469 Binary files /dev/null and b/theme/debian/icons/unmute.png differ diff --git a/theme/debian/icons/vote.png b/theme/debian/icons/vote.png new file mode 100644 index 000000000..c810092b0 Binary files /dev/null and b/theme/debian/icons/vote.png differ diff --git a/theme/debian/image.png b/theme/debian/image.png new file mode 100644 index 000000000..729460596 Binary files /dev/null and b/theme/debian/image.png differ diff --git a/theme/debian/left_col_image.png b/theme/debian/left_col_image.png new file mode 100644 index 000000000..268bbd86f Binary files /dev/null and b/theme/debian/left_col_image.png differ diff --git a/theme/debian/login_background.jpg b/theme/debian/login_background.jpg new file mode 100644 index 000000000..8cdcf1537 Binary files /dev/null and b/theme/debian/login_background.jpg differ diff --git a/theme/debian/options_background.jpg b/theme/debian/options_background.jpg new file mode 100644 index 000000000..8cdcf1537 Binary files /dev/null and b/theme/debian/options_background.jpg differ diff --git a/theme/debian/right_col_image.png b/theme/debian/right_col_image.png new file mode 100644 index 000000000..0620821a1 Binary files /dev/null and b/theme/debian/right_col_image.png differ diff --git a/theme/debian/search_banner.png b/theme/debian/search_banner.png new file mode 100644 index 000000000..b0575eb58 Binary files /dev/null and b/theme/debian/search_banner.png differ diff --git a/theme/debian/theme.json b/theme/debian/theme.json new file mode 100644 index 000000000..a373ed144 --- /dev/null +++ b/theme/debian/theme.json @@ -0,0 +1,87 @@ +{ + "today-circle": "#03a494", + "main-link-color-hover": "blue", + "font-size-newswire-mobile": "32px", + "newswire-date-color": "#00a594", + "column-right-fg-color": "black", + "button-highlighted": "#2b5c6d", + "button-selected-highlighted": "#2b5c6d", + "button-approve": "#2b5c6d", + "login-button-color": "#2b5c6d", + "button-event-background-color": "#2b5c6d", + "post-separator-margin-top": "10px", + "post-separator-margin-bottom": "10px", + "vertical-between-posts": "10px", + "time-vertical-align": "10px", + "button-corner-radius": "5px", + "timeline-border-radius": "5px", + "newswire-publish-icon": "True", + "full-width-timeline-buttons": "False", + "icons-as-buttons": "False", + "rss-icon-at-top": "True", + "publish-button-at-top": "False", + "newswire-item-moderated-color": "grey", + "newswire-date-moderated-color": "grey", + "search-banner-height": "25vh", + "search-banner-height-mobile": "15vh", + "banner-height": "20vh", + "banner-height-mobile": "10vh", + "hashtag-background-color": "grey", + "focus-color": "grey", + "font-size-button-mobile": "26px", + "font-size": "26px", + "font-size2": "20px", + "font-size3": "34px", + "font-size4": "18px", + "font-size5": "16px", + "font-size-likes": "14px", + "font-size-links": "14px", + "font-size-newswire": "14px", + "rgba(0, 0, 0, 0.5)": "rgba(0, 0, 0, 0.0)", + "column-left-color": "#e6ebf0", + "main-bg-color": "#e6ebf0", + "login-bg-color": "#010026", + "options-bg-color": "#010026", + "post-bg-color": "#e6ebf0", + "timeline-posts-background-color": "#e6ebf0", + "header-bg-color": "#e6ebf0", + "main-bg-color-dm": "#e3dbf0", + "link-bg-color": "#e6ebf0", + "main-bg-color-reply": "white", + "main-bg-color-report": "#e3dbf0", + "main-header-color-roles": "#ebebf0", + "main-fg-color": "#2d2c37", + "login-fg-color": "white", + "options-fg-color": "lightgrey", + "column-left-fg-color": "#2d2c37", + "border-color": "#c0cdd9", + "border-width": "1px", + "border-width-header": "1px", + "main-link-color": "darkblue", + "title-color": "#2a2c37", + "main-visited-color": "#232c37", + "text-entry-foreground": "#111", + "text-entry-background": "white", + "font-color-header": "black", + "dropdown-fg-color": "#222", + "dropdown-fg-color-hover": "#222", + "dropdown-bg-color": "white", + "dropdown-bg-color-hover": "lightgrey", + "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", + "event-background": "lightgrey", + "event-foreground": "white", + "title-text": "white", + "title-background": "#2b5c6d", + "gallery-text-color": "black", + "header-font": "'NimbusSanL'", + "*font-family": "'NimbusSanL'", + "*src": "url('./fonts/NimbusSanL.otf') format('opentype')", + "**src": "url('./fonts/NimbusSanL-italic.otf') format('opentype')" +} diff --git a/theme/default/icons/categoriesrss.png b/theme/default/icons/categoriesrss.png new file mode 100644 index 000000000..219adb9bc Binary files /dev/null and b/theme/default/icons/categoriesrss.png differ diff --git a/theme/hacker/icons/categoriesrss.png b/theme/hacker/icons/categoriesrss.png new file mode 100644 index 000000000..a746c2337 Binary files /dev/null and b/theme/hacker/icons/categoriesrss.png differ diff --git a/theme/hacker/theme.json b/theme/hacker/theme.json index e7681d9b4..68551095e 100644 --- a/theme/hacker/theme.json +++ b/theme/hacker/theme.json @@ -6,6 +6,8 @@ "publish-button-at-top": "False", "focus-color": "green", "main-bg-color": "black", + "login-bg-color": "black", + "options-bg-color": "black", "post-bg-color": "black", "timeline-posts-background-color": "black", "header-bg-color": "black", @@ -16,6 +18,8 @@ "main-bg-color-report": "#050202", "main-header-color-roles": "#1f192d", "main-fg-color": "#00ff00", + "login-fg-color": "#00ff00", + "options-fg-color": "#00ff00", "column-left-fg-color": "#00ff00", "border-color": "#035103", "main-link-color": "#2fff2f", diff --git a/theme/henge/icons/categoriesrss.png b/theme/henge/icons/categoriesrss.png new file mode 100644 index 000000000..15b8eaf81 Binary files /dev/null and b/theme/henge/icons/categoriesrss.png differ diff --git a/theme/henge/icons/qrcode.png b/theme/henge/icons/qrcode.png new file mode 100644 index 000000000..933a2671c Binary files /dev/null and b/theme/henge/icons/qrcode.png differ diff --git a/theme/henge/theme.json b/theme/henge/theme.json index 9ea96f4a3..1216992c4 100644 --- a/theme/henge/theme.json +++ b/theme/henge/theme.json @@ -1,4 +1,9 @@ { + "time-color": "grey", + "event-color": "white", + "login-bg-color": "#567726", + "login-fg-color": "black", + "options-bg-color": "black", "newswire-publish-icon": "True", "full-width-timeline-buttons": "False", "icons-as-buttons": "False", @@ -25,6 +30,7 @@ "title-color": "white", "main-visited-color": "#e1c4bc", "main-fg-color": "white", + "options-fg-color": "white", "column-left-fg-color": "white", "main-bg-color-dm": "#343335", "border-color": "#222", @@ -49,7 +55,7 @@ "lines-color": "#c5d2b9", "day-number": "#c5d2b9", "day-number2": "#ccc", - "event-background": "#333", + "event-background": "#555", "timeline-border-radius": "20px", "image-corners": "8%", "quote-right-margin": "0.1em", diff --git a/theme/indymediaclassic/icons/categoriesrss.png b/theme/indymediaclassic/icons/categoriesrss.png new file mode 100644 index 000000000..00e96e5bb Binary files /dev/null and b/theme/indymediaclassic/icons/categoriesrss.png differ diff --git a/theme/indymediaclassic/theme.json b/theme/indymediaclassic/theme.json index 932129063..b56890f30 100644 --- a/theme/indymediaclassic/theme.json +++ b/theme/indymediaclassic/theme.json @@ -27,6 +27,8 @@ "font-size4": "24px", "font-size5": "22px", "main-bg-color": "black", + "login-bg-color": "black", + "options-bg-color": "black", "post-bg-color": "black", "timeline-posts-background-color": "black", "header-bg-color": "black", @@ -40,6 +42,8 @@ "main-link-color-hover": "#d09338", "main-visited-color": "#ffb900", "main-fg-color": "white", + "login-fg-color": "white", + "options-fg-color": "white", "column-left-fg-color": "white", "main-bg-color-dm": "#0b0a0a", "border-color": "#003366", diff --git a/theme/indymediamodern/icons/categoriesrss.png b/theme/indymediamodern/icons/categoriesrss.png new file mode 100644 index 000000000..278592ecd Binary files /dev/null and b/theme/indymediamodern/icons/categoriesrss.png differ diff --git a/theme/indymediamodern/left_col_image.png b/theme/indymediamodern/left_col_image.png index fdf06cfd5..fd2241cd5 100644 Binary files a/theme/indymediamodern/left_col_image.png and b/theme/indymediamodern/left_col_image.png differ diff --git a/theme/indymediamodern/theme.json b/theme/indymediamodern/theme.json index d9570b5c7..228197afc 100644 --- a/theme/indymediamodern/theme.json +++ b/theme/indymediamodern/theme.json @@ -1,4 +1,6 @@ { + "timeline-icon-width": "30px", + "timeline-icon-width-mobile": "60px", "button-bottom-margin": "0", "header-vertical-offset": "10px", "header-bg-color": "#efefef", @@ -15,7 +17,6 @@ "hashtag-size2": "30px", "font-size-calendar-header": "2rem", "font-size-calendar-cell": "2rem", - "calendar-horizontal-padding": "20%", "time-vertical-align": "10px", "publish-button-vertical-offset": "15px", "vertical-between-posts": "0", @@ -52,7 +53,7 @@ "container-button-padding": "0px", "container-button-margin": "0px", "column-left-icon-size": "15%", - "column-right-icon-size": "8%", + "column-right-icon-size": "9.5%", "button-margin": "2px", "button-height-padding": "5px", "icon-brightness-change": "70%", @@ -64,8 +65,8 @@ "login-button-color": "#25408f", "login-button-fg-color": "white", "column-left-width": "10vw", - "column-center-width": "70vw", - "column-right-width": "20vw", + "column-center-width": "75vw", + "column-right-width": "15vw", "column-right-fg-color": "#25408f", "column-right-fg-color-voted-on": "red", "newswire-item-moderated-color": "red", @@ -88,6 +89,8 @@ "rgba(0, 0, 0, 0.5)": "rgba(0, 0, 0, 0.0)", "column-left-color": "#efefef", "main-bg-color": "#efefef", + "login-bg-color": "#efefef", + "options-bg-color": "#efefef", "post-bg-color": "white", "timeline-posts-background-color": "white", "main-bg-color-dm": "white", @@ -96,6 +99,8 @@ "main-bg-color-report": "white", "main-header-color-roles": "#ebebf0", "main-fg-color": "black", + "login-fg-color": "black", + "options-fg-color": "black", "column-left-fg-color": "#25408f", "border-color": "#c0cdd9", "main-link-color": "#25408f", diff --git a/theme/lcd/icons/categoriesrss.png b/theme/lcd/icons/categoriesrss.png new file mode 100644 index 000000000..622d7cb0a Binary files /dev/null and b/theme/lcd/icons/categoriesrss.png differ diff --git a/theme/lcd/theme.json b/theme/lcd/theme.json index 66da23085..68395282b 100644 --- a/theme/lcd/theme.json +++ b/theme/lcd/theme.json @@ -8,6 +8,8 @@ "column-left-header-background": "#9fb42b", "column-left-header-color": "#33390d", "main-bg-color": "#9fb42b", + "login-bg-color": "#9fb42b", + "options-bg-color": "#9fb42b", "post-bg-color": "#9fb42b", "timeline-posts-background-color": "#9fb42b", "header-bg-color": "#9fb42b", @@ -21,6 +23,8 @@ "main-bg-color-dm": "#5fb42b", "main-header-color-roles": "#9fb42b", "main-fg-color": "#33390d", + "login-fg-color": "#33390d", + "options-fg-color": "#33390d", "border-color": "#33390d", "border-width": "5px", "border-width-header": "5px", diff --git a/theme/light/icons/categoriesrss.png b/theme/light/icons/categoriesrss.png new file mode 100644 index 000000000..219adb9bc Binary files /dev/null and b/theme/light/icons/categoriesrss.png differ diff --git a/theme/light/theme.json b/theme/light/theme.json index 829094cae..cea6c4389 100644 --- a/theme/light/theme.json +++ b/theme/light/theme.json @@ -22,15 +22,19 @@ "rgba(0, 0, 0, 0.5)": "rgba(0, 0, 0, 0.0)", "column-left-color": "#e6ebf0", "main-bg-color": "#e6ebf0", + "login-bg-color": "#e6ebf0", + "options-bg-color": "#e6ebf0", "post-bg-color": "#e6ebf0", "timeline-posts-background-color": "#e6ebf0", "header-bg-color": "#e6ebf0", "main-bg-color-dm": "#e3dbf0", "link-bg-color": "#e6ebf0", - "main-bg-color-reply": "#e0dbf0", + "main-bg-color-reply": "white", "main-bg-color-report": "#e3dbf0", "main-header-color-roles": "#ebebf0", "main-fg-color": "#2d2c37", + "login-fg-color": "#2d2c37", + "options-fg-color": "#2d2c37", "column-left-fg-color": "#2d2c37", "border-color": "#c0cdd9", "main-link-color": "#2a2c37", diff --git a/theme/night/icons/categoriesrss.png b/theme/night/icons/categoriesrss.png new file mode 100644 index 000000000..219adb9bc Binary files /dev/null and b/theme/night/icons/categoriesrss.png differ diff --git a/theme/night/theme.json b/theme/night/theme.json index 7e1ca228a..b7cddde8c 100644 --- a/theme/night/theme.json +++ b/theme/night/theme.json @@ -20,6 +20,8 @@ "font-size4": "24px", "font-size5": "22px", "main-bg-color": "#0f0d10", + "login-bg-color": "#0f0d10", + "options-bg-color": "#0f0d10", "post-bg-color": "#0f0d10", "timeline-posts-background-color": "#0f0d10", "header-bg-color": "#0f0d10", @@ -29,6 +31,8 @@ "main-link-color": "#6481f5", "main-link-color-hover": "#d09338", "main-fg-color": "#0481f5", + "login-fg-color": "#0481f5", + "options-fg-color": "#0481f5", "column-left-fg-color": "#0481f5", "main-bg-color-dm": "#0b0a0a", "border-color": "#606984", diff --git a/theme/purple/icons/categoriesrss.png b/theme/purple/icons/categoriesrss.png new file mode 100644 index 000000000..77ab236b3 Binary files /dev/null and b/theme/purple/icons/categoriesrss.png differ diff --git a/theme/purple/theme.json b/theme/purple/theme.json index 62368f6c1..2f500c7fc 100644 --- a/theme/purple/theme.json +++ b/theme/purple/theme.json @@ -13,6 +13,8 @@ "font-size4": "24px", "font-size5": "22px", "main-bg-color": "#1f152d", + "login-bg-color": "#1f152d", + "options-bg-color": "#1f152d", "post-bg-color": "#1f152d", "timeline-posts-background-color": "#1f152d", "header-bg-color": "#1f152d", @@ -22,6 +24,8 @@ "main-bg-color-report": "#12152d", "main-header-color-roles": "#1f192d", "main-fg-color": "#f98bb0", + "login-fg-color": "#f98bb0", + "options-fg-color": "#f98bb0", "column-left-fg-color": "#f98bb0", "border-color": "#3f2145", "main-link-color": "#ff42a0", diff --git a/theme/rc3/icons/categoriesrss.png b/theme/rc3/icons/categoriesrss.png new file mode 100644 index 000000000..7a96b6a51 Binary files /dev/null and b/theme/rc3/icons/categoriesrss.png differ diff --git a/theme/rc3/theme.json b/theme/rc3/theme.json index 904998878..1ab809f18 100644 --- a/theme/rc3/theme.json +++ b/theme/rc3/theme.json @@ -41,6 +41,8 @@ "font-size5": "12px", "font-size-likes": "10px", "main-bg-color": "#100e23", + "login-bg-color": "#100e23", + "options-bg-color": "#100e23", "post-bg-color": "#100e23", "timeline-posts-background-color": "#100e23", "header-bg-color": "#100e23", @@ -50,6 +52,8 @@ "main-link-color": "#05b9ec", "main-link-color-hover": "#46eed5", "main-fg-color": "white", + "login-fg-color": "white", + "options-fg-color": "white", "title-color": "white", "column-left-fg-color": "#05b9ec", "main-bg-color-dm": "#0b0a0a", diff --git a/theme/solidaric/icons/categoriesrss.png b/theme/solidaric/icons/categoriesrss.png new file mode 100644 index 000000000..f5c9f3b33 Binary files /dev/null and b/theme/solidaric/icons/categoriesrss.png differ diff --git a/theme/solidaric/theme.json b/theme/solidaric/theme.json index 6ad6b0011..58d0ee155 100644 --- a/theme/solidaric/theme.json +++ b/theme/solidaric/theme.json @@ -29,6 +29,8 @@ "font-size5": "22px", "rgba(0, 0, 0, 0.5)": "rgba(0, 0, 0, 0.0)", "main-bg-color": "white", + "login-bg-color": "white", + "options-bg-color": "white", "post-bg-color": "white", "timeline-posts-background-color": "white", "header-bg-color": "#ddd", @@ -39,6 +41,8 @@ "main-bg-color-report": "white", "main-header-color-roles": "#ebebf0", "main-fg-color": "#2d2c37", + "login-fg-color": "#2d2c37", + "options-fg-color": "#2d2c37", "column-left-fg-color": "#2d2c37", "border-color": "#c0cdd9", "main-link-color": "#2a2c37", diff --git a/theme/starlight/icons/categoriesrss.png b/theme/starlight/icons/categoriesrss.png new file mode 100644 index 000000000..13103aab5 Binary files /dev/null and b/theme/starlight/icons/categoriesrss.png differ diff --git a/theme/starlight/icons/separator_right.png b/theme/starlight/icons/separator_right.png new file mode 100644 index 000000000..3f31f0fce Binary files /dev/null and b/theme/starlight/icons/separator_right.png differ diff --git a/theme/starlight/theme.json b/theme/starlight/theme.json index 71b1c1795..450aa35a0 100644 --- a/theme/starlight/theme.json +++ b/theme/starlight/theme.json @@ -1,4 +1,6 @@ { + "post-separator-margin-top": "10px", + "post-separator-margin-bottom": "10px", "newswire-publish-icon": "True", "full-width-timeline-buttons": "False", "icons-as-buttons": "False", @@ -16,6 +18,8 @@ "font-size4": "24px", "font-size5": "22px", "main-bg-color": "#0f0d10", + "login-bg-color": "#0f0d10", + "options-bg-color": "#0f0d10", "post-bg-color": "#0f0d10", "timeline-posts-background-color": "#0f0d10", "header-bg-color": "#0f0d10", @@ -27,6 +31,8 @@ "title-color": "#ffc4bc", "main-visited-color": "#e1c4bc", "main-fg-color": "#ffc4bc", + "login-fg-color": "#ffc4bc", + "options-fg-color": "#ffc4bc", "column-left-fg-color": "#ffc4bc", "main-bg-color-dm": "#0b0a0a", "border-color": "#69282c", diff --git a/theme/zen/icons/categoriesrss.png b/theme/zen/icons/categoriesrss.png new file mode 100644 index 000000000..35d5393b1 Binary files /dev/null and b/theme/zen/icons/categoriesrss.png differ diff --git a/theme/zen/theme.json b/theme/zen/theme.json index 50524edd3..ff229fea0 100644 --- a/theme/zen/theme.json +++ b/theme/zen/theme.json @@ -8,6 +8,8 @@ "banner-height-mobile": "10vh", "newswire-date-color": "yellow", "main-bg-color": "#5c4e41", + "login-bg-color": "#5c4e41", + "options-bg-color": "#5c4e41", "post-bg-color": "#5c4e41", "timeline-posts-background-color": "#5c4e41", "header-bg-color": "#5c4e41", diff --git a/translations/ar.json b/translations/ar.json index b94660914..b3752456c 100644 --- a/translations/ar.json +++ b/translations/ar.json @@ -330,5 +330,13 @@ "RSS feed for your blog": "تغذية RSS لمدونتك", "Create a new shared item": "إنشاء عنصر مشترك جديد", "Rc3": "Rc3", - "Hashtag origins": "أصول الهاشتاق" + "Hashtag origins": "أصول الهاشتاق", + "admin": "مشرف", + "moderator": "الوسيط", + "editor": "محرر", + "delegator": "المفوض", + "Debian": "Debian", + "Select the edit icon to add RSS feeds": "حدد رمز التحرير لإضافة موجز ويب لـ RSS", + "Select the edit icon to add web links": "حدد رمز التحرير لإضافة روابط الويب", + "Hashtag Categories RSS Feed": "Hashtag Categories RSS Feed" } diff --git a/translations/ca.json b/translations/ca.json index 79570c6ed..45a703b61 100644 --- a/translations/ca.json +++ b/translations/ca.json @@ -330,5 +330,13 @@ "RSS feed for your blog": "Feed RSS del vostre bloc", "Create a new shared item": "Creeu un element compartit nou", "Rc3": "Rc3", - "Hashtag origins": "Orígens de hashtag" + "Hashtag origins": "Orígens de hashtag", + "admin": "administrador", + "moderator": "moderador", + "editor": "editor", + "delegator": "delegador", + "Debian": "Debian", + "Select the edit icon to add RSS feeds": "Seleccioneu la icona d'edició per afegir canals RSS", + "Select the edit icon to add web links": "Seleccioneu la icona d'edició per afegir enllaços web", + "Hashtag Categories RSS Feed": "Feed RSS de categories de hashtag" } diff --git a/translations/cy.json b/translations/cy.json index 257465985..fd9d78d3f 100644 --- a/translations/cy.json +++ b/translations/cy.json @@ -330,5 +330,13 @@ "RSS feed for your blog": "Porthiant RSS ar gyfer eich blog", "Create a new shared item": "Creu eitem newydd a rennir", "Rc3": "Rc3", - "Hashtag origins": "Gwreiddiau Hashtag" + "Hashtag origins": "Gwreiddiau Hashtag", + "admin": "admin", + "moderator": "cymedrolwr", + "editor": "golygydd", + "delegator": "dirprwy", + "Debian": "Debian", + "Select the edit icon to add RSS feeds": "Dewiswch yr eicon golygu i ychwanegu porthwyr RSS", + "Select the edit icon to add web links": "Dewiswch yr eicon golygu i ychwanegu dolenni gwe", + "Hashtag Categories RSS Feed": "Categorïau Hashtag RSS Feed" } diff --git a/translations/de.json b/translations/de.json index be34734d7..27500796b 100644 --- a/translations/de.json +++ b/translations/de.json @@ -330,5 +330,13 @@ "RSS feed for your blog": "RSS-Feed für Ihr Blog", "Create a new shared item": "Erstellen Sie ein neues freigegebenes Element", "Rc3": "Rc3", - "Hashtag origins": "Hashtag-Ursprünge" + "Hashtag origins": "Hashtag-Ursprünge", + "admin": "Administrator", + "moderator": "Moderator", + "editor": "Editor", + "delegator": "Delegator", + "Debian": "Debian", + "Select the edit icon to add RSS feeds": "Wählen Sie das Bearbeitungssymbol, um RSS-Feeds hinzuzufügen", + "Select the edit icon to add web links": "Wählen Sie das Bearbeitungssymbol, um Weblinks hinzuzufügen", + "Hashtag Categories RSS Feed": "Hashtag Kategorien RSS Feed" } diff --git a/translations/en.json b/translations/en.json index 695b6dfc6..cdf939477 100644 --- a/translations/en.json +++ b/translations/en.json @@ -330,5 +330,13 @@ "RSS feed for your blog": "RSS feed for your blog", "Create a new shared item": "Create a new shared item", "Rc3": "Rc3", - "Hashtag origins": "Hashtag origins" + "Hashtag origins": "Hashtag origins", + "admin": "admin", + "moderator": "moderator", + "editor": "editor", + "delegator": "delegator", + "Debian": "Debian", + "Select the edit icon to add RSS feeds": "Select the edit icon to add RSS feeds", + "Select the edit icon to add web links": "Select the edit icon to add web links", + "Hashtag Categories RSS Feed": "Hashtag Categories RSS Feed" } diff --git a/translations/es.json b/translations/es.json index a6a6af083..7a8471633 100644 --- a/translations/es.json +++ b/translations/es.json @@ -330,5 +330,13 @@ "RSS feed for your blog": "Fuente RSS para tu blog", "Create a new shared item": "Crea un nuevo elemento compartido", "Rc3": "Rc3", - "Hashtag origins": "Orígenes del hashtag" + "Hashtag origins": "Orígenes del hashtag", + "admin": "administración", + "moderator": "moderador", + "editor": "editor", + "delegator": "delegador", + "Debian": "Debian", + "Select the edit icon to add RSS feeds": "Seleccione el icono de edición para agregar fuentes RSS", + "Select the edit icon to add web links": "Seleccione el icono de edición para agregar enlaces web", + "Hashtag Categories RSS Feed": "Feed RSS de categorías de hashtags" } diff --git a/translations/fr.json b/translations/fr.json index 9891601b8..0b00ed4b1 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -330,5 +330,13 @@ "RSS feed for your blog": "Flux RSS pour votre blog", "Create a new shared item": "Créer un nouvel élément partagé", "Rc3": "Rc3", - "Hashtag origins": "Origines des hashtags" + "Hashtag origins": "Origines des hashtags", + "admin": "admin", + "moderator": "modérateur", + "editor": "éditeur", + "delegator": "délégant", + "Debian": "Debian", + "Select the edit icon to add RSS feeds": "Sélectionnez l'icône d'édition pour ajouter des flux RSS", + "Select the edit icon to add web links": "Sélectionnez l'icône de modification pour ajouter des liens Web", + "Hashtag Categories RSS Feed": "Flux RSS des catégories Hashtag" } diff --git a/translations/ga.json b/translations/ga.json index edda6258c..118845962 100644 --- a/translations/ga.json +++ b/translations/ga.json @@ -330,5 +330,13 @@ "RSS feed for your blog": "Fotha RSS do do bhlag", "Create a new shared item": "Cruthaigh mír nua roinnte", "Rc3": "Rc3", - "Hashtag origins": "Bunús Hashtag" + "Hashtag origins": "Bunús Hashtag", + "admin": "admin", + "moderator": "modhnóir", + "editor": "eagarthóir", + "delegator": "toscaire", + "Debian": "Debian", + "Select the edit icon to add RSS feeds": "Roghnaigh an deilbhín eagar chun fothaí RSS a chur leis", + "Select the edit icon to add web links": "Roghnaigh an deilbhín eagar chun naisc ghréasáin a chur leis", + "Hashtag Categories RSS Feed": "Catagóirí Hashtag RSS Feed" } diff --git a/translations/hi.json b/translations/hi.json index a036404e2..1bca2fa83 100644 --- a/translations/hi.json +++ b/translations/hi.json @@ -330,5 +330,13 @@ "RSS feed for your blog": "RSS आपके ब्लॉग के लिए फ़ीड करता है", "Create a new shared item": "एक नया साझा आइटम बनाएं", "Rc3": "Rc3", - "Hashtag origins": "हैशटैग की उत्पत्ति" + "Hashtag origins": "हैशटैग की उत्पत्ति", + "admin": "व्यवस्थापक", + "moderator": "मध्यस्थ", + "editor": "संपादक", + "delegator": "डैलिगेटर", + "Debian": "Debian", + "Select the edit icon to add RSS feeds": "RSS फ़ीड जोड़ने के लिए संपादन आइकन का चयन करें", + "Select the edit icon to add web links": "वेब लिंक जोड़ने के लिए संपादन आइकन का चयन करें", + "Hashtag Categories RSS Feed": "हैशटैग श्रेणियाँ आरएसएस फ़ीड" } diff --git a/translations/it.json b/translations/it.json index 0e6d10dfb..1c05b8908 100644 --- a/translations/it.json +++ b/translations/it.json @@ -330,5 +330,13 @@ "RSS feed for your blog": "Feed RSS per il tuo blog", "Create a new shared item": "Crea un nuovo elemento condiviso", "Rc3": "Rc3", - "Hashtag origins": "Origini hashtag" + "Hashtag origins": "Origini hashtag", + "admin": "admin", + "moderator": "moderatore", + "editor": "editore", + "delegator": "delegatore", + "Debian": "Debian", + "Select the edit icon to add RSS feeds": "Seleziona l'icona di modifica per aggiungere feed RSS", + "Select the edit icon to add web links": "Seleziona l'icona di modifica per aggiungere link web", + "Hashtag Categories RSS Feed": "Feed RSS delle categorie hashtag" } diff --git a/translations/ja.json b/translations/ja.json index b7e8a027f..4e1065001 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -330,5 +330,13 @@ "RSS feed for your blog": "ブログのRSSフィード", "Create a new shared item": "新しい共有アイテムを作成する", "Rc3": "Rc3", - "Hashtag origins": "ハッシュタグの起源" + "Hashtag origins": "ハッシュタグの起源", + "admin": "管理者", + "moderator": "モデレータ", + "editor": "編集者", + "delegator": "委任者", + "Debian": "Debian", + "Select the edit icon to add RSS feeds": "編集アイコンを選択してRSSフィードを追加します", + "Select the edit icon to add web links": "編集アイコンを選択してWebリンクを追加します", + "Hashtag Categories RSS Feed": "ハッシュタグカテゴリRSSフィード" } diff --git a/translations/oc.json b/translations/oc.json index 2a8664f09..b8fd14c6d 100644 --- a/translations/oc.json +++ b/translations/oc.json @@ -326,5 +326,13 @@ "RSS feed for your blog": "RSS feed for your blog", "Create a new shared item": "Create a new shared item", "Rc3": "Rc3", - "Hashtag origins": "Hashtag origins" + "Hashtag origins": "Hashtag origins", + "admin": "admin", + "moderator": "moderator", + "editor": "editor", + "delegator": "delegator", + "Debian": "Debian", + "Select the edit icon to add RSS feeds": "Select the edit icon to add RSS feeds", + "Select the edit icon to add web links": "Select the edit icon to add web links", + "Hashtag Categories RSS Feed": "Hashtag Categories RSS Feed" } diff --git a/translations/pt.json b/translations/pt.json index e0119ef91..eda0cfe87 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -330,5 +330,13 @@ "RSS feed for your blog": "Feed RSS para o seu blog", "Create a new shared item": "Crie um novo item compartilhado", "Rc3": "Rc3", - "Hashtag origins": "Origens de hashtag" + "Hashtag origins": "Origens de hashtag", + "admin": "admin", + "moderator": "moderador", + "editor": "editor", + "delegator": "delegador", + "Debian": "Debian", + "Select the edit icon to add RSS feeds": "Selecione o ícone de edição para adicionar feeds RSS", + "Select the edit icon to add web links": "Selecione o ícone de edição para adicionar links da web", + "Hashtag Categories RSS Feed": "Feed RSS das categorias de hashtag" } diff --git a/translations/ru.json b/translations/ru.json index 7431690e6..043800408 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -330,5 +330,13 @@ "RSS feed for your blog": "RSS-канал для вашего блога", "Create a new shared item": "Создать новый общий элемент", "Rc3": "Rc3", - "Hashtag origins": "Происхождение хэштегов" + "Hashtag origins": "Происхождение хэштегов", + "admin": "админ", + "moderator": "Модератор", + "editor": "редактор", + "delegator": "делегат", + "Debian": "Debian", + "Select the edit icon to add RSS feeds": "Щелкните значок редактирования, чтобы добавить RSS-каналы", + "Select the edit icon to add web links": "Щелкните значок редактирования, чтобы добавить веб-ссылки", + "Hashtag Categories RSS Feed": "RSS-канал категорий хэштегов" } diff --git a/translations/zh.json b/translations/zh.json index 89fb68163..38b30eabc 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -330,5 +330,13 @@ "RSS feed for your blog": "您博客的RSS供稿", "Create a new shared item": "创建一个新的共享项目", "Rc3": "Rc3", - "Hashtag origins": "标签起源" + "Hashtag origins": "标签起源", + "admin": "管理员", + "moderator": "主持人", + "editor": "编辑", + "delegator": "委托人", + "Debian": "Debian", + "Select the edit icon to add RSS feeds": "选择编辑图标以添加RSS feed", + "Select the edit icon to add web links": "选择编辑图标以添加Web链接", + "Hashtag Categories RSS Feed": "标签类别RSS提要" } diff --git a/utils.py b/utils.py index 3bd40da77..414f46bd8 100644 --- a/utils.py +++ b/utils.py @@ -19,6 +19,152 @@ from calendar import monthrange from followingCalendar import addPersonToCalendar +def getHashtagCategory(baseDir: str, hashtag: str) -> str: + """Returns the category for the hashtag + """ + categoryFilename = baseDir + '/tags/' + hashtag + '.category' + if not os.path.isfile(categoryFilename): + categoryFilename = baseDir + '/tags/' + hashtag.title() + '.category' + if not os.path.isfile(categoryFilename): + categoryFilename = \ + baseDir + '/tags/' + hashtag.upper() + '.category' + if not os.path.isfile(categoryFilename): + return '' + + with open(categoryFilename, 'r') as fp: + categoryStr = fp.read() + if categoryStr: + return categoryStr + return '' + + +def getHashtagCategories(baseDir: str, category=None) -> None: + """Returns a dictionary containing hashtag categories + """ + hashtagCategories = {} + + for subdir, dirs, files in os.walk(baseDir + '/tags'): + for f in files: + if not f.endswith('.category'): + continue + categoryFilename = os.path.join(baseDir + '/tags', f) + if not os.path.isfile(categoryFilename): + continue + hashtag = f.split('.')[0] + with open(categoryFilename, 'r') as fp: + categoryStr = fp.read() + + if not categoryStr: + continue + + if category: + # only return a dictionary for a specific category + if categoryStr != category: + continue + + if not hashtagCategories.get(categoryStr): + hashtagCategories[categoryStr] = [hashtag] + else: + if hashtag not in hashtagCategories[categoryStr]: + hashtagCategories[categoryStr].append(hashtag) + return hashtagCategories + + +def updateHashtagCategories(baseDir: str) -> None: + """Regenerates the list of hashtag categories + """ + categoryListFilename = baseDir + '/accounts/categoryList.txt' + hashtagCategories = getHashtagCategories(baseDir) + if not hashtagCategories: + if os.path.isfile(categoryListFilename): + os.remove(categoryListFilename) + return + + categoryList = [] + for categoryStr, hashtagList in hashtagCategories.items(): + categoryList.append(categoryStr) + categoryList.sort() + + categoryListStr = '' + for categoryStr in categoryList: + categoryListStr += categoryStr + '\n' + + # save a list of available categories for quick lookup + with open(categoryListFilename, 'w+') as fp: + fp.write(categoryListStr) + + +def validHashtagCategory(category: str) -> bool: + """Returns true if the category name is valid + """ + if not category: + return False + + invalidChars = (',', ' ', '<', ';', '\\') + for ch in invalidChars: + if ch in category: + return False + + # too long + if len(category) > 40: + return False + + return True + + +def setHashtagCategory(baseDir: str, hashtag: str, category: str) -> bool: + """Sets the category for the hashtag + """ + if not validHashtagCategory(category): + return False + + hashtagFilename = baseDir + '/tags/' + hashtag + '.txt' + if not os.path.isfile(hashtagFilename): + hashtag = hashtag.title() + hashtagFilename = baseDir + '/tags/' + hashtag + '.txt' + if not os.path.isfile(hashtagFilename): + hashtag = hashtag.upper() + hashtagFilename = baseDir + '/tags/' + hashtag + '.txt' + if not os.path.isfile(hashtagFilename): + return False + + categoryFilename = baseDir + '/tags/' + hashtag + '.category' + with open(categoryFilename, 'w+') as fp: + fp.write(category) + updateHashtagCategories(baseDir) + return True + + return False + + +def isEditor(baseDir: str, nickname: str) -> bool: + """Returns true if the given nickname is an editor + """ + editorsFile = baseDir + '/accounts/editors.txt' + + if not os.path.isfile(editorsFile): + adminName = getConfigParam(baseDir, 'admin') + if not adminName: + return False + if adminName == nickname: + return True + return False + + with open(editorsFile, "r") as f: + lines = f.readlines() + if len(lines) == 0: + adminName = getConfigParam(baseDir, 'admin') + if not adminName: + return False + if adminName == nickname: + return True + for editor in lines: + editor = editor.strip('\n').strip('\r') + if editor == nickname: + return True + return False + + def getImageExtensions() -> []: """Returns a list of the possible image file extensions """ @@ -522,6 +668,37 @@ def getDomainFromActor(actor: str) -> (str, int): return domain, port +def setDefaultPetName(baseDir: str, nickname: str, domain: str, + followNickname: str, followDomain: str) -> None: + """Sets a default petname + This helps especially when using onion or i2p address + """ + if ':' in domain: + domain = domain.split(':')[0] + userPath = baseDir + '/accounts/' + nickname + '@' + domain + petnamesFilename = userPath + '/petnames.txt' + + petnameLookupEntry = followNickname + ' ' + \ + followNickname + '@' + followDomain + '\n' + if not os.path.isfile(petnamesFilename): + # if there is no existing petnames lookup file + with open(petnamesFilename, 'w+') as petnamesFile: + petnamesFile.write(petnameLookupEntry) + return + + with open(petnamesFilename, 'r') as petnamesFile: + petnamesStr = petnamesFile.read() + if petnamesStr: + petnamesList = petnamesStr.split('\n') + for pet in petnamesList: + if pet.startswith(followNickname + ' '): + # petname already exists + return + # petname doesn't already exist + with open(petnamesFilename, 'a+') as petnamesFile: + petnamesFile.write(petnameLookupEntry) + + def followPerson(baseDir: str, nickname: str, domain: str, followNickname: str, followDomain: str, federationList: [], debug: bool, @@ -593,9 +770,9 @@ def followPerson(baseDir: str, nickname: str, domain: str, with open(filename, 'w+') as f: f.write(handleToFollow + '\n') - # Default to adding new follows to the calendar. - # Possibly this could be made optional if followFile.endswith('following.txt'): + # Default to adding new follows to the calendar. + # Possibly this could be made optional # if following a person add them to the list of # calendar follows print('DEBUG: adding ' + @@ -603,6 +780,9 @@ def followPerson(baseDir: str, nickname: str, domain: str, nickname + '@' + domain) addPersonToCalendar(baseDir, nickname, domain, followNickname, followDomain) + # add a default petname + setDefaultPetName(baseDir, nickname, domain, + followNickname, followDomain) return True @@ -935,16 +1115,17 @@ def validNickname(domain: str, nickname: str) -> bool: for c in forbiddenChars: if c in nickname: return False + # this should only apply for the shared inbox if nickname == domain: return False reservedNames = ('inbox', 'dm', 'outbox', 'following', - 'public', 'followers', + 'public', 'followers', 'category', 'channel', 'calendar', 'tlreplies', 'tlmedia', 'tlblogs', - 'tlevents', 'tlblogs', + 'tlevents', 'tlblogs', 'tlfeatures', 'moderation', 'activity', 'undo', 'reply', 'replies', 'question', 'like', - 'likes', 'users', 'statuses', + 'likes', 'users', 'statuses', 'tags', 'accounts', 'channels', 'profile', 'updates', 'repeat', 'announce', 'shares', 'fonts', 'icons', 'avatars') diff --git a/webapp_column_left.py b/webapp_column_left.py index 928c89df6..d882400d7 100644 --- a/webapp_column_left.py +++ b/webapp_column_left.py @@ -10,7 +10,7 @@ import os from shutil import copyfile from utils import getConfigParam from utils import getNicknameFromActor -from posts import isEditor +from utils import isEditor from webapp_utils import htmlPostSeparator from webapp_utils import getLeftImageFile from webapp_utils import getImageFile @@ -21,6 +21,13 @@ from webapp_utils import htmlFooter from webapp_utils import getBannerFile +def linksExist(baseDir: str) -> bool: + """Returns true if links have been created + """ + linksFilename = baseDir + '/accounts/links.txt' + return os.path.isfile(linksFilename) + + def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str, httpPrefix: str, translate: {}, iconsPath: str, editor: bool, @@ -232,12 +239,19 @@ def htmlLinksMobile(cssCache: {}, baseDir: str, headerButtonsFrontScreen(translate, nickname, 'links', authorized, iconsAsButtons, iconsPath) + '' - htmlStr += \ - getLeftColumnContent(baseDir, nickname, domainFull, - httpPrefix, translate, - iconsPath, editor, - False, timelinePath, - rssIconAtTop, False, False) + if linksExist(baseDir): + htmlStr += \ + getLeftColumnContent(baseDir, nickname, domainFull, + httpPrefix, translate, + iconsPath, editor, + False, timelinePath, + rssIconAtTop, False, False) + else: + if editor: + htmlStr += '


\n' + htmlStr += '
\n ' + htmlStr += translate['Select the edit icon to add web links'] + htmlStr += '\n
\n' # end of col-left-mobile htmlStr += '\n' diff --git a/webapp_column_right.py b/webapp_column_right.py index 921a22d41..78df295f6 100644 --- a/webapp_column_right.py +++ b/webapp_column_right.py @@ -16,7 +16,7 @@ from utils import loadJson from utils import getConfigParam from utils import votesOnNewswireItem from utils import getNicknameFromActor -from posts import isEditor +from utils import isEditor from posts import isModerator from webapp_utils import getRightImageFile from webapp_utils import getImageFile @@ -155,8 +155,15 @@ def getRightColumnContent(baseDir: str, nickname: str, domainFull: str, translate['Edit newswire'] + '" src="/' + \ iconsPath + '/edit.png" />\n' - # show the RSS icon + # show the RSS icons rssIconStr = \ + ' ' + \ + '' + \
+        translate['Hashtag Categories RSS Feed'] + '\n' + rssIconStr += \ ' ' + \ '' + \
@@ -443,14 +450,21 @@ def htmlNewswireMobile(cssCache: {}, baseDir: str, nickname: str,
         headerButtonsFrontScreen(translate, nickname,
                                  'newswire', authorized,
                                  iconsAsButtons, iconsPath) + '</center>'
-    htmlStr += \
-        getRightColumnContent(baseDir, nickname, domainFull,
-                              httpPrefix, translate,
-                              iconsPath, moderator, editor,
-                              newswire, positiveVoting,
-                              False, timelinePath, showPublishButton,
-                              showPublishAsIcon, rssIconAtTop, False,
-                              authorized, False)
+    if newswire:
+        htmlStr += \
+            getRightColumnContent(baseDir, nickname, domainFull,
+                                  httpPrefix, translate,
+                                  iconsPath, moderator, editor,
+                                  newswire, positiveVoting,
+                                  False, timelinePath, showPublishButton,
+                                  showPublishAsIcon, rssIconAtTop, False,
+                                  authorized, False)
+    else:
+        if editor:
+            htmlStr += '<br><br><br>\n'
+            htmlStr += '<center>\n  '
+            htmlStr += translate['Select the edit icon to add RSS feeds']
+            htmlStr += '\n</center>\n'
     # end of col-right-mobile
     htmlStr += '</div\n>'
 
diff --git a/webapp.py b/webapp_confirm.py
similarity index 81%
rename from webapp.py
rename to webapp_confirm.py
index 821816d49..3a468ef7f 100644
--- a/webapp.py
+++ b/webapp_confirm.py
@@ -1,4 +1,4 @@
-__filename__ = str: - """Returns a list of handles being followed - """ - with open(followingFilename, 'r') as followingFile: - msg = followingFile.read() - followingList = msg.split('\n') - followingList.sort() - if followingList: - cssFilename = baseDir + '/epicyon-profile.css' - if os.path.isfile(baseDir + '/epicyon.css'): - cssFilename = baseDir + '/epicyon.css' - - followingListHtml = htmlHeaderWithExternalStyle(cssFilename) - for followingAddress in followingList: - if followingAddress: - followingListHtml += \ - '

@' + followingAddress + '

' - followingListHtml += htmlFooter() - msg = followingListHtml - return msg - return '' - - -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' - - blockedHashtagForm = htmlHeaderWithExternalStyle(cssFilename) - blockedHashtagForm += '
\n' - blockedHashtagForm += htmlFooter() - return blockedHashtagForm - - -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 - """ - itemID = getValidSharedItemID(shareName) - nickname = getNicknameFromActor(actor) - domain, port = getDomainFromActor(actor) - domainFull = domain - if port: - if port != 80 and port != 443: - domainFull = domain + ':' + str(port) - sharesFile = baseDir + '/accounts/' + \ - nickname + '@' + domain + '/shares.json' - if not os.path.isfile(sharesFile): - print('ERROR: no shares file ' + sharesFile) - return None - sharesJson = loadJson(sharesFile) - if not sharesJson: - print('ERROR: unable to load shares.json') - return None - if not sharesJson.get(itemID): - print('ERROR: share named "' + itemID + '" is not in ' + sharesFile) - return None - sharedItemDisplayName = sharesJson[itemID]['displayName'] - sharedItemImageUrl = None - if sharesJson[itemID].get('imageUrl'): - sharedItemImageUrl = sharesJson[itemID]['imageUrl'] - - if os.path.isfile(baseDir + '/img/shares-background.png'): - if not os.path.isfile(baseDir + '/accounts/shares-background.png'): - copyfile(baseDir + '/img/shares-background.png', - baseDir + '/accounts/shares-background.png') - - cssFilename = baseDir + '/epicyon-follow.css' - if os.path.isfile(baseDir + '/follow.css'): - cssFilename = baseDir + '/follow.css' - - sharesStr = htmlHeaderWithExternalStyle(cssFilename) - sharesStr += '\n' - sharesStr += htmlFooter() - return sharesStr - - -def htmlDeletePost(cssCache: {}, - recentPostsCache: {}, maxRecentPosts: int, - translate, pageNumber: int, - session, baseDir: str, messageId: str, - httpPrefix: str, projectVersion: str, - wfRequest: {}, personCache: {}, - callingDomain: str, - YTReplacementDomain: str, - showPublishedDateOnly: bool) -> str: +def htmlConfirmDelete(cssCache: {}, + recentPostsCache: {}, maxRecentPosts: int, + translate, pageNumber: int, + session, baseDir: str, messageId: str, + httpPrefix: str, projectVersion: str, + wfRequest: {}, personCache: {}, + callingDomain: str, + YTReplacementDomain: str, + showPublishedDateOnly: bool) -> str: """Shows a screen asking to confirm the deletion of a post """ if '/statuses/' not in messageId: @@ -210,7 +97,75 @@ def htmlDeletePost(cssCache: {}, return deletePostStr -def htmlFollowConfirm(cssCache: {}, translate: {}, baseDir: str, +def htmlConfirmRemoveSharedItem(cssCache: {}, translate: {}, baseDir: str, + actor: str, shareName: str, + callingDomain: str) -> str: + """Shows a screen asking to confirm the removal of a shared item + """ + itemID = getValidSharedItemID(shareName) + nickname = getNicknameFromActor(actor) + domain, port = getDomainFromActor(actor) + domainFull = domain + if port: + if port != 80 and port != 443: + domainFull = domain + ':' + str(port) + sharesFile = baseDir + '/accounts/' + \ + nickname + '@' + domain + '/shares.json' + if not os.path.isfile(sharesFile): + print('ERROR: no shares file ' + sharesFile) + return None + sharesJson = loadJson(sharesFile) + if not sharesJson: + print('ERROR: unable to load shares.json') + return None + if not sharesJson.get(itemID): + print('ERROR: share named "' + itemID + '" is not in ' + sharesFile) + return None + sharedItemDisplayName = sharesJson[itemID]['displayName'] + sharedItemImageUrl = None + if sharesJson[itemID].get('imageUrl'): + sharedItemImageUrl = sharesJson[itemID]['imageUrl'] + + if os.path.isfile(baseDir + '/img/shares-background.png'): + if not os.path.isfile(baseDir + '/accounts/shares-background.png'): + copyfile(baseDir + '/img/shares-background.png', + baseDir + '/accounts/shares-background.png') + + cssFilename = baseDir + '/epicyon-follow.css' + if os.path.isfile(baseDir + '/follow.css'): + cssFilename = baseDir + '/follow.css' + + sharesStr = htmlHeaderWithExternalStyle(cssFilename) + sharesStr += '\n' + sharesStr += htmlFooter() + return sharesStr + + +def htmlConfirmFollow(cssCache: {}, translate: {}, baseDir: str, originPathStr: str, followActor: str, followProfileUrl: str) -> str: @@ -254,7 +209,7 @@ def htmlFollowConfirm(cssCache: {}, translate: {}, baseDir: str, return followStr -def htmlUnfollowConfirm(cssCache: {}, translate: {}, baseDir: str, +def htmlConfirmUnfollow(cssCache: {}, translate: {}, baseDir: str, originPathStr: str, followActor: str, followProfileUrl: str) -> str: @@ -299,7 +254,7 @@ def htmlUnfollowConfirm(cssCache: {}, translate: {}, baseDir: str, return followStr -def htmlUnblockConfirm(cssCache: {}, translate: {}, baseDir: str, +def htmlConfirmUnblock(cssCache: {}, translate: {}, baseDir: str, originPathStr: str, blockActor: str, blockProfileUrl: str) -> str: diff --git a/webapp_create_post.py b/webapp_create_post.py index db15f0f8b..073146103 100644 --- a/webapp_create_post.py +++ b/webapp_create_post.py @@ -92,7 +92,7 @@ def htmlNewPostDropDown(scopeIcon: str, scopeDescription: str, iconsPath + '/scope_public.png"/>' + \ translate['Public'] + '
' + \ translate['Visible to anyone'] + '\n' - if defaultTimeline == 'tlnews': + if defaultTimeline == 'tlfeatures': dropDownContent += \ '
  • str: + """Shows posts on the front screen of a news instance + These should only be public blog posts from the features timeline + which is the blog timeline of the news actor + """ + iconsPath = getIconsWebPath(baseDir) + separatorStr = htmlPostSeparator(baseDir, None) + profileStr = '' + maxItems = 4 + ctr = 0 + currPage = 1 + boxName = 'tlfeatures' + authorized = True + while ctr < maxItems and currPage < 4: + outboxFeed = \ + personBoxJson({}, session, baseDir, domain, port, + '/users/' + nickname + '/' + boxName + + '?page=' + str(currPage), + httpPrefix, 10, boxName, + authorized, 0, False, 0) + if not outboxFeed: + break + if len(outboxFeed['orderedItems']) == 0: + break + for item in outboxFeed['orderedItems']: + if item['type'] == 'Create': + postStr = \ + individualPostAsHtml(True, recentPostsCache, + maxRecentPosts, + iconsPath, translate, None, + baseDir, session, wfRequest, + personCache, + nickname, domain, port, item, + None, True, False, + httpPrefix, projectVersion, 'inbox', + YTReplacementDomain, + showPublishedDateOnly, + False, False, False, True, False) + if postStr: + profileStr += postStr + separatorStr + ctr += 1 + if ctr >= maxItems: + break + currPage += 1 + return profileStr + + +def htmlFrontScreen(rssIconAtTop: bool, + cssCache: {}, iconsAsButtons: bool, + defaultTimeline: str, + recentPostsCache: {}, maxRecentPosts: int, + translate: {}, projectVersion: str, + baseDir: str, httpPrefix: str, authorized: bool, + profileJson: {}, selected: str, + session, wfRequest: {}, personCache: {}, + YTReplacementDomain: str, + showPublishedDateOnly: bool, + newswire: {}, extraJson=None, + pageNumber=None, maxItemsPerPage=None) -> str: + """Show the news instance front screen + """ + nickname = profileJson['preferredUsername'] + if not nickname: + return "" + if not isSystemAccount(nickname): + return "" + domain, port = getDomainFromActor(profileJson['id']) + if not domain: + return "" + domainFull = domain + if port: + domainFull = domain + ':' + str(port) + + iconsPath = getIconsWebPath(baseDir) + loginButton = headerButtonsFrontScreen(translate, nickname, + 'features', authorized, + iconsAsButtons, iconsPath) + + # If this is the news account then show a different banner + bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) + profileHeaderStr = \ + '\n' + if loginButton: + profileHeaderStr += '
    ' + loginButton + '
    \n' + + profileHeaderStr += '\n' + profileHeaderStr += ' \n' + profileHeaderStr += ' \n' + profileHeaderStr += ' \n' + profileHeaderStr += ' \n' + profileHeaderStr += ' \n' + profileHeaderStr += ' \n' + profileHeaderStr += ' \n' + profileHeaderStr += ' \n' + profileHeaderStr += ' \n' + profileFooterStr += ' \n' + profileFooterStr += ' \n' + profileFooterStr += ' \n' + profileFooterStr += '
    \n' + iconsPath = getIconsWebPath(baseDir) + profileHeaderStr += \ + getLeftColumnContent(baseDir, 'news', domainFull, + httpPrefix, translate, + iconsPath, False, + False, None, rssIconAtTop, True, + True) + profileHeaderStr += ' \n' + + profileStr = profileHeaderStr + + cssFilename = baseDir + '/epicyon-profile.css' + if os.path.isfile(baseDir + '/epicyon.css'): + cssFilename = baseDir + '/epicyon.css' + + licenseStr = '' + bannerFile, bannerFilename = \ + getBannerFile(baseDir, nickname, domain) + profileStr += \ + htmlFrontScreenPosts(recentPostsCache, maxRecentPosts, + translate, + baseDir, httpPrefix, + nickname, domain, port, + session, wfRequest, personCache, + projectVersion, + YTReplacementDomain, + showPublishedDateOnly) + licenseStr + + # Footer which is only used for system accounts + profileFooterStr = ' \n' + iconsPath = getIconsWebPath(baseDir) + profileFooterStr += \ + getRightColumnContent(baseDir, 'news', domainFull, + httpPrefix, translate, + iconsPath, False, False, + newswire, False, + False, None, False, False, + False, True, authorized, True) + profileFooterStr += '
    \n' + + profileStr = \ + htmlHeaderWithExternalStyle(cssFilename) + \ + profileStr + profileFooterStr + htmlFooter() + return profileStr diff --git a/webapp_hashtagswarm.py b/webapp_hashtagswarm.py index e26a8bd3f..8fa03b5cb 100644 --- a/webapp_hashtagswarm.py +++ b/webapp_hashtagswarm.py @@ -7,8 +7,50 @@ __email__ = "bob@freedombone.net" __status__ = "Production" import os -from blocking import isBlockedHashtag +from shutil import copyfile from datetime import datetime +from utils import getConfigParam +from utils import getNicknameFromActor +from utils import getHashtagCategories +from utils import getHashtagCategory +from webapp_utils import getSearchBannerFile +from webapp_utils import getImageFile +from webapp_utils import getContentWarningButton +from webapp_utils import htmlHeaderWithExternalStyle +from webapp_utils import htmlFooter + + +def getHashtagCategoriesFeed(baseDir: str, + hashtagCategories=None) -> str: + """Returns an rss feed for hashtag categories + """ + if not hashtagCategories: + hashtagCategories = getHashtagCategories(baseDir) + if not hashtagCategories: + return None + + rssStr = "\n" + rssStr += "\n" + rssStr += '\n' + rssStr += ' #categories\n' + + rssDateStr = \ + datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S UT") + + for categoryStr, hashtagList in hashtagCategories.items(): + rssStr += '\n' + rssStr += ' ' + categoryStr + '\n' + listStr = '' + for hashtag in hashtagList: + listStr += hashtag + ' ' + rssStr += ' ' + listStr.strip() + '\n' + rssStr += ' \n' + rssStr += ' ' + rssDateStr + '\n' + rssStr += '\n' + + rssStr += '\n' + rssStr += '\n' + return rssStr def getHashtagDomainMax(domainHistogram: {}) -> str: @@ -79,10 +121,21 @@ def htmlHashTagSwarm(baseDir: str, actor: str, translate: {}) -> str: daysSinceEpochStr2 = str(daysSinceEpoch - 1) + ' ' recently = daysSinceEpoch - 1 tagSwarm = [] + categorySwarm = [] domainHistogram = {} + # Load the blocked hashtags into memory. + # This avoids needing to repeatedly load the blocked file for each hashtag + blockedStr = '' + globalBlockingFilename = baseDir + '/accounts/blocking.txt' + if os.path.isfile(globalBlockingFilename): + with open(globalBlockingFilename, 'r') as fp: + blockedStr = fp.read() + for subdir, dirs, files in os.walk(baseDir + '/tags'): for f in files: + if not f.endswith('.txt'): + continue tagsFilename = os.path.join(baseDir + '/tags', f) if not os.path.isfile(tagsFilename): continue @@ -98,7 +151,7 @@ def htmlHashTagSwarm(baseDir: str, actor: str, translate: {}) -> str: continue hashTagName = f.split('.')[0] - if isBlockedHashtag(baseDir, hashTagName): + if '#' + hashTagName + '\n' in blockedStr: continue with open(tagsFilename, 'r') as fp: # only read one line, which saves time and memory @@ -129,24 +182,113 @@ def htmlHashTagSwarm(baseDir: str, actor: str, translate: {}) -> str: postDomain = postUrl.split('##')[1] if '#' in postDomain: postDomain = postDomain.split('#')[0] + if domainHistogram.get(postDomain): domainHistogram[postDomain] = \ domainHistogram[postDomain] + 1 else: domainHistogram[postDomain] = 1 tagSwarm.append(hashTagName) + categoryFilename = \ + tagsFilename.replace('.txt', '.category') + if os.path.isfile(categoryFilename): + categoryStr = \ + getHashtagCategory(baseDir, hashTagName) + if categoryStr not in categorySwarm: + categorySwarm.append(categoryStr) break if not tagSwarm: return '' tagSwarm.sort() + + # swarm of categories + categorySwarmStr = '' + if categorySwarm: + if len(categorySwarm) > 3: + categorySwarm.sort() + for categoryStr in categorySwarm: + categorySwarmStr += \ + '
    ' + categoryStr + '\n' + categorySwarmStr += '
    \n' + + # swarm of tags tagSwarmStr = '' - ctr = 0 for tagName in tagSwarm: tagSwarmStr += \ '' + tagName + '\n' - ctr += 1 - tagSwarmHtml = tagSwarmStr.strip() + '\n' - tagSwarmHtml += getHashtagDomainHistogram(domainHistogram, translate) + + if categorySwarmStr: + tagSwarmStr = \ + getContentWarningButton('alltags', translate, tagSwarmStr) + + tagSwarmHtml = categorySwarmStr + tagSwarmStr.strip() + '\n' + # tagSwarmHtml += getHashtagDomainHistogram(domainHistogram, translate) return tagSwarmHtml + + +def htmlSearchHashtagCategory(cssCache: {}, translate: {}, + baseDir: str, path: str, domain: str) -> str: + """Show hashtags after selecting a category on the main search screen + """ + actor = path.split('/category/')[0] + categoryStr = path.split('/category/')[1].strip() + searchNickname = getNicknameFromActor(actor) + + if os.path.isfile(baseDir + '/img/search-background.png'): + if not os.path.isfile(baseDir + '/accounts/search-background.png'): + copyfile(baseDir + '/img/search-background.png', + baseDir + '/accounts/search-background.png') + + cssFilename = baseDir + '/epicyon-search.css' + if os.path.isfile(baseDir + '/search.css'): + cssFilename = baseDir + '/search.css' + + htmlStr = htmlHeaderWithExternalStyle(cssFilename) + + # show a banner above the search box + searchBannerFile, searchBannerFilename = \ + getSearchBannerFile(baseDir, searchNickname, domain) + if not os.path.isfile(searchBannerFilename): + # get the default search banner for the theme + theme = getConfigParam(baseDir, 'theme').lower() + if theme == 'default': + theme = '' + else: + theme = '_' + theme + themeSearchImageFile, themeSearchBannerFilename = \ + getImageFile(baseDir, 'search_banner', baseDir + '/img', + searchNickname, domain) + if os.path.isfile(themeSearchBannerFilename): + searchBannerFilename = \ + baseDir + '/accounts/' + \ + searchNickname + '@' + domain + '/' + themeSearchImageFile + copyfile(themeSearchBannerFilename, + searchBannerFilename) + searchBannerFile = themeSearchImageFile + + if os.path.isfile(searchBannerFilename): + htmlStr += '\n' + htmlStr += '\n' + + htmlStr += '' + htmlStr += htmlFooter() + return htmlStr diff --git a/webapp_headerbuttons.py b/webapp_headerbuttons.py index 9fea8bd2b..ae987be95 100644 --- a/webapp_headerbuttons.py +++ b/webapp_headerbuttons.py @@ -7,6 +7,7 @@ __email__ = "bob@freedombone.net" __status__ = "Production" +import os import time from datetime import datetime from happening import todaysEventsCheck @@ -21,6 +22,7 @@ def headerButtonsTimeline(defaultTimeline: str, usersPath: str, mediaButton: str, blogsButton: str, + featuresButton: str, newsButton: str, inboxButton: str, dmButton: str, @@ -61,11 +63,11 @@ def headerButtonsTimeline(defaultTimeline: str, '/tlblogs">' - elif defaultTimeline == 'tlnews': + elif defaultTimeline == 'tlfeatures': tlStr += \ '' else: tlStr += \ @@ -75,26 +77,30 @@ def headerButtonsTimeline(defaultTimeline: str, translate['Inbox'] + '' # if this is a news instance and we are viewing the news timeline - newsHeader = False - if defaultTimeline == 'tlnews' and boxName == 'tlnews': - newsHeader = True + featuresHeader = False + if defaultTimeline == 'tlfeatures' and boxName == 'tlfeatures': + featuresHeader = True - if not newsHeader: + if not featuresHeader: tlStr += \ '' - tlStr += \ - '' + repliesIndexFilename = \ + baseDir + '/accounts/' + \ + nickname + '@' + domain + '/tlreplies.index' + if os.path.isfile(repliesIndexFilename): + tlStr += \ + '' # typically the media button if defaultTimeline != 'tlmedia': - if not minimal and not newsHeader: + if not minimal and not featuresHeader: tlStr += \ '' - isFeaturesTimeline = \ - defaultTimeline == 'tlnews' and boxName == 'tlnews' - - if not isFeaturesTimeline: + if not featuresHeader: # typically the blogs button # but may change if this is a blogging oriented instance if defaultTimeline != 'tlblogs': - if not minimal and not isFeaturesTimeline: + if not minimal: titleStr = translate['Blogs'] - if defaultTimeline == 'tlnews': + if defaultTimeline == 'tlfeatures': titleStr = translate['Article'] tlStr += \ '' - else: - if not newsHeader: + if defaultTimeline == 'tlfeatures': + if not featuresHeader: tlStr += \ '' - if not newsHeader: + if not featuresHeader: # button for the outbox tlStr += \ '' + \ '' - if newsHeader: + if featuresHeader: tlStr += \ '' + \ '' - if not newsHeader: + if not featuresHeader: tlStr += followApprovals if not iconsAsButtons: diff --git a/webapp_media.py b/webapp_media.py index e551624d3..bb32d8fd7 100644 --- a/webapp_media.py +++ b/webapp_media.py @@ -98,12 +98,12 @@ def addEmbeddedVideoFromSites(translate: {}, content: str, 'vault.mle.party', 'hostyour.tv', 'diode.zone', 'visionon.tv', 'artitube.artifaille.fr', 'peertube.fr', - 'peertube.live', + 'peertube.live', 'kolektiva.media', 'tube.ac-lyon.fr', 'www.yiny.org', 'betamax.video', 'tube.piweb.be', 'pe.ertu.be', 'peertube.social', 'videos.lescommuns.org', 'peertube.nogafa.org', 'skeptikon.fr', 'video.tedomum.net', - 'tube.p2p.legal', + 'tube.p2p.legal', 'tilvids.com', 'sikke.fi', 'exode.me', 'peertube.video') for site in peerTubeSites: if '"https://' + site in content: diff --git a/webapp_person_options.py b/webapp_person_options.py index 564974146..d6c7c506f 100644 --- a/webapp_person_options.py +++ b/webapp_person_options.py @@ -34,6 +34,7 @@ def htmlPersonOptions(defaultTimeline: str, ssbAddress: str, blogAddress: str, toxAddress: str, + jamiAddress: str, PGPpubKey: str, PGPfingerprint: str, emailAddress) -> str: @@ -131,6 +132,9 @@ def htmlPersonOptions(defaultTimeline: str, if toxAddress: optionsStr += \ '

    Tox: ' + toxAddress + '

    \n' + if jamiAddress: + optionsStr += \ + '

    Jami: ' + jamiAddress + '

    \n' if PGPfingerprint: optionsStr += '

    PGP: ' + \ PGPfingerprint.replace('\n', '
    ') + '

    \n' diff --git a/webapp_post.py b/webapp_post.py index 24d83b3af..366a56b64 100644 --- a/webapp_post.py +++ b/webapp_post.py @@ -17,12 +17,12 @@ from bookmarks import bookmarkedByPerson from like import likedByPerson from like import noOfLikes from follow import isFollowingActor -from posts import isEditor from posts import postIsMuted from posts import getPersonBox from posts import isDM from posts import downloadAnnounce from posts import populateRepliesJson +from utils import isEditor from utils import locatePost from utils import loadJson from utils import getCachedPostDirectory @@ -61,6 +61,16 @@ from webapp_question import insertQuestion from devices import E2EEdecryptMessageFromDevice +def logPostTiming(enableTimingLog: bool, postStartTime, debugId: str) -> None: + """Create a log of timings for performance tuning + """ + if not enableTimingLog: + return + timeDiff = int((time.time() - postStartTime) * 1000) + if timeDiff > 100: + print('TIMING INDIV ' + debugId + ' = ' + str(timeDiff)) + + def preparePostFromHtmlCache(postHtml: str, boxName: str, pageNumber: int) -> str: """Sets the page number on a cached html post @@ -106,6 +116,986 @@ def saveIndividualPostAsHtmlToCache(baseDir: str, return False +def getPostFromRecentCache(session, + baseDir: str, + httpPrefix: str, + nickname: str, domain: str, + postJsonObject: {}, + postActor: str, + personCache: {}, + allowDownloads: bool, + showPublicOnly: bool, + storeToCache: bool, + boxName: str, + avatarUrl: str, + enableTimingLog: bool, + postStartTime, + pageNumber: int, + recentPostsCache: {}, + maxRecentPosts: int) -> str: + """Attempts to get the html post from the recent posts cache in memory + """ + if boxName == 'tlmedia': + return None + + if showPublicOnly: + return None + + tryCache = False + bmTimeline = boxName == 'bookmarks' or boxName == 'tlbookmarks' + if storeToCache or bmTimeline: + tryCache = True + + if not tryCache: + return None + + # update avatar if needed + if not avatarUrl: + avatarUrl = \ + getPersonAvatarUrl(baseDir, postActor, personCache, + allowDownloads) + + logPostTiming(enableTimingLog, postStartTime, '2.1') + + updateAvatarImageCache(session, baseDir, httpPrefix, + postActor, avatarUrl, personCache, + allowDownloads) + + logPostTiming(enableTimingLog, postStartTime, '2.2') + + postHtml = \ + loadIndividualPostAsHtmlFromCache(baseDir, nickname, domain, + postJsonObject) + if not postHtml: + return None + + postHtml = preparePostFromHtmlCache(postHtml, boxName, pageNumber) + updateRecentPostsCache(recentPostsCache, maxRecentPosts, + postJsonObject, postHtml) + logPostTiming(enableTimingLog, postStartTime, '3') + return postHtml + + +def getAvatarImageUrl(session, + baseDir: str, httpPrefix: str, + postActor: str, personCache: {}, + avatarUrl: str, allowDownloads: bool) -> str: + """Returns the avatar image url + """ + # get the avatar image url for the post actor + if not avatarUrl: + avatarUrl = \ + getPersonAvatarUrl(baseDir, postActor, personCache, + allowDownloads) + avatarUrl = \ + updateAvatarImageCache(session, baseDir, httpPrefix, + postActor, avatarUrl, personCache, + allowDownloads) + else: + updateAvatarImageCache(session, baseDir, httpPrefix, + postActor, avatarUrl, personCache, + allowDownloads) + + if not avatarUrl: + avatarUrl = postActor + '/avatar.png' + + return avatarUrl + + +def getAvatarImageHtml(showAvatarOptions: bool, + nickname: str, domainFull: str, + avatarUrl: str, postActor: str, + translate: {}, avatarPosition: str, + pageNumber: int, messageIdStr: str) -> str: + """Get html for the avatar image + """ + avatarLink = '' + if '/users/news/' not in avatarUrl: + avatarLink = ' ' + avatarLink += \ + '  \n' + + if showAvatarOptions and \ + domainFull + '/users/' + nickname not in postActor: + if '/users/news/' not in avatarUrl: + avatarLink = \ + ' \n' + avatarLink += \ + ' \n' + else: + # don't link to the person options for the news account + avatarLink += \ + ' \n' + return avatarLink.strip() + + +def getReplyIconHtml(nickname: str, isPublicRepeat: bool, + showIcons: bool, commentsEnabled: bool, + postJsonObject: {}, pageNumberParam: str, + iconsPath: str, translate: {}) -> str: + """Returns html for the reply icon/button + """ + replyStr = '' + if not (showIcons and commentsEnabled): + return replyStr + + # reply is permitted - create reply icon + replyToLink = postJsonObject['object']['id'] + if postJsonObject['object'].get('attributedTo'): + if isinstance(postJsonObject['object']['attributedTo'], str): + replyToLink += \ + '?mention=' + postJsonObject['object']['attributedTo'] + if postJsonObject['object'].get('content'): + mentionedActors = \ + getMentionsFromHtml(postJsonObject['object']['content']) + if mentionedActors: + for actorUrl in mentionedActors: + if '?mention=' + actorUrl not in replyToLink: + replyToLink += '?mention=' + actorUrl + if len(replyToLink) > 500: + break + replyToLink += pageNumberParam + + replyStr = '' + replyToThisPostStr = translate['Reply to this post'] + if isPublicRepeat: + replyStr += \ + ' \n' + else: + if isDM(postJsonObject): + replyStr += \ + ' ' + \ + '\n' + else: + replyStr += \ + ' ' + \ + '\n' + + replyStr += \ + ' ' + \ + '' + replyToThisPostStr + \
+        ' |\n' + return replyStr + + +def getEditIconHtml(baseDir: str, nickname: str, domainFull: str, + postJsonObject: {}, actorNickname: str, + translate: {}, iconsPath: str, isEvent: bool) -> str: + """Returns html for the edit icon/button + """ + editStr = '' + actor = postJsonObject['actor'] + if (actor.endswith(domainFull + '/users/' + nickname) or + (isEditor(baseDir, nickname) and + actor.endswith(domainFull + '/users/news'))): + + postId = postJsonObject['object']['id'] + + if '/statuses/' not in postId: + return editStr + + if isBlogPost(postJsonObject): + editBlogPostStr = translate['Edit blog post'] + if not isNewsPost(postJsonObject): + editStr += \ + ' ' + \ + '' + \ + '' + editBlogPostStr + \
+                    ' |\n' + else: + editStr += \ + ' ' + \ + '' + \ + '' + editBlogPostStr + \
+                    ' |\n' + elif isEvent: + editEventStr = translate['Edit event'] + editStr += \ + ' ' + \ + '' + \ + '' + editEventStr + \
+                ' |\n' + return editStr + + +def getAnnounceIconHtml(nickname: str, domainFull: str, + postJsonObject: {}, + isPublicRepeat: bool, + isModerationPost: bool, + showRepeatIcon: bool, + translate: {}, + pageNumberParam: str, + timelinePostBookmark: str, + boxName: str, iconsPath: str) -> str: + """Returns html for announce icon/button + """ + announceStr = '' + if not isModerationPost and showRepeatIcon: + # don't allow announce/repeat of your own posts + announceIcon = 'repeat_inactive.png' + announceLink = 'repeat' + if not isPublicRepeat: + announceLink = 'repeatprivate' + announceTitle = translate['Repeat this post'] + + if announcedByPerson(postJsonObject, nickname, domainFull): + announceIcon = 'repeat.png' + if not isPublicRepeat: + announceLink = 'unrepeatprivate' + announceTitle = translate['Undo the repeat'] + + announceStr = \ + ' \n' + + announceStr += \ + ' ' + \ + '' + translate['Repeat this post'] + \
+            ' |\n' + return announceStr + + +def getLikeIconHtml(nickname: str, domainFull: str, + isModerationPost: bool, + showLikeButton: bool, + postJsonObject: {}, + enableTimingLog: bool, + postStartTime, + translate: {}, pageNumberParam: str, + timelinePostBookmark: str, + boxName: str, iconsPath: str) -> str: + """Returns html for like icon/button + """ + likeStr = '' + if not isModerationPost and showLikeButton: + likeIcon = 'like_inactive.png' + likeLink = 'like' + likeTitle = translate['Like this post'] + likeCount = noOfLikes(postJsonObject) + + logPostTiming(enableTimingLog, postStartTime, '12.1') + + likeCountStr = '' + if likeCount > 0: + if likeCount <= 10: + likeCountStr = ' (' + str(likeCount) + ')' + else: + likeCountStr = ' (10+)' + if likedByPerson(postJsonObject, nickname, domainFull): + if likeCount == 1: + # liked by the reader only + likeCountStr = '' + likeIcon = 'like.png' + likeLink = 'unlike' + likeTitle = translate['Undo the like'] + + logPostTiming(enableTimingLog, postStartTime, '12.2') + + likeStr = '' + if likeCountStr: + # show the number of likes next to icon + likeStr += '\n' + likeStr += \ + ' \n' + likeStr += \ + ' ' + \ + '' + likeTitle + \
+            ' |\n' + return likeStr + + +def getBookmarkIconHtml(nickname: str, domainFull: str, + postJsonObject: {}, + isModerationPost: bool, + translate: {}, + enableTimingLog: bool, + postStartTime, boxName: str, + pageNumberParam: str, + timelinePostBookmark: str, + iconsPath: str) -> str: + """Returns html for bookmark icon/button + """ + bookmarkStr = '' + + if isModerationPost: + return bookmarkStr + + bookmarkIcon = 'bookmark_inactive.png' + bookmarkLink = 'bookmark' + bookmarkTitle = translate['Bookmark this post'] + if bookmarkedByPerson(postJsonObject, nickname, domainFull): + bookmarkIcon = 'bookmark.png' + bookmarkLink = 'unbookmark' + bookmarkTitle = translate['Undo the bookmark'] + logPostTiming(enableTimingLog, postStartTime, '12.6') + bookmarkStr = \ + ' \n' + bookmarkStr += \ + ' ' + \ + '' + \
+        bookmarkTitle + ' |\n' + return bookmarkStr + + +def getMuteIconHtml(isMuted: bool, + postActor: str, + messageId: str, + nickname: str, domainFull: str, + allowDeletion: bool, + pageNumberParam: str, + iconsPath: str, + boxName: str, + timelinePostBookmark: str, + translate: {}) -> str: + """Returns html for mute icon/button + """ + muteStr = '' + if (allowDeletion or + ('/' + domainFull + '/' in postActor and + messageId.startswith(postActor))): + return muteStr + + if not isMuted: + muteStr = \ + ' \n' + muteStr += \ + ' ' + \ + '' + \
+            translate['Mute this post'] + \
+            ' |\n' + else: + muteStr = \ + ' \n' + muteStr += \ + ' ' + \ + '' + translate['Undo mute'] + \
+            ' |\n' + return muteStr + + +def getDeleteIconHtml(nickname: str, domainFull: str, + allowDeletion: bool, + postActor: str, + messageId: str, + postJsonObject: {}, + pageNumberParam: str, + iconsPath: str, + translate: {}) -> str: + """Returns html for delete icon/button + """ + deleteStr = '' + if (allowDeletion or + ('/' + domainFull + '/' in postActor and + messageId.startswith(postActor))): + if '/users/' + nickname + '/' in messageId: + if not isNewsPost(postJsonObject): + deleteStr = \ + ' \n' + deleteStr += \ + ' ' + \ + '' + \
+                    translate['Delete this post'] + \
+                    ' |\n' + return deleteStr + + +def getPublishedDateStr(postJsonObject: {}, + showPublishedDateOnly: bool) -> str: + """Return the html for the published date on a post + """ + publishedStr = '' + + if not postJsonObject['object'].get('published'): + return publishedStr + + publishedStr = postJsonObject['object']['published'] + if '.' not in publishedStr: + if '+' not in publishedStr: + datetimeObject = \ + datetime.strptime(publishedStr, "%Y-%m-%dT%H:%M:%SZ") + else: + datetimeObject = \ + datetime.strptime(publishedStr.split('+')[0] + 'Z', + "%Y-%m-%dT%H:%M:%SZ") + else: + publishedStr = \ + publishedStr.replace('T', ' ').split('.')[0] + datetimeObject = parse(publishedStr) + if not showPublishedDateOnly: + publishedStr = datetimeObject.strftime("%a %b %d, %H:%M") + else: + publishedStr = datetimeObject.strftime("%a %b %d") + + # if the post has replies then append a symbol to indicate this + if postJsonObject.get('hasReplies'): + if postJsonObject['hasReplies'] is True: + publishedStr = '[' + publishedStr + ']' + return publishedStr + + +def getBlogCitationsHtml(boxName: str, + postJsonObject: {}, + translate: {}) -> str: + """Returns blog citations as html + """ + # show blog citations + citationsStr = '' + if not (boxName == 'tlblogs' or boxName == 'tlfeatures'): + return citationsStr + + if not postJsonObject['object'].get('tag'): + return citationsStr + + for tagJson in postJsonObject['object']['tag']: + if not isinstance(tagJson, dict): + continue + if not tagJson.get('type'): + continue + if tagJson['type'] != 'Article': + continue + if not tagJson.get('name'): + continue + if not tagJson.get('url'): + continue + citationsStr += \ + '
  • ' + \ + '' + tagJson['name'] + '
  • \n' + + if citationsStr: + citationsStr = '

    ' + translate['Citations'] + ':

    ' + \ + '
      \n' + citationsStr + '
    \n' + return citationsStr + + +def boostOwnTootHtml(translate: {}, iconsPath) -> str: + """The html title for announcing your own post + """ + return ' ' + translate['announces'] + \
+        '\n' + + +def announceUnattributedHtml(translate: {}, iconsPath: str, + postJsonObject: {}) -> str: + """Returns the html for an announce title where there + is no attribution on the announced post + """ + return ' ' + \
+        translate['announces'] + '\n' + \ + ' @unattributed\n' + + +def announceWithoutDisplayNameHtml(translate: {}, iconsPath: str, + announceNickname: str, + announceDomain: str, + postJsonObject: {}) -> str: + """Returns html for an announce title where there is no display name + only a handle nick@domain + """ + return ' ' + translate['announces'] + \
+        '\n' + \ + ' @' + \ + announceNickname + '@' + announceDomain + '\n' + + +def announceWithDisplayNameHtml(translate: {}, + iconsPath: str, + postJsonObject: {}, + announceDisplayName: str) -> str: + """Returns html for an announce having a display name + """ + return ' ' + \
+        translate['announces'] + '\n' + \ + ' ' + announceDisplayName + '\n' + + +def getPostTitleAnnounceHtml(baseDir: str, + httpPrefix: str, + nickname: str, domain: str, + showRepeatIcon: bool, + isAnnounced: bool, + postJsonObject: {}, + postActor: str, + translate: {}, + iconsPath: str, + enableTimingLog: bool, + postStartTime, + boxName: str, + personCache: {}, + allowDownloads: bool, + avatarPosition: str, + pageNumber: int, + messageIdStr: str, + containerClassIcons: str, + containerClass: str) -> (str, str, str, str): + """Returns the announce title of a post containing names of participants + x announces y + """ + titleStr = '' + replyAvatarImageInPost = '' + + if postJsonObject['object'].get('attributedTo'): + attributedTo = '' + if isinstance(postJsonObject['object']['attributedTo'], str): + attributedTo = postJsonObject['object']['attributedTo'] + + if attributedTo.startswith(postActor): + titleStr += boostOwnTootHtml(translate, iconsPath) + else: + # boosting another person's post + logPostTiming(enableTimingLog, postStartTime, '13.2') + announceNickname = None + if attributedTo: + announceNickname = getNicknameFromActor(attributedTo) + if announceNickname: + announceDomain, announcePort = \ + getDomainFromActor(attributedTo) + getPersonFromCache(baseDir, attributedTo, + personCache, allowDownloads) + announceDisplayName = \ + getDisplayName(baseDir, attributedTo, personCache) + if announceDisplayName: + logPostTiming(enableTimingLog, postStartTime, '13.3') + + # add any emoji to the display name + if ':' in announceDisplayName: + announceDisplayName = \ + addEmojiToDisplayName(baseDir, httpPrefix, + nickname, domain, + announceDisplayName, + False) + logPostTiming(enableTimingLog, postStartTime, '13.3.1') + titleStr += \ + announceWithDisplayNameHtml(translate, + iconsPath, + postJsonObject, + announceDisplayName) + # show avatar of person replied to + announceActor = \ + postJsonObject['object']['attributedTo'] + announceAvatarUrl = \ + getPersonAvatarUrl(baseDir, announceActor, + personCache, allowDownloads) + + logPostTiming(enableTimingLog, postStartTime, '13.4') + + if announceAvatarUrl: + idx = 'Show options for this person' + if '/users/news/' not in announceAvatarUrl: + replyAvatarImageInPost = \ + ' ' \ + '
    \n' \ + ' ' + \ + '' \ + ' \n
    \n' + else: + titleStr += \ + announceWithoutDisplayNameHtml(translate, iconsPath, + announceNickname, + announceDomain, + postJsonObject) + else: + titleStr += \ + announceUnattributedHtml(translate, iconsPath, + postJsonObject) + else: + titleStr += \ + announceUnattributedHtml(translate, iconsPath, postJsonObject) + + return (titleStr, replyAvatarImageInPost, + containerClassIcons, containerClass) + + +def replyToYourselfHtml(translate: {}, iconsPath: str) -> str: + """Returns html for a title which is a reply to yourself + """ + return ' ' + translate['replying to themselves'] + \
+        '\n' + + +def replyToUnknownHtml(translate: {}, iconsPath: str, + postJsonObject: {}) -> str: + """Returns the html title for a reply to an unknown handle + """ + return ' ' + \
+        translate['replying to'] + '\n' + \ + ' @unknown\n' + + +def replyWithUnknownPathHtml(translate: {}, iconsPath: str, + postJsonObject: {}, + postDomain: str) -> str: + """Returns html title for a reply with an unknown path + eg. does not contain /statuses/ + """ + return ' ' + translate['replying to'] + \
+        '\n' + \ + ' ' + \ + postDomain + '\n' + + +def getReplyHtml(translate: {}, iconsPath: str, + inReplyTo: str, replyDisplayName: str) -> str: + """Returns html title for a reply + """ + return ' ' + \ + '' + \
+        translate['replying to'] + '\n' + \ + ' ' + \ + replyDisplayName + '\n' + + +def getReplyWithoutDisplayName(translate: {}, iconsPath: str, + inReplyTo: str, + replyNickname: str, replyDomain: str) -> str: + """Returns html for a reply without a display name, + only a handle nick@domain + """ + return ' ' + \ + '' + translate['replying to'] + \
+        '\n' + ' @' + \ + replyNickname + '@' + replyDomain + '\n' + + +def getPostTitleReplyHtml(baseDir: str, + httpPrefix: str, + nickname: str, domain: str, + showRepeatIcon: bool, + isAnnounced: bool, + postJsonObject: {}, + postActor: str, + translate: {}, + iconsPath: str, + enableTimingLog: bool, + postStartTime, + boxName: str, + personCache: {}, + allowDownloads: bool, + avatarPosition: str, + pageNumber: int, + messageIdStr: str, + containerClassIcons: str, + containerClass: str) -> (str, str, str, str): + """Returns the reply title of a post containing names of participants + x replies to y + """ + titleStr = '' + replyAvatarImageInPost = '' + + if not postJsonObject['object'].get('inReplyTo'): + return (titleStr, replyAvatarImageInPost, + containerClassIcons, containerClass) + + containerClassIcons = 'containericons darker' + containerClass = 'container darker' + if postJsonObject['object']['inReplyTo'].startswith(postActor): + titleStr += replyToYourselfHtml(translate, iconsPath) + return (titleStr, replyAvatarImageInPost, + containerClassIcons, containerClass) + + if '/statuses/' in postJsonObject['object']['inReplyTo']: + inReplyTo = postJsonObject['object']['inReplyTo'] + replyActor = inReplyTo.split('/statuses/')[0] + replyNickname = getNicknameFromActor(replyActor) + if replyNickname: + replyDomain, replyPort = \ + getDomainFromActor(replyActor) + if replyNickname and replyDomain: + getPersonFromCache(baseDir, replyActor, + personCache, + allowDownloads) + replyDisplayName = \ + getDisplayName(baseDir, replyActor, + personCache) + if replyDisplayName: + # add emoji to the display name + if ':' in replyDisplayName: + logPostTiming(enableTimingLog, postStartTime, '13.5') + + replyDisplayName = \ + addEmojiToDisplayName(baseDir, + httpPrefix, + nickname, + domain, + replyDisplayName, + False) + logPostTiming(enableTimingLog, postStartTime, '13.6') + + titleStr += \ + getReplyHtml(translate, iconsPath, + inReplyTo, replyDisplayName) + + logPostTiming(enableTimingLog, postStartTime, '13.7') + + # show avatar of person replied to + replyAvatarUrl = \ + getPersonAvatarUrl(baseDir, + replyActor, + personCache, + allowDownloads) + + logPostTiming(enableTimingLog, postStartTime, '13.8') + + if replyAvatarUrl: + replyAvatarImageInPost = \ + '
    \n' + replyAvatarImageInPost += \ + ' ' + \ + '\n' + replyAvatarImageInPost += \ + ' ' + \ + ' \n' + \ + '
    \n' + else: + inReplyTo = \ + postJsonObject['object']['inReplyTo'] + titleStr += \ + getReplyWithoutDisplayName(translate, iconsPath, + inReplyTo, + replyNickname, replyDomain) + else: + titleStr += \ + replyToUnknownHtml(translate, iconsPath, postJsonObject) + else: + postDomain = \ + postJsonObject['object']['inReplyTo'] + prefixes = getProtocolPrefixes() + for prefix in prefixes: + postDomain = postDomain.replace(prefix, '') + if '/' in postDomain: + postDomain = postDomain.split('/', 1)[0] + if postDomain: + titleStr += \ + replyWithUnknownPathHtml(translate, iconsPath, + postJsonObject, postDomain) + + return (titleStr, replyAvatarImageInPost, + containerClassIcons, containerClass) + + +def getPostTitleHtml(baseDir: str, + httpPrefix: str, + nickname: str, domain: str, + showRepeatIcon: bool, + isAnnounced: bool, + postJsonObject: {}, + postActor: str, + translate: {}, + iconsPath: str, + enableTimingLog: bool, + postStartTime, + boxName: str, + personCache: {}, + allowDownloads: bool, + avatarPosition: str, + pageNumber: int, + messageIdStr: str, + containerClassIcons: str, + containerClass: str) -> (str, str, str, str): + """Returns the title of a post containing names of participants + x replies to y, x announces y, etc + """ + titleStr = '' + replyAvatarImageInPost = '' + if not showRepeatIcon: + return (titleStr, replyAvatarImageInPost, + containerClassIcons, containerClass) + + if isAnnounced: + return getPostTitleAnnounceHtml(baseDir, + httpPrefix, + nickname, domain, + showRepeatIcon, + isAnnounced, + postJsonObject, + postActor, + translate, + iconsPath, + enableTimingLog, + postStartTime, + boxName, + personCache, + allowDownloads, + avatarPosition, + pageNumber, + messageIdStr, + containerClassIcons, + containerClass) + + return getPostTitleReplyHtml(baseDir, + httpPrefix, + nickname, domain, + showRepeatIcon, + isAnnounced, + postJsonObject, + postActor, + translate, + iconsPath, + enableTimingLog, + postStartTime, + boxName, + personCache, + allowDownloads, + avatarPosition, + pageNumber, + messageIdStr, + containerClassIcons, + containerClass) + + +def getFooterWithIcons(showIcons: bool, + containerClassIcons: str, + replyStr: str, announceStr: str, + likeStr: str, bookmarkStr: str, + deleteStr: str, muteStr: str, editStr: str, + postJsonObject: {}, publishedLink: str, + timeClass: str, publishedStr: str) -> str: + """Returns the html for a post footer containing icons + """ + if not showIcons: + return None + + footerStr = '\n
    \n' + footerStr += replyStr + announceStr + likeStr + bookmarkStr + footerStr += deleteStr + muteStr + editStr + if not isNewsPost(postJsonObject): + footerStr += ' ' + publishedStr + '\n' + else: + footerStr += ' ' + publishedStr + '\n' + footerStr += '
    \n' + return footerStr + + def individualPostAsHtml(allowDownloads: bool, recentPostsCache: {}, maxRecentPosts: int, iconsPath: str, translate: {}, @@ -137,161 +1127,94 @@ def individualPostAsHtml(allowDownloads: bool, if isPersonSnoozed(baseDir, nickname, domain, postActor): return '' - # benchmark 1 - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + ' 1 = ' + str(timeDiff)) + # if downloads of avatar images aren't enabled then we can do more + # accurate timing of different parts of the code + enableTimingLog = not allowDownloads + + logPostTiming(enableTimingLog, postStartTime, '1') avatarPosition = '' messageId = '' if postJsonObject.get('id'): messageId = removeIdEnding(postJsonObject['id']) - # benchmark 2 - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + ' 2 = ' + str(timeDiff)) + logPostTiming(enableTimingLog, postStartTime, '2') messageIdStr = '' if messageId: messageIdStr = ';' + messageId - fullDomain = domain + domainFull = domain if port: if port != 80 and port != 443: if ':' not in domain: - fullDomain = domain + ':' + str(port) + domainFull = domain + ':' + str(port) pageNumberParam = '' if pageNumber: pageNumberParam = '?page=' + str(pageNumber) - if (not showPublicOnly and - (storeToCache or boxName == 'bookmarks' or - boxName == 'tlbookmarks') and - boxName != 'tlmedia'): - # update avatar if needed - if not avatarUrl: - avatarUrl = \ - getPersonAvatarUrl(baseDir, postActor, personCache, - allowDownloads) + # get the html post from the recent posts cache if it exists there + postHtml = \ + getPostFromRecentCache(session, baseDir, + httpPrefix, nickname, domain, + postJsonObject, + postActor, + personCache, + allowDownloads, + showPublicOnly, + storeToCache, + boxName, + avatarUrl, + enableTimingLog, + postStartTime, + pageNumber, + recentPostsCache, + maxRecentPosts) + if postHtml: + return postHtml - # benchmark 2.1 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + - ' 2.1 = ' + str(timeDiff)) + logPostTiming(enableTimingLog, postStartTime, '4') - updateAvatarImageCache(session, baseDir, httpPrefix, - postActor, avatarUrl, personCache, - allowDownloads) + avatarUrl = \ + getAvatarImageUrl(session, + baseDir, httpPrefix, + postActor, personCache, + avatarUrl, allowDownloads) - # benchmark 2.2 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + - ' 2.2 = ' + str(timeDiff)) + logPostTiming(enableTimingLog, postStartTime, '5') - postHtml = \ - loadIndividualPostAsHtmlFromCache(baseDir, nickname, domain, - postJsonObject) - if postHtml: - postHtml = preparePostFromHtmlCache(postHtml, boxName, pageNumber) - updateRecentPostsCache(recentPostsCache, maxRecentPosts, - postJsonObject, postHtml) - # benchmark 3 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + - ' 3 = ' + str(timeDiff)) - return postHtml - - # benchmark 4 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + ' 4 = ' + str(timeDiff)) - - if not avatarUrl: - avatarUrl = \ - getPersonAvatarUrl(baseDir, postActor, personCache, - allowDownloads) - avatarUrl = \ - updateAvatarImageCache(session, baseDir, httpPrefix, - postActor, avatarUrl, personCache, - allowDownloads) - else: - updateAvatarImageCache(session, baseDir, httpPrefix, - postActor, avatarUrl, personCache, - allowDownloads) - - # benchmark 5 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + ' 5 = ' + str(timeDiff)) - - if not avatarUrl: - avatarUrl = postActor + '/avatar.png' - - if fullDomain not in postActor: + # get the display name + if domainFull not in postActor: (inboxUrl, pubKeyId, pubKey, fromPersonId, sharedInbox, avatarUrl2, displayName) = getPersonBox(baseDir, session, wfRequest, personCache, projectVersion, httpPrefix, nickname, domain, 'outbox') - # benchmark 6 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + ' 6 = ' + str(timeDiff)) + logPostTiming(enableTimingLog, postStartTime, '6') if avatarUrl2: avatarUrl = avatarUrl2 if displayName: + # add any emoji to the display name if ':' in displayName: displayName = \ addEmojiToDisplayName(baseDir, httpPrefix, nickname, domain, displayName, False) - # benchmark 7 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + ' 7 = ' + str(timeDiff)) + logPostTiming(enableTimingLog, postStartTime, '7') - avatarLink = '' - if '/users/news/' not in avatarUrl: - avatarLink = ' ' - avatarLink += \ - '  \n' + avatarLink = \ + getAvatarImageHtml(showAvatarOptions, + nickname, domainFull, + avatarUrl, postActor, + translate, avatarPosition, + pageNumber, messageIdStr) - if showAvatarOptions and \ - fullDomain + '/users/' + nickname not in postActor: - if '/users/news/' not in avatarUrl: - avatarLink = \ - ' \n' - avatarLink += \ - ' \n' - else: - # don't link to the person options for the news account - avatarLink += \ - ' \n' avatarImageInPost = \ - '
    ' + avatarLink.strip() + '
    \n' + '
    ' + avatarLink + '
    \n' # don't create new html within the bookmarks timeline # it should already have been created for the inbox @@ -328,11 +1251,7 @@ def individualPostAsHtml(allowDownloads: bool, postJsonObject = postJsonAnnounce isAnnounced = True - # benchmark 8 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + ' 8 = ' + str(timeDiff)) + logPostTiming(enableTimingLog, postStartTime, '8') if not isinstance(postJsonObject['object'], dict): return '' @@ -383,10 +1302,7 @@ def individualPostAsHtml(allowDownloads: bool, '">@' + actorNickname + '@' + actorDomain + '\n' # benchmark 9 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + ' 9 = ' + str(timeDiff)) + logPostTiming(enableTimingLog, postStartTime, '9') # Show a DM icon for DMs in the inbox timeline if showDMicon: @@ -394,151 +1310,39 @@ def individualPostAsHtml(allowDownloads: bool, titleStr + ' \n' - replyStr = '' # check if replying is permitted commentsEnabled = True if 'commentsEnabled' in postJsonObject['object']: if postJsonObject['object']['commentsEnabled'] is False: commentsEnabled = False - if showIcons and commentsEnabled: - # reply is permitted - create reply icon - replyToLink = postJsonObject['object']['id'] - if postJsonObject['object'].get('attributedTo'): - if isinstance(postJsonObject['object']['attributedTo'], str): - replyToLink += \ - '?mention=' + postJsonObject['object']['attributedTo'] - if postJsonObject['object'].get('content'): - mentionedActors = \ - getMentionsFromHtml(postJsonObject['object']['content']) - if mentionedActors: - for actorUrl in mentionedActors: - if '?mention=' + actorUrl not in replyToLink: - replyToLink += '?mention=' + actorUrl - if len(replyToLink) > 500: - break - replyToLink += pageNumberParam - replyStr = '' - if isPublicRepeat: - replyStr += \ - ' \n' - else: - if isDM(postJsonObject): - replyStr += \ - ' ' + \ - '\n' - else: - replyStr += \ - ' ' + \ - '\n' + replyStr = getReplyIconHtml(nickname, isPublicRepeat, + showIcons, commentsEnabled, + postJsonObject, pageNumberParam, + iconsPath, translate) - replyStr += \ - ' ' + \ - '' + \
-            translate['Reply to this post'] + \
-            ' |\n' - - # benchmark 10 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + ' 10 = ' + str(timeDiff)) + logPostTiming(enableTimingLog, postStartTime, '10') isEvent = isEventPost(postJsonObject) - # benchmark 11 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + ' 11 = ' + str(timeDiff)) + logPostTiming(enableTimingLog, postStartTime, '11') - editStr = '' - if (postJsonObject['actor'].endswith(fullDomain + '/users/' + nickname) or - (isEditor(baseDir, nickname) and - postJsonObject['actor'].endswith(fullDomain + '/users/news'))): - if '/statuses/' in postJsonObject['object']['id']: - if isBlogPost(postJsonObject): - blogPostId = postJsonObject['object']['id'] - if not isNewsPost(postJsonObject): - editStr += \ - ' ' + \ - '' + \ - '' + \
-                        translate['Edit blog post'] + \
-                        ' |\n' - else: - editStr += \ - ' ' + \ - '' + \ - '' + \
-                        translate['Edit blog post'] + \
-                        ' |\n' - elif isEvent: - eventPostId = postJsonObject['object']['id'] - editStr += \ - ' ' + \ - '' + \ - '' + \
-                    translate['Edit event'] + \
-                    ' |\n' + editStr = getEditIconHtml(baseDir, nickname, domainFull, + postJsonObject, actorNickname, + translate, iconsPath, isEvent) - announceStr = '' - if not isModerationPost and showRepeatIcon: - # don't allow announce/repeat of your own posts - announceIcon = 'repeat_inactive.png' - announceLink = 'repeat' - if not isPublicRepeat: - announceLink = 'repeatprivate' - announceTitle = translate['Repeat this post'] - if announcedByPerson(postJsonObject, nickname, fullDomain): - announceIcon = 'repeat.png' - if not isPublicRepeat: - announceLink = 'unrepeatprivate' - announceTitle = translate['Undo the repeat'] - announceStr = \ - ' \n' - announceStr += \ - ' ' + \ - '' + translate['Repeat this post'] + \
-            ' |\n' + announceStr = \ + getAnnounceIconHtml(nickname, domainFull, + postJsonObject, + isPublicRepeat, + isModerationPost, + showRepeatIcon, + translate, + pageNumberParam, + timelinePostBookmark, + boxName, iconsPath) - # benchmark 12 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + ' 12 = ' + str(timeDiff)) + logPostTiming(enableTimingLog, postStartTime, '12') # whether to show a like button hideLikeButtonFile = \ @@ -547,491 +1351,96 @@ def individualPostAsHtml(allowDownloads: bool, if os.path.isfile(hideLikeButtonFile): showLikeButton = False - likeStr = '' - if not isModerationPost and showLikeButton: - likeIcon = 'like_inactive.png' - likeLink = 'like' - likeTitle = translate['Like this post'] - likeCount = noOfLikes(postJsonObject) + likeStr = getLikeIconHtml(nickname, domainFull, + isModerationPost, + showLikeButton, + postJsonObject, + enableTimingLog, + postStartTime, + translate, pageNumberParam, + timelinePostBookmark, + boxName, iconsPath) - # benchmark 12.1 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + ' 12.1 = ' + str(timeDiff)) + logPostTiming(enableTimingLog, postStartTime, '12.5') - likeCountStr = '' - if likeCount > 0: - if likeCount <= 10: - likeCountStr = ' (' + str(likeCount) + ')' - else: - likeCountStr = ' (10+)' - if likedByPerson(postJsonObject, nickname, fullDomain): - if likeCount == 1: - # liked by the reader only - likeCountStr = '' - likeIcon = 'like.png' - likeLink = 'unlike' - likeTitle = translate['Undo the like'] + bookmarkStr = \ + getBookmarkIconHtml(nickname, domainFull, + postJsonObject, + isModerationPost, + translate, + enableTimingLog, + postStartTime, boxName, + pageNumberParam, + timelinePostBookmark, + iconsPath) - # benchmark 12.2 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + ' 12.2 = ' + str(timeDiff)) - - likeStr = '' - if likeCountStr: - # show the number of likes next to icon - likeStr += '\n' - likeStr += \ - ' \n' - likeStr += \ - ' ' + \ - '' + likeTitle + \
-            ' |\n' - - # benchmark 12.5 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + ' 12.5 = ' + str(timeDiff)) - - bookmarkStr = '' - if not isModerationPost: - bookmarkIcon = 'bookmark_inactive.png' - bookmarkLink = 'bookmark' - bookmarkTitle = translate['Bookmark this post'] - if bookmarkedByPerson(postJsonObject, nickname, fullDomain): - bookmarkIcon = 'bookmark.png' - bookmarkLink = 'unbookmark' - bookmarkTitle = translate['Undo the bookmark'] - # benchmark 12.6 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + ' 12.6 = ' + str(timeDiff)) - bookmarkStr = \ - ' \n' - bookmarkStr += \ - ' ' + \ - '' + \
-            bookmarkTitle + ' |\n' - - # benchmark 12.9 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + ' 12.9 = ' + str(timeDiff)) + logPostTiming(enableTimingLog, postStartTime, '12.9') isMuted = postIsMuted(baseDir, nickname, domain, postJsonObject, messageId) - # benchmark 13 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + ' 13 = ' + str(timeDiff)) + logPostTiming(enableTimingLog, postStartTime, '13') - deleteStr = '' - muteStr = '' - if (allowDeletion or - ('/' + fullDomain + '/' in postActor and - messageId.startswith(postActor))): - if '/users/' + nickname + '/' in messageId: - if not isNewsPost(postJsonObject): - deleteStr = \ - ' \n' - deleteStr += \ - ' ' + \ - '' + \
-                    translate['Delete this post'] + \
-                    ' |\n' - else: - if not isMuted: - muteStr = \ - ' \n' - muteStr += \ - ' ' + \ - '' + \
-                translate['Mute this post'] + \
-                ' |\n' - else: - muteStr = \ - ' \n' - muteStr += \ - ' ' + \ - '' + translate['Undo mute'] + \
-                ' |\n' + muteStr = \ + getMuteIconHtml(isMuted, + postActor, + messageId, + nickname, domainFull, + allowDeletion, + pageNumberParam, + iconsPath, + boxName, + timelinePostBookmark, + translate) - # benchmark 13.1 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + ' 13.1 = ' + str(timeDiff)) + deleteStr = \ + getDeleteIconHtml(nickname, domainFull, + allowDeletion, + postActor, + messageId, + postJsonObject, + pageNumberParam, + iconsPath, + translate) - replyAvatarImageInPost = '' - if showRepeatIcon: - if isAnnounced: - if postJsonObject['object'].get('attributedTo'): - attributedTo = '' - if isinstance(postJsonObject['object']['attributedTo'], str): - attributedTo = postJsonObject['object']['attributedTo'] - if attributedTo.startswith(postActor): - titleStr += \ - ' ' + translate['announces'] + \
-                        '\n' - else: - # benchmark 13.2 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + - ' 13.2 = ' + str(timeDiff)) - announceNickname = None - if attributedTo: - announceNickname = getNicknameFromActor(attributedTo) - if announceNickname: - announceDomain, announcePort = \ - getDomainFromActor(attributedTo) - getPersonFromCache(baseDir, attributedTo, - personCache, allowDownloads) - announceDisplayName = \ - getDisplayName(baseDir, attributedTo, personCache) - if announceDisplayName: - # benchmark 13.3 - if not allowDownloads: - timeDiff = \ - int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + - ' 13.3 = ' + str(timeDiff)) + logPostTiming(enableTimingLog, postStartTime, '13.1') - if ':' in announceDisplayName: - announceDisplayName = \ - addEmojiToDisplayName(baseDir, httpPrefix, - nickname, domain, - announceDisplayName, - False) - # benchmark 13.3.1 - if not allowDownloads: - timeDiff = \ - int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + - ' 13.3.1 = ' + str(timeDiff)) + # get the title: x replies to y, x announces y, etc + (titleStr2, + replyAvatarImageInPost, + containerClassIcons, + containerClass) = getPostTitleHtml(baseDir, + httpPrefix, + nickname, domain, + showRepeatIcon, + isAnnounced, + postJsonObject, + postActor, + translate, + iconsPath, + enableTimingLog, + postStartTime, + boxName, + personCache, + allowDownloads, + avatarPosition, + pageNumber, + messageIdStr, + containerClassIcons, + containerClass) + titleStr += titleStr2 - titleStr += \ - ' ' + \ - '' + \
-                                translate['announces'] + '\n' + \ - ' ' + \ - announceDisplayName + '\n' - # show avatar of person replied to - announceActor = \ - postJsonObject['object']['attributedTo'] - announceAvatarUrl = \ - getPersonAvatarUrl(baseDir, announceActor, - personCache, allowDownloads) - - # benchmark 13.4 - if not allowDownloads: - timeDiff = \ - int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + - ' 13.4 = ' + str(timeDiff)) - - if announceAvatarUrl: - idx = 'Show options for this person' - if '/users/news/' not in announceAvatarUrl: - replyAvatarImageInPost = \ - ' ' \ - '
    \n' \ - ' ' + \ - '' \ - ' \n
    \n' - else: - titleStr += \ - ' ' + translate['announces'] + \
-                                '\n' + \ - ' @' + \ - announceNickname + '@' + \ - announceDomain + '\n' - else: - titleStr += \ - ' ' + \
-                            translate['announces'] + '\n' + \ - ' @unattributed\n' - else: - titleStr += \ - ' ' + \ - '' + translate['announces'] + \
-                    '\n' + \ - ' @unattributed\n' - else: - if postJsonObject['object'].get('inReplyTo'): - containerClassIcons = 'containericons darker' - containerClass = 'container darker' - if postJsonObject['object']['inReplyTo'].startswith(postActor): - titleStr += \ - ' ' + translate['replying to themselves'] + \
-                        '\n' - else: - if '/statuses/' in postJsonObject['object']['inReplyTo']: - inReplyTo = postJsonObject['object']['inReplyTo'] - replyActor = inReplyTo.split('/statuses/')[0] - replyNickname = getNicknameFromActor(replyActor) - if replyNickname: - replyDomain, replyPort = \ - getDomainFromActor(replyActor) - if replyNickname and replyDomain: - getPersonFromCache(baseDir, replyActor, - personCache, - allowDownloads) - replyDisplayName = \ - getDisplayName(baseDir, replyActor, - personCache) - if replyDisplayName: - if ':' in replyDisplayName: - # benchmark 13.5 - if not allowDownloads: - timeDiff = \ - int((time.time() - - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + - boxName + ' 13.5 = ' + - str(timeDiff)) - repDisp = replyDisplayName - replyDisplayName = \ - addEmojiToDisplayName(baseDir, - httpPrefix, - nickname, - domain, - repDisp, - False) - # benchmark 13.6 - if not allowDownloads: - timeDiff = \ - int((time.time() - - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + - boxName + ' 13.6 = ' + - str(timeDiff)) - titleStr += \ - ' ' + \ - '' + \
-                                        translate['replying to'] + \
-                                        '\n' + \ - ' ' + \ - '' + \ - replyDisplayName + '\n' - - # benchmark 13.7 - if not allowDownloads: - timeDiff = int((time.time() - - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + - ' 13.7 = ' + str(timeDiff)) - - # show avatar of person replied to - replyAvatarUrl = \ - getPersonAvatarUrl(baseDir, - replyActor, - personCache, - allowDownloads) - - # benchmark 13.8 - if not allowDownloads: - timeDiff = int((time.time() - - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + - ' 13.8 = ' + str(timeDiff)) - - if replyAvatarUrl: - replyAvatarImageInPost = \ - '
    \n' - replyAvatarImageInPost += \ - ' ' + \ - '\n' - replyAvatarImageInPost += \ - ' ' + \ - ' \n' + \ - '
    \n' - else: - inReplyTo = \ - postJsonObject['object']['inReplyTo'] - titleStr += \ - ' ' + \ - '' + \
-                                        translate['replying to'] + \
-                                        '\n' + \ - ' @' + \ - replyNickname + '@' + \ - replyDomain + '\n' - else: - titleStr += \ - ' ' + \
-                                translate['replying to'] + \
-                                '\n' + \ - ' @unknown\n' - else: - postDomain = \ - postJsonObject['object']['inReplyTo'] - prefixes = getProtocolPrefixes() - for prefix in prefixes: - postDomain = postDomain.replace(prefix, '') - if '/' in postDomain: - postDomain = postDomain.split('/', 1)[0] - if postDomain: - titleStr += \ - ' ' + translate['replying to'] + \
-                                '\n' + \ - ' ' + \ - postDomain + '\n' - - # benchmark 14 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + ' 14 = ' + str(timeDiff)) + logPostTiming(enableTimingLog, postStartTime, '14') attachmentStr, galleryStr = \ getPostAttachmentsAsHtml(postJsonObject, boxName, translate, - isMuted, avatarLink.strip(), + isMuted, avatarLink, replyStr, announceStr, likeStr, bookmarkStr, deleteStr, muteStr) - publishedStr = '' - if postJsonObject['object'].get('published'): - publishedStr = postJsonObject['object']['published'] - if '.' not in publishedStr: - if '+' not in publishedStr: - datetimeObject = \ - datetime.strptime(publishedStr, "%Y-%m-%dT%H:%M:%SZ") - else: - datetimeObject = \ - datetime.strptime(publishedStr.split('+')[0] + 'Z', - "%Y-%m-%dT%H:%M:%SZ") - else: - publishedStr = \ - publishedStr.replace('T', ' ').split('.')[0] - datetimeObject = parse(publishedStr) - if not showPublishedDateOnly: - publishedStr = datetimeObject.strftime("%a %b %d, %H:%M") - else: - publishedStr = datetimeObject.strftime("%a %b %d") + publishedStr = \ + getPublishedDateStr(postJsonObject, showPublishedDateOnly) - # benchmark 15 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + ' 15 = ' + str(timeDiff)) + logPostTiming(enableTimingLog, postStartTime, '15') publishedLink = messageId # blog posts should have no /statuses/ in their link @@ -1058,19 +1467,15 @@ def individualPostAsHtml(allowDownloads: bool, containerClassIcons = 'containericons dm' containerClass = 'container dm' - if showIcons: - footerStr = '\n
    \n' - footerStr += replyStr + announceStr + likeStr + bookmarkStr + \ - deleteStr + muteStr + editStr - if not isNewsPost(postJsonObject): - footerStr += ' ' + publishedStr + '\n' - else: - footerStr += ' ' + publishedStr + '\n' - footerStr += '
    \n' + newFooterStr = getFooterWithIcons(showIcons, + containerClassIcons, + replyStr, announceStr, + likeStr, bookmarkStr, + deleteStr, muteStr, editStr, + postJsonObject, publishedLink, + timeClass, publishedStr) + if newFooterStr: + footerStr = newFooterStr postIsSensitive = False if postJsonObject['object'].get('sensitive'): @@ -1101,11 +1506,7 @@ def individualPostAsHtml(allowDownloads: bool, postJsonObject['object']['summary'], postJsonObject['object']['content']) - # benchmark 16 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + ' 16 = ' + str(timeDiff)) + logPostTiming(enableTimingLog, postStartTime, '16') if not isPatch: objectContent = \ @@ -1148,11 +1549,7 @@ def individualPostAsHtml(allowDownloads: bool, else: contentStr += cwContentStr - # benchmark 17 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + ' 17 = ' + str(timeDiff)) + logPostTiming(enableTimingLog, postStartTime, '17') if postJsonObject['object'].get('tag') and not isPatch: contentStr = \ @@ -1173,27 +1570,8 @@ def individualPostAsHtml(allowDownloads: bool, '\n' # show blog citations - citationsStr = '' - if boxName == 'tlblog': - if postJsonObject['object'].get('tag'): - for tagJson in postJsonObject['object']['tag']: - if not isinstance(tagJson, dict): - continue - if not tagJson.get('type'): - continue - if tagJson['type'] != 'Article': - continue - if not tagJson.get('name'): - continue - if not tagJson.get('url'): - continue - citationsStr += \ - '
  • ' + \ - '' + tagJson['name'] + '
  • \n' - if citationsStr: - citationsStr = '

    ' + translate['Citations'] + \ - ':

    ' + \ - '
      \n' + citationsStr + '
    \n' + citationsStr = \ + getBlogCitationsHtml(boxName, postJsonObject, translate) postHtml = '' if boxName != 'tlmedia': @@ -1208,12 +1586,9 @@ def individualPostAsHtml(allowDownloads: bool, else: postHtml = galleryStr - # benchmark 18 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + ' 18 = ' + str(timeDiff)) + logPostTiming(enableTimingLog, postStartTime, '18') + # save the created html to the recent posts cache if not showPublicOnly and storeToCache and \ boxName != 'tlmedia' and boxName != 'tlbookmarks' and \ boxName != 'bookmarks': @@ -1222,11 +1597,7 @@ def individualPostAsHtml(allowDownloads: bool, updateRecentPostsCache(recentPostsCache, maxRecentPosts, postJsonObject, postHtml) - # benchmark 19 - if not allowDownloads: - timeDiff = int((time.time() - postStartTime) * 1000) - if timeDiff > 100: - print('TIMING INDIV ' + boxName + ' 19 = ' + str(timeDiff)) + logPostTiming(enableTimingLog, postStartTime, '19') return postHtml diff --git a/webapp_profile.py b/webapp_profile.py index 1e5d2acfd..a7f3df790 100644 --- a/webapp_profile.py +++ b/webapp_profile.py @@ -31,19 +31,18 @@ from pgp import getEmailAddress from pgp import getPGPfingerprint from pgp import getPGPpubKey from tox import getToxAddress +from jami import getJamiAddress +from webapp_frontscreen import htmlFrontScreen from webapp_utils import scheduledPostsExist from webapp_utils import getPersonAvatarUrl from webapp_utils import getIconsWebPath from webapp_utils import htmlHeaderWithExternalStyle from webapp_utils import htmlFooter from webapp_utils import addEmojiToDisplayName -from webapp_utils import headerButtonsFrontScreen from webapp_utils import getBannerFile from webapp_utils import htmlPostSeparator from webapp_utils import getBlogAddress from webapp_post import individualPostAsHtml -from webapp_column_left import getLeftColumnContent -from webapp_column_right import getRightColumnContent from webapp_timeline import htmlIndividualShare @@ -374,6 +373,20 @@ def htmlProfile(rssIconAtTop: bool, nickname = profileJson['preferredUsername'] if not nickname: return "" + if isSystemAccount(nickname): + return htmlFrontScreen(rssIconAtTop, + cssCache, iconsAsButtons, + defaultTimeline, + recentPostsCache, maxRecentPosts, + translate, projectVersion, + baseDir, httpPrefix, authorized, + profileJson, selected, + session, wfRequest, personCache, + YTReplacementDomain, + showPublishedDateOnly, + newswire, extraJson, + pageNumber, maxItemsPerPage) + domain, port = getDomainFromActor(profileJson['id']) if not domain: return "" @@ -424,8 +437,9 @@ def htmlProfile(rssIconAtTop: bool, matrixAddress = getMatrixAddress(profileJson) ssbAddress = getSSBAddress(profileJson) toxAddress = getToxAddress(profileJson) + jamiAddress = getJamiAddress(profileJson) if donateUrl or xmppAddress or matrixAddress or \ - ssbAddress or toxAddress or PGPpubKey or \ + ssbAddress or toxAddress or jamiAddress or PGPpubKey or \ PGPfingerprint or emailAddress: donateSection = '
    \n' donateSection += '
    \n' @@ -453,6 +467,10 @@ def htmlProfile(rssIconAtTop: bool, donateSection += \ '

    Tox:

    \n' + if jamiAddress: + donateSection += \ + '

    Jami:

    \n' if PGPfingerprint: donateSection += \ '

    PGP: ' + \ @@ -464,11 +482,7 @@ def htmlProfile(rssIconAtTop: bool, donateSection += '

    \n' iconsPath = getIconsWebPath(baseDir) - if not authorized: - loginButton = headerButtonsFrontScreen(translate, nickname, - 'features', authorized, - iconsAsButtons, iconsPath) - else: + if authorized: editProfileStr = \ '' + \ '\n' - if loginButton: - profileHeaderStr += '
    ' + loginButton + '
    \n' - - profileHeaderStr += '\n' - profileHeaderStr += ' \n' - profileHeaderStr += ' \n' - profileHeaderStr += ' \n' - profileHeaderStr += ' \n' - profileHeaderStr += ' \n' - profileHeaderStr += ' \n' - profileHeaderStr += ' \n' - profileHeaderStr += ' \n' - profileHeaderStr += ' \n' tlStr += ' \n' - # benchmark 9 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 9 = ' + str(timeDiff)) + logTimelineTiming(enableTimingLog, timelineStartTime, boxName, '9') tlStr += ' \n' tlStr += '
    \n' - iconsPath = getIconsWebPath(baseDir) - profileHeaderStr += \ - getLeftColumnContent(baseDir, 'news', domainFull, - httpPrefix, translate, - iconsPath, False, - False, None, rssIconAtTop, True, - True) - profileHeaderStr += ' \n' - else: - avatarUrl = profileJson['icon']['url'] - profileHeaderStr = \ - getProfileHeader(baseDir, nickname, domain, - domainFull, translate, iconsPath, - defaultTimeline, displayName, - avatarDescription, - profileDescriptionShort, - loginButton, avatarUrl) + avatarUrl = profileJson['icon']['url'] + profileHeaderStr = \ + getProfileHeader(baseDir, nickname, domain, + domainFull, translate, iconsPath, + defaultTimeline, displayName, + avatarDescription, + profileDescriptionShort, + loginButton, avatarUrl) profileStr = profileHeaderStr + donateSection - if not isSystemAccount(nickname): - profileStr += '
    \n' - profileStr += '
    ' - profileStr += \ - ' ' - profileStr += \ - ' ' + \ - '' - profileStr += \ - ' ' + \ - '' - profileStr += \ - ' ' + \ - '' - profileStr += \ - ' ' + \ - '' - profileStr += \ - ' ' + \ - '' - profileStr += logoutStr + editProfileStr - profileStr += '
    ' - profileStr += '
    ' + profileStr += '
    \n' + profileStr += '
    ' + profileStr += \ + ' ' + profileStr += \ + ' ' + \ + '' + profileStr += \ + ' ' + \ + '' + profileStr += \ + ' ' + \ + '' + profileStr += \ + ' ' + \ + '' + profileStr += \ + ' ' + \ + '' + profileStr += logoutStr + editProfileStr + profileStr += '
    ' + profileStr += '
    ' profileStr += followApprovalsSection @@ -623,10 +608,6 @@ def htmlProfile(rssIconAtTop: bool, if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' - if isSystemAccount(nickname): - bannerFile, bannerFilename = \ - getBannerFile(baseDir, nickname, domain) - licenseStr = \ '' + \ '' + \
@@ -643,7 +624,7 @@ def htmlProfile(rssIconAtTop: bool,
                              projectVersion,
                              YTReplacementDomain,
                              showPublishedDateOnly) + licenseStr
-    if selected == 'following':
+    elif selected == 'following':
         profileStr += \
             htmlProfileFollowing(translate, baseDir, httpPrefix,
                                  authorized, nickname,
@@ -651,7 +632,7 @@ def htmlProfile(rssIconAtTop: bool,
                                  wfRequest, personCache, extraJson,
                                  projectVersion, [\n' + \ ' ' + \
@@ -790,7 +755,7 @@ def htmlProfileFollowing(translate: {}, baseDir: str, httpPrefix: str,
             profileStr += \
                 '  <center>\n' + \
                 '    <a href=' + \
@@ -809,7 +774,10 @@ def htmlProfileRoles(translate: {}, nickname: str, domain: str,
             '<div class=\n

    ' + project + \ '

    \n
    \n' for role in rolesList: - profileStr += '

    ' + role + '

    \n' + if translate.get(role): + profileStr += '

    ' + translate[role] + '

    \n' + else: + profileStr += '

    ' + role + '

    \n' profileStr += '
    \n' if len(profileStr) == 0: profileStr += \ @@ -901,6 +869,7 @@ def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str, ssbAddress = getSSBAddress(actorJson) blogAddress = getBlogAddress(actorJson) toxAddress = getToxAddress(actorJson) + jamiAddress = getJamiAddress(actorJson) emailAddress = getEmailAddress(actorJson) PGPpubKey = getPGPpubKey(actorJson) PGPfingerprint = getPGPfingerprint(actorJson) @@ -1041,6 +1010,7 @@ def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str, moderatorsStr = '' themesDropdown = '' instanceStr = '' + editorsStr = '' adminNickname = getConfigParam(baseDir, 'admin') if adminNickname: @@ -1231,6 +1201,12 @@ def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str, editProfileForm += \ ' \n' + + editProfileForm += '
    \n' + editProfileForm += \ + ' \n' + editProfileForm += '
    \n' editProfileForm += \ diff --git a/webapp_search.py b/webapp_search.py index d3418d404..1d0a3636f 100644 --- a/webapp_search.py +++ b/webapp_search.py @@ -10,6 +10,7 @@ import os from shutil import copyfile import urllib.parse from datetime import datetime +from utils import isEditor from utils import loadJson from utils import getDomainFromActor from utils import getNicknameFromActor @@ -18,6 +19,7 @@ from utils import locatePost from utils import isPublicPost from utils import firstParagraphFromString from utils import searchBoxPosts +from utils import getHashtagCategory from feeds import rss2TagHeader from feeds import rss2TagFooter from webapp_utils import getAltPath @@ -658,18 +660,37 @@ def htmlHashtagSearch(cssCache: {}, if nickname: hashtagSearchForm += '
    \n' + \ '

    #' + \ - hashtag + '

    \n' + '
    \n' + hashtag + '\n' else: hashtagSearchForm += '
    \n' + \ - '

    #' + hashtag + '

    \n' + '
    \n' + '

    #' + hashtag + '

    \n' # RSS link for hashtag feed - hashtagSearchForm += '
    ' + hashtagSearchForm += '' hashtagSearchForm += \ 'RSS 2.0
    ' + 'loading="lazy" alt="RSS 2.0" title="RSS 2.0" src="/' + \ + iconsPath + '/logorss.png" />\n' + + # edit the category for this hashtag + if isEditor(baseDir, nickname): + category = getHashtagCategory(baseDir, hashtag) + hashtagSearchForm += '
    \n' + hashtagSearchForm += '
    \n' + hashtagSearchForm += '
    \n' + hashtagSearchForm += translate['Category'] + hashtagSearchForm += \ + ' \n' + hashtagSearchForm += \ + ' \n' + hashtagSearchForm += '
    \n' + hashtagSearchForm += '
    \n' + hashtagSearchForm += '
    \n' if startIndex > 0: # previous page link @@ -702,32 +723,44 @@ def htmlHashtagSearch(cssCache: {}, index += 1 continue postJsonObject = loadJson(postFilename) - if postJsonObject: - if not isPublicPost(postJsonObject): - index += 1 - continue - showIndividualPostIcons = False - if nickname: - showIndividualPostIcons = True - allowDeletion = False - postStr = \ - individualPostAsHtml(True, recentPostsCache, - maxRecentPosts, - iconsPath, translate, None, - baseDir, session, wfRequest, - personCache, - nickname, domain, port, - postJsonObject, - None, True, allowDeletion, - httpPrefix, projectVersion, - 'search', - YTReplacementDomain, - showPublishedDateOnly, - showIndividualPostIcons, - showIndividualPostIcons, - False, False, False) - if postStr: - hashtagSearchForm += separatorStr + postStr + if not postJsonObject: + index += 1 + continue + if not isPublicPost(postJsonObject): + index += 1 + continue + showIndividualPostIcons = False + if nickname: + showIndividualPostIcons = True + allowDeletion = False + showRepeats = showIndividualPostIcons + showIcons = showIndividualPostIcons + manuallyApprovesFollowers = False + showPublicOnly = False + storeToCache = False + allowDownloads = True + avatarUrl = None + showAvatarOptions = True + postStr = \ + individualPostAsHtml(allowDownloads, recentPostsCache, + maxRecentPosts, + iconsPath, translate, None, + baseDir, session, wfRequest, + personCache, + nickname, domain, port, + postJsonObject, + avatarUrl, showAvatarOptions, + allowDeletion, + httpPrefix, projectVersion, + 'search', + YTReplacementDomain, + showPublishedDateOnly, + showRepeats, showIcons, + manuallyApprovesFollowers, + showPublicOnly, + storeToCache) + if postStr: + hashtagSearchForm += separatorStr + postStr index += 1 if endIndex < noOfLines - 1: diff --git a/webapp_timeline.py b/webapp_timeline.py index 617fee84f..ada794366 100644 --- a/webapp_timeline.py +++ b/webapp_timeline.py @@ -8,6 +8,7 @@ __status__ = "Production" import os import time +from utils import isEditor from utils import removeIdEnding from follow import followerApprovalActive from person import isPersonSnoozed @@ -24,7 +25,18 @@ from webapp_column_left import getLeftColumnContent from webapp_column_right import getRightColumnContent from webapp_headerbuttons import headerButtonsTimeline from posts import isModerator -from posts import isEditor + + +def logTimelineTiming(enableTimingLog: bool, timelineStartTime, + boxName: str, debugId: str) -> None: + """Create a log of timings for performance tuning + """ + if not enableTimingLog: + return + timeDiff = int((time.time() - timelineStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE TIMING ' + + boxName + ' ' + debugId + ' = ' + str(timeDiff)) def htmlTimeline(cssCache: {}, defaultTimeline: str, @@ -50,6 +62,8 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str, authorized: bool) -> str: """Show the timeline as html """ + enableTimingLog = False + timelineStartTime = time.time() accountDir = baseDir + '/accounts/' + nickname + '@' + domain @@ -114,10 +128,7 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str, # filename of the banner shown at the top bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) - # benchmark 1 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 1 = ' + str(timeDiff)) + logTimelineTiming(enableTimingLog, timelineStartTime, boxName, '1') # is the user a moderator? if not moderator: @@ -127,14 +138,12 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str, if not editor: editor = isEditor(baseDir, nickname) - # benchmark 2 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 2 = ' + str(timeDiff)) + logTimelineTiming(enableTimingLog, timelineStartTime, boxName, '2') # the appearance of buttons - highlighted or not inboxButton = 'button' blogsButton = 'button' + featuresButton = 'button' newsButton = 'button' dmButton = 'button' if newDM: @@ -156,6 +165,8 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str, inboxButton = 'buttonselected' elif boxName == 'tlblogs': blogsButton = 'buttonselected' + elif boxName == 'tlfeatures': + featuresButton = 'buttonselected' elif boxName == 'tlnews': newsButton = 'buttonselected' elif boxName == 'dm': @@ -214,10 +225,7 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str, '" src="/' + iconsPath + '/person.png"/>\n' break - # benchmark 3 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 3 = ' + str(timeDiff)) + logTimelineTiming(enableTimingLog, timelineStartTime, boxName, '3') # moderation / reports button moderationButtonStr = '' @@ -252,14 +260,11 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str, tlStr = htmlHeaderWithExternalStyle(cssFilename) - # benchmark 4 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 4 = ' + str(timeDiff)) + logTimelineTiming(enableTimingLog, timelineStartTime, boxName, '4') # if this is a news instance and we are viewing the news timeline newsHeader = False - if defaultTimeline == 'tlnews' and boxName == 'tlnews': + if defaultTimeline == 'tlfeatures' and boxName == 'tlfeatures': newsHeader = True newPostButtonStr = '' @@ -283,7 +288,9 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str, '' + \ '' - elif boxName == 'tlblogs' or boxName == 'tlnews': + elif (boxName == 'tlblogs' or + boxName == 'tlnews' or + boxName == 'tlfeatures'): if not iconsAsButtons: newPostButtonStr += \ '\n' tlStr += '\n\n' - # benchmark 6 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 6 = ' + str(timeDiff)) + logTimelineTiming(enableTimingLog, timelineStartTime, boxName, '6') if boxName == 'tlshares': maxSharesPerAccount = itemsPerPage @@ -480,15 +486,7 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str, maxSharesPerAccount, httpPrefix) + htmlFooter()) - # benchmark 7 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 7 = ' + str(timeDiff)) - - # benchmark 8 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 8 = ' + str(timeDiff)) + logTimelineTiming(enableTimingLog, timelineStartTime, boxName, '7') # page up arrow if pageNumber > 1: @@ -513,8 +511,6 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str, # show each post in the timeline for item in timelineJson['orderedItems']: - timelinePostStartTime = time.time() - if item['type'] == 'Create' or \ item['type'] == 'Announce' or \ item['type'] == 'Update': @@ -536,22 +532,14 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str, preparePostFromHtmlCache(currTlStr, boxName, pageNumber) - # benchmark cache post - timeDiff = \ - int((time.time() - - timelinePostStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE POST CACHE TIMING ' + - boxName + ' = ' + str(timeDiff)) + logTimelineTiming(enableTimingLog, + timelineStartTime, + boxName, '10') if not currTlStr: - # benchmark cache post - timeDiff = \ - int((time.time() - - timelinePostStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE POST DISK TIMING START ' + - boxName + ' = ' + str(timeDiff)) + logTimelineTiming(enableTimingLog, + timelineStartTime, + boxName, '11') # read the post from disk currTlStr = \ @@ -571,13 +559,8 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str, showIndividualPostIcons, manuallyApproveFollowers, False, True) - # benchmark cache post - timeDiff = \ - int((time.time() - - timelinePostStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE POST DISK TIMING ' + - boxName + ' = ' + str(timeDiff)) + logTimelineTiming(enableTimingLog, + timelineStartTime, boxName, '12') if currTlStr: itemCtr += 1 @@ -618,10 +601,7 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str, rightColumnStr + '
    \n' @@ -996,6 +976,39 @@ def htmlInboxBlogs(cssCache: {}, defaultTimeline: str, authorized) +def htmlInboxFeatures(cssCache: {}, defaultTimeline: str, + recentPostsCache: {}, maxRecentPosts: int, + translate: {}, pageNumber: int, itemsPerPage: int, + session, baseDir: str, wfRequest: {}, personCache: {}, + nickname: str, domain: str, port: int, inboxJson: {}, + allowDeletion: bool, + httpPrefix: str, projectVersion: str, + minimal: bool, YTReplacementDomain: str, + showPublishedDateOnly: bool, + newswire: {}, positiveVoting: bool, + showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: + """Show the features timeline as html + """ + return htmlTimeline(cssCache, defaultTimeline, + recentPostsCache, maxRecentPosts, + translate, pageNumber, + itemsPerPage, session, baseDir, wfRequest, personCache, + nickname, domain, port, inboxJson, 'tlfeatures', + allowDeletion, httpPrefix, projectVersion, False, + minimal, YTReplacementDomain, + showPublishedDateOnly, + newswire, False, False, + positiveVoting, showPublishAsIcon, + fullWidthTimelineButtonHeader, + iconsAsButtons, rssIconAtTop, publishButtonAtTop, + authorized) + + def htmlInboxNews(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, diff --git a/webapp_utils.py b/webapp_utils.py index 9736aaaa2..3019d40ff 100644 --- a/webapp_utils.py +++ b/webapp_utils.py @@ -20,6 +20,121 @@ from content import addHtmlTags from content import replaceEmojiFromTags +def htmlFollowingList(cssCache: {}, baseDir: str, + followingFilename: str) -> str: + """Returns a list of handles being followed + """ + with open(followingFilename, 'r') as followingFile: + msg = followingFile.read() + followingList = msg.split('\n') + followingList.sort() + if followingList: + cssFilename = baseDir + '/epicyon-profile.css' + if os.path.isfile(baseDir + '/epicyon.css'): + cssFilename = baseDir + '/epicyon.css' + + followingListHtml = htmlHeaderWithExternalStyle(cssFilename) + for followingAddress in followingList: + if followingAddress: + followingListHtml += \ + '

    @' + followingAddress + '

    ' + followingListHtml += htmlFooter() + msg = followingListHtml + return msg + return '' + + +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' + + blockedHashtagForm = htmlHeaderWithExternalStyle(cssFilename) + blockedHashtagForm += '
    \n' + blockedHashtagForm += htmlFooter() + return blockedHashtagForm + + +def headerButtonsFrontScreen(translate: {}, + nickname: str, boxName: str, + authorized: bool, + iconsAsButtons: bool, + iconsPath: 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 += \ + ' ' + \ + '' + if not authorized: + headerStr += \ + ' ' + \ + '' + if iconsAsButtons: + headerStr += \ + ' ' + \ + '' + headerStr += \ + ' ' + \ + '' + else: + headerStr += \ + ' ' + \ + '| ' + translate['Newswire'] + '\n' + headerStr += \ + ' ' + \ + '| ' + translate['Links'] + '\n' + else: + if not authorized: + headerStr += \ + ' ' + \ + '' + + if headerStr: + headerStr = \ + '\n
    \n' + \ + headerStr + \ + '
    \n' + return headerStr + + def getAltPath(actor: str, domainFull: str, callingDomain: str) -> str: """Returns alternate path from the actor eg. https://clearnetdomain/path becomes http://oniondomain/path @@ -737,76 +852,6 @@ def htmlPostSeparator(baseDir: str, column: str) -> str: return separatorStr -def headerButtonsFrontScreen(translate: {}, - nickname: str, boxName: str, - authorized: bool, - iconsAsButtons: bool, - iconsPath: 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 += \ - ' ' + \ - '' - if not authorized: - headerStr += \ - ' ' + \ - '' - if iconsAsButtons: - headerStr += \ - ' ' + \ - '' - headerStr += \ - ' ' + \ - '' - else: - headerStr += \ - ' ' + \ - '| ' + translate['Newswire'] + '\n' - headerStr += \ - ' ' + \ - '| ' + translate['Links'] + '\n' - else: - if not authorized: - headerStr += \ - ' ' + \ - '' - - if headerStr: - headerStr = \ - '\n
    \n' + \ - headerStr + \ - '
    \n' - return headerStr - - def htmlHighlightLabel(label: str, highlight: bool) -> str: """If the given text should be highlighted then return the appropriate markup.