diff --git a/webapp_post.py b/webapp_post.py new file mode 100644 index 000000000..95fea56c2 --- /dev/null +++ b/webapp_post.py @@ -0,0 +1,1217 @@ +__filename__ = "webapp_post.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "1.1.0" +__maintainer__ = "Bob Mottram" +__email__ = "bob@freedombone.net" +__status__ = "Production" + +import os +import time +from dateutil.parser import parse +from auth import createPassword +from git import isGitPatch +from datetime import datetime +from cache import getPersonFromCache +from bookmarks import bookmarkedByPerson +from like import likedByPerson +from like import noOfLikes +from posts import isEditor +from posts import postIsMuted +from posts import getPersonBox +from posts import isDM +from posts import downloadAnnounce +from utils import getCachedPostDirectory +from utils import getCachedPostFilename +from utils import getProtocolPrefixes +from utils import isNewsPost +from utils import isBlogPost +from utils import getDisplayName +from utils import isPublicPost +from utils import updateRecentPostsCache +from utils import removeIdEnding +from utils import getNicknameFromActor +from utils import getDomainFromActor +from utils import isEventPost +from content import replaceEmojiFromTags +from content import htmlReplaceQuoteMarks +from content import htmlReplaceEmailQuote +from content import removeTextFormatting +from content import removeLongWords +from content import getMentionsFromHtml +from content import switchWords +from person import isPersonSnoozed +from announce import announcedByPerson +from webapp_utils import getPersonAvatarUrl +from webapp_utils import updateAvatarImageCache +from webapp_utils import loadIndividualPostAsHtmlFromCache +from webapp_utils import addEmojiToDisplayName +from webapp_utils import postContainsPublic +from webapp_utils import getContentWarningButton +from webapp_utils import getPostAttachmentsAsHtml +from webapp_media import addEmbeddedElements +from webapp_question import insertQuestion +from devices import E2EEdecryptMessageFromDevice + + +def preparePostFromHtmlCache(postHtml: str, boxName: str, + pageNumber: int) -> str: + """Sets the page number on a cached html post + """ + # if on the bookmarks timeline then remain there + if boxName == 'tlbookmarks' or boxName == 'bookmarks': + postHtml = postHtml.replace('?tl=inbox', '?tl=tlbookmarks') + if '?page=' in postHtml: + pageNumberStr = postHtml.split('?page=')[1] + if '?' in pageNumberStr: + pageNumberStr = pageNumberStr.split('?')[0] + postHtml = postHtml.replace('?page=' + pageNumberStr, '?page=-999') + + withPageNumber = postHtml.replace(';-999;', ';' + str(pageNumber) + ';') + withPageNumber = withPageNumber.replace('?page=-999', + '?page=' + str(pageNumber)) + return withPageNumber + + +def saveIndividualPostAsHtmlToCache(baseDir: str, + nickname: str, domain: str, + postJsonObject: {}, + postHtml: str) -> bool: + """Saves the given html for a post to a cache file + This is so that it can be quickly reloaded on subsequent + refresh of the timeline + """ + htmlPostCacheDir = \ + getCachedPostDirectory(baseDir, nickname, domain) + cachedPostFilename = \ + getCachedPostFilename(baseDir, nickname, domain, postJsonObject) + + # create the cache directory if needed + if not os.path.isdir(htmlPostCacheDir): + os.mkdir(htmlPostCacheDir) + + try: + with open(cachedPostFilename, 'w+') as fp: + fp.write(postHtml) + return True + except Exception as e: + print('ERROR: saving post to cache ' + str(e)) + return False + + +def individualPostAsHtml(allowDownloads: bool, + recentPostsCache: {}, maxRecentPosts: int, + iconsDir: str, translate: {}, + pageNumber: int, baseDir: str, + session, wfRequest: {}, personCache: {}, + nickname: str, domain: str, port: int, + postJsonObject: {}, + avatarUrl: str, showAvatarOptions: bool, + allowDeletion: bool, + httpPrefix: str, projectVersion: str, + boxName: str, YTReplacementDomain: str, + showPublishedDateOnly: bool, + showRepeats=True, + showIcons=False, + manuallyApprovesFollowers=False, + showPublicOnly=False, + storeToCache=True) -> str: + """ Shows a single post as html + """ + if not postJsonObject: + return '' + + # benchmark + postStartTime = time.time() + + postActor = postJsonObject['actor'] + + # ZZZzzz + 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)) + + 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)) + + messageIdStr = '' + if messageId: + messageIdStr = ';' + messageId + + fullDomain = domain + if port: + if port != 80 and port != 443: + if ':' not in domain: + fullDomain = 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) + + # benchmark 2.1 + if not allowDownloads: + timeDiff = int((time.time() - postStartTime) * 1000) + if timeDiff > 100: + print('TIMING INDIV ' + boxName + + ' 2.1 = ' + str(timeDiff)) + + updateAvatarImageCache(session, baseDir, httpPrefix, + postActor, avatarUrl, personCache, + allowDownloads) + + # benchmark 2.2 + if not allowDownloads: + timeDiff = int((time.time() - postStartTime) * 1000) + if timeDiff > 100: + print('TIMING INDIV ' + boxName + + ' 2.2 = ' + str(timeDiff)) + + 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: + (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)) + + if avatarUrl2: + avatarUrl = avatarUrl2 + if displayName: + 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)) + + if '/users/news/' not in avatarUrl: + avatarLink = ' ' + avatarLink += \ + '  \n' + + 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' + + # don't create new html within the bookmarks timeline + # it should already have been created for the inbox + if boxName == 'tlbookmarks' or boxName == 'bookmarks': + return '' + + timelinePostBookmark = removeIdEnding(postJsonObject['id']) + timelinePostBookmark = timelinePostBookmark.replace('://', '-') + timelinePostBookmark = timelinePostBookmark.replace('/', '-') + + # If this is the inbox timeline then don't show the repeat icon on any DMs + showRepeatIcon = showRepeats + isPublicRepeat = False + showDMicon = False + if showRepeats: + if isDM(postJsonObject): + showDMicon = True + showRepeatIcon = False + else: + if not isPublicPost(postJsonObject): + isPublicRepeat = True + + titleStr = '' + galleryStr = '' + isAnnounced = False + if postJsonObject['type'] == 'Announce': + postJsonAnnounce = \ + downloadAnnounce(session, baseDir, httpPrefix, + nickname, domain, postJsonObject, + projectVersion, translate, + YTReplacementDomain) + if not postJsonAnnounce: + return '' + 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)) + + if not isinstance(postJsonObject['object'], dict): + return '' + + # if this post should be public then check its recipients + if showPublicOnly: + if not postContainsPublic(postJsonObject): + return '' + + isModerationPost = False + if postJsonObject['object'].get('moderationStatus'): + isModerationPost = True + containerClass = 'container' + containerClassIcons = 'containericons' + timeClass = 'time-right' + actorNickname = getNicknameFromActor(postActor) + if not actorNickname: + # single user instance + actorNickname = 'dev' + actorDomain, actorPort = getDomainFromActor(postActor) + + displayName = getDisplayName(baseDir, postActor, personCache) + if displayName: + if ':' in displayName: + displayName = \ + addEmojiToDisplayName(baseDir, httpPrefix, + nickname, domain, + displayName, False) + titleStr += \ + ' ' + displayName + '\n' + else: + if not messageId: + # pprint(postJsonObject) + print('ERROR: no messageId') + if not actorNickname: + # pprint(postJsonObject) + print('ERROR: no actorNickname') + if not actorDomain: + # pprint(postJsonObject) + print('ERROR: no actorDomain') + titleStr += \ + ' @' + actorNickname + '@' + actorDomain + '\n' + + # benchmark 9 + if not allowDownloads: + timeDiff = int((time.time() - postStartTime) * 1000) + if timeDiff > 100: + print('TIMING INDIV ' + boxName + ' 9 = ' + str(timeDiff)) + + # Show a DM icon for DMs in the inbox timeline + if showDMicon: + titleStr = \ + 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 += \ + ' ' + \ + '' + \
+            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)) + + isEvent = isEventPost(postJsonObject) + + # benchmark 11 + if not allowDownloads: + timeDiff = int((time.time() - postStartTime) * 1000) + if timeDiff > 100: + print('TIMING INDIV ' + boxName + ' 11 = ' + str(timeDiff)) + + 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' + + 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' + + # benchmark 12 + if not allowDownloads: + timeDiff = int((time.time() - postStartTime) * 1000) + if timeDiff > 100: + print('TIMING INDIV ' + boxName + ' 12 = ' + str(timeDiff)) + + # whether to show a like button + hideLikeButtonFile = \ + baseDir + '/accounts/' + nickname + '@' + domain + '/.hideLikeButton' + showLikeButton = True + 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) + + # benchmark 12.1 + if not allowDownloads: + timeDiff = int((time.time() - postStartTime) * 1000) + if timeDiff > 100: + print('TIMING INDIV ' + boxName + ' 12.1 = ' + str(timeDiff)) + + 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'] + + # 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)) + + 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)) + + 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' + + # benchmark 13.1 + if not allowDownloads: + timeDiff = int((time.time() - postStartTime) * 1000) + if timeDiff > 100: + print('TIMING INDIV ' + boxName + ' 13.1 = ' + str(timeDiff)) + + 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)) + + 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)) + + 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)) + + attachmentStr, galleryStr = \ + getPostAttachmentsAsHtml(postJsonObject, boxName, translate, + isMuted, avatarLink.strip(), + 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") + + # benchmark 15 + if not allowDownloads: + timeDiff = int((time.time() - postStartTime) * 1000) + if timeDiff > 100: + print('TIMING INDIV ' + boxName + ' 15 = ' + str(timeDiff)) + + publishedLink = messageId + # blog posts should have no /statuses/ in their link + if isBlogPost(postJsonObject): + # is this a post to the local domain? + if '://' + domain in messageId: + publishedLink = messageId.replace('/statuses/', '/') + # if this is a local link then make it relative so that it works + # on clearnet or onion address + if domain + '/users/' in publishedLink or \ + domain + ':' + str(port) + '/users/' in publishedLink: + publishedLink = '/users/' + publishedLink.split('/users/')[1] + + if not isNewsPost(postJsonObject): + footerStr = '' + publishedStr + '\n' + else: + footerStr = '' + publishedStr + '\n' + + # change the background color for DMs in inbox timeline + if showDMicon: + 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' + + postIsSensitive = False + if postJsonObject['object'].get('sensitive'): + # sensitive posts should have a summary + if postJsonObject['object'].get('summary'): + postIsSensitive = postJsonObject['object']['sensitive'] + else: + # add a generic summary if none is provided + postJsonObject['object']['summary'] = translate['Sensitive'] + + # add an extra line if there is a content warning, + # for better vertical spacing on mobile + if postIsSensitive: + footerStr = '
' + footerStr + + if not postJsonObject['object'].get('summary'): + postJsonObject['object']['summary'] = '' + + if postJsonObject['object'].get('cipherText'): + postJsonObject['object']['content'] = \ + E2EEdecryptMessageFromDevice(postJsonObject['object']) + + if not postJsonObject['object'].get('content'): + return '' + + isPatch = isGitPatch(baseDir, nickname, domain, + postJsonObject['object']['type'], + 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)) + + if not isPatch: + objectContent = \ + removeLongWords(postJsonObject['object']['content'], 40, []) + objectContent = removeTextFormatting(objectContent) + objectContent = \ + switchWords(baseDir, nickname, domain, objectContent) + objectContent = htmlReplaceEmailQuote(objectContent) + objectContent = htmlReplaceQuoteMarks(objectContent) + else: + objectContent = \ + postJsonObject['object']['content'] + + if not postIsSensitive: + contentStr = objectContent + attachmentStr + contentStr = addEmbeddedElements(translate, contentStr) + contentStr = insertQuestion(baseDir, translate, + nickname, domain, port, + contentStr, postJsonObject, + pageNumber) + else: + postID = 'post' + str(createPassword(8)) + contentStr = '' + if postJsonObject['object'].get('summary'): + contentStr += \ + '' + str(postJsonObject['object']['summary']) + '\n ' + if isModerationPost: + containerClass = 'container report' + # get the content warning text + cwContentStr = objectContent + attachmentStr + if not isPatch: + cwContentStr = addEmbeddedElements(translate, cwContentStr) + cwContentStr = \ + insertQuestion(baseDir, translate, nickname, domain, port, + cwContentStr, postJsonObject, pageNumber) + if not isBlogPost(postJsonObject): + # get the content warning button + contentStr += \ + getContentWarningButton(postID, translate, cwContentStr) + else: + contentStr += cwContentStr + + # benchmark 17 + if not allowDownloads: + timeDiff = int((time.time() - postStartTime) * 1000) + if timeDiff > 100: + print('TIMING INDIV ' + boxName + ' 17 = ' + str(timeDiff)) + + if postJsonObject['object'].get('tag') and not isPatch: + contentStr = \ + replaceEmojiFromTags(contentStr, + postJsonObject['object']['tag'], + 'content') + + if isMuted: + contentStr = '' + else: + if not isPatch: + contentStr = '
' + \ + contentStr + \ + '
\n' + else: + contentStr = \ + '
' + contentStr + \
+                '
\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' + + postHtml = '' + if boxName != 'tlmedia': + postHtml = '
    \n' + postHtml += avatarImageInPost + postHtml += '
    \n' + \ + ' ' + titleStr + \ + replyAvatarImageInPost + '
    \n' + postHtml += contentStr + citationsStr + footerStr + '\n' + postHtml += '
    \n' + else: + postHtml = galleryStr + + # benchmark 18 + if not allowDownloads: + timeDiff = int((time.time() - postStartTime) * 1000) + if timeDiff > 100: + print('TIMING INDIV ' + boxName + ' 18 = ' + str(timeDiff)) + + if not showPublicOnly and storeToCache and \ + boxName != 'tlmedia' and boxName != 'tlbookmarks' and \ + boxName != 'bookmarks': + saveIndividualPostAsHtmlToCache(baseDir, nickname, domain, + postJsonObject, postHtml) + 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)) + + return postHtml