__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
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 '
\n' + \
markdownToHtml(helpText) + '\n' + \
'
\n'
return ''
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) -> 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'
# elif boxName == 'tlevents':
# eventsButton = '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 = \
'' + \
'\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'
# 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 = \
'
'
else:
textModeSeparator = ''
# page up arrow
if pageNumber > 1:
tlStr += textModeSeparator
tlStr += \
'
\n' + \
' \n' + \
'
\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 += ' '
tlStr += '
\n'
# show each post in the timeline
for item in timelineJson['orderedItems']:
if item['type'] == 'Create' or \
item['type'] == 'Announce' or \
item['type'] == 'Update':
# is the actor who sent this post snoozed?
if isPersonSnoozed(baseDir, nickname, domain, item['actor']):
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')
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,
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'
# page down arrow
if itemCtr > 2:
tlStr += textModeSeparator
tlStr += \
'
\n' + \
' \n' + \
'
\n'
tlStr += textModeSeparator
elif itemCtr == 0:
tlStr += _getHelpForTimeline(baseDir, boxName)
# end of timeline-posts
tlStr += '