__filename__ = "webapp_timeline.py" __author__ = "Bob Mottram" __license__ = "AGPL3+" __version__ = "1.2.0" __maintainer__ = "Bob Mottram" __email__ = "bob@libreserver.org" __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 utils import acctDir from utils import isfloat from utils import localActorUrl 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 '
' + \ 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, wantedButtonStr,
bookmarksButtonStr,
eventsButtonStr, moderationButtonStr,
newPostButtonStr, baseDir, nickname,
domain, timelineStartTime,
newCalendarEvent, calendarPath,
calendarImage, followApprovals,
iconsAsButtons, accessKeys)
tlStr += ' \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,
sharedItemsFederatedDomains, 'shares') +
_htmlTimelineEnd(baseDir, nickname, domainFull,
httpPrefix, translate,
moderator, editor,
newswire, positiveVoting,
showPublishAsIcon,
rssIconAtTop, publishButtonAtTop,
authorized, theme,
defaultTimeline, accessKeys,
boxName,
enableTimingLog, timelineStartTime) +
htmlFooter())
elif boxName == 'tlwanted':
maxSharesPerAccount = itemsPerPage
return (tlStr +
_htmlSharesTimeline(translate, pageNumber, itemsPerPage,
baseDir, actor, nickname, domain, port,
maxSharesPerAccount, httpPrefix,
sharedItemsFederatedDomains, 'wanted') +
_htmlTimelineEnd(baseDir, nickname, domainFull,
httpPrefix, translate,
moderator, editor,
newswire, positiveVoting,
showPublishAsIcon,
rssIconAtTop, publishButtonAtTop,
authorized, theme,
defaultTimeline, accessKeys,
boxName,
enableTimingLog, timelineStartTime) +
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 = \
' ' 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(signingPrivateKeyPem,
False, recentPostsCache,
maxRecentPosts,
translate, pageNumber,
baseDir, session,
cachedWebfingers,
personCache,
nickname, domain, port,
item, None, True,
allowDeletion,
httpPrefix, projectVersion,
boxName,
YTReplacementDomain,
showPublishedDateOnly,
peertubeInstances,
allowLocalNetworkAccess,
theme, systemLanguage,
maxLikeCount,
boxName != 'dm',
showIndividualPostIcons,
manuallyApproveFollowers,
False, True, useCacheOnly)
_logTimelineTiming(enableTimingLog,
timelineStartTime, boxName, '12')
if currTlStr:
if currTlStr not in tlStr:
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 += \
' \n'
profileStr += '\n'
profileStr += \
'\n\n'
profileStr += ' \n'
return profileStr
def _htmlSharesTimeline(translate: {}, pageNumber: int, itemsPerPage: int,
baseDir: str, actor: str,
nickname: str, domain: str, port: int,
maxSharesPerAccount: int, httpPrefix: str,
sharedItemsFederatedDomains: [],
sharesFileType: str) -> str:
"""Show shared items timeline as html
"""
sharesJson, lastPage = \
sharesTimelineJson(actor, pageNumber, itemsPerPage,
baseDir, domain, nickname, maxSharesPerAccount,
sharedItemsFederatedDomains, sharesFileType)
domainFull = getFullDomain(domain, port)
actor = localActorUrl(httpPrefix, nickname, domainFull)
adminNickname = getConfigParam(baseDir, 'admin')
adminActor = ''
if adminNickname:
adminActor = \
localActorUrl(httpPrefix, adminNickname, domainFull)
timelineStr = ''
if pageNumber > 1:
timelineStr += \
' \n'
if item.get('imageUrl'):
profileStr += '' + item['summary'] + ' \n'
if item.get('itemQty'):
if item['itemQty'] > 1:
profileStr += \
'' + translate['Quantity'] + ': ' + \
str(item['itemQty']) + ' ' + \ '\n' profileStr += \ '\n' if removeButton and domain in shareId: if sharesFileType == 'shares': profileStr += \ ' \n' else: profileStr += \ ' \n' profileStr += ' |