Modern ActivityPub compliant server, designed for simplicity and accessibility. Includes calendar and sharing economy features to empower your federated community. https://code.freedombone.net/bashrc/epicyon Docs: https://epicyon.net/#install
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

webapp_post.py 74KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869
  1. __filename__ = "webapp_post.py"
  2. __author__ = "Bob Mottram"
  3. __license__ = "AGPL3+"
  4. __version__ = "1.2.0"
  5. __maintainer__ = "Bob Mottram"
  6. __email__ = "bob@freedombone.net"
  7. __status__ = "Production"
  8. import os
  9. import time
  10. from dateutil.parser import parse
  11. from auth import createPassword
  12. from git import isGitPatch
  13. from datetime import datetime
  14. from cache import getPersonFromCache
  15. from bookmarks import bookmarkedByPerson
  16. from like import likedByPerson
  17. from like import noOfLikes
  18. from follow import isFollowingActor
  19. from posts import postIsMuted
  20. from posts import getPersonBox
  21. from posts import downloadAnnounce
  22. from posts import populateRepliesJson
  23. from utils import updateAnnounceCollection
  24. from utils import isPGPEncrypted
  25. from utils import isDM
  26. from utils import rejectPostId
  27. from utils import isRecentPost
  28. from utils import getConfigParam
  29. from utils import getFullDomain
  30. from utils import isEditor
  31. from utils import locatePost
  32. from utils import loadJson
  33. from utils import getCachedPostDirectory
  34. from utils import getCachedPostFilename
  35. from utils import getProtocolPrefixes
  36. from utils import isNewsPost
  37. from utils import isBlogPost
  38. from utils import getDisplayName
  39. from utils import isPublicPost
  40. from utils import updateRecentPostsCache
  41. from utils import removeIdEnding
  42. from utils import getNicknameFromActor
  43. from utils import getDomainFromActor
  44. from utils import isEventPost
  45. from content import replaceEmojiFromTags
  46. from content import htmlReplaceQuoteMarks
  47. from content import htmlReplaceEmailQuote
  48. from content import removeTextFormatting
  49. from content import removeLongWords
  50. from content import getMentionsFromHtml
  51. from content import switchWords
  52. from person import isPersonSnoozed
  53. from announce import announcedByPerson
  54. from webapp_utils import getAvatarImageUrl
  55. from webapp_utils import getPersonAvatarUrl
  56. from webapp_utils import updateAvatarImageCache
  57. from webapp_utils import loadIndividualPostAsHtmlFromCache
  58. from webapp_utils import addEmojiToDisplayName
  59. from webapp_utils import postContainsPublic
  60. from webapp_utils import getContentWarningButton
  61. from webapp_utils import getPostAttachmentsAsHtml
  62. from webapp_utils import htmlHeaderWithExternalStyle
  63. from webapp_utils import htmlFooter
  64. from webapp_utils import getBrokenLinkSubstitute
  65. from webapp_media import addEmbeddedElements
  66. from webapp_question import insertQuestion
  67. from devices import E2EEdecryptMessageFromDevice
  68. from webfinger import webfingerHandle
  69. from speaker import updateSpeaker
  70. def _logPostTiming(enableTimingLog: bool, postStartTime, debugId: str) -> None:
  71. """Create a log of timings for performance tuning
  72. """
  73. if not enableTimingLog:
  74. return
  75. timeDiff = int((time.time() - postStartTime) * 1000)
  76. if timeDiff > 100:
  77. print('TIMING INDIV ' + debugId + ' = ' + str(timeDiff))
  78. def prepareHtmlPostNickname(nickname: str, postHtml: str) -> str:
  79. """html posts stored in memory are for all accounts on the instance
  80. and they're indexed by id. However, some incoming posts may be
  81. destined for multiple accounts (followers). This creates a problem
  82. where the icon links whose urls begin with href="/users/nickname?
  83. need to be changed for different nicknames to display correctly
  84. within their timelines.
  85. This function changes the nicknames for the icon links.
  86. """
  87. # replace the nickname
  88. usersStr = ' href="/users/'
  89. if usersStr not in postHtml:
  90. return postHtml
  91. userFound = True
  92. postStr = postHtml
  93. newPostStr = ''
  94. while userFound:
  95. if usersStr not in postStr:
  96. newPostStr += postStr
  97. break
  98. # the next part, after href="/users/nickname?
  99. nextStr = postStr.split(usersStr, 1)[1]
  100. if '?' in nextStr:
  101. nextStr = nextStr.split('?', 1)[1]
  102. else:
  103. newPostStr += postStr
  104. break
  105. # append the previous text to the result
  106. newPostStr += postStr.split(usersStr)[0]
  107. newPostStr += usersStr + nickname + '?'
  108. # post is now the next part
  109. postStr = nextStr
  110. return newPostStr
  111. def preparePostFromHtmlCache(nickname: str, postHtml: str, boxName: str,
  112. pageNumber: int) -> str:
  113. """Sets the page number on a cached html post
  114. """
  115. # if on the bookmarks timeline then remain there
  116. if boxName == 'tlbookmarks' or boxName == 'bookmarks':
  117. postHtml = postHtml.replace('?tl=inbox', '?tl=tlbookmarks')
  118. if '?page=' in postHtml:
  119. pageNumberStr = postHtml.split('?page=')[1]
  120. if '?' in pageNumberStr:
  121. pageNumberStr = pageNumberStr.split('?')[0]
  122. postHtml = postHtml.replace('?page=' + pageNumberStr, '?page=-999')
  123. withPageNumber = postHtml.replace(';-999;', ';' + str(pageNumber) + ';')
  124. withPageNumber = withPageNumber.replace('?page=-999',
  125. '?page=' + str(pageNumber))
  126. return prepareHtmlPostNickname(nickname, withPageNumber)
  127. def _saveIndividualPostAsHtmlToCache(baseDir: str,
  128. nickname: str, domain: str,
  129. postJsonObject: {},
  130. postHtml: str) -> bool:
  131. """Saves the given html for a post to a cache file
  132. This is so that it can be quickly reloaded on subsequent
  133. refresh of the timeline
  134. """
  135. htmlPostCacheDir = \
  136. getCachedPostDirectory(baseDir, nickname, domain)
  137. cachedPostFilename = \
  138. getCachedPostFilename(baseDir, nickname, domain, postJsonObject)
  139. # create the cache directory if needed
  140. if not os.path.isdir(htmlPostCacheDir):
  141. os.mkdir(htmlPostCacheDir)
  142. try:
  143. with open(cachedPostFilename, 'w+') as fp:
  144. fp.write(postHtml)
  145. return True
  146. except Exception as e:
  147. print('ERROR: saving post to cache ' + str(e))
  148. return False
  149. def _getPostFromRecentCache(session,
  150. baseDir: str,
  151. httpPrefix: str,
  152. nickname: str, domain: str,
  153. postJsonObject: {},
  154. postActor: str,
  155. personCache: {},
  156. allowDownloads: bool,
  157. showPublicOnly: bool,
  158. storeToCache: bool,
  159. boxName: str,
  160. avatarUrl: str,
  161. enableTimingLog: bool,
  162. postStartTime,
  163. pageNumber: int,
  164. recentPostsCache: {},
  165. maxRecentPosts: int) -> str:
  166. """Attempts to get the html post from the recent posts cache in memory
  167. """
  168. if boxName == 'tlmedia':
  169. return None
  170. if showPublicOnly:
  171. return None
  172. tryCache = False
  173. bmTimeline = boxName == 'bookmarks' or boxName == 'tlbookmarks'
  174. if storeToCache or bmTimeline:
  175. tryCache = True
  176. if not tryCache:
  177. return None
  178. # update avatar if needed
  179. if not avatarUrl:
  180. avatarUrl = \
  181. getPersonAvatarUrl(baseDir, postActor, personCache,
  182. allowDownloads)
  183. _logPostTiming(enableTimingLog, postStartTime, '2.1')
  184. updateAvatarImageCache(session, baseDir, httpPrefix,
  185. postActor, avatarUrl, personCache,
  186. allowDownloads)
  187. _logPostTiming(enableTimingLog, postStartTime, '2.2')
  188. postHtml = \
  189. loadIndividualPostAsHtmlFromCache(baseDir, nickname, domain,
  190. postJsonObject)
  191. if not postHtml:
  192. return None
  193. postHtml = \
  194. preparePostFromHtmlCache(nickname, postHtml, boxName, pageNumber)
  195. updateRecentPostsCache(recentPostsCache, maxRecentPosts,
  196. postJsonObject, postHtml)
  197. _logPostTiming(enableTimingLog, postStartTime, '3')
  198. return postHtml
  199. def _getAvatarImageHtml(showAvatarOptions: bool,
  200. nickname: str, domainFull: str,
  201. avatarUrl: str, postActor: str,
  202. translate: {}, avatarPosition: str,
  203. pageNumber: int, messageIdStr: str) -> str:
  204. """Get html for the avatar image
  205. """
  206. avatarLink = ''
  207. if '/users/news/' not in avatarUrl:
  208. avatarLink = ' <a class="imageAnchor" href="' + postActor + '">'
  209. avatarLink += \
  210. '<img loading="lazy" src="' + avatarUrl + '" title="' + \
  211. translate['Show profile'] + '" alt=" "' + avatarPosition + \
  212. getBrokenLinkSubstitute() + '/></a>\n'
  213. if showAvatarOptions and \
  214. domainFull + '/users/' + nickname not in postActor:
  215. if '/users/news/' not in avatarUrl:
  216. avatarLink = \
  217. ' <a class="imageAnchor" href="/users/' + \
  218. nickname + '?options=' + postActor + \
  219. ';' + str(pageNumber) + ';' + avatarUrl + messageIdStr + '">\n'
  220. avatarLink += \
  221. ' <img loading="lazy" title="' + \
  222. translate['Show options for this person'] + '" ' + \
  223. 'alt="👤 ' + \
  224. translate['Show options for this person'] + '" ' + \
  225. 'src="' + avatarUrl + '" ' + avatarPosition + \
  226. getBrokenLinkSubstitute() + '/></a>\n'
  227. else:
  228. # don't link to the person options for the news account
  229. avatarLink += \
  230. ' <img loading="lazy" title="' + \
  231. translate['Show options for this person'] + '" ' + \
  232. 'alt="👤 ' + \
  233. translate['Show options for this person'] + '" ' + \
  234. 'src="' + avatarUrl + '" ' + avatarPosition + \
  235. getBrokenLinkSubstitute() + '/>\n'
  236. return avatarLink.strip()
  237. def _getReplyIconHtml(nickname: str, isPublicRepeat: bool,
  238. showIcons: bool, commentsEnabled: bool,
  239. postJsonObject: {}, pageNumberParam: str,
  240. translate: {}) -> str:
  241. """Returns html for the reply icon/button
  242. """
  243. replyStr = ''
  244. if not (showIcons and commentsEnabled):
  245. return replyStr
  246. # reply is permitted - create reply icon
  247. replyToLink = postJsonObject['object']['id']
  248. if postJsonObject['object'].get('attributedTo'):
  249. if isinstance(postJsonObject['object']['attributedTo'], str):
  250. replyToLink += \
  251. '?mention=' + postJsonObject['object']['attributedTo']
  252. if postJsonObject['object'].get('content'):
  253. mentionedActors = \
  254. getMentionsFromHtml(postJsonObject['object']['content'])
  255. if mentionedActors:
  256. for actorUrl in mentionedActors:
  257. if '?mention=' + actorUrl not in replyToLink:
  258. replyToLink += '?mention=' + actorUrl
  259. if len(replyToLink) > 500:
  260. break
  261. replyToLink += pageNumberParam
  262. replyStr = ''
  263. replyToThisPostStr = translate['Reply to this post']
  264. if isPublicRepeat:
  265. replyStr += \
  266. ' <a class="imageAnchor" href="/users/' + \
  267. nickname + '?replyto=' + replyToLink + \
  268. '?actor=' + postJsonObject['actor'] + \
  269. '" title="' + replyToThisPostStr + '">\n'
  270. else:
  271. if isDM(postJsonObject):
  272. replyStr += \
  273. ' ' + \
  274. '<a class="imageAnchor" href="/users/' + nickname + \
  275. '?replydm=' + replyToLink + \
  276. '?actor=' + postJsonObject['actor'] + \
  277. '" title="' + replyToThisPostStr + '">\n'
  278. else:
  279. replyStr += \
  280. ' ' + \
  281. '<a class="imageAnchor" href="/users/' + nickname + \
  282. '?replyfollowers=' + replyToLink + \
  283. '?actor=' + postJsonObject['actor'] + \
  284. '" title="' + replyToThisPostStr + '">\n'
  285. replyStr += \
  286. ' ' + \
  287. '<img loading="lazy" title="' + \
  288. replyToThisPostStr + '" alt="' + replyToThisPostStr + \
  289. ' |" src="/icons/reply.png"/></a>\n'
  290. return replyStr
  291. def _getEditIconHtml(baseDir: str, nickname: str, domainFull: str,
  292. postJsonObject: {}, actorNickname: str,
  293. translate: {}, isEvent: bool) -> str:
  294. """Returns html for the edit icon/button
  295. """
  296. editStr = ''
  297. actor = postJsonObject['actor']
  298. # This should either be a post which you created,
  299. # or it could be generated from the newswire (see
  300. # _addBlogsToNewswire) in which case anyone with
  301. # editor status should be able to alter it
  302. if (actor.endswith('/' + domainFull + '/users/' + nickname) or
  303. (isEditor(baseDir, nickname) and
  304. actor.endswith('/' + domainFull + '/users/news'))):
  305. postId = postJsonObject['object']['id']
  306. if '/statuses/' not in postId:
  307. return editStr
  308. if isBlogPost(postJsonObject):
  309. editBlogPostStr = translate['Edit blog post']
  310. if not isNewsPost(postJsonObject):
  311. editStr += \
  312. ' ' + \
  313. '<a class="imageAnchor" href="/users/' + \
  314. nickname + \
  315. '/tlblogs?editblogpost=' + \
  316. postId.split('/statuses/')[1] + \
  317. '?actor=' + actorNickname + \
  318. '" title="' + editBlogPostStr + '">' + \
  319. '<img loading="lazy" title="' + \
  320. editBlogPostStr + '" alt="' + editBlogPostStr + \
  321. ' |" src="/icons/edit.png"/></a>\n'
  322. else:
  323. editStr += \
  324. ' ' + \
  325. '<a class="imageAnchor" href="/users/' + \
  326. nickname + '/editnewspost=' + \
  327. postId.split('/statuses/')[1] + \
  328. '?actor=' + actorNickname + \
  329. '" title="' + editBlogPostStr + '">' + \
  330. '<img loading="lazy" title="' + \
  331. editBlogPostStr + '" alt="' + editBlogPostStr + \
  332. ' |" src="/icons/edit.png"/></a>\n'
  333. elif isEvent:
  334. editEventStr = translate['Edit event']
  335. editStr += \
  336. ' ' + \
  337. '<a class="imageAnchor" href="/users/' + nickname + \
  338. '/tlblogs?editeventpost=' + \
  339. postId.split('/statuses/')[1] + \
  340. '?actor=' + actorNickname + \
  341. '" title="' + editEventStr + '">' + \
  342. '<img loading="lazy" title="' + \
  343. editEventStr + '" alt="' + editEventStr + \
  344. ' |" src="/icons/edit.png"/></a>\n'
  345. return editStr
  346. def _getAnnounceIconHtml(isAnnounced: bool,
  347. postActor: str,
  348. nickname: str, domainFull: str,
  349. announceJsonObject: {},
  350. postJsonObject: {},
  351. isPublicRepeat: bool,
  352. isModerationPost: bool,
  353. showRepeatIcon: bool,
  354. translate: {},
  355. pageNumberParam: str,
  356. timelinePostBookmark: str,
  357. boxName: str) -> str:
  358. """Returns html for announce icon/button
  359. """
  360. announceStr = ''
  361. if not showRepeatIcon:
  362. return announceStr
  363. if isModerationPost:
  364. return announceStr
  365. # don't allow announce/repeat of your own posts
  366. announceIcon = 'repeat_inactive.png'
  367. announceLink = 'repeat'
  368. announceEmoji = ''
  369. if not isPublicRepeat:
  370. announceLink = 'repeatprivate'
  371. announceTitle = translate['Repeat this post']
  372. unannounceLinkStr = ''
  373. if announcedByPerson(isAnnounced,
  374. postActor, nickname, domainFull):
  375. announceIcon = 'repeat.png'
  376. announceEmoji = '🔁 '
  377. announceLink = 'unrepeat'
  378. if not isPublicRepeat:
  379. announceLink = 'unrepeatprivate'
  380. announceTitle = translate['Undo the repeat']
  381. if announceJsonObject:
  382. unannounceLinkStr = '?unannounce=' + \
  383. removeIdEnding(announceJsonObject['id'])
  384. announceLinkStr = '?' + \
  385. announceLink + '=' + postJsonObject['object']['id'] + pageNumberParam
  386. announceStr = \
  387. ' <a class="imageAnchor" href="/users/' + \
  388. nickname + announceLinkStr + unannounceLinkStr + \
  389. '?actor=' + postJsonObject['actor'] + \
  390. '?bm=' + timelinePostBookmark + \
  391. '?tl=' + boxName + '" title="' + announceTitle + '">\n'
  392. announceStr += \
  393. ' ' + \
  394. '<img loading="lazy" title="' + announceTitle + \
  395. '" alt="' + announceEmoji + announceTitle + \
  396. ' |" src="/icons/' + announceIcon + '"/></a>\n'
  397. return announceStr
  398. def _getLikeIconHtml(nickname: str, domainFull: str,
  399. isModerationPost: bool,
  400. showLikeButton: bool,
  401. postJsonObject: {},
  402. enableTimingLog: bool,
  403. postStartTime,
  404. translate: {}, pageNumberParam: str,
  405. timelinePostBookmark: str,
  406. boxName: str) -> str:
  407. """Returns html for like icon/button
  408. """
  409. likeStr = ''
  410. if not isModerationPost and showLikeButton:
  411. likeIcon = 'like_inactive.png'
  412. likeLink = 'like'
  413. likeTitle = translate['Like this post']
  414. likeEmoji = ''
  415. likeCount = noOfLikes(postJsonObject)
  416. _logPostTiming(enableTimingLog, postStartTime, '12.1')
  417. likeCountStr = ''
  418. if likeCount > 0:
  419. if likeCount <= 10:
  420. likeCountStr = ' (' + str(likeCount) + ')'
  421. else:
  422. likeCountStr = ' (10+)'
  423. if likedByPerson(postJsonObject, nickname, domainFull):
  424. if likeCount == 1:
  425. # liked by the reader only
  426. likeCountStr = ''
  427. likeIcon = 'like.png'
  428. likeLink = 'unlike'
  429. likeTitle = translate['Undo the like']
  430. likeEmoji = '👍 '
  431. _logPostTiming(enableTimingLog, postStartTime, '12.2')
  432. likeStr = ''
  433. if likeCountStr:
  434. # show the number of likes next to icon
  435. likeStr += '<label class="likesCount">'
  436. likeStr += likeCountStr.replace('(', '').replace(')', '').strip()
  437. likeStr += '</label>\n'
  438. likeStr += \
  439. ' <a class="imageAnchor" href="/users/' + nickname + '?' + \
  440. likeLink + '=' + postJsonObject['object']['id'] + \
  441. pageNumberParam + \
  442. '?actor=' + postJsonObject['actor'] + \
  443. '?bm=' + timelinePostBookmark + \
  444. '?tl=' + boxName + '" title="' + \
  445. likeTitle + likeCountStr + '">\n'
  446. likeStr += \
  447. ' ' + \
  448. '<img loading="lazy" title="' + likeTitle + likeCountStr + \
  449. '" alt="' + likeEmoji + likeTitle + \
  450. ' |" src="/icons/' + likeIcon + '"/></a>\n'
  451. return likeStr
  452. def _getBookmarkIconHtml(nickname: str, domainFull: str,
  453. postJsonObject: {},
  454. isModerationPost: bool,
  455. translate: {},
  456. enableTimingLog: bool,
  457. postStartTime, boxName: str,
  458. pageNumberParam: str,
  459. timelinePostBookmark: str) -> str:
  460. """Returns html for bookmark icon/button
  461. """
  462. bookmarkStr = ''
  463. if isModerationPost:
  464. return bookmarkStr
  465. bookmarkIcon = 'bookmark_inactive.png'
  466. bookmarkLink = 'bookmark'
  467. bookmarkEmoji = ''
  468. bookmarkTitle = translate['Bookmark this post']
  469. if bookmarkedByPerson(postJsonObject, nickname, domainFull):
  470. bookmarkIcon = 'bookmark.png'
  471. bookmarkLink = 'unbookmark'
  472. bookmarkEmoji = '🔖 '
  473. bookmarkTitle = translate['Undo the bookmark']
  474. _logPostTiming(enableTimingLog, postStartTime, '12.6')
  475. bookmarkStr = \
  476. ' <a class="imageAnchor" href="/users/' + nickname + '?' + \
  477. bookmarkLink + '=' + postJsonObject['object']['id'] + \
  478. pageNumberParam + \
  479. '?actor=' + postJsonObject['actor'] + \
  480. '?bm=' + timelinePostBookmark + \
  481. '?tl=' + boxName + '" title="' + bookmarkTitle + '">\n'
  482. bookmarkStr += \
  483. ' ' + \
  484. '<img loading="lazy" title="' + bookmarkTitle + '" alt="' + \
  485. bookmarkEmoji + bookmarkTitle + ' |" src="/icons' + \
  486. '/' + bookmarkIcon + '"/></a>\n'
  487. return bookmarkStr
  488. def _getMuteIconHtml(isMuted: bool,
  489. postActor: str,
  490. messageId: str,
  491. nickname: str, domainFull: str,
  492. allowDeletion: bool,
  493. pageNumberParam: str,
  494. boxName: str,
  495. timelinePostBookmark: str,
  496. translate: {}) -> str:
  497. """Returns html for mute icon/button
  498. """
  499. muteStr = ''
  500. if (allowDeletion or
  501. ('/' + domainFull + '/' in postActor and
  502. messageId.startswith(postActor))):
  503. return muteStr
  504. if not isMuted:
  505. muteStr = \
  506. ' <a class="imageAnchor" href="/users/' + nickname + \
  507. '?mute=' + messageId + pageNumberParam + '?tl=' + boxName + \
  508. '?bm=' + timelinePostBookmark + \
  509. '" title="' + translate['Mute this post'] + '">\n'
  510. muteStr += \
  511. ' ' + \
  512. '<img loading="lazy" alt="' + \
  513. translate['Mute this post'] + \
  514. ' |" title="' + translate['Mute this post'] + \
  515. '" src="/icons/mute.png"/></a>\n'
  516. else:
  517. muteStr = \
  518. ' <a class="imageAnchor" href="/users/' + \
  519. nickname + '?unmute=' + messageId + \
  520. pageNumberParam + '?tl=' + boxName + '?bm=' + \
  521. timelinePostBookmark + '" title="' + \
  522. translate['Undo mute'] + '">\n'
  523. muteStr += \
  524. ' ' + \
  525. '<img loading="lazy" alt="🔇 ' + translate['Undo mute'] + \
  526. ' |" title="' + translate['Undo mute'] + \
  527. '" src="/icons/unmute.png"/></a>\n'
  528. return muteStr
  529. def _getDeleteIconHtml(nickname: str, domainFull: str,
  530. allowDeletion: bool,
  531. postActor: str,
  532. messageId: str,
  533. postJsonObject: {},
  534. pageNumberParam: str,
  535. translate: {}) -> str:
  536. """Returns html for delete icon/button
  537. """
  538. deleteStr = ''
  539. if (allowDeletion or
  540. ('/' + domainFull + '/' in postActor and
  541. messageId.startswith(postActor))):
  542. if '/users/' + nickname + '/' in messageId:
  543. if not isNewsPost(postJsonObject):
  544. deleteStr = \
  545. ' <a class="imageAnchor" href="/users/' + \
  546. nickname + \
  547. '?delete=' + messageId + pageNumberParam + \
  548. '" title="' + translate['Delete this post'] + '">\n'
  549. deleteStr += \
  550. ' ' + \
  551. '<img loading="lazy" alt="' + \
  552. translate['Delete this post'] + \
  553. ' |" title="' + translate['Delete this post'] + \
  554. '" src="/icons/delete.png"/></a>\n'
  555. return deleteStr
  556. def _getPublishedDateStr(postJsonObject: {},
  557. showPublishedDateOnly: bool) -> str:
  558. """Return the html for the published date on a post
  559. """
  560. publishedStr = ''
  561. if not postJsonObject['object'].get('published'):
  562. return publishedStr
  563. publishedStr = postJsonObject['object']['published']
  564. if '.' not in publishedStr:
  565. if '+' not in publishedStr:
  566. datetimeObject = \
  567. datetime.strptime(publishedStr, "%Y-%m-%dT%H:%M:%SZ")
  568. else:
  569. datetimeObject = \
  570. datetime.strptime(publishedStr.split('+')[0] + 'Z',
  571. "%Y-%m-%dT%H:%M:%SZ")
  572. else:
  573. publishedStr = \
  574. publishedStr.replace('T', ' ').split('.')[0]
  575. datetimeObject = parse(publishedStr)
  576. if not showPublishedDateOnly:
  577. publishedStr = datetimeObject.strftime("%a %b %d, %H:%M")
  578. else:
  579. publishedStr = datetimeObject.strftime("%a %b %d")
  580. # if the post has replies then append a symbol to indicate this
  581. if postJsonObject.get('hasReplies'):
  582. if postJsonObject['hasReplies'] is True:
  583. publishedStr = '[' + publishedStr + ']'
  584. return publishedStr
  585. def _getBlogCitationsHtml(boxName: str,
  586. postJsonObject: {},
  587. translate: {}) -> str:
  588. """Returns blog citations as html
  589. """
  590. # show blog citations
  591. citationsStr = ''
  592. if not (boxName == 'tlblogs' or boxName == 'tlfeatures'):
  593. return citationsStr
  594. if not postJsonObject['object'].get('tag'):
  595. return citationsStr
  596. for tagJson in postJsonObject['object']['tag']:
  597. if not isinstance(tagJson, dict):
  598. continue
  599. if not tagJson.get('type'):
  600. continue
  601. if tagJson['type'] != 'Article':
  602. continue
  603. if not tagJson.get('name'):
  604. continue
  605. if not tagJson.get('url'):
  606. continue
  607. citationsStr += \
  608. '<li><a href="' + tagJson['url'] + '">' + \
  609. '<cite>' + tagJson['name'] + '</cite></a></li>\n'
  610. if citationsStr:
  611. citationsStr = '<p><b>' + translate['Citations'] + ':</b></p>' + \
  612. '<ul>\n' + citationsStr + '</ul>\n'
  613. return citationsStr
  614. def _boostOwnTootHtml(translate: {}) -> str:
  615. """The html title for announcing your own post
  616. """
  617. return ' <img loading="lazy" title="' + \
  618. translate['announces'] + \
  619. '" alt="' + translate['announces'] + \
  620. '" src="/icons' + \
  621. '/repeat_inactive.png" class="announceOrReply"/>\n'
  622. def _announceUnattributedHtml(translate: {},
  623. postJsonObject: {}) -> str:
  624. """Returns the html for an announce title where there
  625. is no attribution on the announced post
  626. """
  627. return ' <img loading="lazy" title="' + \
  628. translate['announces'] + '" alt="' + \
  629. translate['announces'] + '" src="/icons' + \
  630. '/repeat_inactive.png" ' + \
  631. 'class="announceOrReply"/>\n' + \
  632. ' <a href="' + \
  633. postJsonObject['object']['id'] + \
  634. '" class="announceOrReply">@unattributed</a>\n'
  635. def _announceWithoutDisplayNameHtml(translate: {},
  636. announceNickname: str,
  637. announceDomain: str,
  638. postJsonObject: {}) -> str:
  639. """Returns html for an announce title where there is no display name
  640. only a handle nick@domain
  641. """
  642. return ' <img loading="lazy" title="' + \
  643. translate['announces'] + '" alt="' + translate['announces'] + \
  644. '" src="/icons/repeat_inactive.png" ' + \
  645. 'class="announceOrReply"/>\n' + \
  646. ' <a href="' + postJsonObject['object']['id'] + '" ' + \
  647. 'class="announceOrReply">@' + \
  648. announceNickname + '@' + announceDomain + '</a>\n'
  649. def _announceWithDisplayNameHtml(translate: {},
  650. postJsonObject: {},
  651. announceDisplayName: str) -> str:
  652. """Returns html for an announce having a display name
  653. """
  654. return ' <img loading="lazy" title="' + \
  655. translate['announces'] + '" alt="' + \
  656. translate['announces'] + '" src="/' + \
  657. 'icons/repeat_inactive.png" ' + \
  658. 'class="announceOrReply"/>\n' + \
  659. ' <a href="' + \
  660. postJsonObject['object']['id'] + '" ' + \
  661. 'class="announceOrReply">' + announceDisplayName + '</a>\n'
  662. def _getPostTitleAnnounceHtml(baseDir: str,
  663. httpPrefix: str,
  664. nickname: str, domain: str,
  665. showRepeatIcon: bool,
  666. isAnnounced: bool,
  667. postJsonObject: {},
  668. postActor: str,
  669. translate: {},
  670. enableTimingLog: bool,
  671. postStartTime,
  672. boxName: str,
  673. personCache: {},
  674. allowDownloads: bool,
  675. avatarPosition: str,
  676. pageNumber: int,
  677. messageIdStr: str,
  678. containerClassIcons: str,
  679. containerClass: str) -> (str, str, str, str):
  680. """Returns the announce title of a post containing names of participants
  681. x announces y
  682. """
  683. titleStr = ''
  684. replyAvatarImageInPost = ''
  685. if postJsonObject['object'].get('attributedTo'):
  686. attributedTo = ''
  687. if isinstance(postJsonObject['object']['attributedTo'], str):
  688. attributedTo = postJsonObject['object']['attributedTo']
  689. if attributedTo.startswith(postActor):
  690. titleStr += _boostOwnTootHtml(translate)
  691. else:
  692. # boosting another person's post
  693. _logPostTiming(enableTimingLog, postStartTime, '13.2')
  694. announceNickname = None
  695. if attributedTo:
  696. announceNickname = getNicknameFromActor(attributedTo)
  697. if announceNickname:
  698. announceDomain, announcePort = \
  699. getDomainFromActor(attributedTo)
  700. getPersonFromCache(baseDir, attributedTo,
  701. personCache, allowDownloads)
  702. announceDisplayName = \
  703. getDisplayName(baseDir, attributedTo, personCache)
  704. if announceDisplayName:
  705. _logPostTiming(enableTimingLog, postStartTime, '13.3')
  706. # add any emoji to the display name
  707. if ':' in announceDisplayName:
  708. announceDisplayName = \
  709. addEmojiToDisplayName(baseDir, httpPrefix,
  710. nickname, domain,
  711. announceDisplayName,
  712. False)
  713. _logPostTiming(enableTimingLog, postStartTime, '13.3.1')
  714. titleStr += \
  715. _announceWithDisplayNameHtml(translate,
  716. postJsonObject,
  717. announceDisplayName)
  718. # show avatar of person replied to
  719. announceActor = \
  720. postJsonObject['object']['attributedTo']
  721. announceAvatarUrl = \
  722. getPersonAvatarUrl(baseDir, announceActor,
  723. personCache, allowDownloads)
  724. _logPostTiming(enableTimingLog, postStartTime, '13.4')
  725. if announceAvatarUrl:
  726. idx = 'Show options for this person'
  727. if '/users/news/' not in announceAvatarUrl:
  728. replyAvatarImageInPost = \
  729. ' ' \
  730. '<div class=' + \
  731. '"timeline-avatar-reply">\n' \
  732. ' ' + \
  733. '<a class="imageAnchor" ' + \
  734. 'href="/users/' + nickname + \
  735. '?options=' + \
  736. announceActor + ';' + \
  737. str(pageNumber) + \
  738. ';' + announceAvatarUrl + \
  739. messageIdStr + '">' \
  740. '<img loading="lazy" src="' + \
  741. announceAvatarUrl + '" ' + \
  742. 'title="' + translate[idx] + \
  743. '" alt=" "' + avatarPosition + \
  744. getBrokenLinkSubstitute() + \
  745. '/></a>\n </div>\n'
  746. else:
  747. titleStr += \
  748. _announceWithoutDisplayNameHtml(translate,
  749. announceNickname,
  750. announceDomain,
  751. postJsonObject)
  752. else:
  753. titleStr += \
  754. _announceUnattributedHtml(translate,
  755. postJsonObject)
  756. else:
  757. titleStr += \
  758. _announceUnattributedHtml(translate, postJsonObject)
  759. return (titleStr, replyAvatarImageInPost,
  760. containerClassIcons, containerClass)
  761. def _replyToYourselfHtml(translate: {}) -> str:
  762. """Returns html for a title which is a reply to yourself
  763. """
  764. return ' <img loading="lazy" title="' + \
  765. translate['replying to themselves'] + \
  766. '" alt="' + translate['replying to themselves'] + \
  767. '" src="/icons' + \
  768. '/reply.png" class="announceOrReply"/>\n'
  769. def _replyToUnknownHtml(translate: {},
  770. postJsonObject: {}) -> str:
  771. """Returns the html title for a reply to an unknown handle
  772. """
  773. return ' <img loading="lazy" title="' + \
  774. translate['replying to'] + '" alt="' + \
  775. translate['replying to'] + '" src="/icons' + \
  776. '/reply.png" class="announceOrReply"/>\n' + \
  777. ' <a href="' + \
  778. postJsonObject['object']['inReplyTo'] + \
  779. '" class="announceOrReply">@unknown</a>\n'
  780. def _replyWithUnknownPathHtml(translate: {},
  781. postJsonObject: {},
  782. postDomain: str) -> str:
  783. """Returns html title for a reply with an unknown path
  784. eg. does not contain /statuses/
  785. """
  786. return ' <img loading="lazy" title="' + \
  787. translate['replying to'] + \
  788. '" alt="' + translate['replying to'] + \
  789. '" src="/icons/reply.png" ' + \
  790. 'class="announceOrReply"/>\n' + \
  791. ' <a href="' + \
  792. postJsonObject['object']['inReplyTo'] + \
  793. '" class="announceOrReply">' + \
  794. postDomain + '</a>\n'
  795. def _getReplyHtml(translate: {},
  796. inReplyTo: str, replyDisplayName: str) -> str:
  797. """Returns html title for a reply
  798. """
  799. return ' ' + \
  800. '<img loading="lazy" title="' + \
  801. translate['replying to'] + '" alt="' + \
  802. translate['replying to'] + '" src="/' + \
  803. 'icons/reply.png" ' + \
  804. 'class="announceOrReply"/>\n' + \
  805. ' <a href="' + inReplyTo + \
  806. '" class="announceOrReply">' + \
  807. replyDisplayName + '</a>\n'
  808. def _getReplyWithoutDisplayName(translate: {},
  809. inReplyTo: str,
  810. replyNickname: str, replyDomain: str) -> str:
  811. """Returns html for a reply without a display name,
  812. only a handle nick@domain
  813. """
  814. return ' ' + \
  815. '<img loading="lazy" title="' + translate['replying to'] + \
  816. '" alt="' + translate['replying to'] + \
  817. '" src="/icons/reply.png" ' + \
  818. 'class="announceOrReply"/>\n' + ' <a href="' + \
  819. inReplyTo + '" class="announceOrReply">@' + \
  820. replyNickname + '@' + replyDomain + '</a>\n'
  821. def _getPostTitleReplyHtml(baseDir: str,
  822. httpPrefix: str,
  823. nickname: str, domain: str,
  824. showRepeatIcon: bool,
  825. isAnnounced: bool,
  826. postJsonObject: {},
  827. postActor: str,
  828. translate: {},
  829. enableTimingLog: bool,
  830. postStartTime,
  831. boxName: str,
  832. personCache: {},
  833. allowDownloads: bool,
  834. avatarPosition: str,
  835. pageNumber: int,
  836. messageIdStr: str,
  837. containerClassIcons: str,
  838. containerClass: str) -> (str, str, str, str):
  839. """Returns the reply title of a post containing names of participants
  840. x replies to y
  841. """
  842. titleStr = ''
  843. replyAvatarImageInPost = ''
  844. if not postJsonObject['object'].get('inReplyTo'):
  845. return (titleStr, replyAvatarImageInPost,
  846. containerClassIcons, containerClass)
  847. containerClassIcons = 'containericons darker'
  848. containerClass = 'container darker'
  849. if postJsonObject['object']['inReplyTo'].startswith(postActor):
  850. titleStr += _replyToYourselfHtml(translate)
  851. return (titleStr, replyAvatarImageInPost,
  852. containerClassIcons, containerClass)
  853. if '/statuses/' in postJsonObject['object']['inReplyTo']:
  854. inReplyTo = postJsonObject['object']['inReplyTo']
  855. replyActor = inReplyTo.split('/statuses/')[0]
  856. replyNickname = getNicknameFromActor(replyActor)
  857. if replyNickname:
  858. replyDomain, replyPort = \
  859. getDomainFromActor(replyActor)
  860. if replyNickname and replyDomain:
  861. getPersonFromCache(baseDir, replyActor,
  862. personCache,
  863. allowDownloads)
  864. replyDisplayName = \
  865. getDisplayName(baseDir, replyActor,
  866. personCache)
  867. if replyDisplayName:
  868. # add emoji to the display name
  869. if ':' in replyDisplayName:
  870. _logPostTiming(enableTimingLog, postStartTime, '13.5')
  871. replyDisplayName = \
  872. addEmojiToDisplayName(baseDir,
  873. httpPrefix,
  874. nickname,
  875. domain,
  876. replyDisplayName,
  877. False)
  878. _logPostTiming(enableTimingLog, postStartTime, '13.6')
  879. titleStr += \
  880. _getReplyHtml(translate, inReplyTo, replyDisplayName)
  881. _logPostTiming(enableTimingLog, postStartTime, '13.7')
  882. # show avatar of person replied to
  883. replyAvatarUrl = \
  884. getPersonAvatarUrl(baseDir,
  885. replyActor,
  886. personCache,
  887. allowDownloads)
  888. _logPostTiming(enableTimingLog, postStartTime, '13.8')
  889. if replyAvatarUrl:
  890. replyAvatarImageInPost = \
  891. ' <div class=' + \
  892. '"timeline-avatar-reply">\n'
  893. replyAvatarImageInPost += \
  894. ' ' + \
  895. '<a class="imageAnchor" ' + \
  896. 'href="/users/' + nickname + \
  897. '?options=' + replyActor + \
  898. ';' + str(pageNumber) + ';' + \
  899. replyAvatarUrl + \
  900. messageIdStr + '">\n'
  901. replyAvatarImageInPost += \
  902. ' ' + \
  903. '<img loading="lazy" src="' + \
  904. replyAvatarUrl + '" '
  905. replyAvatarImageInPost += \
  906. 'title="' + \
  907. translate['Show profile']
  908. replyAvatarImageInPost += \
  909. '" alt=" "' + \
  910. avatarPosition + \
  911. getBrokenLinkSubstitute() + \
  912. '/></a>\n </div>\n'
  913. else:
  914. inReplyTo = \
  915. postJsonObject['object']['inReplyTo']
  916. titleStr += \
  917. _getReplyWithoutDisplayName(translate,
  918. inReplyTo,
  919. replyNickname,
  920. replyDomain)
  921. else:
  922. titleStr += \
  923. _replyToUnknownHtml(translate, postJsonObject)
  924. else:
  925. postDomain = \
  926. postJsonObject['object']['inReplyTo']
  927. prefixes = getProtocolPrefixes()
  928. for prefix in prefixes:
  929. postDomain = postDomain.replace(prefix, '')
  930. if '/' in postDomain:
  931. postDomain = postDomain.split('/', 1)[0]
  932. if postDomain:
  933. titleStr += \
  934. _replyWithUnknownPathHtml(translate,
  935. postJsonObject, postDomain)
  936. return (titleStr, replyAvatarImageInPost,
  937. containerClassIcons, containerClass)
  938. def _getPostTitleHtml(baseDir: str,
  939. httpPrefix: str,
  940. nickname: str, domain: str,
  941. showRepeatIcon: bool,
  942. isAnnounced: bool,
  943. postJsonObject: {},
  944. postActor: str,
  945. translate: {},
  946. enableTimingLog: bool,
  947. postStartTime,
  948. boxName: str,
  949. personCache: {},
  950. allowDownloads: bool,
  951. avatarPosition: str,
  952. pageNumber: int,
  953. messageIdStr: str,
  954. containerClassIcons: str,
  955. containerClass: str) -> (str, str, str, str):
  956. """Returns the title of a post containing names of participants
  957. x replies to y, x announces y, etc
  958. """
  959. titleStr = ''
  960. replyAvatarImageInPost = ''
  961. if not showRepeatIcon:
  962. return (titleStr, replyAvatarImageInPost,
  963. containerClassIcons, containerClass)
  964. if isAnnounced:
  965. return _getPostTitleAnnounceHtml(baseDir,
  966. httpPrefix,
  967. nickname, domain,
  968. showRepeatIcon,
  969. isAnnounced,
  970. postJsonObject,
  971. postActor,
  972. translate,
  973. enableTimingLog,
  974. postStartTime,
  975. boxName,
  976. personCache,
  977. allowDownloads,
  978. avatarPosition,
  979. pageNumber,
  980. messageIdStr,
  981. containerClassIcons,
  982. containerClass)
  983. return _getPostTitleReplyHtml(baseDir,
  984. httpPrefix,
  985. nickname, domain,
  986. showRepeatIcon,
  987. isAnnounced,
  988. postJsonObject,
  989. postActor,
  990. translate,
  991. enableTimingLog,
  992. postStartTime,
  993. boxName,
  994. personCache,
  995. allowDownloads,
  996. avatarPosition,
  997. pageNumber,
  998. messageIdStr,
  999. containerClassIcons,
  1000. containerClass)
  1001. def _getFooterWithIcons(showIcons: bool,
  1002. containerClassIcons: str,
  1003. replyStr: str, announceStr: str,
  1004. likeStr: str, bookmarkStr: str,
  1005. deleteStr: str, muteStr: str, editStr: str,
  1006. postJsonObject: {}, publishedLink: str,
  1007. timeClass: str, publishedStr: str) -> str:
  1008. """Returns the html for a post footer containing icons
  1009. """
  1010. if not showIcons:
  1011. return None
  1012. footerStr = '\n <nav>\n'
  1013. footerStr += ' <div class="' + containerClassIcons + '">\n'
  1014. footerStr += replyStr + announceStr + likeStr + bookmarkStr
  1015. footerStr += deleteStr + muteStr + editStr
  1016. if not isNewsPost(postJsonObject):
  1017. footerStr += ' <a href="' + publishedLink + '" class="' + \
  1018. timeClass + '">' + publishedStr + '</a>\n'
  1019. else:
  1020. footerStr += ' <a href="' + \
  1021. publishedLink.replace('/news/', '/news/statuses/') + \
  1022. '" class="' + timeClass + '">' + publishedStr + '</a>\n'
  1023. footerStr += ' </div>\n'
  1024. footerStr += ' </nav>\n'
  1025. return footerStr
  1026. def individualPostAsHtml(allowDownloads: bool,
  1027. recentPostsCache: {}, maxRecentPosts: int,
  1028. translate: {},
  1029. pageNumber: int, baseDir: str,
  1030. session, cachedWebfingers: {}, personCache: {},
  1031. nickname: str, domain: str, port: int,
  1032. postJsonObject: {},
  1033. avatarUrl: str, showAvatarOptions: bool,
  1034. allowDeletion: bool,
  1035. httpPrefix: str, projectVersion: str,
  1036. boxName: str, YTReplacementDomain: str,
  1037. showPublishedDateOnly: bool,
  1038. peertubeInstances: [],
  1039. allowLocalNetworkAccess: bool,
  1040. themeName: str,
  1041. showRepeats=True,
  1042. showIcons=False,
  1043. manuallyApprovesFollowers=False,
  1044. showPublicOnly=False,
  1045. storeToCache=True) -> str:
  1046. """ Shows a single post as html
  1047. """
  1048. if not postJsonObject:
  1049. return ''
  1050. # benchmark
  1051. postStartTime = time.time()
  1052. postActor = postJsonObject['actor']
  1053. # ZZZzzz
  1054. if isPersonSnoozed(baseDir, nickname, domain, postActor):
  1055. return ''
  1056. # if downloads of avatar images aren't enabled then we can do more
  1057. # accurate timing of different parts of the code
  1058. enableTimingLog = not allowDownloads
  1059. _logPostTiming(enableTimingLog, postStartTime, '1')
  1060. avatarPosition = ''
  1061. messageId = ''
  1062. if postJsonObject.get('id'):
  1063. messageId = removeIdEnding(postJsonObject['id'])
  1064. _logPostTiming(enableTimingLog, postStartTime, '2')
  1065. messageIdStr = ''
  1066. if messageId:
  1067. messageIdStr = ';' + messageId
  1068. domainFull = getFullDomain(domain, port)
  1069. pageNumberParam = ''
  1070. if pageNumber:
  1071. pageNumberParam = '?page=' + str(pageNumber)
  1072. # get the html post from the recent posts cache if it exists there
  1073. postHtml = \
  1074. _getPostFromRecentCache(session, baseDir,
  1075. httpPrefix, nickname, domain,
  1076. postJsonObject,
  1077. postActor,
  1078. personCache,
  1079. allowDownloads,
  1080. showPublicOnly,
  1081. storeToCache,
  1082. boxName,
  1083. avatarUrl,
  1084. enableTimingLog,
  1085. postStartTime,
  1086. pageNumber,
  1087. recentPostsCache,
  1088. maxRecentPosts)
  1089. if postHtml:
  1090. return postHtml
  1091. _logPostTiming(enableTimingLog, postStartTime, '4')
  1092. avatarUrl = \
  1093. getAvatarImageUrl(session,
  1094. baseDir, httpPrefix,
  1095. postActor, personCache,
  1096. avatarUrl, allowDownloads)
  1097. _logPostTiming(enableTimingLog, postStartTime, '5')
  1098. # get the display name
  1099. if domainFull not in postActor:
  1100. # lookup the correct webfinger for the postActor
  1101. postActorNickname = getNicknameFromActor(postActor)
  1102. postActorDomain, postActorPort = getDomainFromActor(postActor)
  1103. postActorDomainFull = getFullDomain(postActorDomain, postActorPort)
  1104. postActorHandle = postActorNickname + '@' + postActorDomainFull
  1105. postActorWf = \
  1106. webfingerHandle(session, postActorHandle, httpPrefix,
  1107. cachedWebfingers,
  1108. domain, __version__, False)
  1109. avatarUrl2 = None
  1110. displayName = None
  1111. if postActorWf:
  1112. (inboxUrl, pubKeyId, pubKey,
  1113. fromPersonId, sharedInbox,
  1114. avatarUrl2, displayName) = getPersonBox(baseDir, session,
  1115. postActorWf,
  1116. personCache,
  1117. projectVersion,
  1118. httpPrefix,
  1119. nickname, domain,
  1120. 'outbox', 72367)
  1121. _logPostTiming(enableTimingLog, postStartTime, '6')
  1122. if avatarUrl2:
  1123. avatarUrl = avatarUrl2
  1124. if displayName:
  1125. # add any emoji to the display name
  1126. if ':' in displayName:
  1127. displayName = \
  1128. addEmojiToDisplayName(baseDir, httpPrefix,
  1129. nickname, domain,
  1130. displayName, False)
  1131. _logPostTiming(enableTimingLog, postStartTime, '7')
  1132. avatarLink = \
  1133. _getAvatarImageHtml(showAvatarOptions,
  1134. nickname, domainFull,
  1135. avatarUrl, postActor,
  1136. translate, avatarPosition,
  1137. pageNumber, messageIdStr)
  1138. avatarImageInPost = \
  1139. ' <div class="timeline-avatar">' + avatarLink + '</div>\n'
  1140. timelinePostBookmark = removeIdEnding(postJsonObject['id'])
  1141. timelinePostBookmark = timelinePostBookmark.replace('://', '-')
  1142. timelinePostBookmark = timelinePostBookmark.replace('/', '-')
  1143. # If this is the inbox timeline then don't show the repeat icon on any DMs
  1144. showRepeatIcon = showRepeats
  1145. isPublicRepeat = False
  1146. postIsDM = isDM(postJsonObject)
  1147. if showRepeats:
  1148. if postIsDM:
  1149. showRepeatIcon = False
  1150. else:
  1151. if not isPublicPost(postJsonObject):
  1152. isPublicRepeat = True
  1153. titleStr = ''
  1154. galleryStr = ''
  1155. isAnnounced = False
  1156. announceJsonObject = None
  1157. if postJsonObject['type'] == 'Announce':
  1158. announceJsonObject = postJsonObject.copy()
  1159. postJsonAnnounce = \
  1160. downloadAnnounce(session, baseDir, httpPrefix,
  1161. nickname, domain, postJsonObject,
  1162. projectVersion, translate,
  1163. YTReplacementDomain,
  1164. allowLocalNetworkAccess,
  1165. recentPostsCache, False)
  1166. if not postJsonAnnounce:
  1167. # if the announce could not be downloaded then mark it as rejected
  1168. rejectPostId(baseDir, nickname, domain, postJsonObject['id'],
  1169. recentPostsCache)
  1170. return ''
  1171. postJsonObject = postJsonAnnounce
  1172. announceFilename = \
  1173. locatePost(baseDir, nickname, domain,
  1174. postJsonObject['id'])
  1175. if announceFilename:
  1176. updateAnnounceCollection(recentPostsCache,
  1177. baseDir, announceFilename,
  1178. postActor, domainFull, False)
  1179. # create a file for use by text-to-speech
  1180. if isRecentPost(postJsonObject):
  1181. if postJsonObject.get('actor'):
  1182. if not os.path.isfile(announceFilename + '.tts'):
  1183. updateSpeaker(baseDir, httpPrefix,
  1184. nickname, domain, domainFull,
  1185. postJsonObject, personCache,
  1186. translate, postJsonObject['actor'],
  1187. themeName)
  1188. ttsFile = open(announceFilename + '.tts', "w+")
  1189. if ttsFile:
  1190. ttsFile.write('\n')
  1191. ttsFile.close()
  1192. isAnnounced = True
  1193. _logPostTiming(enableTimingLog, postStartTime, '8')
  1194. if not isinstance(postJsonObject['object'], dict):
  1195. return ''
  1196. # if this post should be public then check its recipients
  1197. if showPublicOnly:
  1198. if not postContainsPublic(postJsonObject):
  1199. return ''
  1200. isModerationPost = False
  1201. if postJsonObject['object'].get('moderationStatus'):
  1202. isModerationPost = True
  1203. containerClass = 'container'
  1204. containerClassIcons = 'containericons'
  1205. timeClass = 'time-right'
  1206. actorNickname = getNicknameFromActor(postActor)
  1207. if not actorNickname:
  1208. # single user instance
  1209. actorNickname = 'dev'
  1210. actorDomain, actorPort = getDomainFromActor(postActor)
  1211. displayName = getDisplayName(baseDir, postActor, personCache)
  1212. if displayName:
  1213. if ':' in displayName:
  1214. displayName = \
  1215. addEmojiToDisplayName(baseDir, httpPrefix,
  1216. nickname, domain,
  1217. displayName, False)
  1218. titleStr += \
  1219. ' <a class="imageAnchor" href="/users/' + \
  1220. nickname + '?options=' + postActor + \
  1221. ';' + str(pageNumber) + ';' + avatarUrl + messageIdStr + \
  1222. '">' + displayName + '</a>\n'
  1223. else:
  1224. if not messageId:
  1225. # pprint(postJsonObject)
  1226. print('ERROR: no messageId')
  1227. if not actorNickname:
  1228. # pprint(postJsonObject)
  1229. print('ERROR: no actorNickname')
  1230. if not actorDomain:
  1231. # pprint(postJsonObject)
  1232. print('ERROR: no actorDomain')
  1233. titleStr += \
  1234. ' <a class="imageAnchor" href="/users/' + \
  1235. nickname + '?options=' + postActor + \
  1236. ';' + str(pageNumber) + ';' + avatarUrl + messageIdStr + \
  1237. '">@' + actorNickname + '@' + actorDomain + '</a>\n'
  1238. # benchmark 9
  1239. _logPostTiming(enableTimingLog, postStartTime, '9')
  1240. # Show a DM icon for DMs in the inbox timeline
  1241. if postIsDM:
  1242. titleStr = \
  1243. titleStr + ' <img loading="lazy" src="/' + \
  1244. 'icons/dm.png" class="DMicon"/>\n'
  1245. # check if replying is permitted
  1246. commentsEnabled = True
  1247. if 'commentsEnabled' in postJsonObject['object']:
  1248. if postJsonObject['object']['commentsEnabled'] is False:
  1249. commentsEnabled = False
  1250. replyStr = _getReplyIconHtml(nickname, isPublicRepeat,
  1251. showIcons, commentsEnabled,
  1252. postJsonObject, pageNumberParam,
  1253. translate)
  1254. _logPostTiming(enableTimingLog, postStartTime, '10')
  1255. isEvent = isEventPost(postJsonObject)
  1256. _logPostTiming(enableTimingLog, postStartTime, '11')
  1257. editStr = _getEditIconHtml(baseDir, nickname, domainFull,
  1258. postJsonObject, actorNickname,
  1259. translate, isEvent)
  1260. announceStr = \
  1261. _getAnnounceIconHtml(isAnnounced,
  1262. postActor,
  1263. nickname, domainFull,
  1264. announceJsonObject,
  1265. postJsonObject,
  1266. isPublicRepeat,
  1267. isModerationPost,
  1268. showRepeatIcon,
  1269. translate,
  1270. pageNumberParam,
  1271. timelinePostBookmark,
  1272. boxName)
  1273. _logPostTiming(enableTimingLog, postStartTime, '12')
  1274. # whether to show a like button
  1275. hideLikeButtonFile = \
  1276. baseDir + '/accounts/' + nickname + '@' + domain + '/.hideLikeButton'
  1277. showLikeButton = True
  1278. if os.path.isfile(hideLikeButtonFile):
  1279. showLikeButton = False
  1280. likeStr = _getLikeIconHtml(nickname, domainFull,
  1281. isModerationPost,
  1282. showLikeButton,
  1283. postJsonObject,
  1284. enableTimingLog,
  1285. postStartTime,
  1286. translate, pageNumberParam,
  1287. timelinePostBookmark,
  1288. boxName)
  1289. _logPostTiming(enableTimingLog, postStartTime, '12.5')
  1290. bookmarkStr = \
  1291. _getBookmarkIconHtml(nickname, domainFull,
  1292. postJsonObject,
  1293. isModerationPost,
  1294. translate,
  1295. enableTimingLog,
  1296. postStartTime, boxName,
  1297. pageNumberParam,
  1298. timelinePostBookmark)
  1299. _logPostTiming(enableTimingLog, postStartTime, '12.9')
  1300. isMuted = postIsMuted(baseDir, nickname, domain, postJsonObject, messageId)
  1301. _logPostTiming(enableTimingLog, postStartTime, '13')
  1302. muteStr = \
  1303. _getMuteIconHtml(isMuted,
  1304. postActor,
  1305. messageId,
  1306. nickname, domainFull,
  1307. allowDeletion,
  1308. pageNumberParam,
  1309. boxName,
  1310. timelinePostBookmark,
  1311. translate)
  1312. deleteStr = \
  1313. _getDeleteIconHtml(nickname, domainFull,
  1314. allowDeletion,
  1315. postActor,
  1316. messageId,
  1317. postJsonObject,
  1318. pageNumberParam,
  1319. translate)
  1320. _logPostTiming(enableTimingLog, postStartTime, '13.1')
  1321. # get the title: x replies to y, x announces y, etc
  1322. (titleStr2,
  1323. replyAvatarImageInPost,
  1324. containerClassIcons,
  1325. containerClass) = _getPostTitleHtml(baseDir,
  1326. httpPrefix,
  1327. nickname, domain,
  1328. showRepeatIcon,
  1329. isAnnounced,
  1330. postJsonObject,
  1331. postActor,
  1332. translate,
  1333. enableTimingLog,
  1334. postStartTime,
  1335. boxName,
  1336. personCache,
  1337. allowDownloads,
  1338. avatarPosition,
  1339. pageNumber,
  1340. messageIdStr,
  1341. containerClassIcons,
  1342. containerClass)
  1343. titleStr += titleStr2
  1344. _logPostTiming(enableTimingLog, postStartTime, '14')
  1345. attachmentStr, galleryStr = \
  1346. getPostAttachmentsAsHtml(postJsonObject, boxName, translate,
  1347. isMuted, avatarLink,
  1348. replyStr, announceStr, likeStr,
  1349. bookmarkStr, deleteStr, muteStr)
  1350. publishedStr = \
  1351. _getPublishedDateStr(postJsonObject, showPublishedDateOnly)
  1352. _logPostTiming(enableTimingLog, postStartTime, '15')
  1353. publishedLink = messageId
  1354. # blog posts should have no /statuses/ in their link
  1355. if isBlogPost(postJsonObject):
  1356. # is this a post to the local domain?
  1357. if '://' + domain in messageId:
  1358. publishedLink = messageId.replace('/statuses/', '/')
  1359. # if this is a local link then make it relative so that it works
  1360. # on clearnet or onion address
  1361. if domain + '/users/' in publishedLink or \
  1362. domain + ':' + str(port) + '/users/' in publishedLink:
  1363. publishedLink = '/users/' + publishedLink.split('/users/')[1]
  1364. if not isNewsPost(postJsonObject):
  1365. footerStr = '<a href="' + publishedLink + \
  1366. '" class="' + timeClass + '">' + publishedStr + '</a>\n'
  1367. else:
  1368. footerStr = '<a href="' + \
  1369. publishedLink.replace('/news/', '/news/statuses/') + \
  1370. '" class="' + timeClass + '">' + publishedStr + '</a>\n'
  1371. # change the background color for DMs in inbox timeline
  1372. if postIsDM:
  1373. containerClassIcons = 'containericons dm'
  1374. containerClass = 'container dm'
  1375. newFooterStr = _getFooterWithIcons(showIcons,
  1376. containerClassIcons,
  1377. replyStr, announceStr,
  1378. likeStr, bookmarkStr,
  1379. deleteStr, muteStr, editStr,
  1380. postJsonObject, publishedLink,
  1381. timeClass, publishedStr)
  1382. if newFooterStr:
  1383. footerStr = newFooterStr
  1384. postIsSensitive = False
  1385. if postJsonObject['object'].get('sensitive'):
  1386. # sensitive posts should have a summary
  1387. if postJsonObject['object'].get('summary'):
  1388. postIsSensitive = postJsonObject['object']['sensitive']
  1389. else:
  1390. # add a generic summary if none is provided
  1391. postJsonObject['object']['summary'] = translate['Sensitive']
  1392. # add an extra line if there is a content warning,
  1393. # for better vertical spacing on mobile
  1394. if postIsSensitive:
  1395. footerStr = '<br>' + footerStr
  1396. if not postJsonObject['object'].get('summary'):
  1397. postJsonObject['object']['summary'] = ''
  1398. if postJsonObject['object'].get('cipherText'):
  1399. postJsonObject['object']['content'] = \
  1400. E2EEdecryptMessageFromDevice(postJsonObject['object'])
  1401. if not postJsonObject['object'].get('content'):
  1402. return ''
  1403. isPatch = isGitPatch(baseDir, nickname, domain,
  1404. postJsonObject['object']['type'],
  1405. postJsonObject['object']['summary'],
  1406. postJsonObject['object']['content'])
  1407. _logPostTiming(enableTimingLog, postStartTime, '16')
  1408. if not isPGPEncrypted(postJsonObject['object']['content']):
  1409. if not isPatch:
  1410. objectContent = \
  1411. removeLongWords(postJsonObject['object']['content'], 40, [])
  1412. objectContent = removeTextFormatting(objectContent)
  1413. objectContent = \
  1414. switchWords(baseDir, nickname, domain, objectContent)
  1415. objectContent = htmlReplaceEmailQuote(objectContent)
  1416. objectContent = htmlReplaceQuoteMarks(objectContent)
  1417. else:
  1418. objectContent = \
  1419. postJsonObject['object']['content']
  1420. else:
  1421. objectContent = '🔒 ' + translate['Encrypted']
  1422. objectContent = '<article>' + objectContent + '</article>'
  1423. if not postIsSensitive:
  1424. contentStr = objectContent + attachmentStr
  1425. contentStr = addEmbeddedElements(translate, contentStr,
  1426. peertubeInstances)
  1427. contentStr = insertQuestion(baseDir, translate,
  1428. nickname, domain, port,
  1429. contentStr, postJsonObject,
  1430. pageNumber)
  1431. else:
  1432. postID = 'post' + str(createPassword(8))
  1433. contentStr = ''
  1434. if postJsonObject['object'].get('summary'):
  1435. cwStr = str(postJsonObject['object']['summary'])
  1436. cwStr = \
  1437. addEmojiToDisplayName(baseDir, httpPrefix,
  1438. nickname, domain,
  1439. cwStr, False)
  1440. contentStr += \
  1441. '<label class="cw">' + cwStr + '</label>\n '
  1442. if isModerationPost:
  1443. containerClass = 'container report'
  1444. # get the content warning text
  1445. cwContentStr = objectContent + attachmentStr
  1446. if not isPatch:
  1447. cwContentStr = addEmbeddedElements(translate, cwContentStr,
  1448. peertubeInstances)
  1449. cwContentStr = \
  1450. insertQuestion(baseDir, translate, nickname, domain, port,
  1451. cwContentStr, postJsonObject, pageNumber)
  1452. cwContentStr = \
  1453. switchWords(baseDir, nickname, domain, cwContentStr)
  1454. if not isBlogPost(postJsonObject):
  1455. # get the content warning button
  1456. contentStr += \
  1457. getContentWarningButton(postID, translate, cwContentStr)
  1458. else:
  1459. contentStr += cwContentStr
  1460. _logPostTiming(enableTimingLog, postStartTime, '17')
  1461. if postJsonObject['object'].get('tag') and not isPatch:
  1462. contentStr = \
  1463. replaceEmojiFromTags(contentStr,
  1464. postJsonObject['object']['tag'],
  1465. 'content')
  1466. if isMuted:
  1467. contentStr = ''
  1468. else:
  1469. if not isPatch:
  1470. contentStr = ' <div class="message">' + \
  1471. contentStr + \
  1472. ' </div>\n'
  1473. else:
  1474. contentStr = \
  1475. '<div class="gitpatch"><pre><code>' + contentStr + \
  1476. '</code></pre></div>\n'
  1477. # show blog citations
  1478. citationsStr = \
  1479. _getBlogCitationsHtml(boxName, postJsonObject, translate)
  1480. postHtml = ''
  1481. if boxName != 'tlmedia':
  1482. postHtml = ' <div id="' + timelinePostBookmark + \
  1483. '" class="' + containerClass + '">\n'
  1484. postHtml += avatarImageInPost
  1485. postHtml += ' <div class="post-title">\n' + \
  1486. ' ' + titleStr + \
  1487. replyAvatarImageInPost + ' </div>\n'
  1488. postHtml += contentStr + citationsStr + footerStr + '\n'
  1489. postHtml += ' </div>\n'
  1490. else:
  1491. postHtml = galleryStr
  1492. _logPostTiming(enableTimingLog, postStartTime, '18')
  1493. # save the created html to the recent posts cache
  1494. if not showPublicOnly and storeToCache and \
  1495. boxName != 'tlmedia' and boxName != 'tlbookmarks' and \
  1496. boxName != 'bookmarks':
  1497. _saveIndividualPostAsHtmlToCache(baseDir, nickname, domain,
  1498. postJsonObject, postHtml)
  1499. updateRecentPostsCache(recentPostsCache, maxRecentPosts,
  1500. postJsonObject, postHtml)
  1501. _logPostTiming(enableTimingLog, postStartTime, '19')
  1502. return postHtml
  1503. def htmlIndividualPost(cssCache: {},
  1504. recentPostsCache: {}, maxRecentPosts: int,
  1505. translate: {},
  1506. baseDir: str, session, cachedWebfingers: {},
  1507. personCache: {},
  1508. nickname: str, domain: str, port: int, authorized: bool,
  1509. postJsonObject: {}, httpPrefix: str,
  1510. projectVersion: str, likedBy: str,
  1511. YTReplacementDomain: str,
  1512. showPublishedDateOnly: bool,
  1513. peertubeInstances: [],
  1514. allowLocalNetworkAccess: bool,
  1515. themeName: str) -> str:
  1516. """Show an individual post as html
  1517. """
  1518. postStr = ''
  1519. if likedBy:
  1520. likedByNickname = getNicknameFromActor(likedBy)
  1521. likedByDomain, likedByPort = getDomainFromActor(likedBy)
  1522. likedByDomain = getFullDomain(likedByDomain, likedByPort)
  1523. likedByHandle = likedByNickname + '@' + likedByDomain
  1524. postStr += \
  1525. '<p>' + translate['Liked by'] + \
  1526. ' <a href="' + likedBy + '">@' + \
  1527. likedByHandle + '</a>\n'
  1528. domainFull = getFullDomain(domain, port)
  1529. actor = '/users/' + nickname
  1530. followStr = ' <form method="POST" ' + \
  1531. 'accept-charset="UTF-8" action="' + actor + '/searchhandle">\n'
  1532. followStr += \
  1533. ' <input type="hidden" name="actor" value="' + actor + '">\n'
  1534. followStr += \
  1535. ' <input type="hidden" name="searchtext" value="' + \
  1536. likedByHandle + '">\n'
  1537. if not isFollowingActor(baseDir, nickname, domainFull, likedBy):
  1538. followStr += ' <button type="submit" class="button" ' + \
  1539. 'name="submitSearch">' + translate['Follow'] + '</button>\n'
  1540. followStr += ' <button type="submit" class="button" ' + \
  1541. 'name="submitBack">' + translate['Go Back'] + '</button>\n'
  1542. followStr += ' </form>\n'
  1543. postStr += followStr + '</p>\n'
  1544. postStr += \
  1545. individualPostAsHtml(True, recentPostsCache, maxRecentPosts,
  1546. translate, None,
  1547. baseDir, session, cachedWebfingers, personCache,
  1548. nickname, domain, port, postJsonObject,
  1549. None, True, False,
  1550. httpPrefix, projectVersion, 'inbox',
  1551. YTReplacementDomain,
  1552. showPublishedDateOnly,
  1553. peertubeInstances,
  1554. allowLocalNetworkAccess, themeName,
  1555. False, authorized, False, False, False)
  1556. messageId = removeIdEnding(postJsonObject['id'])
  1557. # show the previous posts
  1558. if isinstance(postJsonObject['object'], dict):
  1559. while postJsonObject['object'].get('inReplyTo'):
  1560. postFilename = \
  1561. locatePost(baseDir, nickname, domain,
  1562. postJsonObject['object']['inReplyTo'])
  1563. if not postFilename:
  1564. break
  1565. postJsonObject = loadJson(postFilename)
  1566. if postJsonObject:
  1567. postStr = \
  1568. individualPostAsHtml(True, recentPostsCache,
  1569. maxRecentPosts,
  1570. translate, None,
  1571. baseDir, session, cachedWebfingers,
  1572. personCache,
  1573. nickname, domain, port,
  1574. postJsonObject,
  1575. None, True, False,
  1576. httpPrefix, projectVersion, 'inbox',
  1577. YTReplacementDomain,
  1578. showPublishedDateOnly,
  1579. peertubeInstances,
  1580. allowLocalNetworkAccess,
  1581. themeName,
  1582. False, authorized,
  1583. False, False, False) + postStr
  1584. # show the following posts
  1585. postFilename = locatePost(baseDir, nickname, domain, messageId)
  1586. if postFilename:
  1587. # is there a replies file for this post?
  1588. repliesFilename = postFilename.replace('.json', '.replies')
  1589. if os.path.isfile(repliesFilename):
  1590. # get items from the replies file
  1591. repliesJson = {
  1592. 'orderedItems': []
  1593. }
  1594. populateRepliesJson(baseDir, nickname, domain,
  1595. repliesFilename, authorized, repliesJson)
  1596. # add items to the html output
  1597. for item in repliesJson['orderedItems']:
  1598. postStr += \
  1599. individualPostAsHtml(True, recentPostsCache,
  1600. maxRecentPosts,
  1601. translate, None,
  1602. baseDir, session, cachedWebfingers,
  1603. personCache,
  1604. nickname, domain, port, item,
  1605. None, True, False,
  1606. httpPrefix, projectVersion, 'inbox',
  1607. YTReplacementDomain,
  1608. showPublishedDateOnly,
  1609. peertubeInstances,
  1610. allowLocalNetworkAccess,
  1611. themeName,
  1612. False, authorized,
  1613. False, False, False)
  1614. cssFilename = baseDir + '/epicyon-profile.css'
  1615. if os.path.isfile(baseDir + '/epicyon.css'):
  1616. cssFilename = baseDir + '/epicyon.css'
  1617. instanceTitle = \
  1618. getConfigParam(baseDir, 'instanceTitle')
  1619. return htmlHeaderWithExternalStyle(cssFilename, instanceTitle) + \
  1620. postStr + htmlFooter()
  1621. def htmlPostReplies(cssCache: {},
  1622. recentPostsCache: {}, maxRecentPosts: int,
  1623. translate: {}, baseDir: str,
  1624. session, cachedWebfingers: {}, personCache: {},
  1625. nickname: str, domain: str, port: int, repliesJson: {},
  1626. httpPrefix: str, projectVersion: str,
  1627. YTReplacementDomain: str,
  1628. showPublishedDateOnly: bool,
  1629. peertubeInstances: [],
  1630. allowLocalNetworkAccess: bool,
  1631. themeName: str) -> str:
  1632. """Show the replies to an individual post as html
  1633. """
  1634. repliesStr = ''
  1635. if repliesJson.get('orderedItems'):
  1636. for item in repliesJson['orderedItems']:
  1637. repliesStr += \
  1638. individualPostAsHtml(True, recentPostsCache,
  1639. maxRecentPosts,
  1640. translate, None,
  1641. baseDir, session, cachedWebfingers,
  1642. personCache,
  1643. nickname, domain, port, item,
  1644. None, True, False,
  1645. httpPrefix, projectVersion, 'inbox',
  1646. YTReplacementDomain,
  1647. showPublishedDateOnly,
  1648. peertubeInstances,
  1649. allowLocalNetworkAccess,
  1650. themeName,
  1651. False, False, False, False, False)
  1652. cssFilename = baseDir + '/epicyon-profile.css'
  1653. if os.path.isfile(baseDir + '/epicyon.css'):
  1654. cssFilename = baseDir + '/epicyon.css'
  1655. instanceTitle = \
  1656. getConfigParam(baseDir, 'instanceTitle')
  1657. return htmlHeaderWithExternalStyle(cssFilename, instanceTitle) + \
  1658. repliesStr + htmlFooter()