__filename__ = "webapp_timeline.py" __author__ = "Bob Mottram" __license__ = "AGPL3+" __version__ = "1.2.0" __maintainer__ = "Bob Mottram" __email__ = "bob@freedombone.net" __status__ = "Production" __module_group__ = "Timeline" import os import time from shutil import copyfile from utils import dangerousMarkup from utils import getConfigParam from utils import getFullDomain from utils import isEditor from utils import removeIdEnding from follow import followerApprovalActive from person import isPersonSnoozed from markdown import markdownToHtml from webapp_utils import htmlKeyboardNavigation from webapp_utils import htmlHideFromScreenReader from webapp_utils import htmlPostSeparator from webapp_utils import getBannerFile from webapp_utils import htmlHeaderWithExternalStyle from webapp_utils import htmlFooter from webapp_utils import sharesTimelineJson from webapp_utils import htmlHighlightLabel from webapp_post import preparePostFromHtmlCache from webapp_post import individualPostAsHtml from webapp_column_left import getLeftColumnContent from webapp_column_right import getRightColumnContent from webapp_headerbuttons import headerButtonsTimeline from posts import isModerator from announce import isSelfAnnounce 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 _getHelpForTimeline(baseDir: str, boxName: str) -> str: """Shows help text for the given timeline """ # get the filename for help for this timeline helpFilename = baseDir + '/accounts/help_' + boxName + '.md' if not os.path.isfile(helpFilename): language = \ getConfigParam(baseDir, 'language') if not language: language = 'en' themeName = \ getConfigParam(baseDir, 'theme') defaultFilename = None if themeName: defaultFilename = \ baseDir + '/theme/' + themeName + '/welcome/' + \ 'help_' + boxName + '_' + language + '.md' if not os.path.isfile(defaultFilename): defaultFilename = None if not defaultFilename: defaultFilename = \ baseDir + '/defaultwelcome/' + \ 'help_' + boxName + '_' + language + '.md' if not os.path.isfile(defaultFilename): defaultFilename = \ baseDir + '/defaultwelcome/help_' + boxName + '_en.md' if os.path.isfile(defaultFilename): copyfile(defaultFilename, helpFilename) # show help text if os.path.isfile(helpFilename): instanceTitle = \ getConfigParam(baseDir, 'instanceTitle') if not instanceTitle: instanceTitle = 'Epicyon' with open(helpFilename, 'r') as helpFile: helpText = helpFile.read() if dangerousMarkup(helpText, False): return '' helpText = helpText.replace('INSTANCE', instanceTitle) return '<div class="container">\n' + \ markdownToHtml(helpText) + '\n' + \ '</div>\n' return '' def _htmlTimelineNewPost(manuallyApproveFollowers: bool, boxName: str, iconsAsButtons: bool, usersPath: str, translate: {}) -> str: """Returns html for the new post button """ newPostButtonStr = '' if boxName == 'dm': if not iconsAsButtons: newPostButtonStr += \ '<a class="imageAnchor" href="' + usersPath + \ '/newdm?nodropdown"><img loading="lazy" src="/' + \ 'icons/newpost.png" title="' + \ translate['Create a new DM'] + \ '" alt="| ' + translate['Create a new DM'] + \ '" class="timelineicon"/></a>\n' else: newPostButtonStr += \ '<a href="' + usersPath + '/newdm?nodropdown">' + \ '<button class="button"><span>' + \ translate['Post'] + ' </span></button></a>' elif (boxName == 'tlblogs' or boxName == 'tlnews' or boxName == 'tlfeatures'): if not iconsAsButtons: newPostButtonStr += \ '<a class="imageAnchor" href="' + usersPath + \ '/newblog"><img loading="lazy" src="/' + \ 'icons/newpost.png" title="' + \ translate['Create a new post'] + '" alt="| ' + \ translate['Create a new post'] + \ '" class="timelineicon"/></a>\n' else: newPostButtonStr += \ '<a href="' + usersPath + '/newblog">' + \ '<button class="button"><span>' + \ translate['Post'] + '</span></button></a>' elif boxName == 'tlshares': if not iconsAsButtons: newPostButtonStr += \ '<a class="imageAnchor" href="' + usersPath + \ '/newshare?nodropdown"><img loading="lazy" src="/' + \ 'icons/newpost.png" title="' + \ translate['Create a new shared item'] + '" alt="| ' + \ translate['Create a new shared item'] + \ '" class="timelineicon"/></a>\n' else: newPostButtonStr += \ '<a href="' + usersPath + '/newshare?nodropdown">' + \ '<button class="button"><span>' + \ translate['Post'] + '</span></button></a>' else: if not manuallyApproveFollowers: if not iconsAsButtons: newPostButtonStr += \ '<a class="imageAnchor" href="' + usersPath + \ '/newpost"><img loading="lazy" src="/' + \ 'icons/newpost.png" title="' + \ translate['Create a new post'] + '" alt="| ' + \ translate['Create a new post'] + \ '" class="timelineicon"/></a>\n' else: newPostButtonStr += \ '<a href="' + usersPath + '/newpost">' + \ '<button class="button"><span>' + \ translate['Post'] + '</span></button></a>' else: if not iconsAsButtons: newPostButtonStr += \ '<a class="imageAnchor" href="' + usersPath + \ '/newfollowers"><img loading="lazy" src="/' + \ 'icons/newpost.png" title="' + \ translate['Create a new post'] + \ '" alt="| ' + translate['Create a new post'] + \ '" class="timelineicon"/></a>\n' else: newPostButtonStr += \ '<a href="' + usersPath + '/newfollowers">' + \ '<button class="button"><span>' + \ translate['Post'] + '</span></button></a>' return newPostButtonStr def _htmlTimelineModerationButtons(moderator: bool, boxName: str, nickname: str, moderationActionStr: str, translate: {}) -> str: """Returns html for the moderation screen buttons """ tlStr = '' if moderator and boxName == 'moderation': tlStr += \ '<form id="modtimeline" method="POST" action="/users/' + \ nickname + '/moderationaction">' tlStr += '<div class="container">\n' idx = 'Nickname or URL. Block using *@domain or nickname@domain' tlStr += \ ' <b>' + translate[idx] + '</b><br>\n' if moderationActionStr: tlStr += ' <input type="text" ' + \ 'name="moderationAction" value="' + \ moderationActionStr + '" autofocus><br>\n' else: tlStr += ' <input type="text" ' + \ 'name="moderationAction" value="" autofocus><br>\n' tlStr += \ ' <input type="submit" title="' + \ translate['Information about current blocks/suspensions'] + \ '" alt="' + \ translate['Information about current blocks/suspensions'] + \ ' | " ' + \ 'name="submitInfo" value="' + translate['Info'] + '">\n' tlStr += \ ' <input type="submit" title="' + \ translate['Remove the above item'] + '" ' + \ 'alt="' + translate['Remove the above item'] + ' | " ' + \ 'name="submitRemove" value="' + \ translate['Remove'] + '">\n' tlStr += \ ' <input type="submit" title="' + \ translate['Suspend the above account nickname'] + '" ' + \ 'alt="' + \ translate['Suspend the above account nickname'] + ' | " ' + \ 'name="submitSuspend" value="' + translate['Suspend'] + '">\n' tlStr += \ ' <input type="submit" title="' + \ translate['Remove a suspension for an account nickname'] + '" ' + \ 'alt="' + \ translate['Remove a suspension for an account nickname'] + \ ' | " ' + \ 'name="submitUnsuspend" value="' + \ translate['Unsuspend'] + '">\n' tlStr += \ ' <input type="submit" title="' + \ translate['Block an account on another instance'] + '" ' + \ 'alt="' + \ translate['Block an account on another instance'] + ' | " ' + \ 'name="submitBlock" value="' + translate['Block'] + '">\n' tlStr += \ ' <input type="submit" title="' + \ translate['Unblock an account on another instance'] + '" ' + \ 'alt="' + \ translate['Unblock an account on another instance'] + ' | " ' + \ 'name="submitUnblock" value="' + translate['Unblock'] + '">\n' tlStr += \ ' <input type="submit" title="' + \ translate['Filter out words'] + '" ' + \ 'alt="' + \ translate['Filter out words'] + ' | " ' + \ 'name="submitFilter" value="' + translate['Filter'] + '">\n' tlStr += \ ' <input type="submit" title="' + \ translate['Unfilter words'] + '" ' + \ 'alt="' + \ translate['Unfilter words'] + ' | " ' + \ 'name="submitUnfilter" value="' + translate['Unfilter'] + '">\n' tlStr += '</div>\n</form>\n' return tlStr def _htmlTimelineKeyboard(moderator: bool, textModeBanner: str, usersPath: str, nickname: str, newCalendarEvent: bool, newDM: bool, newReply: bool, newShare: bool, followApprovals: bool, accessKeys: {}, translate: {}) -> str: """Returns html for timeline keyboard navigation """ calendarStr = translate['Calendar'] if newCalendarEvent: calendarStr = '<strong>' + calendarStr + '</strong>' dmStr = translate['DM'] if newDM: dmStr = '<strong>' + dmStr + '</strong>' repliesStr = translate['Replies'] if newReply: repliesStr = '<strong>' + repliesStr + '</strong>' sharesStr = translate['Shares'] if newShare: sharesStr = '<strong>' + sharesStr + '</strong>' menuProfile = \ htmlHideFromScreenReader('👤') + ' ' + \ translate['Switch to profile view'] menuInbox = \ htmlHideFromScreenReader('📥') + ' ' + translate['Inbox'] menuOutbox = \ htmlHideFromScreenReader('📤') + ' ' + translate['Sent'] menuSearch = \ htmlHideFromScreenReader('🔍') + ' ' + \ translate['Search and follow'] menuCalendar = \ htmlHideFromScreenReader('📅') + ' ' + calendarStr menuDM = \ htmlHideFromScreenReader('📩') + ' ' + dmStr menuReplies = \ htmlHideFromScreenReader('📨') + ' ' + repliesStr menuBookmarks = \ htmlHideFromScreenReader('🔖') + ' ' + translate['Bookmarks'] menuShares = \ htmlHideFromScreenReader('🤝') + ' ' + sharesStr menuBlogs = \ htmlHideFromScreenReader('📝') + ' ' + translate['Blogs'] menuNewswire = \ htmlHideFromScreenReader('📰') + ' ' + translate['Newswire'] menuLinks = \ htmlHideFromScreenReader('🔗') + ' ' + translate['Links'] menuNewPost = \ htmlHideFromScreenReader('➕') + ' ' + translate['Create a new post'] menuModeration = \ htmlHideFromScreenReader('⚡️') + ' ' + translate['Mod'] navLinks = { menuProfile: '/users/' + nickname, menuInbox: usersPath + '/inbox#timelineposts', menuSearch: usersPath + '/search', menuNewPost: usersPath + '/newpost', menuCalendar: usersPath + '/calendar', menuDM: usersPath + '/dm#timelineposts', menuReplies: usersPath + '/tlreplies#timelineposts', menuOutbox: usersPath + '/outbox#timelineposts', menuBookmarks: usersPath + '/tlbookmarks#timelineposts', menuShares: usersPath + '/tlshares#timelineposts', menuBlogs: usersPath + '/tlblogs#timelineposts', menuNewswire: usersPath + '/newswiremobile', menuLinks: usersPath + '/linksmobile' } navAccessKeys = {} for variableName, key in accessKeys.items(): if not locals().get(variableName): continue navAccessKeys[locals()[variableName]] = key if moderator: navLinks[menuModeration] = usersPath + '/moderation#modtimeline' return htmlKeyboardNavigation(textModeBanner, navLinks, navAccessKeys, None, usersPath, translate, followApprovals) def htmlTimeline(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, baseDir: str, cachedWebfingers: {}, personCache: {}, nickname: str, domain: str, port: int, timelineJson: {}, boxName: str, allowDeletion: bool, httpPrefix: str, projectVersion: str, manuallyApproveFollowers: bool, minimal: bool, YTReplacementDomain: str, showPublishedDateOnly: bool, newswire: {}, moderator: bool, editor: bool, positiveVoting: bool, showPublishAsIcon: bool, fullWidthTimelineButtonHeader: bool, iconsAsButtons: bool, rssIconAtTop: bool, publishButtonAtTop: bool, authorized: bool, moderationActionStr: str, theme: str, peertubeInstances: [], allowLocalNetworkAccess: bool, textModeBanner: str, accessKeys: {}) -> str: """Show the timeline as html """ enableTimingLog = False timelineStartTime = time.time() accountDir = baseDir + '/accounts/' + nickname + '@' + domain # should the calendar icon be highlighted? newCalendarEvent = False calendarImage = 'calendar.png' calendarPath = '/calendar' calendarFile = accountDir + '/.newCalendar' if os.path.isfile(calendarFile): newCalendarEvent = True calendarImage = 'calendar_notify.png' with open(calendarFile, 'r') as calfile: calendarPath = calfile.read().replace('##sent##', '') calendarPath = calendarPath.replace('\n', '').replace('\r', '') # should the DM button be highlighted? newDM = False dmFile = accountDir + '/.newDM' if os.path.isfile(dmFile): newDM = True if boxName == 'dm': os.remove(dmFile) # should the Replies button be highlighted? newReply = False replyFile = accountDir + '/.newReply' if os.path.isfile(replyFile): newReply = True if boxName == 'tlreplies': os.remove(replyFile) # should the Shares button be highlighted? newShare = False newShareFile = accountDir + '/.newShare' if os.path.isfile(newShareFile): newShare = True if boxName == 'tlshares': os.remove(newShareFile) # should the Moderation/reports button be highlighted? newReport = False newReportFile = accountDir + '/.newReport' if os.path.isfile(newReportFile): newReport = True if boxName == 'moderation': os.remove(newReportFile) separatorStr = '' if boxName != 'tlmedia': separatorStr = htmlPostSeparator(baseDir, None) # the css filename cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' # filename of the banner shown at the top bannerFile, bannerFilename = \ getBannerFile(baseDir, nickname, domain, theme) _logTimelineTiming(enableTimingLog, timelineStartTime, boxName, '1') # is the user a moderator? if not moderator: moderator = isModerator(baseDir, nickname) # is the user a site editor? if not editor: editor = isEditor(baseDir, nickname) _logTimelineTiming(enableTimingLog, timelineStartTime, boxName, '2') # the appearance of buttons - highlighted or not inboxButton = 'button' blogsButton = 'button' featuresButton = 'button' newsButton = 'button' dmButton = 'button' if newDM: dmButton = 'buttonhighlighted' repliesButton = 'button' if newReply: repliesButton = 'buttonhighlighted' mediaButton = 'button' bookmarksButton = 'button' # eventsButton = 'button' sentButton = 'button' sharesButton = 'button' if newShare: sharesButton = 'buttonhighlighted' moderationButton = 'button' if newReport: moderationButton = 'buttonhighlighted' if boxName == 'inbox': inboxButton = 'buttonselected' elif boxName == 'tlblogs': blogsButton = 'buttonselected' elif boxName == 'tlfeatures': featuresButton = 'buttonselected' elif boxName == 'tlnews': newsButton = 'buttonselected' elif boxName == 'dm': dmButton = 'buttonselected' if newDM: dmButton = 'buttonselectedhighlighted' elif boxName == 'tlreplies': repliesButton = 'buttonselected' if newReply: repliesButton = 'buttonselectedhighlighted' elif boxName == 'tlmedia': mediaButton = 'buttonselected' elif boxName == 'outbox': sentButton = 'buttonselected' elif boxName == 'moderation': moderationButton = 'buttonselected' if newReport: moderationButton = 'buttonselectedhighlighted' elif boxName == 'tlshares': sharesButton = 'buttonselected' if newShare: sharesButton = 'buttonselectedhighlighted' elif boxName == 'tlbookmarks' or boxName == 'bookmarks': bookmarksButton = 'buttonselected' # get the full domain, including any port number fullDomain = getFullDomain(domain, port) usersPath = '/users/' + nickname actor = httpPrefix + '://' + fullDomain + usersPath showIndividualPostIcons = True # show an icon for new follow approvals followApprovals = '' followRequestsFilename = \ baseDir + '/accounts/' + \ nickname + '@' + domain + '/followrequests.txt' if os.path.isfile(followRequestsFilename): with open(followRequestsFilename, 'r') as f: for line in f: if len(line) > 0: # show follow approvals icon followApprovals = \ '<a href="' + usersPath + \ '/followers#buttonheader" ' + \ 'accesskey="' + accessKeys['followButton'] + '">' + \ '<img loading="lazy" ' + \ 'class="timelineicon" alt="' + \ translate['Approve follow requests'] + \ '" title="' + translate['Approve follow requests'] + \ '" src="/icons/person.png"/></a>\n' break _logTimelineTiming(enableTimingLog, timelineStartTime, boxName, '3') # moderation / reports button moderationButtonStr = '' if moderator and not minimal: moderationButtonStr = \ '<a href="' + usersPath + \ '/moderation"><button class="' + \ moderationButton + '"><span>' + \ htmlHighlightLabel(translate['Mod'], newReport) + \ ' </span></button></a>' # shares, bookmarks and events buttons sharesButtonStr = '' bookmarksButtonStr = '' eventsButtonStr = '' if not minimal: sharesButtonStr = \ '<a href="' + usersPath + '/tlshares"><button class="' + \ sharesButton + '"><span>' + \ htmlHighlightLabel(translate['Shares'], newShare) + \ '</span></button></a>' bookmarksButtonStr = \ '<a href="' + usersPath + '/tlbookmarks"><button class="' + \ bookmarksButton + '"><span>' + translate['Bookmarks'] + \ '</span></button></a>' instanceTitle = \ getConfigParam(baseDir, 'instanceTitle') tlStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle) _logTimelineTiming(enableTimingLog, timelineStartTime, boxName, '4') # if this is a news instance and we are viewing the news timeline newsHeader = False if defaultTimeline == 'tlfeatures' and boxName == 'tlfeatures': newsHeader = True newPostButtonStr = '' # start of headericons div if not newsHeader: if not iconsAsButtons: newPostButtonStr += '<div class="headericons">' # what screen to go to when a new post is created newPostButtonStr += \ _htmlTimelineNewPost(manuallyApproveFollowers, boxName, iconsAsButtons, usersPath, translate) # keyboard navigation tlStr += \ _htmlTimelineKeyboard(moderator, textModeBanner, usersPath, nickname, newCalendarEvent, newDM, newReply, newShare, followApprovals, accessKeys, translate) # banner and row of buttons tlStr += \ '<header>\n' + \ '<a href="/users/' + nickname + '" title="' + \ translate['Switch to profile view'] + '" alt="' + \ translate['Switch to profile view'] + '">\n' tlStr += '<img loading="lazy" class="timeline-banner" ' + \ 'alt="" ' + \ 'src="' + usersPath + '/' + bannerFile + '" /></a>\n' + \ '</header>\n' if fullWidthTimelineButtonHeader: tlStr += \ headerButtonsTimeline(defaultTimeline, boxName, pageNumber, translate, usersPath, mediaButton, blogsButton, featuresButton, newsButton, inboxButton, dmButton, newDM, repliesButton, newReply, minimal, sentButton, sharesButtonStr, bookmarksButtonStr, eventsButtonStr, moderationButtonStr, newPostButtonStr, baseDir, nickname, domain, timelineStartTime, newCalendarEvent, calendarPath, calendarImage, followApprovals, iconsAsButtons, accessKeys) # start the timeline tlStr += '<table class="timeline">\n' tlStr += ' <colgroup>\n' tlStr += ' <col span="1" class="column-left">\n' tlStr += ' <col span="1" class="column-center">\n' tlStr += ' <col span="1" class="column-right">\n' tlStr += ' </colgroup>\n' tlStr += ' <tbody>\n' tlStr += ' <tr>\n' domainFull = getFullDomain(domain, port) # left column leftColumnStr = \ getLeftColumnContent(baseDir, nickname, domainFull, httpPrefix, translate, editor, False, None, rssIconAtTop, True, False, theme, accessKeys) tlStr += ' <td valign="top" class="col-left" ' + \ 'id="links" tabindex="-1">' + \ leftColumnStr + ' </td>\n' # center column containing posts tlStr += ' <td valign="top" class="col-center">\n' if not fullWidthTimelineButtonHeader: tlStr += \ headerButtonsTimeline(defaultTimeline, boxName, pageNumber, translate, usersPath, mediaButton, blogsButton, featuresButton, newsButton, inboxButton, dmButton, newDM, repliesButton, newReply, minimal, sentButton, sharesButtonStr, bookmarksButtonStr, eventsButtonStr, moderationButtonStr, newPostButtonStr, baseDir, nickname, domain, timelineStartTime, newCalendarEvent, calendarPath, calendarImage, followApprovals, iconsAsButtons, accessKeys) tlStr += ' <div id="timelineposts" class="timeline-posts">\n' # second row of buttons for moderator actions tlStr += \ _htmlTimelineModerationButtons(moderator, boxName, nickname, moderationActionStr, translate) _logTimelineTiming(enableTimingLog, timelineStartTime, boxName, '6') if boxName == 'tlshares': maxSharesPerAccount = itemsPerPage return (tlStr + _htmlSharesTimeline(translate, pageNumber, itemsPerPage, baseDir, actor, nickname, domain, port, maxSharesPerAccount, httpPrefix) + htmlFooter()) _logTimelineTiming(enableTimingLog, timelineStartTime, boxName, '7') # separator between posts which only appears in shell browsers # such as Lynx and is not read by screen readers if boxName != 'tlmedia': textModeSeparator = \ '<div class="transparent"><hr></div>' else: textModeSeparator = '' # page up arrow if pageNumber > 1: tlStr += textModeSeparator tlStr += \ ' <center>\n' + \ ' <a href="' + usersPath + '/' + boxName + \ '?page=' + str(pageNumber - 1) + \ '" accesskey="' + accessKeys['Page up'] + '">' + \ '<img loading="lazy" class="pageicon" src="/' + \ 'icons/pageup.png" title="' + \ translate['Page up'] + '" alt="' + \ translate['Page up'] + '"></a>\n' + \ ' </center>\n' # show the posts itemCtr = 0 if timelineJson: # if this is the media timeline then add an extra gallery container if boxName == 'tlmedia': if pageNumber > 1: tlStr += '<br>' tlStr += '<div class="galleryContainer">\n' # show each post in the timeline for item in timelineJson['orderedItems']: if item['type'] == 'Create' or \ item['type'] == 'Announce': # is the actor who sent this post snoozed? if isPersonSnoozed(baseDir, nickname, domain, item['actor']): continue if isSelfAnnounce(item): continue # is the post in the memory cache of recent ones? currTlStr = None if boxName != 'tlmedia' and \ recentPostsCache.get('index'): postId = \ removeIdEnding(item['id']).replace('/', '#') if postId in recentPostsCache['index']: if not item.get('muted'): if recentPostsCache['html'].get(postId): currTlStr = recentPostsCache['html'][postId] currTlStr = \ preparePostFromHtmlCache(nickname, currTlStr, boxName, pageNumber) _logTimelineTiming(enableTimingLog, timelineStartTime, boxName, '10') else: print('Muted post in timeline ' + boxName) if not currTlStr: _logTimelineTiming(enableTimingLog, timelineStartTime, boxName, '11') # read the post from disk currTlStr = \ individualPostAsHtml(False, recentPostsCache, maxRecentPosts, translate, pageNumber, baseDir, session, cachedWebfingers, personCache, nickname, domain, port, item, None, True, allowDeletion, httpPrefix, projectVersion, boxName, YTReplacementDomain, showPublishedDateOnly, peertubeInstances, allowLocalNetworkAccess, theme, boxName != 'dm', showIndividualPostIcons, manuallyApproveFollowers, False, True) _logTimelineTiming(enableTimingLog, timelineStartTime, boxName, '12') if currTlStr: itemCtr += 1 tlStr += textModeSeparator + currTlStr if separatorStr: tlStr += separatorStr if boxName == 'tlmedia': tlStr += '</div>\n' if itemCtr < 3: print('Items added to html timeline ' + boxName + ': ' + str(itemCtr) + ' ' + str(timelineJson['orderedItems'])) # page down arrow if itemCtr > 0: tlStr += textModeSeparator tlStr += \ ' <center>\n' + \ ' <a href="' + usersPath + '/' + boxName + '?page=' + \ str(pageNumber + 1) + \ '" accesskey="' + accessKeys['Page down'] + '">' + \ '<img loading="lazy" class="pageicon" src="/' + \ 'icons/pagedown.png" title="' + \ translate['Page down'] + '" alt="' + \ translate['Page down'] + '"></a>\n' + \ ' </center>\n' tlStr += textModeSeparator elif itemCtr == 0: tlStr += _getHelpForTimeline(baseDir, boxName) # end of timeline-posts tlStr += ' </div>\n' # end of column-center tlStr += ' </td>\n' # right column rightColumnStr = getRightColumnContent(baseDir, nickname, domainFull, httpPrefix, translate, moderator, editor, newswire, positiveVoting, False, None, True, showPublishAsIcon, rssIconAtTop, publishButtonAtTop, authorized, True, theme, defaultTimeline, accessKeys) tlStr += ' <td valign="top" class="col-right" ' + \ 'id="newswire" tabindex="-1">' + \ rightColumnStr + ' </td>\n' tlStr += ' </tr>\n' _logTimelineTiming(enableTimingLog, timelineStartTime, boxName, '9') tlStr += ' </tbody>\n' tlStr += '</table>\n' tlStr += htmlFooter() return tlStr def htmlIndividualShare(actor: str, item: {}, translate: {}, showContact: bool, removeButton: bool) -> str: """Returns an individual shared item as html """ profileStr = '<div class="container">\n' profileStr += '<p class="share-title">' + item['displayName'] + '</p>\n' if item.get('imageUrl'): profileStr += '<a href="' + item['imageUrl'] + '">\n' profileStr += \ '<img loading="lazy" src="' + item['imageUrl'] + \ '" alt="' + translate['Item image'] + '">\n</a>\n' profileStr += '<p>' + item['summary'] + '</p>\n' profileStr += \ '<p><b>' + translate['Type'] + ':</b> ' + item['itemType'] + ' ' profileStr += \ '<b>' + translate['Category'] + ':</b> ' + item['category'] + ' ' profileStr += \ '<b>' + translate['Location'] + ':</b> ' + item['location'] + '</p>\n' sharedesc = item['displayName'] if '<' not in sharedesc and '?' not in sharedesc: if showContact: contactActor = item['actor'] profileStr += \ '<p><a href="' + actor + \ '?replydm=sharedesc:' + sharedesc + \ '?mention=' + contactActor + '"><button class="button">' + \ translate['Contact'] + '</button></a>\n' if removeButton: profileStr += \ ' <a href="' + actor + '?rmshare=' + sharedesc + \ '"><button class="button">' + \ translate['Remove'] + '</button></a>\n' profileStr += '</div>\n' return profileStr def _htmlSharesTimeline(translate: {}, pageNumber: int, itemsPerPage: int, baseDir: str, actor: str, nickname: str, domain: str, port: int, maxSharesPerAccount: int, httpPrefix: str) -> str: """Show shared items timeline as html """ sharesJson, lastPage = \ sharesTimelineJson(actor, pageNumber, itemsPerPage, baseDir, maxSharesPerAccount) domainFull = getFullDomain(domain, port) actor = httpPrefix + '://' + domainFull + '/users/' + nickname timelineStr = '' if pageNumber > 1: timelineStr += \ ' <center>\n' + \ ' <a href="' + actor + '/tlshares?page=' + \ str(pageNumber - 1) + \ '"><img loading="lazy" class="pageicon" src="/' + \ 'icons/pageup.png" title="' + translate['Page up'] + \ '" alt="' + translate['Page up'] + '"></a>\n' + \ ' </center>\n' separatorStr = htmlPostSeparator(baseDir, None) ctr = 0 for published, item in sharesJson.items(): showContactButton = False if item['actor'] != actor: showContactButton = True showRemoveButton = False if item['actor'] == actor: showRemoveButton = True timelineStr += \ htmlIndividualShare(actor, item, translate, showContactButton, showRemoveButton) timelineStr += separatorStr ctr += 1 if ctr == 0: timelineStr += _getHelpForTimeline(baseDir, 'tlshares') if not lastPage: timelineStr += \ ' <center>\n' + \ ' <a href="' + actor + '/tlshares?page=' + \ str(pageNumber + 1) + \ '"><img loading="lazy" class="pageicon" src="/' + \ 'icons/pagedown.png" title="' + translate['Page down'] + \ '" alt="' + translate['Page down'] + '"></a>\n' + \ ' </center>\n' return timelineStr def htmlShares(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, baseDir: str, cachedWebfingers: {}, personCache: {}, nickname: str, domain: str, port: int, allowDeletion: bool, httpPrefix: str, projectVersion: str, YTReplacementDomain: str, showPublishedDateOnly: bool, newswire: {}, positiveVoting: bool, showPublishAsIcon: bool, fullWidthTimelineButtonHeader: bool, iconsAsButtons: bool, rssIconAtTop: bool, publishButtonAtTop: bool, authorized: bool, theme: str, peertubeInstances: [], allowLocalNetworkAccess: bool, textModeBanner: str, accessKeys: {}) -> str: """Show the shares timeline as html """ manuallyApproveFollowers = \ followerApprovalActive(baseDir, nickname, domain) return htmlTimeline(cssCache, defaultTimeline, recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, cachedWebfingers, personCache, nickname, domain, port, None, 'tlshares', allowDeletion, httpPrefix, projectVersion, manuallyApproveFollowers, False, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, positiveVoting, showPublishAsIcon, fullWidthTimelineButtonHeader, iconsAsButtons, rssIconAtTop, publishButtonAtTop, authorized, None, theme, peertubeInstances, allowLocalNetworkAccess, textModeBanner, accessKeys) def htmlInbox(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, baseDir: str, cachedWebfingers: {}, 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, theme: str, peertubeInstances: [], allowLocalNetworkAccess: bool, textModeBanner: str, accessKeys: {}) -> str: """Show the inbox as html """ manuallyApproveFollowers = \ followerApprovalActive(baseDir, nickname, domain) return htmlTimeline(cssCache, defaultTimeline, recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, cachedWebfingers, personCache, nickname, domain, port, inboxJson, 'inbox', allowDeletion, httpPrefix, projectVersion, manuallyApproveFollowers, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, positiveVoting, showPublishAsIcon, fullWidthTimelineButtonHeader, iconsAsButtons, rssIconAtTop, publishButtonAtTop, authorized, None, theme, peertubeInstances, allowLocalNetworkAccess, textModeBanner, accessKeys) def htmlBookmarks(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, baseDir: str, cachedWebfingers: {}, personCache: {}, nickname: str, domain: str, port: int, bookmarksJson: {}, 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, theme: str, peertubeInstances: [], allowLocalNetworkAccess: bool, textModeBanner: str, accessKeys: {}) -> str: """Show the bookmarks as html """ manuallyApproveFollowers = \ followerApprovalActive(baseDir, nickname, domain) return htmlTimeline(cssCache, defaultTimeline, recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, cachedWebfingers, personCache, nickname, domain, port, bookmarksJson, 'tlbookmarks', allowDeletion, httpPrefix, projectVersion, manuallyApproveFollowers, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, positiveVoting, showPublishAsIcon, fullWidthTimelineButtonHeader, iconsAsButtons, rssIconAtTop, publishButtonAtTop, authorized, None, theme, peertubeInstances, allowLocalNetworkAccess, textModeBanner, accessKeys) def htmlInboxDMs(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, baseDir: str, cachedWebfingers: {}, 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, theme: str, peertubeInstances: [], allowLocalNetworkAccess: bool, textModeBanner: str, accessKeys: {}) -> str: """Show the DM timeline as html """ return htmlTimeline(cssCache, defaultTimeline, recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, cachedWebfingers, personCache, nickname, domain, port, inboxJson, 'dm', allowDeletion, httpPrefix, projectVersion, False, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, positiveVoting, showPublishAsIcon, fullWidthTimelineButtonHeader, iconsAsButtons, rssIconAtTop, publishButtonAtTop, authorized, None, theme, peertubeInstances, allowLocalNetworkAccess, textModeBanner, accessKeys) def htmlInboxReplies(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, baseDir: str, cachedWebfingers: {}, 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, theme: str, peertubeInstances: [], allowLocalNetworkAccess: bool, textModeBanner: str, accessKeys: {}) -> str: """Show the replies timeline as html """ return htmlTimeline(cssCache, defaultTimeline, recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, cachedWebfingers, personCache, nickname, domain, port, inboxJson, 'tlreplies', allowDeletion, httpPrefix, projectVersion, False, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, positiveVoting, showPublishAsIcon, fullWidthTimelineButtonHeader, iconsAsButtons, rssIconAtTop, publishButtonAtTop, authorized, None, theme, peertubeInstances, allowLocalNetworkAccess, textModeBanner, accessKeys) def htmlInboxMedia(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, baseDir: str, cachedWebfingers: {}, 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, theme: str, peertubeInstances: [], allowLocalNetworkAccess: bool, textModeBanner: str, accessKeys: {}) -> str: """Show the media timeline as html """ return htmlTimeline(cssCache, defaultTimeline, recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, cachedWebfingers, personCache, nickname, domain, port, inboxJson, 'tlmedia', allowDeletion, httpPrefix, projectVersion, False, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, positiveVoting, showPublishAsIcon, fullWidthTimelineButtonHeader, iconsAsButtons, rssIconAtTop, publishButtonAtTop, authorized, None, theme, peertubeInstances, allowLocalNetworkAccess, textModeBanner, accessKeys) def htmlInboxBlogs(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, baseDir: str, cachedWebfingers: {}, 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, theme: str, peertubeInstances: [], allowLocalNetworkAccess: bool, textModeBanner: str, accessKeys: {}) -> str: """Show the blogs timeline as html """ return htmlTimeline(cssCache, defaultTimeline, recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, cachedWebfingers, personCache, nickname, domain, port, inboxJson, 'tlblogs', allowDeletion, httpPrefix, projectVersion, False, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, positiveVoting, showPublishAsIcon, fullWidthTimelineButtonHeader, iconsAsButtons, rssIconAtTop, publishButtonAtTop, authorized, None, theme, peertubeInstances, allowLocalNetworkAccess, textModeBanner, accessKeys) def htmlInboxFeatures(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, baseDir: str, cachedWebfingers: {}, 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, theme: str, peertubeInstances: [], allowLocalNetworkAccess: bool, textModeBanner: str, accessKeys: {}) -> str: """Show the features timeline as html """ return htmlTimeline(cssCache, defaultTimeline, recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, cachedWebfingers, personCache, nickname, domain, port, inboxJson, 'tlfeatures', allowDeletion, httpPrefix, projectVersion, False, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, positiveVoting, showPublishAsIcon, fullWidthTimelineButtonHeader, iconsAsButtons, rssIconAtTop, publishButtonAtTop, authorized, None, theme, peertubeInstances, allowLocalNetworkAccess, textModeBanner, accessKeys) def htmlInboxNews(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, baseDir: str, cachedWebfingers: {}, personCache: {}, nickname: str, domain: str, port: int, inboxJson: {}, allowDeletion: bool, httpPrefix: str, projectVersion: str, minimal: bool, YTReplacementDomain: str, showPublishedDateOnly: bool, newswire: {}, moderator: bool, editor: bool, positiveVoting: bool, showPublishAsIcon: bool, fullWidthTimelineButtonHeader: bool, iconsAsButtons: bool, rssIconAtTop: bool, publishButtonAtTop: bool, authorized: bool, theme: str, peertubeInstances: [], allowLocalNetworkAccess: bool, textModeBanner: str, accessKeys: {}) -> str: """Show the news timeline as html """ return htmlTimeline(cssCache, defaultTimeline, recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, cachedWebfingers, personCache, nickname, domain, port, inboxJson, 'tlnews', allowDeletion, httpPrefix, projectVersion, False, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, moderator, editor, positiveVoting, showPublishAsIcon, fullWidthTimelineButtonHeader, iconsAsButtons, rssIconAtTop, publishButtonAtTop, authorized, None, theme, peertubeInstances, allowLocalNetworkAccess, textModeBanner, accessKeys) def htmlOutbox(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, baseDir: str, cachedWebfingers: {}, personCache: {}, nickname: str, domain: str, port: int, outboxJson: {}, 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, theme: str, peertubeInstances: [], allowLocalNetworkAccess: bool, textModeBanner: str, accessKeys: {}) -> str: """Show the Outbox as html """ manuallyApproveFollowers = \ followerApprovalActive(baseDir, nickname, domain) return htmlTimeline(cssCache, defaultTimeline, recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, cachedWebfingers, personCache, nickname, domain, port, outboxJson, 'outbox', allowDeletion, httpPrefix, projectVersion, manuallyApproveFollowers, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, positiveVoting, showPublishAsIcon, fullWidthTimelineButtonHeader, iconsAsButtons, rssIconAtTop, publishButtonAtTop, authorized, None, theme, peertubeInstances, allowLocalNetworkAccess, textModeBanner, accessKeys)