__filename__ = "webapp_timeline.py" __author__ = "Bob Mottram" __license__ = "AGPL3+" __version__ = "1.2.0" __maintainer__ = "Bob Mottram" __email__ = "bob@freedombone.net" __status__ = "Production" 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 webapp_utils 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 '
![' + translate['Approve follow requests'] + \
                        ' ' + \
                        translate['Approve follow requests'] + \
                        '](/icons/person.png) \n'
                    break
    _logTimelineTiming(enableTimingLog, timelineStartTime, boxName, '3')
    # moderation / reports button
    moderationButtonStr = ''
    if moderator and not minimal:
        moderationButtonStr = \
            ''
    # shares, bookmarks and events buttons
    sharesButtonStr = ''
    bookmarksButtonStr = ''
    eventsButtonStr = ''
    if not minimal:
        sharesButtonStr = \
            ''
        bookmarksButtonStr = \
            ''
#
#        eventsButtonStr = \
#            ''
    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 += '
\n'
                    break
    _logTimelineTiming(enableTimingLog, timelineStartTime, boxName, '3')
    # moderation / reports button
    moderationButtonStr = ''
    if moderator and not minimal:
        moderationButtonStr = \
            ''
    # shares, bookmarks and events buttons
    sharesButtonStr = ''
    bookmarksButtonStr = ''
    eventsButtonStr = ''
    if not minimal:
        sharesButtonStr = \
            ''
        bookmarksButtonStr = \
            ''
#
#        eventsButtonStr = \
#            ''
    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 += '![' + \
                translate['Create a new DM'] + \
                ' | ' + translate['Create a new DM'] + \
                '](/' + \
                'icons/newpost.png) \n'
        else:
            newPostButtonStr += \
                '' + \
                ''
    elif (boxName == 'tlblogs' or
          boxName == 'tlnews' or
          boxName == 'tlfeatures'):
        if not iconsAsButtons:
            newPostButtonStr += \
                '
\n'
        else:
            newPostButtonStr += \
                '' + \
                ''
    elif (boxName == 'tlblogs' or
          boxName == 'tlnews' or
          boxName == 'tlfeatures'):
        if not iconsAsButtons:
            newPostButtonStr += \
                '![' + \
                translate['Create a new post'] + ' | ' + \
                translate['Create a new post'] + \
                '](/' + \
                'icons/newpost.png) \n'
        else:
            newPostButtonStr += \
                '' + \
                ''
    elif boxName == 'tlevents':
        if not iconsAsButtons:
            newPostButtonStr += \
                '
\n'
        else:
            newPostButtonStr += \
                '' + \
                ''
    elif boxName == 'tlevents':
        if not iconsAsButtons:
            newPostButtonStr += \
                '![' + \
                translate['Create a new event'] + ' | ' + \
                translate['Create a new event'] + \
                '](/' + \
                'icons/newpost.png) \n'
        else:
            newPostButtonStr += \
                '' + \
                ''
    elif boxName == 'tlshares':
        if not iconsAsButtons:
            newPostButtonStr += \
                '
\n'
        else:
            newPostButtonStr += \
                '' + \
                ''
    elif boxName == 'tlshares':
        if not iconsAsButtons:
            newPostButtonStr += \
                '![' + \
                translate['Create a new shared item'] + ' | ' + \
                translate['Create a new shared item'] + \
                '](/' + \
                'icons/newpost.png) \n'
        else:
            newPostButtonStr += \
                '' + \
                ''
    else:
        if not manuallyApproveFollowers:
            if not iconsAsButtons:
                newPostButtonStr += \
                    '
\n'
        else:
            newPostButtonStr += \
                '' + \
                ''
    else:
        if not manuallyApproveFollowers:
            if not iconsAsButtons:
                newPostButtonStr += \
                    '![' + \
                    translate['Create a new post'] + ' | ' + \
                    translate['Create a new post'] + \
                    '](/' + \
                    'icons/newpost.png) \n'
            else:
                newPostButtonStr += \
                    '' + \
                    ''
        else:
            if not iconsAsButtons:
                newPostButtonStr += \
                    '
\n'
            else:
                newPostButtonStr += \
                    '' + \
                    ''
        else:
            if not iconsAsButtons:
                newPostButtonStr += \
                    '![' + \
                    translate['Create a new post'] + \
                    ' | ' + translate['Create a new post'] + \
                    '](/' + \
                    'icons/newpost.png) \n'
            else:
                newPostButtonStr += \
                    '' + \
                    ''
    # keyboard navigation
    calendarStr = translate['Calendar']
    if newCalendarEvent:
        calendarStr = '' + calendarStr + ''
    dmStr = translate['DM']
    if newDM:
        dmStr = '' + dmStr + ''
    repliesStr = translate['Replies']
    if newReply:
        repliesStr = '' + repliesStr + ''
    sharesStr = translate['Shares']
    if newShare:
        sharesStr = '' + sharesStr + ''
    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
#    menuEvents = \
#        htmlHideFromScreenReader('🎫') + ' ' + translate['Events']
    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'
    tlStr += htmlKeyboardNavigation(textModeBanner, navLinks, navAccessKeys,
                                    None, usersPath, translate,
                                    followApprovals)
    # banner and row of buttons
    tlStr += \
        '
\n'
            else:
                newPostButtonStr += \
                    '' + \
                    ''
    # keyboard navigation
    calendarStr = translate['Calendar']
    if newCalendarEvent:
        calendarStr = '' + calendarStr + ''
    dmStr = translate['DM']
    if newDM:
        dmStr = '' + dmStr + ''
    repliesStr = translate['Replies']
    if newReply:
        repliesStr = '' + repliesStr + ''
    sharesStr = translate['Shares']
    if newShare:
        sharesStr = '' + sharesStr + ''
    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
#    menuEvents = \
#        htmlHideFromScreenReader('🎫') + ' ' + translate['Events']
    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'
    tlStr += htmlKeyboardNavigation(textModeBanner, navLinks, navAccessKeys,
                                    None, usersPath, translate,
                                    followApprovals)
    # banner and row of buttons
    tlStr += \
        '| ' + \ leftColumnStr + '\n' # center column containing posts tlStr += ' | \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 += ' \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 += '\n'
    # second row of buttons for moderator actions
    if moderator and boxName == 'moderation':
        tlStr += \
            '\n'
    _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 = \
            '\n'
    # end of column-center
    tlStr += ' ![' + \
            translate['Page up'] + ' ' + \
            translate['Page up'] + '](/' + \
            'icons/pageup.png) \n' + \
            ' ' tlStr += ' \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 += '\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 += \
            ' ![' + \
            translate['Page down'] + ' ' + \
            translate['Page down'] + '](/' + \
            'icons/pagedown.png) \n' + \
            ' | ' + \ rightColumnStr + '\n' tlStr += ' | 
' + item['summary'] + '
\n' profileStr += \ '' + translate['Type'] + ': ' + item['itemType'] + ' ' profileStr += \ '' + translate['Category'] + ': ' + item['category'] + ' ' profileStr += \ '' + translate['Location'] + ': ' + item['location'] + '
\n' sharedesc = item['displayName'] if '<' not in sharedesc and '?' not in sharedesc: if showContact: contactActor = item['actor'] profileStr += \ '![' + translate['Page up'] + \
            ' ' + translate['Page up'] + '](/' + \
            'icons/pageup.png) \n' + \
            '
\n' + \
            '  ![' + translate['Page down'] + \
            ' ' + translate['Page down'] + '](/' + \
            'icons/pagedown.png) \n' + \
            '
\n' + \
            '