forked from indymedia/epicyon
1752 lines
69 KiB
Python
1752 lines
69 KiB
Python
__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 follow import isFollowingActor
|
|
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
|
|
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_utils import getIconsWebPath
|
|
from webapp_utils import htmlHeaderWithExternalStyle
|
|
from webapp_utils import htmlFooter
|
|
from webapp_media import addEmbeddedElements
|
|
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
|
|
"""
|
|
# 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 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 = ' <a class="imageAnchor" href="' + postActor + '">'
|
|
avatarLink += \
|
|
' <img loading="lazy" src="' + avatarUrl + '" title="' + \
|
|
translate['Show profile'] + '" alt=" "' + avatarPosition + \
|
|
'/></a>\n'
|
|
|
|
if showAvatarOptions and \
|
|
domainFull + '/users/' + nickname not in postActor:
|
|
if '/users/news/' not in avatarUrl:
|
|
avatarLink = \
|
|
' <a class="imageAnchor" href="/users/' + \
|
|
nickname + '?options=' + postActor + \
|
|
';' + str(pageNumber) + ';' + avatarUrl + messageIdStr + '">\n'
|
|
avatarLink += \
|
|
' <img loading="lazy" title="' + \
|
|
translate['Show options for this person'] + \
|
|
'" src="' + avatarUrl + '" ' + avatarPosition + '/></a>\n'
|
|
else:
|
|
# don't link to the person options for the news account
|
|
avatarLink += \
|
|
' <img loading="lazy" title="' + \
|
|
translate['Show options for this person'] + \
|
|
'" src="' + avatarUrl + '" ' + avatarPosition + '/>\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 += \
|
|
' <a class="imageAnchor" href="/users/' + \
|
|
nickname + '?replyto=' + replyToLink + \
|
|
'?actor=' + postJsonObject['actor'] + \
|
|
'" title="' + replyToThisPostStr + '">\n'
|
|
else:
|
|
if isDM(postJsonObject):
|
|
replyStr += \
|
|
' ' + \
|
|
'<a class="imageAnchor" href="/users/' + nickname + \
|
|
'?replydm=' + replyToLink + \
|
|
'?actor=' + postJsonObject['actor'] + \
|
|
'" title="' + replyToThisPostStr + '">\n'
|
|
else:
|
|
replyStr += \
|
|
' ' + \
|
|
'<a class="imageAnchor" href="/users/' + nickname + \
|
|
'?replyfollowers=' + replyToLink + \
|
|
'?actor=' + postJsonObject['actor'] + \
|
|
'" title="' + replyToThisPostStr + '">\n'
|
|
|
|
replyStr += \
|
|
' ' + \
|
|
'<img loading="lazy" title="' + \
|
|
replyToThisPostStr + '" alt="' + replyToThisPostStr + \
|
|
' |" src="/' + iconsPath + '/reply.png"/></a>\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 += \
|
|
' ' + \
|
|
'<a class="imageAnchor" href="/users/' + \
|
|
nickname + \
|
|
'/tlblogs?editblogpost=' + \
|
|
postId.split('/statuses/')[1] + \
|
|
'?actor=' + actorNickname + \
|
|
'" title="' + editBlogPostStr + '">' + \
|
|
'<img loading="lazy" title="' + \
|
|
editBlogPostStr + '" alt="' + editBlogPostStr + \
|
|
' |" src="/' + iconsPath + '/edit.png"/></a>\n'
|
|
else:
|
|
editStr += \
|
|
' ' + \
|
|
'<a class="imageAnchor" href="/users/' + \
|
|
nickname + '/editnewspost=' + \
|
|
postId.split('/statuses/')[1] + \
|
|
'?actor=' + actorNickname + \
|
|
'" title="' + editBlogPostStr + '">' + \
|
|
'<img loading="lazy" title="' + \
|
|
editBlogPostStr + '" alt="' + editBlogPostStr + \
|
|
' |" src="/' + iconsPath + '/edit.png"/></a>\n'
|
|
elif isEvent:
|
|
editEventStr = translate['Edit event']
|
|
editStr += \
|
|
' ' + \
|
|
'<a class="imageAnchor" href="/users/' + nickname + \
|
|
'/tlblogs?editeventpost=' + \
|
|
postId.split('/statuses/')[1] + \
|
|
'?actor=' + actorNickname + \
|
|
'" title="' + editEventStr + '">' + \
|
|
'<img loading="lazy" title="' + \
|
|
editEventStr + '" alt="' + editEventStr + \
|
|
' |" src="/' + iconsPath + '/edit.png"/></a>\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 = \
|
|
' <a class="imageAnchor" href="/users/' + \
|
|
nickname + '?' + announceLink + \
|
|
'=' + postJsonObject['object']['id'] + pageNumberParam + \
|
|
'?actor=' + postJsonObject['actor'] + \
|
|
'?bm=' + timelinePostBookmark + \
|
|
'?tl=' + boxName + '" title="' + announceTitle + '">\n'
|
|
|
|
announceStr += \
|
|
' ' + \
|
|
'<img loading="lazy" title="' + translate['Repeat this post'] + \
|
|
'" alt="' + translate['Repeat this post'] + \
|
|
' |" src="/' + iconsPath + '/' + announceIcon + '"/></a>\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 += '<label class="likesCount">'
|
|
likeStr += likeCountStr.replace('(', '').replace(')', '').strip()
|
|
likeStr += '</label>\n'
|
|
likeStr += \
|
|
' <a class="imageAnchor" href="/users/' + nickname + '?' + \
|
|
likeLink + '=' + postJsonObject['object']['id'] + \
|
|
pageNumberParam + \
|
|
'?actor=' + postJsonObject['actor'] + \
|
|
'?bm=' + timelinePostBookmark + \
|
|
'?tl=' + boxName + '" title="' + \
|
|
likeTitle + likeCountStr + '">\n'
|
|
likeStr += \
|
|
' ' + \
|
|
'<img loading="lazy" title="' + likeTitle + likeCountStr + \
|
|
'" alt="' + likeTitle + \
|
|
' |" src="/' + iconsPath + '/' + likeIcon + '"/></a>\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 = \
|
|
' <a class="imageAnchor" href="/users/' + nickname + '?' + \
|
|
bookmarkLink + '=' + postJsonObject['object']['id'] + \
|
|
pageNumberParam + \
|
|
'?actor=' + postJsonObject['actor'] + \
|
|
'?bm=' + timelinePostBookmark + \
|
|
'?tl=' + boxName + '" title="' + bookmarkTitle + '">\n'
|
|
bookmarkStr += \
|
|
' ' + \
|
|
'<img loading="lazy" title="' + bookmarkTitle + '" alt="' + \
|
|
bookmarkTitle + ' |" src="/' + iconsPath + \
|
|
'/' + bookmarkIcon + '"/></a>\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 = \
|
|
' <a class="imageAnchor" href="/users/' + nickname + \
|
|
'?mute=' + messageId + pageNumberParam + '?tl=' + boxName + \
|
|
'?bm=' + timelinePostBookmark + \
|
|
'" title="' + translate['Mute this post'] + '">\n'
|
|
muteStr += \
|
|
' ' + \
|
|
'<img loading="lazy" alt="' + \
|
|
translate['Mute this post'] + \
|
|
' |" title="' + translate['Mute this post'] + \
|
|
'" src="/' + iconsPath + '/mute.png"/></a>\n'
|
|
else:
|
|
muteStr = \
|
|
' <a class="imageAnchor" href="/users/' + \
|
|
nickname + '?unmute=' + messageId + \
|
|
pageNumberParam + '?tl=' + boxName + '?bm=' + \
|
|
timelinePostBookmark + '" title="' + \
|
|
translate['Undo mute'] + '">\n'
|
|
muteStr += \
|
|
' ' + \
|
|
'<img loading="lazy" alt="' + translate['Undo mute'] + \
|
|
' |" title="' + translate['Undo mute'] + \
|
|
'" src="/' + iconsPath + '/unmute.png"/></a>\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 = \
|
|
' <a class="imageAnchor" href="/users/' + \
|
|
nickname + \
|
|
'?delete=' + messageId + pageNumberParam + \
|
|
'" title="' + translate['Delete this post'] + '">\n'
|
|
deleteStr += \
|
|
' ' + \
|
|
'<img loading="lazy" alt="' + \
|
|
translate['Delete this post'] + \
|
|
' |" title="' + translate['Delete this post'] + \
|
|
'" src="/' + iconsPath + '/delete.png"/></a>\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 += \
|
|
'<li><a href="' + tagJson['url'] + '">' + \
|
|
'<cite>' + tagJson['name'] + '</cite></a></li>\n'
|
|
|
|
if citationsStr:
|
|
citationsStr = '<p><b>' + translate['Citations'] + ':</b></p>' + \
|
|
'<ul>\n' + citationsStr + '</ul>\n'
|
|
return citationsStr
|
|
|
|
|
|
def boostOwnTootHtml(translate: {}, iconsPath) -> str:
|
|
"""The html title for announcing your own post
|
|
"""
|
|
return ' <img loading="lazy" title="' + \
|
|
translate['announces'] + \
|
|
'" alt="' + translate['announces'] + \
|
|
'" src="/' + iconsPath + \
|
|
'/repeat_inactive.png" class="announceOrReply"/>\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 ' <img loading="lazy" title="' + \
|
|
translate['announces'] + '" alt="' + \
|
|
translate['announces'] + '" src="/' + iconsPath + \
|
|
'/repeat_inactive.png" ' + \
|
|
'class="announceOrReply"/>\n' + \
|
|
' <a href="' + \
|
|
postJsonObject['object']['id'] + \
|
|
'" class="announceOrReply">@unattributed</a>\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 ' <img loading="lazy" title="' + \
|
|
translate['announces'] + '" alt="' + translate['announces'] + \
|
|
'" src="/' + iconsPath + '/repeat_inactive.png" ' + \
|
|
'class="announceOrReply"/>\n' + \
|
|
' <a href="' + postJsonObject['object']['id'] + '" ' + \
|
|
'class="announceOrReply">@' + \
|
|
announceNickname + '@' + announceDomain + '</a>\n'
|
|
|
|
|
|
def announceWithDisplayNameHtml(translate: {},
|
|
iconsPath: str,
|
|
postJsonObject: {},
|
|
announceDisplayName: str) -> str:
|
|
"""Returns html for an announce having a display name
|
|
"""
|
|
return ' <img loading="lazy" title="' + \
|
|
translate['announces'] + '" alt="' + \
|
|
translate['announces'] + '" src="/' + \
|
|
iconsPath + '/repeat_inactive.png" ' + \
|
|
'class="announceOrReply"/>\n' + \
|
|
' <a href="' + \
|
|
postJsonObject['object']['id'] + '" ' + \
|
|
'class="announceOrReply">' + announceDisplayName + '</a>\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 = \
|
|
' ' \
|
|
'<div class=' + \
|
|
'"timeline-avatar-reply">\n' \
|
|
' ' + \
|
|
'<a class="imageAnchor" ' + \
|
|
'href="/users/' + nickname + \
|
|
'?options=' + \
|
|
announceActor + ';' + \
|
|
str(pageNumber) + \
|
|
';' + announceAvatarUrl + \
|
|
messageIdStr + '">' \
|
|
'<img loading="lazy" src="' + \
|
|
announceAvatarUrl + '" ' \
|
|
'title="' + translate[idx] + \
|
|
'" alt=" "' + avatarPosition + \
|
|
'/></a>\n </div>\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 ' <img loading="lazy" title="' + \
|
|
translate['replying to themselves'] + \
|
|
'" alt="' + translate['replying to themselves'] + \
|
|
'" src="/' + iconsPath + \
|
|
'/reply.png" class="announceOrReply"/>\n'
|
|
|
|
|
|
def replyToUnknownHtml(translate: {}, iconsPath: str,
|
|
postJsonObject: {}) -> str:
|
|
"""Returns the html title for a reply to an unknown handle
|
|
"""
|
|
return ' <img loading="lazy" title="' + \
|
|
translate['replying to'] + '" alt="' + \
|
|
translate['replying to'] + '" src="/' + \
|
|
iconsPath + \
|
|
'/reply.png" class="announceOrReply"/>\n' + \
|
|
' <a href="' + \
|
|
postJsonObject['object']['inReplyTo'] + \
|
|
'" class="announceOrReply">@unknown</a>\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 ' <img loading="lazy" title="' + \
|
|
translate['replying to'] + \
|
|
'" alt="' + translate['replying to'] + \
|
|
'" src="/' + \
|
|
iconsPath + '/reply.png" ' + \
|
|
'class="announceOrReply"/>\n' + \
|
|
' <a href="' + \
|
|
postJsonObject['object']['inReplyTo'] + \
|
|
'" class="announceOrReply">' + \
|
|
postDomain + '</a>\n'
|
|
|
|
|
|
def getReplyHtml(translate: {}, iconsPath: str,
|
|
inReplyTo: str, replyDisplayName: str) -> str:
|
|
"""Returns html title for a reply
|
|
"""
|
|
return ' ' + \
|
|
'<img loading="lazy" title="' + \
|
|
translate['replying to'] + '" alt="' + \
|
|
translate['replying to'] + '" src="/' + \
|
|
iconsPath + '/reply.png" ' + \
|
|
'class="announceOrReply"/>\n' + \
|
|
' <a href="' + inReplyTo + \
|
|
'" class="announceOrReply">' + \
|
|
replyDisplayName + '</a>\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 ' ' + \
|
|
'<img loading="lazy" title="' + translate['replying to'] + \
|
|
'" alt="' + translate['replying to'] + \
|
|
'" src="/' + iconsPath + '/reply.png" ' + \
|
|
'class="announceOrReply"/>\n' + ' <a href="' + \
|
|
inReplyTo + '" class="announceOrReply">@' + \
|
|
replyNickname + '@' + replyDomain + '</a>\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 = \
|
|
' <div class=' + \
|
|
'"timeline-avatar-reply">\n'
|
|
replyAvatarImageInPost += \
|
|
' ' + \
|
|
'<a class="imageAnchor" ' + \
|
|
'href="/users/' + nickname + \
|
|
'?options=' + replyActor + \
|
|
';' + str(pageNumber) + ';' + \
|
|
replyAvatarUrl + \
|
|
messageIdStr + '">\n'
|
|
replyAvatarImageInPost += \
|
|
' ' + \
|
|
'<img loading="lazy" src="' + \
|
|
replyAvatarUrl + '" '
|
|
replyAvatarImageInPost += \
|
|
'title="' + \
|
|
translate['Show profile']
|
|
replyAvatarImageInPost += \
|
|
'" alt=" "' + \
|
|
avatarPosition + '/></a>\n' + \
|
|
' </div>\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 <div class="' + containerClassIcons + '">\n'
|
|
footerStr += replyStr + announceStr + likeStr + bookmarkStr
|
|
footerStr += deleteStr + muteStr + editStr
|
|
if not isNewsPost(postJsonObject):
|
|
footerStr += ' <a href="' + publishedLink + '" class="' + \
|
|
timeClass + '">' + publishedStr + '</a>\n'
|
|
else:
|
|
footerStr += ' <a href="' + \
|
|
publishedLink.replace('/news/', '/news/statuses/') + \
|
|
'" class="' + timeClass + '">' + publishedStr + '</a>\n'
|
|
footerStr += ' </div>\n'
|
|
return footerStr
|
|
|
|
|
|
def individualPostAsHtml(allowDownloads: bool,
|
|
recentPostsCache: {}, maxRecentPosts: int,
|
|
iconsPath: 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 ''
|
|
|
|
# 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'])
|
|
|
|
logPostTiming(enableTimingLog, postStartTime, '2')
|
|
|
|
messageIdStr = ''
|
|
if messageId:
|
|
messageIdStr = ';' + messageId
|
|
|
|
domainFull = domain
|
|
if port:
|
|
if port != 80 and port != 443:
|
|
if ':' not in domain:
|
|
domainFull = domain + ':' + str(port)
|
|
|
|
pageNumberParam = ''
|
|
if pageNumber:
|
|
pageNumberParam = '?page=' + str(pageNumber)
|
|
|
|
# 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
|
|
|
|
logPostTiming(enableTimingLog, postStartTime, '4')
|
|
|
|
avatarUrl = \
|
|
getAvatarImageUrl(session,
|
|
baseDir, httpPrefix,
|
|
postActor, personCache,
|
|
avatarUrl, allowDownloads)
|
|
|
|
logPostTiming(enableTimingLog, postStartTime, '5')
|
|
|
|
# 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')
|
|
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)
|
|
|
|
logPostTiming(enableTimingLog, postStartTime, '7')
|
|
|
|
avatarLink = \
|
|
getAvatarImageHtml(showAvatarOptions,
|
|
nickname, domainFull,
|
|
avatarUrl, postActor,
|
|
translate, avatarPosition,
|
|
pageNumber, messageIdStr)
|
|
|
|
avatarImageInPost = \
|
|
' <div class="timeline-avatar">' + avatarLink + '</div>\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
|
|
|
|
logPostTiming(enableTimingLog, postStartTime, '8')
|
|
|
|
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 += \
|
|
' <a class="imageAnchor" href="/users/' + \
|
|
nickname + '?options=' + postActor + \
|
|
';' + str(pageNumber) + ';' + avatarUrl + messageIdStr + \
|
|
'">' + displayName + '</a>\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 += \
|
|
' <a class="imageAnchor" href="/users/' + \
|
|
nickname + '?options=' + postActor + \
|
|
';' + str(pageNumber) + ';' + avatarUrl + messageIdStr + \
|
|
'">@' + actorNickname + '@' + actorDomain + '</a>\n'
|
|
|
|
# benchmark 9
|
|
logPostTiming(enableTimingLog, postStartTime, '9')
|
|
|
|
# Show a DM icon for DMs in the inbox timeline
|
|
if showDMicon:
|
|
titleStr = \
|
|
titleStr + ' <img loading="lazy" src="/' + \
|
|
iconsPath + '/dm.png" class="DMicon"/>\n'
|
|
|
|
# check if replying is permitted
|
|
commentsEnabled = True
|
|
if 'commentsEnabled' in postJsonObject['object']:
|
|
if postJsonObject['object']['commentsEnabled'] is False:
|
|
commentsEnabled = False
|
|
|
|
replyStr = getReplyIconHtml(nickname, isPublicRepeat,
|
|
showIcons, commentsEnabled,
|
|
postJsonObject, pageNumberParam,
|
|
iconsPath, translate)
|
|
|
|
logPostTiming(enableTimingLog, postStartTime, '10')
|
|
|
|
isEvent = isEventPost(postJsonObject)
|
|
|
|
logPostTiming(enableTimingLog, postStartTime, '11')
|
|
|
|
editStr = getEditIconHtml(baseDir, nickname, domainFull,
|
|
postJsonObject, actorNickname,
|
|
translate, iconsPath, isEvent)
|
|
|
|
announceStr = \
|
|
getAnnounceIconHtml(nickname, domainFull,
|
|
postJsonObject,
|
|
isPublicRepeat,
|
|
isModerationPost,
|
|
showRepeatIcon,
|
|
translate,
|
|
pageNumberParam,
|
|
timelinePostBookmark,
|
|
boxName, iconsPath)
|
|
|
|
logPostTiming(enableTimingLog, postStartTime, '12')
|
|
|
|
# whether to show a like button
|
|
hideLikeButtonFile = \
|
|
baseDir + '/accounts/' + nickname + '@' + domain + '/.hideLikeButton'
|
|
showLikeButton = True
|
|
if os.path.isfile(hideLikeButtonFile):
|
|
showLikeButton = False
|
|
|
|
likeStr = getLikeIconHtml(nickname, domainFull,
|
|
isModerationPost,
|
|
showLikeButton,
|
|
postJsonObject,
|
|
enableTimingLog,
|
|
postStartTime,
|
|
translate, pageNumberParam,
|
|
timelinePostBookmark,
|
|
boxName, iconsPath)
|
|
|
|
logPostTiming(enableTimingLog, postStartTime, '12.5')
|
|
|
|
bookmarkStr = \
|
|
getBookmarkIconHtml(nickname, domainFull,
|
|
postJsonObject,
|
|
isModerationPost,
|
|
translate,
|
|
enableTimingLog,
|
|
postStartTime, boxName,
|
|
pageNumberParam,
|
|
timelinePostBookmark,
|
|
iconsPath)
|
|
|
|
logPostTiming(enableTimingLog, postStartTime, '12.9')
|
|
|
|
isMuted = postIsMuted(baseDir, nickname, domain, postJsonObject, messageId)
|
|
|
|
logPostTiming(enableTimingLog, postStartTime, '13')
|
|
|
|
muteStr = \
|
|
getMuteIconHtml(isMuted,
|
|
postActor,
|
|
messageId,
|
|
nickname, domainFull,
|
|
allowDeletion,
|
|
pageNumberParam,
|
|
iconsPath,
|
|
boxName,
|
|
timelinePostBookmark,
|
|
translate)
|
|
|
|
deleteStr = \
|
|
getDeleteIconHtml(nickname, domainFull,
|
|
allowDeletion,
|
|
postActor,
|
|
messageId,
|
|
postJsonObject,
|
|
pageNumberParam,
|
|
iconsPath,
|
|
translate)
|
|
|
|
logPostTiming(enableTimingLog, postStartTime, '13.1')
|
|
|
|
# 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
|
|
|
|
logPostTiming(enableTimingLog, postStartTime, '14')
|
|
|
|
attachmentStr, galleryStr = \
|
|
getPostAttachmentsAsHtml(postJsonObject, boxName, translate,
|
|
isMuted, avatarLink,
|
|
replyStr, announceStr, likeStr,
|
|
bookmarkStr, deleteStr, muteStr)
|
|
|
|
publishedStr = \
|
|
getPublishedDateStr(postJsonObject, showPublishedDateOnly)
|
|
|
|
logPostTiming(enableTimingLog, postStartTime, '15')
|
|
|
|
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 = '<a href="' + publishedLink + \
|
|
'" class="' + timeClass + '">' + publishedStr + '</a>\n'
|
|
else:
|
|
footerStr = '<a href="' + \
|
|
publishedLink.replace('/news/', '/news/statuses/') + \
|
|
'" class="' + timeClass + '">' + publishedStr + '</a>\n'
|
|
|
|
# change the background color for DMs in inbox timeline
|
|
if showDMicon:
|
|
containerClassIcons = 'containericons dm'
|
|
containerClass = 'container dm'
|
|
|
|
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'):
|
|
# 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 = '<br>' + 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'])
|
|
|
|
logPostTiming(enableTimingLog, postStartTime, '16')
|
|
|
|
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 += \
|
|
'<b>' + str(postJsonObject['object']['summary']) + '</b>\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
|
|
|
|
logPostTiming(enableTimingLog, postStartTime, '17')
|
|
|
|
if postJsonObject['object'].get('tag') and not isPatch:
|
|
contentStr = \
|
|
replaceEmojiFromTags(contentStr,
|
|
postJsonObject['object']['tag'],
|
|
'content')
|
|
|
|
if isMuted:
|
|
contentStr = ''
|
|
else:
|
|
if not isPatch:
|
|
contentStr = ' <div class="message">' + \
|
|
contentStr + \
|
|
' </div>\n'
|
|
else:
|
|
contentStr = \
|
|
'<div class="gitpatch"><pre><code>' + contentStr + \
|
|
'</code></pre></div>\n'
|
|
|
|
# show blog citations
|
|
citationsStr = \
|
|
getBlogCitationsHtml(boxName, postJsonObject, translate)
|
|
|
|
postHtml = ''
|
|
if boxName != 'tlmedia':
|
|
postHtml = ' <div id="' + timelinePostBookmark + \
|
|
'" class="' + containerClass + '">\n'
|
|
postHtml += avatarImageInPost
|
|
postHtml += ' <div class="post-title">\n' + \
|
|
' ' + titleStr + \
|
|
replyAvatarImageInPost + ' </div>\n'
|
|
postHtml += contentStr + citationsStr + footerStr + '\n'
|
|
postHtml += ' </div>\n'
|
|
else:
|
|
postHtml = galleryStr
|
|
|
|
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':
|
|
saveIndividualPostAsHtmlToCache(baseDir, nickname, domain,
|
|
postJsonObject, postHtml)
|
|
updateRecentPostsCache(recentPostsCache, maxRecentPosts,
|
|
postJsonObject, postHtml)
|
|
|
|
logPostTiming(enableTimingLog, postStartTime, '19')
|
|
|
|
return postHtml
|
|
|
|
|
|
def htmlIndividualPost(cssCache: {},
|
|
recentPostsCache: {}, maxRecentPosts: int,
|
|
translate: {},
|
|
baseDir: str, session, wfRequest: {}, personCache: {},
|
|
nickname: str, domain: str, port: int, authorized: bool,
|
|
postJsonObject: {}, httpPrefix: str,
|
|
projectVersion: str, likedBy: str,
|
|
YTReplacementDomain: str,
|
|
showPublishedDateOnly: bool) -> str:
|
|
"""Show an individual post as html
|
|
"""
|
|
iconsPath = getIconsWebPath(baseDir)
|
|
postStr = ''
|
|
if likedBy:
|
|
likedByNickname = getNicknameFromActor(likedBy)
|
|
likedByDomain, likedByPort = getDomainFromActor(likedBy)
|
|
if likedByPort:
|
|
if likedByPort != 80 and likedByPort != 443:
|
|
likedByDomain += ':' + str(likedByPort)
|
|
likedByHandle = likedByNickname + '@' + likedByDomain
|
|
postStr += \
|
|
'<p>' + translate['Liked by'] + \
|
|
' <a href="' + likedBy + '">@' + \
|
|
likedByHandle + '</a>\n'
|
|
|
|
domainFull = domain
|
|
if port:
|
|
if port != 80 and port != 443:
|
|
domainFull = domain + ':' + str(port)
|
|
actor = '/users/' + nickname
|
|
followStr = ' <form method="POST" ' + \
|
|
'accept-charset="UTF-8" action="' + actor + '/searchhandle">\n'
|
|
followStr += \
|
|
' <input type="hidden" name="actor" value="' + actor + '">\n'
|
|
followStr += \
|
|
' <input type="hidden" name="searchtext" value="' + \
|
|
likedByHandle + '">\n'
|
|
if not isFollowingActor(baseDir, nickname, domainFull, likedBy):
|
|
followStr += ' <button type="submit" class="button" ' + \
|
|
'name="submitSearch">' + translate['Follow'] + '</button>\n'
|
|
followStr += ' <button type="submit" class="button" ' + \
|
|
'name="submitBack">' + translate['Go Back'] + '</button>\n'
|
|
followStr += ' </form>\n'
|
|
postStr += followStr + '</p>\n'
|
|
|
|
postStr += \
|
|
individualPostAsHtml(True, recentPostsCache, maxRecentPosts,
|
|
iconsPath, translate, None,
|
|
baseDir, session, wfRequest, personCache,
|
|
nickname, domain, port, postJsonObject,
|
|
None, True, False,
|
|
httpPrefix, projectVersion, 'inbox',
|
|
YTReplacementDomain,
|
|
showPublishedDateOnly,
|
|
False, authorized, False, False, False)
|
|
messageId = removeIdEnding(postJsonObject['id'])
|
|
|
|
# show the previous posts
|
|
if isinstance(postJsonObject['object'], dict):
|
|
while postJsonObject['object'].get('inReplyTo'):
|
|
postFilename = \
|
|
locatePost(baseDir, nickname, domain,
|
|
postJsonObject['object']['inReplyTo'])
|
|
if not postFilename:
|
|
break
|
|
postJsonObject = loadJson(postFilename)
|
|
if postJsonObject:
|
|
postStr = \
|
|
individualPostAsHtml(True, recentPostsCache,
|
|
maxRecentPosts,
|
|
iconsPath, translate, None,
|
|
baseDir, session, wfRequest,
|
|
personCache,
|
|
nickname, domain, port,
|
|
postJsonObject,
|
|
None, True, False,
|
|
httpPrefix, projectVersion, 'inbox',
|
|
YTReplacementDomain,
|
|
showPublishedDateOnly,
|
|
False, authorized,
|
|
False, False, False) + postStr
|
|
|
|
# show the following posts
|
|
postFilename = locatePost(baseDir, nickname, domain, messageId)
|
|
if postFilename:
|
|
# is there a replies file for this post?
|
|
repliesFilename = postFilename.replace('.json', '.replies')
|
|
if os.path.isfile(repliesFilename):
|
|
# get items from the replies file
|
|
repliesJson = {
|
|
'orderedItems': []
|
|
}
|
|
populateRepliesJson(baseDir, nickname, domain,
|
|
repliesFilename, authorized, repliesJson)
|
|
# add items to the html output
|
|
for item in repliesJson['orderedItems']:
|
|
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, authorized,
|
|
False, False, False)
|
|
cssFilename = baseDir + '/epicyon-profile.css'
|
|
if os.path.isfile(baseDir + '/epicyon.css'):
|
|
cssFilename = baseDir + '/epicyon.css'
|
|
|
|
return htmlHeaderWithExternalStyle(cssFilename) + postStr + htmlFooter()
|
|
|
|
|
|
def htmlPostReplies(cssCache: {},
|
|
recentPostsCache: {}, maxRecentPosts: int,
|
|
translate: {}, baseDir: str,
|
|
session, wfRequest: {}, personCache: {},
|
|
nickname: str, domain: str, port: int, repliesJson: {},
|
|
httpPrefix: str, projectVersion: str,
|
|
YTReplacementDomain: str,
|
|
showPublishedDateOnly: bool) -> str:
|
|
"""Show the replies to an individual post as html
|
|
"""
|
|
iconsPath = getIconsWebPath(baseDir)
|
|
repliesStr = ''
|
|
if repliesJson.get('orderedItems'):
|
|
for item in repliesJson['orderedItems']:
|
|
repliesStr += \
|
|
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, False, False)
|
|
|
|
cssFilename = baseDir + '/epicyon-profile.css'
|
|
if os.path.isfile(baseDir + '/epicyon.css'):
|
|
cssFilename = baseDir + '/epicyon.css'
|
|
|
|
return htmlHeaderWithExternalStyle(cssFilename) + repliesStr + htmlFooter()
|