diff --git a/blog.py b/blog.py index 4c3e83c70..9963ae0ea 100644 --- a/blog.py +++ b/blog.py @@ -10,15 +10,16 @@ import os from datetime import datetime from content import replaceEmojiFromTags -from webinterface import getIconsDir -from webinterface import getPostAttachmentsAsHtml -from webinterface import htmlHeader -from webinterface import htmlFooter -from webinterface import addEmbeddedElements +from webapp import getIconsDir +from webapp import htmlHeader +from webapp import htmlFooter +from webapp_media import addEmbeddedElements +from webapp_utils import getPostAttachmentsAsHtml from utils import getNicknameFromActor from utils import getDomainFromActor from utils import locatePost from utils import loadJson +from utils import firstParagraphFromString from posts import createBlogsTimeline from newswire import rss2Header from newswire import rss2Footer @@ -165,10 +166,12 @@ def htmlBlogPostContent(authorized: bool, if postJsonObject['object'].get('id'): messageLink = postJsonObject['object']['id'].replace('/statuses/', '/') titleStr = '' + articleAdded = False if postJsonObject['object'].get('summary'): titleStr = postJsonObject['object']['summary'] - blogStr += '

' + \ + blogStr += '

' + \ titleStr + '

\n' + articleAdded = True # get the handle of the author if postJsonObject['object'].get('attributedTo'): @@ -231,7 +234,10 @@ def htmlBlogPostContent(authorized: bool, contentStr = replaceEmojiFromTags(contentStr, postJsonObject['object']['tag'], 'content') - blogStr += '
' + contentStr + '\n' + if articleAdded: + blogStr += '
' + contentStr + '
\n' + else: + blogStr += '
' + contentStr + '
\n' citationsStr = '' if postJsonObject['object'].get('tag'): @@ -312,9 +318,13 @@ def htmlBlogPostRSS2(authorized: bool, pubDate = datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ") titleStr = postJsonObject['object']['summary'] rssDateStr = pubDate.strftime("%a, %d %b %Y %H:%M:%S UT") + content = postJsonObject['object']['content'] + description = firstParagraphFromString(content) rssStr = ' ' rssStr += ' ' + titleStr + '' rssStr += ' ' + messageLink + '' + rssStr += \ + ' ' + description + '' rssStr += ' ' + rssDateStr + '' rssStr += ' ' return rssStr @@ -339,8 +349,11 @@ def htmlBlogPostRSS3(authorized: bool, pubDate = datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ") titleStr = postJsonObject['object']['summary'] rssDateStr = pubDate.strftime("%a, %d %b %Y %H:%M:%S UT") + content = postJsonObject['object']['content'] + description = firstParagraphFromString(content) rssStr = 'title: ' + titleStr + '\n' rssStr += 'link: ' + messageLink + '\n' + rssStr += 'description: ' + description + '\n' rssStr += 'created: ' + rssDateStr + '\n\n' return rssStr diff --git a/daemon.py b/daemon.py index 10f5e76fa..77642227b 100644 --- a/daemon.py +++ b/daemon.py @@ -113,59 +113,60 @@ from blog import htmlBlogView from blog import htmlBlogPage from blog import htmlBlogPost from blog import htmlEditBlog -from webinterface import htmlCitations -from webinterface import htmlFollowingList -from webinterface import getBlogAddress -from webinterface import setBlogAddress -from webinterface import htmlCalendarDeleteConfirm -from webinterface import htmlDeletePost -from webinterface import htmlAbout -from webinterface import htmlRemoveSharedItem -from webinterface import htmlInboxDMs -from webinterface import htmlInboxReplies -from webinterface import htmlInboxMedia -from webinterface import htmlInboxBlogs -from webinterface import htmlInboxNews -from webinterface import htmlUnblockConfirm -from webinterface import htmlPersonOptions -from webinterface import htmlIndividualPost -from webinterface import htmlProfile -from webinterface import htmlInbox -from webinterface import htmlBookmarks -from webinterface import htmlEvents -from webinterface import htmlShares -from webinterface import htmlOutbox -from webinterface import htmlModeration -from webinterface import htmlPostReplies -from webinterface import htmlLogin -from webinterface import htmlSuspended -from webinterface import htmlGetLoginCredentials -from webinterface import htmlNewPost -from webinterface import htmlFollowConfirm -from webinterface import htmlCalendar -from webinterface import htmlSearch -from webinterface import htmlNewswireMobile -from webinterface import htmlLinksMobile -from webinterface import htmlSearchEmoji -from webinterface import htmlSearchEmojiTextEntry -from webinterface import htmlUnfollowConfirm -from webinterface import htmlProfileAfterSearch -from webinterface import htmlEditProfile -from webinterface import htmlEditLinks -from webinterface import htmlEditNewswire -from webinterface import htmlEditNewsPost -from webinterface import htmlTermsOfService -from webinterface import htmlSkillsSearch -from webinterface import htmlHistorySearch -from webinterface import htmlHashtagSearch -from webinterface import rssHashtagSearch -from webinterface import htmlModerationInfo -from webinterface import htmlSearchSharedItems -from webinterface import htmlHashtagBlocked +from webapp_utils import setBlogAddress +from webapp_utils import getBlogAddress +from webapp_calendar import htmlCalendarDeleteConfirm +from webapp_calendar import htmlCalendar +from webapp import htmlFollowingList +from webapp import htmlDeletePost +from webapp import htmlAbout +from webapp import htmlRemoveSharedItem +from webapp import htmlUnblockConfirm +from webapp_person_options import htmlPersonOptions +from webapp_timeline import htmlShares +from webapp_timeline import htmlInbox +from webapp_timeline import htmlBookmarks +from webapp_timeline import htmlEvents +from webapp_timeline import htmlInboxDMs +from webapp_timeline import htmlInboxReplies +from webapp_timeline import htmlInboxMedia +from webapp_timeline import htmlInboxBlogs +from webapp_timeline import htmlInboxNews +from webapp_timeline import htmlOutbox +from webapp_timeline import htmlModeration +from webapp_create_post import htmlNewPost +from webapp import htmlLogin +from webapp import htmlSuspended +from webapp import htmlGetLoginCredentials +from webapp import htmlFollowConfirm +from webapp import htmlUnfollowConfirm +from webapp import htmlEditNewsPost +from webapp import htmlTermsOfService +from webapp import htmlModerationInfo +from webapp import htmlHashtagBlocked +from webapp_post import htmlPostReplies +from webapp_post import htmlIndividualPost +from webapp_profile import htmlEditProfile +from webapp_profile import htmlProfileAfterSearch +from webapp_profile import htmlProfile +from webapp_column_left import htmlLinksMobile +from webapp_column_left import htmlEditLinks +from webapp_column_right import htmlNewswireMobile +from webapp_column_right import htmlEditNewswire +from webapp_column_right import htmlCitations +from webapp_search import htmlSkillsSearch +from webapp_search import htmlHistorySearch +from webapp_search import htmlHashtagSearch +from webapp_search import rssHashtagSearch +from webapp_search import htmlSearchEmoji +from webapp_search import htmlSearchSharedItems +from webapp_search import htmlSearchEmojiTextEntry +from webapp_search import htmlSearch from shares import getSharesFeedForPerson from shares import addShare from shares import removeShare from shares import expireShares +from utils import firstParagraphFromString from utils import clearFromPostCaches from utils import containsInvalidChars from utils import isSystemAccount @@ -3272,7 +3273,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.newswire[publishedDate][0] = \ newsPostTitle self.server.newswire[publishedDate][4] = \ - newsPostContent + firstParagraphFromString(newsPostContent) # save newswire newswireStateFilename = \ baseDir + '/accounts/.newswirestate.json' diff --git a/delete.py b/delete.py index 751b95aa0..3b195db7e 100644 --- a/delete.py +++ b/delete.py @@ -6,6 +6,8 @@ __maintainer__ = "Bob Mottram" __email__ = "bob@freedombone.net" __status__ = "Production" +import os +from datetime import datetime from utils import removeIdEnding from utils import getStatusNumber from utils import urlPermitted @@ -295,3 +297,33 @@ def outboxDelete(baseDir: str, httpPrefix: str, postFilename, debug, recentPostsCache) if debug: print('DEBUG: post deleted via c2s - ' + postFilename) + + +def removeOldHashtags(baseDir: str, maxMonths: int) -> str: + """Remove old hashtags + """ + if maxMonths > 11: + maxMonths = 11 + maxDaysSinceEpoch = \ + (datetime.utcnow() - datetime(1970, 1 + maxMonths, 1)).days + removeHashtags = [] + + for subdir, dirs, files in os.walk(baseDir + '/tags'): + for f in files: + tagsFilename = os.path.join(baseDir + '/tags', f) + if not os.path.isfile(tagsFilename): + continue + # get last modified datetime + modTimesinceEpoc = os.path.getmtime(tagsFilename) + lastModifiedDate = datetime.fromtimestamp(modTimesinceEpoc) + fileDaysSinceEpoch = (lastModifiedDate - datetime(1970, 1, 1)).days + + # check of the file is too old + if fileDaysSinceEpoch < maxDaysSinceEpoch: + removeHashtags.append(tagsFilename) + + for removeFilename in removeHashtags: + try: + os.remove(removeFilename) + except BaseException: + pass diff --git a/feeds.py b/feeds.py new file mode 100644 index 000000000..a3a77ca5c --- /dev/null +++ b/feeds.py @@ -0,0 +1,23 @@ +__filename__ = "feeds.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "1.1.0" +__maintainer__ = "Bob Mottram" +__email__ = "bob@freedombone.net" +__status__ = "Production" + + +def rss2TagHeader(hashtag: str, httpPrefix: str, domainFull: str) -> str: + rssStr = "" + rssStr += "" + rssStr += '' + rssStr += ' #' + hashtag + '' + rssStr += ' ' + httpPrefix + '://' + domainFull + \ + '/tags/rss2/' + hashtag + '' + return rssStr + + +def rss2TagFooter() -> str: + rssStr = '' + rssStr += '' + return rssStr diff --git a/follow.py b/follow.py index 9f61636eb..9403f92ea 100644 --- a/follow.py +++ b/follow.py @@ -1232,3 +1232,17 @@ def outboxUndoFollow(baseDir: str, messageJson: {}, debug: bool) -> None: if debug: print('WARN: ' + nicknameFollower + ' could not unfollow ' + nicknameFollowing + '@' + domainFollowingFull) + + +def followerApprovalActive(baseDir: str, nickname: str, domain: str) -> bool: + """Returns true if the given account requires follower approval + """ + manuallyApprovesFollowers = False + actorFilename = baseDir + '/accounts/' + nickname + '@' + domain + '.json' + if os.path.isfile(actorFilename): + actorJson = loadJson(actorFilename) + if actorJson: + if actorJson.get('manuallyApprovesFollowers'): + manuallyApprovesFollowers = \ + actorJson['manuallyApprovesFollowers'] + return manuallyApprovesFollowers diff --git a/inbox.py b/inbox.py index a15b9f9c8..5deb64b2a 100644 --- a/inbox.py +++ b/inbox.py @@ -56,9 +56,8 @@ from posts import isMuted from posts import isImageMedia from posts import sendSignedJson from posts import sendToFollowersThread -from webinterface import individualPostAsHtml -from webinterface import getIconsDir -from webinterface import removeOldHashtags +from webapp import individualPostAsHtml +from webapp import getIconsDir from question import questionUpdateVotes from media import replaceYouTube from git import isGitPatch @@ -66,6 +65,7 @@ from git import receiveGitPatch from followingCalendar import receivingCalendarEvents from content import dangerousMarkup from happening import saveEventPost +from delete import removeOldHashtags def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None: diff --git a/newsdaemon.py b/newsdaemon.py index 695eaef71..7b9b43430 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -504,8 +504,8 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, if os.path.isfile(filename): # don't create the post if it already exists # set the url - newswire[originalDateStr][1] = \ - '/users/news/statuses/' + statusNumber + # newswire[originalDateStr][1] = \ + # '/users/news/statuses/' + statusNumber # set the filename newswire[originalDateStr][3] = filename continue @@ -521,6 +521,7 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, if rssDescription.startswith('', '') + rssDescription = rssDescription.replace(']]', '') if '&' in rssDescription: rssDescription = html.unescape(rssDescription) rssDescription = '

' + rssDescription + '

' @@ -662,9 +663,11 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, if os.path.isfile(filename + '.arrived'): os.remove(filename + '.arrived') - # set the url - newswire[originalDateStr][1] = \ - '/users/news/statuses/' + statusNumber + # setting the url here links to the activitypub object + # stored locally + # newswire[originalDateStr][1] = \ + # '/users/news/statuses/' + statusNumber + # set the filename newswire[originalDateStr][3] = filename diff --git a/newswire.py b/newswire.py index 3f76e471b..7874ecb9a 100644 --- a/newswire.py +++ b/newswire.py @@ -12,6 +12,7 @@ from socket import error as SocketError import errno from datetime import datetime from collections import OrderedDict +from utils import firstParagraphFromString from utils import isPublicPost from utils import locatePost from utils import loadJson @@ -386,9 +387,12 @@ def getRSSfromDict(baseDir: str, newswire: {}, continue rssStr += '\n' rssStr += ' ' + fields[0] + '\n' + description = firstParagraphFromString(fields[4]) + rssStr += ' ' + description + '\n' url = fields[1] - if domainFull not in url: - url = httpPrefix + '://' + domainFull + url + if '://' not in url: + if domainFull not in url: + url = httpPrefix + '://' + domainFull + url rssStr += ' ' + url + '\n' rssDateStr = pubDate.strftime("%a, %d %b %Y %H:%M:%S UT") @@ -412,6 +416,7 @@ def isNewswireBlogPost(postJsonObject: {}) -> bool: return False if postJsonObject['object'].get('summary') and \ postJsonObject['object'].get('url') and \ + postJsonObject['object'].get('content') and \ postJsonObject['object'].get('published'): return isPublicPost(postJsonObject) return False @@ -500,7 +505,8 @@ def addAccountBlogsToNewswire(baseDir: str, nickname: str, domain: str, votes = [] if os.path.isfile(fullPostFilename + '.votes'): votes = loadJson(fullPostFilename + '.votes') - description = '' + content = postJsonObject['object']['content'] + description = firstParagraphFromString(content) addNewswireDictEntry(baseDir, domain, newswire, published, postJsonObject['object']['summary'], diff --git a/posts.py b/posts.py index ec59bc6db..5e16b369b 100644 --- a/posts.py +++ b/posts.py @@ -3977,3 +3977,27 @@ def sendUndoBlockViaServer(baseDir: str, session, print('DEBUG: c2s POST block success') return newBlockJson + + +def postIsMuted(baseDir: str, nickname: str, domain: str, + postJsonObject: {}, messageId: str) -> bool: + """ Returns true if the given post is muted + """ + isMuted = postJsonObject.get('muted') + if isMuted is True or isMuted is False: + return isMuted + postDir = baseDir + '/accounts/' + nickname + '@' + domain + muteFilename = \ + postDir + '/inbox/' + messageId.replace('/', '#') + '.json.muted' + if os.path.isfile(muteFilename): + return True + muteFilename = \ + postDir + '/outbox/' + messageId.replace('/', '#') + '.json.muted' + if os.path.isfile(muteFilename): + return True + muteFilename = \ + baseDir + '/accounts/cache/announce/' + nickname + \ + '/' + messageId.replace('/', '#') + '.json.muted' + if os.path.isfile(muteFilename): + return True + return False diff --git a/question.py b/question.py index 9618154ae..31e83f29c 100644 --- a/question.py +++ b/question.py @@ -124,3 +124,22 @@ def questionUpdateVotes(baseDir: str, nickname: str, domain: str, # save the question with altered totals saveJson(questionJson, questionPostFilename) return questionJson + + +def isQuestion(postObjectJson: {}) -> bool: + """ is the given post a question? + """ + if postObjectJson['type'] != 'Create' and \ + postObjectJson['type'] != 'Update': + return False + if not isinstance(postObjectJson['object'], dict): + return False + if not postObjectJson['object'].get('type'): + return False + if postObjectJson['object']['type'] != 'Question': + return False + if not postObjectJson['object'].get('oneOf'): + return False + if not isinstance(postObjectJson['object']['oneOf'], list): + return False + return True diff --git a/tests.py b/tests.py index 8a75dbedc..f835378c0 100644 --- a/tests.py +++ b/tests.py @@ -32,6 +32,7 @@ from follow import clearFollows from follow import clearFollowers from follow import sendFollowRequestViaServer from follow import sendUnfollowRequestViaServer +from utils import firstParagraphFromString from utils import removeIdEnding from utils import siteIsActive from utils import updateRecentPostsCache @@ -2336,8 +2337,22 @@ def testGetNewswireTags(): assert '#ExcitingHashtag' in tags +def testFirstParagraphFromString(): + print('testFirstParagraphFromString') + testStr = \ + '

This is a test

' + \ + '

This is another paragraph

' + resultStr = firstParagraphFromString(testStr) + assert resultStr == 'This is a test' + + testStr = 'Testing without html' + resultStr = firstParagraphFromString(testStr) + assert resultStr == testStr + + def runAllTests(): print('Running tests...') + testFirstParagraphFromString() testGetNewswireTags() testHashtagRuleTree() testRemoveHtmlTag() diff --git a/translations/ar.json b/translations/ar.json index 1118a56b1..f39a708a8 100644 --- a/translations/ar.json +++ b/translations/ar.json @@ -327,5 +327,6 @@ "Create an article": "قم بإنشاء مقال", "Settings": "إعدادات", "Citations": "اقتباسات", - "Choose newswire items referenced in your article": "اختر العناصر الإخبارية المشار إليها في مقالتك" + "Choose newswire items referenced in your article": "اختر العناصر الإخبارية المشار إليها في مقالتك", + "RSS feed for your blog": "تغذية RSS لمدونتك" } diff --git a/translations/ca.json b/translations/ca.json index 797ad5469..29c09497c 100644 --- a/translations/ca.json +++ b/translations/ca.json @@ -327,5 +327,6 @@ "Create an article": "Creeu un article", "Settings": "Configuració", "Citations": "Cites", - "Choose newswire items referenced in your article": "Trieu articles de newswire als quals faci referència el vostre article" + "Choose newswire items referenced in your article": "Trieu articles de newswire als quals faci referència el vostre article", + "RSS feed for your blog": "Feed RSS del vostre bloc" } diff --git a/translations/cy.json b/translations/cy.json index d5cdb532d..d6791b73b 100644 --- a/translations/cy.json +++ b/translations/cy.json @@ -327,5 +327,6 @@ "Create an article": "Creu erthygl", "Settings": "Gosodiadau", "Citations": "Dyfyniadau", - "Choose newswire items referenced in your article": "Dewiswch eitemau newyddion y cyfeirir atynt yn eich erthygl" + "Choose newswire items referenced in your article": "Dewiswch eitemau newyddion y cyfeirir atynt yn eich erthygl", + "RSS feed for your blog": "Porthiant RSS ar gyfer eich blog" } diff --git a/translations/de.json b/translations/de.json index b8eb64df8..b9a4fd7ec 100644 --- a/translations/de.json +++ b/translations/de.json @@ -327,5 +327,6 @@ "Create an article": "Erstellen Sie einen Artikel", "Settings": "Einstellungen", "Citations": "Zitate", - "Choose newswire items referenced in your article": "Wählen Sie Newswire-Artikel aus, auf die in Ihrem Artikel verwiesen wird" + "Choose newswire items referenced in your article": "Wählen Sie Newswire-Artikel aus, auf die in Ihrem Artikel verwiesen wird", + "RSS feed for your blog": "RSS-Feed für Ihr Blog" } diff --git a/translations/en.json b/translations/en.json index 0b7b9b3c3..f2c6a566d 100644 --- a/translations/en.json +++ b/translations/en.json @@ -327,5 +327,6 @@ "Create an article": "Create an article", "Settings": "Settings", "Citations": "Citations", - "Choose newswire items referenced in your article": "Choose newswire items referenced in your article" + "Choose newswire items referenced in your article": "Choose newswire items referenced in your article", + "RSS feed for your blog": "RSS feed for your blog" } diff --git a/translations/es.json b/translations/es.json index dac48137c..693590fb6 100644 --- a/translations/es.json +++ b/translations/es.json @@ -327,5 +327,6 @@ "Create an article": "Crea un articulo", "Settings": "Configuraciones", "Citations": "Citas", - "Choose newswire items referenced in your article": "Elija elementos de Newswire a los que se hace referencia en su artículo" + "Choose newswire items referenced in your article": "Elija elementos de Newswire a los que se hace referencia en su artículo", + "RSS feed for your blog": "Fuente RSS para tu blog" } diff --git a/translations/fr.json b/translations/fr.json index e1edeefb3..f5422ddbb 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -327,5 +327,6 @@ "Create an article": "Créer un article", "Settings": "Réglages", "Citations": "Citations", - "Choose newswire items referenced in your article": "Choisissez les éléments de fil d'actualité référencés dans votre article" + "Choose newswire items referenced in your article": "Choisissez les éléments de fil d'actualité référencés dans votre article", + "RSS feed for your blog": "Flux RSS pour votre blog" } diff --git a/translations/ga.json b/translations/ga.json index ba28b61e6..3836db30d 100644 --- a/translations/ga.json +++ b/translations/ga.json @@ -327,5 +327,6 @@ "Create an article": "Cruthaigh alt", "Settings": "Socruithe", "Citations": "Citations", - "Choose newswire items referenced in your article": "Roghnaigh míreanna sreanga nuachta dá dtagraítear i d’alt" + "Choose newswire items referenced in your article": "Roghnaigh míreanna sreanga nuachta dá dtagraítear i d’alt", + "RSS feed for your blog": "Fotha RSS do do bhlag" } diff --git a/translations/hi.json b/translations/hi.json index 11daf17df..31b486e7f 100644 --- a/translations/hi.json +++ b/translations/hi.json @@ -327,5 +327,6 @@ "Create an article": "एक लेख बनाएँ", "Settings": "समायोजन", "Citations": "उद्धरण", - "Choose newswire items referenced in your article": "अपने लेख में संदर्भित newswire आइटम चुनें" + "Choose newswire items referenced in your article": "अपने लेख में संदर्भित newswire आइटम चुनें", + "RSS feed for your blog": "RSS आपके ब्लॉग के लिए फ़ीड करता है" } diff --git a/translations/it.json b/translations/it.json index 2470fc5d9..f65e18c98 100644 --- a/translations/it.json +++ b/translations/it.json @@ -327,5 +327,6 @@ "Create an article": "Crea un articolo", "Settings": "impostazioni", "Citations": "Citazioni", - "Choose newswire items referenced in your article": "Scegli gli articoli del newswire a cui fa riferimento il tuo articolo" + "Choose newswire items referenced in your article": "Scegli gli articoli del newswire a cui fa riferimento il tuo articolo", + "RSS feed for your blog": "Feed RSS per il tuo blog" } diff --git a/translations/ja.json b/translations/ja.json index d6b71d6ca..bafa38784 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -327,5 +327,6 @@ "Create an article": "記事を作成する", "Settings": "設定", "Citations": "引用", - "Choose newswire items referenced in your article": "あなたの記事で参照されているニュースワイヤーアイテムを選択してください" + "Choose newswire items referenced in your article": "あなたの記事で参照されているニュースワイヤーアイテムを選択してください", + "RSS feed for your blog": "ブログのRSSフィード" } diff --git a/translations/oc.json b/translations/oc.json index efda869e1..250302075 100644 --- a/translations/oc.json +++ b/translations/oc.json @@ -323,5 +323,6 @@ "Create an article": "Create an article", "Settings": "Settings", "Citations": "Citations", - "Choose newswire items referenced in your article": "Choose newswire items referenced in your article" + "Choose newswire items referenced in your article": "Choose newswire items referenced in your article", + "RSS feed for your blog": "RSS feed for your blog" } diff --git a/translations/pt.json b/translations/pt.json index 316fdcf85..831c1d106 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -327,5 +327,6 @@ "Create an article": "Crie um artigo", "Settings": "Definições", "Citations": "Citações", - "Choose newswire items referenced in your article": "Escolha os itens de notícias mencionados em seu artigo" + "Choose newswire items referenced in your article": "Escolha os itens de notícias mencionados em seu artigo", + "RSS feed for your blog": "Feed RSS para o seu blog" } diff --git a/translations/ru.json b/translations/ru.json index 885238420..2665feb49 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -327,5 +327,6 @@ "Create an article": "Создать статью", "Settings": "Настройки", "Citations": "Цитаты", - "Choose newswire items referenced in your article": "Выберите элементы ленты новостей, на которые есть ссылки в вашей статье" + "Choose newswire items referenced in your article": "Выберите элементы ленты новостей, на которые есть ссылки в вашей статье", + "RSS feed for your blog": "RSS-канал для вашего блога" } diff --git a/translations/zh.json b/translations/zh.json index f7ba586ec..635ceeeff 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -327,5 +327,6 @@ "Create an article": "建立文章", "Settings": "设定值", "Citations": "引文", - "Choose newswire items referenced in your article": "选择文章中引用的新闻专栏文章" + "Choose newswire items referenced in your article": "选择文章中引用的新闻专栏文章", + "RSS feed for your blog": "您博客的RSS供稿" } diff --git a/utils.py b/utils.py index 08d21f159..8989b7f52 100644 --- a/utils.py +++ b/utils.py @@ -38,6 +38,18 @@ def removeHtml(content: str) -> str: return result +def firstParagraphFromString(content: str) -> str: + """Get the first paragraph from a blog post + to be used as a summary in the newswire feed + """ + if '

' not in content or '

' not in content: + return removeHtml(content) + paragraph = content.split('

')[1] + if '

' in paragraph: + paragraph = paragraph.split('

')[0] + return removeHtml(paragraph) + + def isSystemAccount(nickname: str) -> bool: """Returns true if the given nickname is a system account """ @@ -1482,3 +1494,11 @@ def siteIsActive(url: str) -> bool: if e.errno == errno.ECONNRESET: print('WARN: connection was reset during siteIsActive') return False + + +def weekDayOfMonthStart(monthNumber: int, year: int) -> int: + """Gets the day number of the first day of the month + 1=sun, 7=sat + """ + firstDayOfMonth = datetime.datetime(year, monthNumber, 1, 0, 0) + return int(firstDayOfMonth.strftime("%w")) + 1 diff --git a/webapp.py b/webapp.py new file mode 100644 index 000000000..f51ff06ff --- /dev/null +++ b/webapp.py @@ -0,0 +1,771 @@ +__filename__ = "webapp.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "1.1.0" +__maintainer__ = "Bob Mottram" +__email__ = "bob@freedombone.net" +__status__ = "Production" + +import time +import os +from shutil import copyfile +from utils import getCSS +from utils import getNicknameFromActor +from utils import getDomainFromActor +from utils import locatePost +from utils import noOfAccounts +from utils import loadJson +from utils import getConfigParam +from posts import isEditor +from shares import getValidSharedItemID +from webapp_utils import getAltPath +from webapp_utils import getIconsDir +from webapp_utils import htmlHeader +from webapp_utils import htmlFooter +from webapp_post import individualPostAsHtml + + +def htmlFollowingList(cssCache: {}, baseDir: str, + followingFilename: str) -> str: + """Returns a list of handles being followed + """ + with open(followingFilename, 'r') as followingFile: + msg = followingFile.read() + followingList = msg.split('\n') + followingList.sort() + if followingList: + cssFilename = baseDir + '/epicyon-profile.css' + if os.path.isfile(baseDir + '/epicyon.css'): + cssFilename = baseDir + '/epicyon.css' + + profileCSS = getCSS(baseDir, cssFilename, cssCache) + if profileCSS: + followingListHtml = htmlHeader(cssFilename, profileCSS) + for followingAddress in followingList: + if followingAddress: + followingListHtml += \ + '

@' + followingAddress + '

' + followingListHtml += htmlFooter() + msg = followingListHtml + return msg + return '' + + +def htmlModerationInfo(cssCache: {}, translate: {}, + baseDir: str, httpPrefix: str) -> str: + msgStr1 = \ + 'These are globally blocked for all accounts on this instance' + msgStr2 = \ + 'Any blocks or suspensions made by moderators will be shown here.' + infoForm = '' + cssFilename = baseDir + '/epicyon-profile.css' + if os.path.isfile(baseDir + '/epicyon.css'): + cssFilename = baseDir + '/epicyon.css' + + infoCSS = getCSS(baseDir, cssFilename, cssCache) + if infoCSS: + if httpPrefix != 'https': + infoCSS = infoCSS.replace('https://', + httpPrefix + '://') + infoForm = htmlHeader(cssFilename, infoCSS) + + infoForm += \ + '

' + \ + translate['Moderation Information'] + \ + '

' + + infoShown = False + suspendedFilename = baseDir + '/accounts/suspended.txt' + if os.path.isfile(suspendedFilename): + with open(suspendedFilename, "r") as f: + suspendedStr = f.read() + infoForm += '
' + infoForm += '
' + \ + translate['Suspended accounts'] + '' + infoForm += '
' + \ + translate['These are currently suspended'] + infoForm += \ + ' ' + infoForm += '
' + infoShown = True + + blockingFilename = baseDir + '/accounts/blocking.txt' + if os.path.isfile(blockingFilename): + with open(blockingFilename, "r") as f: + blockedStr = f.read() + infoForm += '
' + infoForm += \ + '
' + \ + translate['Blocked accounts and hashtags'] + '' + infoForm += \ + '
' + \ + translate[msgStr1] + infoForm += \ + ' ' + infoForm += '
' + infoShown = True + if not infoShown: + infoForm += \ + '

' + \ + translate[msgStr2] + \ + '

' + infoForm += htmlFooter() + return infoForm + + +def htmlEditNewsPost(cssCache: {}, translate: {}, baseDir: str, path: str, + domain: str, port: int, + httpPrefix: str, postUrl: str) -> str: + """Edits a news post + """ + if '/users/' not in path: + return '' + pathOriginal = path + + nickname = getNicknameFromActor(path) + if not nickname: + return '' + + # is the user an editor? + if not isEditor(baseDir, nickname): + return '' + + postUrl = postUrl.replace('/', '#') + postFilename = locatePost(baseDir, nickname, domain, postUrl) + if not postFilename: + return '' + postJsonObject = loadJson(postFilename) + if not postJsonObject: + return '' + + cssFilename = baseDir + '/epicyon-links.css' + if os.path.isfile(baseDir + '/links.css'): + cssFilename = baseDir + '/links.css' + + editCSS = getCSS(baseDir, cssFilename, cssCache) + if editCSS: + if httpPrefix != 'https': + editCSS = \ + editCSS.replace('https://', httpPrefix + '://') + + editNewsPostForm = htmlHeader(cssFilename, editCSS) + editNewsPostForm += \ + '
\n' + editNewsPostForm += \ + '
\n' + editNewsPostForm += \ + '

' + translate['Edit News Post'] + '

' + editNewsPostForm += \ + '
\n' + editNewsPostForm += \ + ' ' + \ + '\n' + editNewsPostForm += \ + ' \n' + editNewsPostForm += \ + '
\n' + + editNewsPostForm += \ + '
' + + editNewsPostForm += \ + ' \n' + + newsPostTitle = postJsonObject['object']['summary'] + editNewsPostForm += \ + '
\n' + + newsPostContent = postJsonObject['object']['content'] + editNewsPostForm += \ + ' ' + + editNewsPostForm += \ + '
' + + editNewsPostForm += htmlFooter() + return editNewsPostForm + + +def htmlGetLoginCredentials(loginParams: str, + lastLoginTime: int) -> (str, str, bool): + """Receives login credentials via HTTPServer POST + """ + if not loginParams.startswith('username='): + return None, None, None + # minimum time between login attempts + currTime = int(time.time()) + if currTime < lastLoginTime+10: + return None, None, None + if '&' not in loginParams: + return None, None, None + loginArgs = loginParams.split('&') + nickname = None + password = None + register = False + for arg in loginArgs: + if '=' in arg: + if arg.split('=', 1)[0] == 'username': + nickname = arg.split('=', 1)[1] + elif arg.split('=', 1)[0] == 'password': + password = arg.split('=', 1)[1] + elif arg.split('=', 1)[0] == 'register': + register = True + return nickname, password, register + + +def htmlLogin(cssCache: {}, translate: {}, + baseDir: str, autocomplete=True) -> str: + """Shows the login screen + """ + accounts = noOfAccounts(baseDir) + + loginImage = 'login.png' + loginImageFilename = None + if os.path.isfile(baseDir + '/accounts/' + loginImage): + loginImageFilename = baseDir + '/accounts/' + loginImage + elif os.path.isfile(baseDir + '/accounts/login.jpg'): + loginImage = 'login.jpg' + loginImageFilename = baseDir + '/accounts/' + loginImage + elif os.path.isfile(baseDir + '/accounts/login.jpeg'): + loginImage = 'login.jpeg' + loginImageFilename = baseDir + '/accounts/' + loginImage + elif os.path.isfile(baseDir + '/accounts/login.gif'): + loginImage = 'login.gif' + loginImageFilename = baseDir + '/accounts/' + loginImage + elif os.path.isfile(baseDir + '/accounts/login.webp'): + loginImage = 'login.webp' + loginImageFilename = baseDir + '/accounts/' + loginImage + elif os.path.isfile(baseDir + '/accounts/login.avif'): + loginImage = 'login.avif' + loginImageFilename = baseDir + '/accounts/' + loginImage + + if not loginImageFilename: + loginImageFilename = baseDir + '/accounts/' + loginImage + copyfile(baseDir + '/img/login.png', loginImageFilename) + + if os.path.isfile(baseDir + '/accounts/login-background-custom.jpg'): + if not os.path.isfile(baseDir + '/accounts/login-background.jpg'): + copyfile(baseDir + '/accounts/login-background-custom.jpg', + baseDir + '/accounts/login-background.jpg') + + if accounts > 0: + loginText = \ + '' + else: + loginText = \ + '' + loginText += \ + '' + if os.path.isfile(baseDir + '/accounts/login.txt'): + # custom login message + with open(baseDir + '/accounts/login.txt', 'r') as file: + loginText = '' + + cssFilename = baseDir + '/epicyon-login.css' + if os.path.isfile(baseDir + '/login.css'): + cssFilename = baseDir + '/login.css' + + loginCSS = getCSS(baseDir, cssFilename, cssCache) + if not loginCSS: + print('ERROR: login css file missing ' + cssFilename) + return None + + # show the register button + registerButtonStr = '' + if getConfigParam(baseDir, 'registration') == 'open': + if int(getConfigParam(baseDir, 'registrationsRemaining')) > 0: + if accounts > 0: + idx = 'Welcome. Please login or register a new account.' + loginText = \ + '' + registerButtonStr = \ + '' + + TOSstr = \ + '' + TOSstr += \ + '' + + loginButtonStr = '' + if accounts > 0: + loginButtonStr = \ + '' + + autocompleteStr = '' + if not autocomplete: + autocompleteStr = 'autocomplete="off" value=""' + + loginForm = htmlHeader(cssFilename, loginCSS) + loginForm += '
\n' + loginForm += '\n' + loginForm += '
\n' + loginForm += \ + ' login image\n' + loginForm += loginText + TOSstr + '\n' + loginForm += '
\n' + loginForm += '\n' + loginForm += '
\n' + loginForm += ' \n' + loginForm += \ + ' \n' + loginForm += '\n' + loginForm += ' \n' + loginForm += \ + ' \n' + loginForm += loginButtonStr + registerButtonStr + '\n' + loginForm += '
\n' + loginForm += '\n' + loginForm += \ + '' + \ + '' + \
+        translate['Get the source code'] + '\n' + loginForm += htmlFooter() + return loginForm + + +def htmlTermsOfService(cssCache: {}, baseDir: str, + httpPrefix: str, domainFull: str) -> str: + """Show the terms of service screen + """ + adminNickname = getConfigParam(baseDir, 'admin') + if not os.path.isfile(baseDir + '/accounts/tos.txt'): + copyfile(baseDir + '/default_tos.txt', + baseDir + '/accounts/tos.txt') + + if os.path.isfile(baseDir + '/accounts/login-background-custom.jpg'): + if not os.path.isfile(baseDir + '/accounts/login-background.jpg'): + copyfile(baseDir + '/accounts/login-background-custom.jpg', + baseDir + '/accounts/login-background.jpg') + + TOSText = 'Terms of Service go here.' + if os.path.isfile(baseDir + '/accounts/tos.txt'): + with open(baseDir + '/accounts/tos.txt', 'r') as file: + TOSText = file.read() + + TOSForm = '' + cssFilename = baseDir + '/epicyon-profile.css' + if os.path.isfile(baseDir + '/epicyon.css'): + cssFilename = baseDir + '/epicyon.css' + + termsCSS = getCSS(baseDir, cssFilename, cssCache) + if termsCSS: + if httpPrefix != 'https': + termsCSS = termsCSS.replace('https://', httpPrefix+'://') + + TOSForm = htmlHeader(cssFilename, termsCSS) + TOSForm += '
' + TOSText + '
\n' + if adminNickname: + adminActor = httpPrefix + '://' + domainFull + \ + '/users/' + adminNickname + TOSForm += \ + '
\n' + \ + '

Administered by ' + adminNickname + '

\n' + \ + '
\n' + TOSForm += htmlFooter() + return TOSForm + + +def htmlAbout(cssCache: {}, baseDir: str, httpPrefix: str, + domainFull: str, onionDomain: str) -> str: + """Show the about screen + """ + adminNickname = getConfigParam(baseDir, 'admin') + if not os.path.isfile(baseDir + '/accounts/about.txt'): + copyfile(baseDir + '/default_about.txt', + baseDir + '/accounts/about.txt') + + if os.path.isfile(baseDir + '/accounts/login-background-custom.jpg'): + if not os.path.isfile(baseDir + '/accounts/login-background.jpg'): + copyfile(baseDir + '/accounts/login-background-custom.jpg', + baseDir + '/accounts/login-background.jpg') + + aboutText = 'Information about this instance goes here.' + if os.path.isfile(baseDir + '/accounts/about.txt'): + with open(baseDir + '/accounts/about.txt', 'r') as aboutFile: + aboutText = aboutFile.read() + + aboutForm = '' + cssFilename = baseDir + '/epicyon-profile.css' + if os.path.isfile(baseDir + '/epicyon.css'): + cssFilename = baseDir + '/epicyon.css' + + aboutCSS = getCSS(baseDir, cssFilename, cssCache) + if aboutCSS: + if httpPrefix != 'http': + aboutCSS = aboutCSS.replace('https://', + httpPrefix + '://') + + aboutForm = htmlHeader(cssFilename, aboutCSS) + aboutForm += '
' + aboutText + '
' + if onionDomain: + aboutForm += \ + '
\n' + \ + '

' + \ + 'http://' + onionDomain + '

\n
\n' + if adminNickname: + adminActor = '/users/' + adminNickname + aboutForm += \ + '
\n' + \ + '

Administered by ' + adminNickname + '

\n' + \ + '
\n' + aboutForm += htmlFooter() + return aboutForm + + +def htmlHashtagBlocked(cssCache: {}, baseDir: str, translate: {}) -> str: + """Show the screen for a blocked hashtag + """ + blockedHashtagForm = '' + cssFilename = baseDir + '/epicyon-suspended.css' + if os.path.isfile(baseDir + '/suspended.css'): + cssFilename = baseDir + '/suspended.css' + + blockedHashtagCSS = getCSS(baseDir, cssFilename, cssCache) + if blockedHashtagCSS: + blockedHashtagForm = htmlHeader(cssFilename, blockedHashtagCSS) + blockedHashtagForm += '
\n' + blockedHashtagForm += \ + '

' + \ + translate['Hashtag Blocked'] + '

\n' + blockedHashtagForm += \ + '

See ' + \ + translate['Terms of Service'] + '

\n' + blockedHashtagForm += '
\n' + blockedHashtagForm += htmlFooter() + return blockedHashtagForm + + +def htmlSuspended(cssCache: {}, baseDir: str) -> str: + """Show the screen for suspended accounts + """ + suspendedForm = '' + cssFilename = baseDir + '/epicyon-suspended.css' + if os.path.isfile(baseDir + '/suspended.css'): + cssFilename = baseDir + '/suspended.css' + + suspendedCSS = getCSS(baseDir, cssFilename, cssCache) + if suspendedCSS: + suspendedForm = htmlHeader(cssFilename, suspendedCSS) + suspendedForm += '
\n' + suspendedForm += '

Account Suspended

\n' + suspendedForm += '

See Terms of Service

\n' + suspendedForm += '
\n' + suspendedForm += htmlFooter() + return suspendedForm + + +def htmlRemoveSharedItem(cssCache: {}, translate: {}, baseDir: str, + actor: str, shareName: str, + callingDomain: str) -> str: + """Shows a screen asking to confirm the removal of a shared item + """ + itemID = getValidSharedItemID(shareName) + nickname = getNicknameFromActor(actor) + domain, port = getDomainFromActor(actor) + domainFull = domain + if port: + if port != 80 and port != 443: + domainFull = domain + ':' + str(port) + sharesFile = baseDir + '/accounts/' + \ + nickname + '@' + domain + '/shares.json' + if not os.path.isfile(sharesFile): + print('ERROR: no shares file ' + sharesFile) + return None + sharesJson = loadJson(sharesFile) + if not sharesJson: + print('ERROR: unable to load shares.json') + return None + if not sharesJson.get(itemID): + print('ERROR: share named "' + itemID + '" is not in ' + sharesFile) + return None + sharedItemDisplayName = sharesJson[itemID]['displayName'] + sharedItemImageUrl = None + if sharesJson[itemID].get('imageUrl'): + sharedItemImageUrl = sharesJson[itemID]['imageUrl'] + + if os.path.isfile(baseDir + '/img/shares-background.png'): + if not os.path.isfile(baseDir + '/accounts/shares-background.png'): + copyfile(baseDir + '/img/shares-background.png', + baseDir + '/accounts/shares-background.png') + + cssFilename = baseDir + '/epicyon-follow.css' + if os.path.isfile(baseDir + '/follow.css'): + cssFilename = baseDir + '/follow.css' + + profileStyle = getCSS(baseDir, cssFilename, cssCache) + sharesStr = htmlHeader(cssFilename, profileStyle) + sharesStr += '\n' + sharesStr += htmlFooter() + return sharesStr + + +def htmlDeletePost(cssCache: {}, + recentPostsCache: {}, maxRecentPosts: int, + translate, pageNumber: int, + session, baseDir: str, messageId: str, + httpPrefix: str, projectVersion: str, + wfRequest: {}, personCache: {}, + callingDomain: str, + YTReplacementDomain: str, + showPublishedDateOnly: bool) -> str: + """Shows a screen asking to confirm the deletion of a post + """ + if '/statuses/' not in messageId: + return None + iconsDir = getIconsDir(baseDir) + actor = messageId.split('/statuses/')[0] + nickname = getNicknameFromActor(actor) + domain, port = getDomainFromActor(actor) + domainFull = domain + if port: + if port != 80 and port != 443: + domainFull = domain + ':' + str(port) + + postFilename = locatePost(baseDir, nickname, domain, messageId) + if not postFilename: + return None + + postJsonObject = loadJson(postFilename) + if not postJsonObject: + return None + + if os.path.isfile(baseDir + '/img/delete-background.png'): + if not os.path.isfile(baseDir + '/accounts/delete-background.png'): + copyfile(baseDir + '/img/delete-background.png', + baseDir + '/accounts/delete-background.png') + + deletePostStr = None + cssFilename = baseDir + '/epicyon-profile.css' + if os.path.isfile(baseDir + '/epicyon.css'): + cssFilename = baseDir + '/epicyon.css' + + profileStyle = getCSS(baseDir, cssFilename, cssCache) + if profileStyle: + if httpPrefix != 'https': + profileStyle = profileStyle.replace('https://', + httpPrefix + '://') + deletePostStr = htmlHeader(cssFilename, profileStyle) + deletePostStr += \ + individualPostAsHtml(True, recentPostsCache, maxRecentPosts, + iconsDir, translate, pageNumber, + baseDir, session, wfRequest, personCache, + nickname, domain, port, postJsonObject, + None, True, False, + httpPrefix, projectVersion, 'outbox', + YTReplacementDomain, + showPublishedDateOnly, + False, False, False, False, False) + deletePostStr += '
' + deletePostStr += \ + '

' + \ + translate['Delete this post?'] + '

' + + postActor = getAltPath(actor, domainFull, callingDomain) + deletePostStr += \ + '
\n' + deletePostStr += \ + ' \n' + deletePostStr += \ + ' \n' + deletePostStr += \ + ' \n' + deletePostStr += \ + ' \n' + deletePostStr += '
\n' + deletePostStr += '
\n' + deletePostStr += htmlFooter() + return deletePostStr + + +def htmlFollowConfirm(cssCache: {}, translate: {}, baseDir: str, + originPathStr: str, + followActor: str, + followProfileUrl: str) -> str: + """Asks to confirm a follow + """ + followDomain, port = getDomainFromActor(followActor) + + if os.path.isfile(baseDir + '/accounts/follow-background-custom.jpg'): + if not os.path.isfile(baseDir + '/accounts/follow-background.jpg'): + copyfile(baseDir + '/accounts/follow-background-custom.jpg', + baseDir + '/accounts/follow-background.jpg') + + cssFilename = baseDir + '/epicyon-follow.css' + if os.path.isfile(baseDir + '/follow.css'): + cssFilename = baseDir + '/follow.css' + + profileStyle = getCSS(baseDir, cssFilename, cssCache) + followStr = htmlHeader(cssFilename, profileStyle) + followStr += '\n' + followStr += htmlFooter() + return followStr + + +def htmlUnfollowConfirm(cssCache: {}, translate: {}, baseDir: str, + originPathStr: str, + followActor: str, + followProfileUrl: str) -> str: + """Asks to confirm unfollowing an actor + """ + followDomain, port = getDomainFromActor(followActor) + + if os.path.isfile(baseDir + '/accounts/follow-background-custom.jpg'): + if not os.path.isfile(baseDir + '/accounts/follow-background.jpg'): + copyfile(baseDir + '/accounts/follow-background-custom.jpg', + baseDir + '/accounts/follow-background.jpg') + + cssFilename = baseDir + '/epicyon-follow.css' + if os.path.isfile(baseDir + '/follow.css'): + cssFilename = baseDir + '/follow.css' + + profileStyle = getCSS(baseDir, cssFilename, cssCache) + + followStr = htmlHeader(cssFilename, profileStyle) + followStr += '\n' + followStr += htmlFooter() + return followStr + + +def htmlUnblockConfirm(cssCache: {}, translate: {}, baseDir: str, + originPathStr: str, + blockActor: str, + blockProfileUrl: str) -> str: + """Asks to confirm unblocking an actor + """ + blockDomain, port = getDomainFromActor(blockActor) + + if os.path.isfile(baseDir + '/img/block-background.png'): + if not os.path.isfile(baseDir + '/accounts/block-background.png'): + copyfile(baseDir + '/img/block-background.png', + baseDir + '/accounts/block-background.png') + + cssFilename = baseDir + '/epicyon-follow.css' + if os.path.isfile(baseDir + '/follow.css'): + cssFilename = baseDir + '/follow.css' + + profileStyle = getCSS(baseDir, cssFilename, cssCache) + + blockStr = htmlHeader(cssFilename, profileStyle) + blockStr += '
\n' + blockStr += '
\n' + blockStr += '
\n' + blockStr += ' \n' + blockStr += ' \n' + blockStr += \ + '

' + translate['Stop blocking'] + ' ' + \ + getNicknameFromActor(blockActor) + '@' + blockDomain + ' ?

\n' + blockStr += '
\n' + blockStr += ' \n' + blockStr += \ + ' \n' + blockStr += \ + ' \n' + blockStr += '
\n' + blockStr += '
\n' + blockStr += '
\n' + blockStr += '
\n' + blockStr += htmlFooter() + return blockStr diff --git a/webapp_calendar.py b/webapp_calendar.py new file mode 100644 index 000000000..f96d067cb --- /dev/null +++ b/webapp_calendar.py @@ -0,0 +1,398 @@ +__filename__ = "webapp_calendar.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "1.1.0" +__maintainer__ = "Bob Mottram" +__email__ = "bob@freedombone.net" +__status__ = "Production" + +import os +from datetime import datetime +from datetime import date +from shutil import copyfile +from utils import getNicknameFromActor +from utils import getDomainFromActor +from utils import locatePost +from utils import loadJson +from utils import getCSS +from utils import weekDayOfMonthStart +from happening import getTodaysEvents +from happening import getCalendarEvents +from webapp_utils import htmlHeader +from webapp_utils import htmlFooter +from webapp_utils import getAltPath +from webapp_utils import getIconsDir + + +def htmlCalendarDeleteConfirm(cssCache: {}, translate: {}, baseDir: str, + path: str, httpPrefix: str, + domainFull: str, postId: str, postTime: str, + year: int, monthNumber: int, + dayNumber: int, callingDomain: str) -> str: + """Shows a screen asking to confirm the deletion of a calendar event + """ + nickname = getNicknameFromActor(path) + actor = httpPrefix + '://' + domainFull + '/users/' + nickname + domain, port = getDomainFromActor(actor) + messageId = actor + '/statuses/' + postId + + postFilename = locatePost(baseDir, nickname, domain, messageId) + if not postFilename: + return None + + postJsonObject = loadJson(postFilename) + if not postJsonObject: + return None + + if os.path.isfile(baseDir + '/img/delete-background.png'): + if not os.path.isfile(baseDir + '/accounts/delete-background.png'): + copyfile(baseDir + '/img/delete-background.png', + baseDir + '/accounts/delete-background.png') + + deletePostStr = None + cssFilename = baseDir + '/epicyon-profile.css' + if os.path.isfile(baseDir + '/epicyon.css'): + cssFilename = baseDir + '/epicyon.css' + + profileStyle = getCSS(baseDir, cssFilename, cssCache) + if profileStyle: + if httpPrefix != 'https': + profileStyle = profileStyle.replace('https://', + httpPrefix + '://') + deletePostStr = htmlHeader(cssFilename, profileStyle) + deletePostStr += \ + '

' + postTime + ' ' + str(year) + '/' + \ + str(monthNumber) + \ + '/' + str(dayNumber) + '

' + deletePostStr += '
' + deletePostStr += '

' + \ + translate['Delete this event'] + '

' + + postActor = getAltPath(actor, domainFull, callingDomain) + deletePostStr += \ + '
\n' + deletePostStr += ' \n' + deletePostStr += ' \n' + deletePostStr += ' \n' + deletePostStr += \ + ' \n' + deletePostStr += \ + ' \n' + deletePostStr += \ + ' \n' + deletePostStr += \ + ' \n' + deletePostStr += '
\n' + deletePostStr += '
\n' + deletePostStr += htmlFooter() + return deletePostStr + + +def htmlCalendarDay(cssCache: {}, translate: {}, + baseDir: str, path: str, + year: int, monthNumber: int, dayNumber: int, + nickname: str, domain: str, dayEvents: [], + monthName: str, actor: str) -> str: + """Show a day within the calendar + """ + accountDir = baseDir + '/accounts/' + nickname + '@' + domain + calendarFile = accountDir + '/.newCalendar' + if os.path.isfile(calendarFile): + os.remove(calendarFile) + + cssFilename = baseDir + '/epicyon-calendar.css' + if os.path.isfile(baseDir + '/calendar.css'): + cssFilename = baseDir + '/calendar.css' + + calendarStyle = getCSS(baseDir, cssFilename, cssCache) + + calActor = actor + if '/users/' in actor: + calActor = '/users/' + actor.split('/users/')[1] + + calendarStr = htmlHeader(cssFilename, calendarStyle) + calendarStr += '
\n' + calendarStr += '\n' + calendarStr += '\n' + + iconsDir = getIconsDir(baseDir) + + if dayEvents: + for eventPost in dayEvents: + eventTime = None + eventDescription = None + eventPlace = None + postId = None + # get the time place and description + for ev in eventPost: + if ev['type'] == 'Event': + if ev.get('postId'): + postId = ev['postId'] + if ev.get('startTime'): + eventDate = \ + datetime.strptime(ev['startTime'], + "%Y-%m-%dT%H:%M:%S%z") + eventTime = eventDate.strftime("%H:%M").strip() + if ev.get('name'): + eventDescription = ev['name'].strip() + elif ev['type'] == 'Place': + if ev.get('name'): + eventPlace = ev['name'] + + deleteButtonStr = '' + if postId: + deleteButtonStr = \ + '\n' + + if eventTime and eventDescription and eventPlace: + calendarStr += \ + '' + deleteButtonStr + '\n' + elif eventTime and eventDescription and not eventPlace: + calendarStr += \ + '' + deleteButtonStr + '\n' + elif not eventTime and eventDescription and not eventPlace: + calendarStr += \ + '' + deleteButtonStr + '\n' + elif not eventTime and eventDescription and eventPlace: + calendarStr += \ + '' + \ + '' + deleteButtonStr + '\n' + elif eventTime and not eventDescription and eventPlace: + calendarStr += \ + '' + \ + deleteButtonStr + '\n' + + calendarStr += '\n' + calendarStr += '
\n' + calendarStr += \ + ' \n' + calendarStr += \ + '

' + str(dayNumber) + ' ' + monthName + \ + '


' + str(year) + '\n' + calendarStr += '
\n' + \
+                    translate['Delete this event'] + ' |
' + eventTime + \ + '' + \ + '' + \ + eventPlace + '
' + eventDescription + \ + '
' + eventTime + \ + '' + \ + eventDescription + '
' + \ + '' + \ + eventDescription + '
' + \ + eventPlace + '
' + eventDescription + \ + '
' + eventTime + \ + '' + \ + '' + \ + eventPlace + '
\n' + calendarStr += htmlFooter() + + return calendarStr + + +def htmlCalendar(cssCache: {}, translate: {}, + baseDir: str, path: str, + httpPrefix: str, domainFull: str) -> str: + """Show the calendar for a person + """ + iconsDir = getIconsDir(baseDir) + domain = domainFull + if ':' in domainFull: + domain = domainFull.split(':')[0] + + monthNumber = 0 + dayNumber = None + year = 1970 + actor = httpPrefix + '://' + domainFull + path.replace('/calendar', '') + if '?' in actor: + first = True + for p in actor.split('?'): + if not first: + if '=' in p: + if p.split('=')[0] == 'year': + numStr = p.split('=')[1] + if numStr.isdigit(): + year = int(numStr) + elif p.split('=')[0] == 'month': + numStr = p.split('=')[1] + if numStr.isdigit(): + monthNumber = int(numStr) + elif p.split('=')[0] == 'day': + numStr = p.split('=')[1] + if numStr.isdigit(): + dayNumber = int(numStr) + first = False + actor = actor.split('?')[0] + + currDate = datetime.now() + if year == 1970 and monthNumber == 0: + year = currDate.year + monthNumber = currDate.month + + nickname = getNicknameFromActor(actor) + + if os.path.isfile(baseDir + '/img/calendar-background.png'): + if not os.path.isfile(baseDir + '/accounts/calendar-background.png'): + copyfile(baseDir + '/img/calendar-background.png', + baseDir + '/accounts/calendar-background.png') + + months = ('January', 'February', 'March', 'April', + 'May', 'June', 'July', 'August', 'September', + 'October', 'November', 'December') + monthName = translate[months[monthNumber - 1]] + + if dayNumber: + dayEvents = None + events = \ + getTodaysEvents(baseDir, nickname, domain, + year, monthNumber, dayNumber) + if events: + if events.get(str(dayNumber)): + dayEvents = events[str(dayNumber)] + return htmlCalendarDay(cssCache, translate, baseDir, path, + year, monthNumber, dayNumber, + nickname, domain, dayEvents, + monthName, actor) + + events = \ + getCalendarEvents(baseDir, nickname, domain, year, monthNumber) + + prevYear = year + prevMonthNumber = monthNumber - 1 + if prevMonthNumber < 1: + prevMonthNumber = 12 + prevYear = year - 1 + + nextYear = year + nextMonthNumber = monthNumber + 1 + if nextMonthNumber > 12: + nextMonthNumber = 1 + nextYear = year + 1 + + print('Calendar year=' + str(year) + ' month=' + str(monthNumber) + + ' ' + str(weekDayOfMonthStart(monthNumber, year))) + + if monthNumber < 12: + daysInMonth = \ + (date(year, monthNumber + 1, 1) - date(year, monthNumber, 1)).days + else: + daysInMonth = \ + (date(year + 1, 1, 1) - date(year, monthNumber, 1)).days + # print('daysInMonth ' + str(monthNumber) + ': ' + str(daysInMonth)) + + cssFilename = baseDir + '/epicyon-calendar.css' + if os.path.isfile(baseDir + '/calendar.css'): + cssFilename = baseDir + '/calendar.css' + + calendarStyle = getCSS(baseDir, cssFilename, cssCache) + + calActor = actor + if '/users/' in actor: + calActor = '/users/' + actor.split('/users/')[1] + + calendarStr = htmlHeader(cssFilename, calendarStyle) + calendarStr += '
\n' + calendarStr += '\n' + calendarStr += '\n' + calendarStr += '\n' + calendarStr += ' \n' + calendarStr += ' \n' + calendarStr += ' \n' + calendarStr += ' \n' + calendarStr += ' \n' + calendarStr += ' \n' + calendarStr += ' \n' + calendarStr += '\n' + calendarStr += '\n' + calendarStr += '\n' + + dayOfMonth = 0 + dow = weekDayOfMonthStart(monthNumber, year) + for weekOfMonth in range(1, 7): + if dayOfMonth == daysInMonth: + continue + calendarStr += ' \n' + for dayNumber in range(1, 8): + if (weekOfMonth > 1 and dayOfMonth < daysInMonth) or \ + (weekOfMonth == 1 and dayNumber >= dow): + dayOfMonth += 1 + + isToday = False + if year == currDate.year: + if currDate.month == monthNumber: + if dayOfMonth == currDate.day: + isToday = True + if events.get(str(dayOfMonth)): + url = calActor + '/calendar?year=' + \ + str(year) + '?month=' + \ + str(monthNumber) + '?day=' + str(dayOfMonth) + dayLink = '' + \ + str(dayOfMonth) + '' + # there are events for this day + if not isToday: + calendarStr += \ + ' \n' + else: + calendarStr += \ + ' \n' + else: + # No events today + if not isToday: + calendarStr += \ + ' \n' + else: + calendarStr += \ + ' \n' + else: + calendarStr += ' \n' + calendarStr += ' \n' + + calendarStr += '\n' + calendarStr += '
\n' + calendarStr += \ + ' ' + calendarStr += \ + ' ' + translate['Previous month'] + \
+        '\n' + calendarStr += ' ' + calendarStr += '

' + monthName + '

\n' + calendarStr += \ + ' ' + calendarStr += \ + ' ' + translate['Next month'] + \
+        '\n' + calendarStr += '
' + \ + translate['Sun'] + '' + \ + translate['Mon'] + '' + \ + translate['Tue'] + '' + \ + translate['Wed'] + '' + \ + translate['Thu'] + '' + \ + translate['Fri'] + '' + \ + translate['Sat'] + '
' + \ + dayLink + '' + \ + dayLink + '' + \ + str(dayOfMonth) + '' + str(dayOfMonth) + '
\n' + calendarStr += htmlFooter() + return calendarStr diff --git a/webapp_column_left.py b/webapp_column_left.py new file mode 100644 index 000000000..736bfb1bb --- /dev/null +++ b/webapp_column_left.py @@ -0,0 +1,317 @@ +__filename__ = "webapp_column_left.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "1.1.0" +__maintainer__ = "Bob Mottram" +__email__ = "bob@freedombone.net" +__status__ = "Production" + +import os +from shutil import copyfile +from utils import getConfigParam +from utils import getCSS +from utils import getNicknameFromActor +from posts import isEditor +from webapp_utils import htmlPostSeparator +from webapp_utils import getLeftImageFile +from webapp_utils import getImageFile +from webapp_utils import headerButtonsFrontScreen +from webapp_utils import getIconsDir +from webapp_utils import htmlHeader +from webapp_utils import htmlFooter +from webapp_utils import getBannerFile + + +def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str, + httpPrefix: str, translate: {}, + iconsDir: str, editor: bool, + showBackButton: bool, timelinePath: str, + rssIconAtTop: bool, showHeaderImage: bool, + frontPage: bool) -> str: + """Returns html content for the left column + """ + htmlStr = '' + + separatorStr = htmlPostSeparator(baseDir, 'left') + domain = domainFull + if ':' in domain: + domain = domain.split(':') + + editImageClass = '' + if showHeaderImage: + leftImageFile, leftColumnImageFilename = \ + getLeftImageFile(baseDir, nickname, domain) + if not os.path.isfile(leftColumnImageFilename): + theme = getConfigParam(baseDir, 'theme').lower() + if theme == 'default': + theme = '' + else: + theme = '_' + theme + themeLeftImageFile, themeLeftColumnImageFilename = \ + getImageFile(baseDir, 'left_col_image', baseDir + '/img', + nickname, domain) + if os.path.isfile(themeLeftColumnImageFilename): + leftColumnImageFilename = \ + baseDir + '/accounts/' + \ + nickname + '@' + domain + '/' + themeLeftImageFile + copyfile(themeLeftColumnImageFilename, + leftColumnImageFilename) + leftImageFile = themeLeftImageFile + + # show the image at the top of the column + editImageClass = 'leftColEdit' + if os.path.isfile(leftColumnImageFilename): + editImageClass = 'leftColEditImage' + htmlStr += \ + '\n
\n' + \ + ' \n' + \ + '
\n' + + if showBackButton: + htmlStr += \ + '
' + \ + ' ' + \ + '\n' + + if (editor or rssIconAtTop) and not showHeaderImage: + htmlStr += '
' + + if editImageClass == 'leftColEdit': + htmlStr += '\n
\n' + + htmlStr += '
\n' + if editor: + # show the edit icon + htmlStr += \ + ' ' + \ + '' + \
+            translate['Edit Links'] + '\n' + + # RSS icon + if nickname != 'news': + # rss feed for this account + rssUrl = httpPrefix + '://' + domainFull + \ + '/blog/' + nickname + '/rss.xml' + else: + # rss feed for all accounts on the instance + rssUrl = httpPrefix + '://' + domainFull + '/blog/rss.xml' + if not frontPage: + rssTitle = translate['RSS feed for your blog'] + else: + rssTitle = translate['RSS feed for this site'] + rssIconStr = \ + ' ' + \ + '' + rssTitle + \
+        '\n' + if rssIconAtTop: + htmlStr += rssIconStr + htmlStr += '
\n' + + if editImageClass == 'leftColEdit': + htmlStr += '
\n' + + if (editor or rssIconAtTop) and not showHeaderImage: + htmlStr += '

' + + # if showHeaderImage: + # htmlStr += '
' + + linksFilename = baseDir + '/accounts/links.txt' + linksFileContainsEntries = False + if os.path.isfile(linksFilename): + linksList = None + with open(linksFilename, "r") as f: + linksList = f.readlines() + if linksList: + for lineStr in linksList: + if ' ' not in lineStr: + if '#' not in lineStr: + if '*' not in lineStr: + continue + lineStr = lineStr.strip() + words = lineStr.split(' ') + # get the link + linkStr = None + for word in words: + if word == '#': + continue + if word == '*': + continue + if '://' in word: + linkStr = word + break + if linkStr: + lineStr = lineStr.replace(linkStr, '').strip() + # avoid any dubious scripts being added + if '<' not in lineStr: + # remove trailing comma if present + if lineStr.endswith(','): + lineStr = lineStr[:len(lineStr)-1] + # add link to the returned html + htmlStr += \ + '

' + \ + lineStr + '

\n' + linksFileContainsEntries = True + else: + if lineStr.startswith('#') or lineStr.startswith('*'): + lineStr = lineStr[1:].strip() + htmlStr += separatorStr + htmlStr += \ + '

' + \ + lineStr + '

\n' + else: + htmlStr += \ + '

' + lineStr + '

\n' + linksFileContainsEntries = True + + if linksFileContainsEntries and not rssIconAtTop: + htmlStr += '
' + rssIconStr + '
' + return htmlStr + + +def htmlLinksMobile(cssCache: {}, baseDir: str, + nickname: str, domainFull: str, + httpPrefix: str, translate, + timelinePath: str, authorized: bool, + rssIconAtTop: bool, + iconsAsButtons: bool, + defaultTimeline: str) -> str: + """Show the left column links within mobile view + """ + htmlStr = '' + + # the css filename + cssFilename = baseDir + '/epicyon-profile.css' + if os.path.isfile(baseDir + '/epicyon.css'): + cssFilename = baseDir + '/epicyon.css' + + profileStyle = getCSS(baseDir, cssFilename, cssCache) + if profileStyle: + # replace any https within the css with whatever prefix is needed + if httpPrefix != 'https': + profileStyle = \ + profileStyle.replace('https://', httpPrefix + '://') + + iconsDir = getIconsDir(baseDir) + + # is the user a site editor? + if nickname == 'news': + editor = False + else: + editor = isEditor(baseDir, nickname) + + domain = domainFull + if ':' in domain: + domain = domain.split(':')[0] + + htmlStr = htmlHeader(cssFilename, profileStyle) + bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) + htmlStr += \ + '' + \ + '\n' + + htmlStr += '
' + \ + headerButtonsFrontScreen(translate, nickname, + 'links', authorized, + iconsAsButtons, iconsDir) + '
' + htmlStr += \ + getLeftColumnContent(baseDir, nickname, domainFull, + httpPrefix, translate, + iconsDir, editor, + False, timelinePath, + rssIconAtTop, False, False) + htmlStr += '
\n' + htmlFooter() + return htmlStr + + +def htmlEditLinks(cssCache: {}, translate: {}, baseDir: str, path: str, + domain: str, port: int, httpPrefix: str, + defaultTimeline: str) -> str: + """Shows the edit links screen + """ + if '/users/' not in path: + return '' + path = path.replace('/inbox', '').replace('/outbox', '') + path = path.replace('/shares', '') + + nickname = getNicknameFromActor(path) + if not nickname: + return '' + + # is the user a moderator? + if not isEditor(baseDir, nickname): + return '' + + cssFilename = baseDir + '/epicyon-links.css' + if os.path.isfile(baseDir + '/links.css'): + cssFilename = baseDir + '/links.css' + + editCSS = getCSS(baseDir, cssFilename, cssCache) + if editCSS: + if httpPrefix != 'https': + editCSS = \ + editCSS.replace('https://', httpPrefix + '://') + + # filename of the banner shown at the top + bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) + + editLinksForm = htmlHeader(cssFilename, editCSS) + + # top banner + editLinksForm += \ + '\n' + editLinksForm += '\n' + + editLinksForm += \ + '
\n' + editLinksForm += \ + '
\n' + editLinksForm += \ + '

' + translate['Edit Links'] + '

' + editLinksForm += \ + '
\n' + # editLinksForm += \ + # ' \n' + editLinksForm += \ + '
\n' + \ + ' \n' + \ + '
\n' + editLinksForm += \ + '
\n' + + linksFilename = baseDir + '/accounts/links.txt' + linksStr = '' + if os.path.isfile(linksFilename): + with open(linksFilename, 'r') as fp: + linksStr = fp.read() + + editLinksForm += \ + '
' + editLinksForm += \ + ' ' + \ + translate['One link per line. Description followed by the link.'] + \ + '
' + editLinksForm += \ + ' ' + editLinksForm += \ + '
' + + editLinksForm += htmlFooter() + return editLinksForm diff --git a/webapp_column_right.py b/webapp_column_right.py new file mode 100644 index 000000000..c8b72a7b4 --- /dev/null +++ b/webapp_column_right.py @@ -0,0 +1,567 @@ +__filename__ = "webapp_column_right.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "1.1.0" +__maintainer__ = "Bob Mottram" +__email__ = "bob@freedombone.net" +__status__ = "Production" + +import os +from datetime import datetime +from shutil import copyfile +from content import removeLongWords +from utils import getCSS +from utils import getConfigParam +from utils import votesOnNewswireItem +from utils import getNicknameFromActor +from posts import isEditor +from posts import isModerator +from webapp_utils import getRightImageFile +from webapp_utils import getImageFile +from webapp_utils import htmlHeader +from webapp_utils import htmlFooter +from webapp_utils import getBannerFile +from webapp_utils import htmlPostSeparator +from webapp_utils import headerButtonsFrontScreen +from webapp_utils import getIconsDir + + +def votesIndicator(totalVotes: int, positiveVoting: bool) -> str: + """Returns an indicator of the number of votes on a newswire item + """ + if totalVotes <= 0: + return '' + totalVotesStr = ' ' + for v in range(totalVotes): + if positiveVoting: + totalVotesStr += '✓' + else: + totalVotesStr += '✗' + return totalVotesStr + + +def getRightColumnContent(baseDir: str, nickname: str, domainFull: str, + httpPrefix: str, translate: {}, + iconsDir: str, moderator: bool, editor: bool, + newswire: {}, positiveVoting: bool, + showBackButton: bool, timelinePath: str, + showPublishButton: bool, + showPublishAsIcon: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool, + showHeaderImage: bool) -> str: + """Returns html content for the right column + """ + htmlStr = '' + + domain = domainFull + if ':' in domain: + domain = domain.split(':') + + if authorized: + # only show the publish button if logged in, otherwise replace it with + # a login button + publishButtonStr = \ + ' ' + \ + '\n' + else: + # if not logged in then replace the publish button with + # a login button + publishButtonStr = \ + ' \n' + + # show publish button at the top if needed + if publishButtonAtTop: + htmlStr += '
' + publishButtonStr + '
' + + # show a column header image, eg. title of the theme or newswire banner + editImageClass = '' + if showHeaderImage: + rightImageFile, rightColumnImageFilename = \ + getRightImageFile(baseDir, nickname, domain) + if not os.path.isfile(rightColumnImageFilename): + theme = getConfigParam(baseDir, 'theme').lower() + if theme == 'default': + theme = '' + else: + theme = '_' + theme + themeRightImageFile, themeRightColumnImageFilename = \ + getImageFile(baseDir, 'right_col_image', baseDir + '/img', + nickname, domain) + if os.path.isfile(themeRightColumnImageFilename): + rightColumnImageFilename = \ + baseDir + '/accounts/' + \ + nickname + '@' + domain + '/' + themeRightImageFile + copyfile(themeRightColumnImageFilename, + rightColumnImageFilename) + rightImageFile = themeRightImageFile + + # show the image at the top of the column + editImageClass = 'rightColEdit' + if os.path.isfile(rightColumnImageFilename): + editImageClass = 'rightColEditImage' + htmlStr += \ + '\n
\n' + \ + ' \n' + \ + '
\n' + + if (showPublishButton or editor or rssIconAtTop) and not showHeaderImage: + htmlStr += '
' + + if editImageClass == 'rightColEdit': + htmlStr += '\n
\n' + + # whether to show a back icon + # This is probably going to be osolete soon + if showBackButton: + htmlStr += \ + ' ' + \ + '\n' + + if showPublishButton and not publishButtonAtTop: + if not showPublishAsIcon: + htmlStr += publishButtonStr + + # show the edit icon + if editor: + if os.path.isfile(baseDir + '/accounts/newswiremoderation.txt'): + # show the edit icon highlighted + htmlStr += \ + ' ' + \ + '' + \
+                translate['Edit newswire'] + '\n' + else: + # show the edit icon + htmlStr += \ + ' ' + \ + '' + \
+                translate['Edit newswire'] + '\n' + + # show the RSS icon + rssIconStr = \ + ' ' + \ + '' + \
+        translate['Newswire RSS Feed'] + '\n' + if rssIconAtTop: + htmlStr += rssIconStr + + # show publish icon at top + if showPublishButton: + if showPublishAsIcon: + htmlStr += \ + ' ' + \ + '' + \
+                translate['Publish a news article'] + '\n' + + if editImageClass == 'rightColEdit': + htmlStr += '
\n' + else: + if showHeaderImage: + htmlStr += '
\n' + + if (showPublishButton or editor or rssIconAtTop) and not showHeaderImage: + htmlStr += '

' + + # show the newswire lines + newswireContentStr = \ + htmlNewswire(baseDir, newswire, nickname, moderator, translate, + positiveVoting, iconsDir) + htmlStr += newswireContentStr + + # show the rss icon at the bottom, typically on the right hand side + if newswireContentStr and not rssIconAtTop: + htmlStr += '
' + rssIconStr + '
' + return htmlStr + + +def htmlNewswire(baseDir: str, newswire: {}, nickname: str, moderator: bool, + translate: {}, positiveVoting: bool, iconsDir: str) -> str: + """Converts a newswire dict into html + """ + separatorStr = htmlPostSeparator(baseDir, 'right') + htmlStr = '' + for dateStr, item in newswire.items(): + publishedDate = \ + datetime.strptime(dateStr, "%Y-%m-%d %H:%M:%S%z") + dateShown = publishedDate.strftime("%Y-%m-%d %H:%M") + + dateStrLink = dateStr.replace('T', ' ') + dateStrLink = dateStrLink.replace('Z', '') + moderatedItem = item[5] + htmlStr += separatorStr + if moderatedItem and 'vote:' + nickname in item[2]: + totalVotesStr = '' + totalVotes = 0 + if moderator: + totalVotes = votesOnNewswireItem(item[2]) + totalVotesStr = \ + votesIndicator(totalVotes, positiveVoting) + + title = removeLongWords(item[0], 16, []).replace('\n', '
') + htmlStr += '

' + \ + '' + \ + '' + title + \ + '' + totalVotesStr + if moderator: + htmlStr += \ + ' ' + dateShown + '' + htmlStr += '

\n' + else: + htmlStr += ' ' + htmlStr += dateShown + '

\n' + else: + totalVotesStr = '' + totalVotes = 0 + if moderator: + if moderatedItem: + totalVotes = votesOnNewswireItem(item[2]) + # show a number of ticks or crosses for how many + # votes for or against + totalVotesStr = \ + votesIndicator(totalVotes, positiveVoting) + + title = removeLongWords(item[0], 16, []).replace('\n', '
') + if moderator and moderatedItem: + htmlStr += '

' + \ + '' + \ + title + '' + totalVotesStr + htmlStr += ' ' + dateShown + htmlStr += '' + htmlStr += '' + htmlStr += '

\n' + else: + htmlStr += '

' + \ + '' + \ + title + '' + \ + totalVotesStr + htmlStr += ' ' + htmlStr += dateShown + '

\n' + return htmlStr + + +def htmlCitations(baseDir: str, nickname: str, domain: str, + httpPrefix: str, defaultTimeline: str, + translate: {}, newswire: {}, cssCache: {}, + blogTitle: str, blogContent: str, + blogImageFilename: str, + blogImageAttachmentMediaType: str, + blogImageDescription: str) -> str: + """Show the citations screen when creating a blog + """ + htmlStr = '' + + # create a list of dates for citations + # these can then be used to re-select checkboxes later + citationsFilename = \ + baseDir + '/accounts/' + \ + nickname + '@' + domain + '/.citations.txt' + citationsSelected = [] + if os.path.isfile(citationsFilename): + citationsSeparator = '#####' + with open(citationsFilename, "r") as f: + citations = f.readlines() + for line in citations: + if citationsSeparator not in line: + continue + sections = line.strip().split(citationsSeparator) + if len(sections) != 3: + continue + dateStr = sections[0] + citationsSelected.append(dateStr) + + # the css filename + cssFilename = baseDir + '/epicyon-profile.css' + if os.path.isfile(baseDir + '/epicyon.css'): + cssFilename = baseDir + '/epicyon.css' + + profileStyle = getCSS(baseDir, cssFilename, cssCache) + if profileStyle: + # replace any https within the css with whatever prefix is needed + if httpPrefix != 'https': + profileStyle = \ + profileStyle.replace('https://', httpPrefix + '://') + + # iconsDir = getIconsDir(baseDir) + + htmlStr = htmlHeader(cssFilename, profileStyle) + + # top banner + bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) + htmlStr += \ + '\n' + htmlStr += '\n' + + htmlStr += \ + '\n' + htmlStr += '
\n' + htmlStr += translate['Choose newswire items ' + + 'referenced in your article'] + '
' + if blogTitle is None: + blogTitle = '' + htmlStr += \ + ' \n' + if blogContent is None: + blogContent = '' + htmlStr += \ + ' \n' + # submit button + htmlStr += \ + ' \n' + htmlStr += '
\n' + + citationsSeparator = '#####' + + # list of newswire items + if newswire: + ctr = 0 + for dateStr, item in newswire.items(): + # should this checkbox be selected? + selectedStr = '' + if dateStr in citationsSelected: + selectedStr = ' checked' + + publishedDate = \ + datetime.strptime(dateStr, "%Y-%m-%d %H:%M:%S%z") + dateShown = publishedDate.strftime("%Y-%m-%d %H:%M") + + title = removeLongWords(item[0], 16, []).replace('\n', '
') + link = item[1] + + citationValue = \ + dateStr + citationsSeparator + \ + title + citationsSeparator + \ + link + htmlStr += \ + '' + \ + '' + title + ' ' + htmlStr += '' + \ + dateShown + '
\n' + ctr += 1 + + htmlStr += '\n' + return htmlStr + htmlFooter() + + +def htmlNewswireMobile(cssCache: {}, baseDir: str, nickname: str, + domain: str, domainFull: str, + httpPrefix: str, translate: {}, + newswire: {}, + positiveVoting: bool, + timelinePath: str, + showPublishAsIcon: bool, + authorized: bool, + rssIconAtTop: bool, + iconsAsButtons: bool, + defaultTimeline: str) -> str: + """Shows the mobile version of the newswire right column + """ + htmlStr = '' + + # the css filename + cssFilename = baseDir + '/epicyon-profile.css' + if os.path.isfile(baseDir + '/epicyon.css'): + cssFilename = baseDir + '/epicyon.css' + + profileStyle = getCSS(baseDir, cssFilename, cssCache) + if profileStyle: + # replace any https within the css with whatever prefix is needed + if httpPrefix != 'https': + profileStyle = \ + profileStyle.replace('https://', + httpPrefix + '://') + + iconsDir = getIconsDir(baseDir) + + if nickname == 'news': + editor = False + moderator = False + else: + # is the user a moderator? + moderator = isModerator(baseDir, nickname) + + # is the user a site editor? + editor = isEditor(baseDir, nickname) + + showPublishButton = editor + + htmlStr = htmlHeader(cssFilename, profileStyle) + + bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) + htmlStr += \ + '' + \ + '\n' + + htmlStr += '
' + \ + headerButtonsFrontScreen(translate, nickname, + 'newswire', authorized, + iconsAsButtons, iconsDir) + '
' + htmlStr += \ + getRightColumnContent(baseDir, nickname, domainFull, + httpPrefix, translate, + iconsDir, moderator, editor, + newswire, positiveVoting, + False, timelinePath, showPublishButton, + showPublishAsIcon, rssIconAtTop, False, + authorized, False) + htmlStr += htmlFooter() + return htmlStr + + +def htmlEditNewswire(cssCache: {}, translate: {}, baseDir: str, path: str, + domain: str, port: int, httpPrefix: str, + defaultTimeline: str) -> str: + """Shows the edit newswire screen + """ + if '/users/' not in path: + return '' + path = path.replace('/inbox', '').replace('/outbox', '') + path = path.replace('/shares', '') + + nickname = getNicknameFromActor(path) + if not nickname: + return '' + + # is the user a moderator? + if not isModerator(baseDir, nickname): + return '' + + cssFilename = baseDir + '/epicyon-links.css' + if os.path.isfile(baseDir + '/links.css'): + cssFilename = baseDir + '/links.css' + + editCSS = getCSS(baseDir, cssFilename, cssCache) + if editCSS: + if httpPrefix != 'https': + editCSS = \ + editCSS.replace('https://', httpPrefix + '://') + + # filename of the banner shown at the top + bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) + + editNewswireForm = htmlHeader(cssFilename, editCSS) + + # top banner + editNewswireForm += \ + '\n' + editNewswireForm += '\n' + + editNewswireForm += \ + '
\n' + editNewswireForm += \ + '
\n' + editNewswireForm += \ + '

' + translate['Edit newswire'] + '

' + editNewswireForm += \ + '
\n' + # editNewswireForm += \ + # ' \n' + editNewswireForm += \ + '
\n' + \ + ' \n' + \ + '
\n' + editNewswireForm += \ + '
\n' + + newswireFilename = baseDir + '/accounts/newswire.txt' + newswireStr = '' + if os.path.isfile(newswireFilename): + with open(newswireFilename, 'r') as fp: + newswireStr = fp.read() + + editNewswireForm += \ + '
' + + editNewswireForm += \ + ' ' + \ + translate['Add RSS feed links below.'] + \ + '
' + editNewswireForm += \ + ' ' + + filterStr = '' + filterFilename = \ + baseDir + '/accounts/news@' + domain + '/filters.txt' + if os.path.isfile(filterFilename): + with open(filterFilename, 'r') as filterfile: + filterStr = filterfile.read() + + editNewswireForm += \ + '
\n' + editNewswireForm += '
' + editNewswireForm += ' \n' + + hashtagRulesStr = '' + hashtagRulesFilename = \ + baseDir + '/accounts/hashtagrules.txt' + if os.path.isfile(hashtagRulesFilename): + with open(hashtagRulesFilename, 'r') as rulesfile: + hashtagRulesStr = rulesfile.read() + + editNewswireForm += \ + '
\n' + editNewswireForm += '
\n' + editNewswireForm += \ + ' ' + translate['See instructions'] + '\n' + editNewswireForm += ' \n' + + editNewswireForm += \ + '
' + + editNewswireForm += htmlFooter() + return editNewswireForm diff --git a/webapp_create_post.py b/webapp_create_post.py new file mode 100644 index 000000000..b69edc7ee --- /dev/null +++ b/webapp_create_post.py @@ -0,0 +1,735 @@ +__filename__ = "webapp_create_post.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "1.1.0" +__maintainer__ = "Bob Mottram" +__email__ = "bob@freedombone.net" +__status__ = "Production" + +import os +from utils import isPublicPostFromUrl +from utils import getCSS +from utils import getNicknameFromActor +from utils import getDomainFromActor +from webapp_utils import getIconsDir +from webapp_utils import getBannerFile +from webapp_utils import htmlHeader +from webapp_utils import htmlFooter + + +def htmlFollowingDataList(baseDir: str, nickname: str, + domain: str, domainFull: str) -> str: + """Returns a datalist of handles being followed + """ + listStr = '\n' + followingFilename = \ + baseDir + '/accounts/' + nickname + '@' + domain + '/following.txt' + if os.path.isfile(followingFilename): + with open(followingFilename, 'r') as followingFile: + msg = followingFile.read() + # add your own handle, so that you can send DMs + # to yourself as reminders + msg += nickname + '@' + domainFull + '\n' + # include petnames + petnamesFilename = \ + baseDir + '/accounts/' + \ + nickname + '@' + domain + '/petnames.txt' + if os.path.isfile(petnamesFilename): + followingList = [] + with open(petnamesFilename, 'r') as petnamesFile: + petStr = petnamesFile.read() + # extract each petname and append it + petnamesList = petStr.split('\n') + for pet in petnamesList: + followingList.append(pet.split(' ')[0]) + # add the following.txt entries + followingList += msg.split('\n') + else: + # no petnames list exists - just use following.txt + followingList = msg.split('\n') + followingList.sort() + if followingList: + for followingAddress in followingList: + if followingAddress: + listStr += \ + '\n' + listStr += '\n' + return listStr + + +def htmlNewPostDropDown(scopeIcon: str, scopeDescription: str, + replyStr: str, + translate: {}, + iconsDir: str, + showPublicOnDropdown: bool, + defaultTimeline: str, + pathBase: str, + dropdownNewPostSuffix: str, + dropdownNewBlogSuffix: str, + dropdownUnlistedSuffix: str, + dropdownFollowersSuffix: str, + dropdownDMSuffix: str, + dropdownReminderSuffix: str, + dropdownEventSuffix: str, + dropdownReportSuffix: str) -> str: + """Returns the html for a drop down list of new post types + """ + dropDownContent = '
\n' + dropDownContent += ' \n' + dropDownContent += ' \n' + dropDownContent += ' \n' + dropDownContent += '
\n' + return dropDownContent + + +def htmlNewPost(cssCache: {}, mediaInstance: bool, translate: {}, + baseDir: str, httpPrefix: str, + path: str, inReplyTo: str, + mentions: [], + reportUrl: str, pageNumber: int, + nickname: str, domain: str, + domainFull: str, + defaultTimeline: str, newswire: {}) -> str: + """New post screen + """ + iconsDir = getIconsDir(baseDir) + replyStr = '' + + showPublicOnDropdown = True + messageBoxHeight = 400 + + # filename of the banner shown at the top + bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) + + if not path.endswith('/newshare'): + if not path.endswith('/newreport'): + if not inReplyTo or path.endswith('/newreminder'): + newPostText = '

' + \ + translate['Write your post text below.'] + '

\n' + else: + newPostText = \ + '

' + \ + translate['Write your reply to'] + \ + ' ' + \ + translate['this post'] + '

\n' + replyStr = '\n' + + # if replying to a non-public post then also make + # this post non-public + if not isPublicPostFromUrl(baseDir, nickname, domain, + inReplyTo): + newPostPath = path + if '?' in newPostPath: + newPostPath = newPostPath.split('?')[0] + if newPostPath.endswith('/newpost'): + path = path.replace('/newpost', '/newfollowers') + elif newPostPath.endswith('/newunlisted'): + path = path.replace('/newunlisted', '/newfollowers') + showPublicOnDropdown = False + else: + newPostText = \ + '

' + \ + translate['Write your report below.'] + '

\n' + + # custom report header with any additional instructions + if os.path.isfile(baseDir + '/accounts/report.txt'): + with open(baseDir + '/accounts/report.txt', 'r') as file: + customReportText = file.read() + if '

' not in customReportText: + customReportText = \ + '\n' + repStr = '

', repStr) + newPostText += customReportText + + idx = 'This message only goes to moderators, even if it ' + \ + 'mentions other fediverse addresses.' + newPostText += \ + '

' + translate[idx] + '

\n' + \ + '

' + translate['Also see'] + \ + ' ' + \ + translate['Terms of Service'] + '

\n' + else: + newPostText = \ + '

' + \ + translate['Enter the details for your shared item below.'] + \ + '

\n' + + if path.endswith('/newquestion'): + newPostText = \ + '

' + \ + translate['Enter the choices for your question below.'] + \ + '

\n' + + if os.path.isfile(baseDir + '/accounts/newpost.txt'): + with open(baseDir + '/accounts/newpost.txt', 'r') as file: + newPostText = \ + '

' + file.read() + '

\n' + + cssFilename = baseDir + '/epicyon-profile.css' + if os.path.isfile(baseDir + '/epicyon.css'): + cssFilename = baseDir + '/epicyon.css' + + newPostCSS = getCSS(baseDir, cssFilename, cssCache) + if newPostCSS: + if httpPrefix != 'https': + newPostCSS = newPostCSS.replace('https://', + httpPrefix + '://') + + if '?' in path: + path = path.split('?')[0] + pathBase = path.replace('/newreport', '').replace('/newpost', '') + pathBase = pathBase.replace('/newblog', '').replace('/newshare', '') + pathBase = pathBase.replace('/newunlisted', '') + pathBase = pathBase.replace('/newevent', '') + pathBase = pathBase.replace('/newreminder', '') + pathBase = pathBase.replace('/newfollowers', '').replace('/newdm', '') + + newPostImageSection = '
' + if not path.endswith('/newevent'): + newPostImageSection += \ + ' \n' + else: + newPostImageSection += \ + ' \n' + newPostImageSection += \ + ' \n' + + if path.endswith('/newevent'): + newPostImageSection += \ + ' \n' + newPostImageSection += \ + ' \n' + else: + newPostImageSection += \ + ' \n' + newPostImageSection += '
\n' + + scopeIcon = 'scope_public.png' + scopeDescription = translate['Public'] + placeholderSubject = \ + translate['Subject or Content Warning (optional)'] + '...' + placeholderMentions = '' + if inReplyTo: + # mentionsAndContent = getMentionsString(content) + placeholderMentions = \ + translate['Replying to'] + '...' + placeholderMessage = translate['Write something'] + '...' + extraFields = '' + endpoint = 'newpost' + if path.endswith('/newblog'): + placeholderSubject = translate['Title'] + scopeIcon = 'scope_blog.png' + if defaultTimeline != 'tlnews': + scopeDescription = translate['Blog'] + else: + scopeDescription = translate['Article'] + endpoint = 'newblog' + elif path.endswith('/newunlisted'): + scopeIcon = 'scope_unlisted.png' + scopeDescription = translate['Unlisted'] + endpoint = 'newunlisted' + elif path.endswith('/newfollowers'): + scopeIcon = 'scope_followers.png' + scopeDescription = translate['Followers'] + endpoint = 'newfollowers' + elif path.endswith('/newdm'): + scopeIcon = 'scope_dm.png' + scopeDescription = translate['DM'] + endpoint = 'newdm' + elif path.endswith('/newreminder'): + scopeIcon = 'scope_reminder.png' + scopeDescription = translate['Reminder'] + endpoint = 'newreminder' + elif path.endswith('/newevent'): + scopeIcon = 'scope_event.png' + scopeDescription = translate['Event'] + endpoint = 'newevent' + placeholderSubject = translate['Event name'] + placeholderMessage = translate['Describe the event'] + '...' + elif path.endswith('/newreport'): + scopeIcon = 'scope_report.png' + scopeDescription = translate['Report'] + endpoint = 'newreport' + elif path.endswith('/newquestion'): + scopeIcon = 'scope_question.png' + scopeDescription = translate['Question'] + placeholderMessage = translate['Enter your question'] + '...' + endpoint = 'newquestion' + extraFields = '
\n' + extraFields += '
\n' + for questionCtr in range(8): + extraFields += \ + '
\n' + extraFields += \ + '
\n' + extraFields += '
' + elif path.endswith('/newshare'): + scopeIcon = 'scope_share.png' + scopeDescription = translate['Shared Item'] + placeholderSubject = translate['Name of the shared item'] + '...' + placeholderMessage = \ + translate['Description of the item being shared'] + '...' + endpoint = 'newshare' + extraFields = '
\n' + extraFields += \ + ' \n' + extraFields += \ + ' \n' + extraFields += \ + '
\n' + extraFields += \ + ' \n' + extraFields += \ + '
\n' + extraFields += ' \n' + extraFields += '
\n' + extraFields += '
\n' + extraFields += \ + '\n' + extraFields += '\n' + extraFields += '
\n' + + citationsStr = '' + if endpoint == 'newblog': + citationsFilename = \ + baseDir + '/accounts/' + \ + nickname + '@' + domain + '/.citations.txt' + if os.path.isfile(citationsFilename): + citationsStr = '
\n' + citationsStr += '

\n' + citationsStr += '
    \n' + citationsSeparator = '#####' + with open(citationsFilename, "r") as f: + citations = f.readlines() + for line in citations: + if citationsSeparator not in line: + continue + sections = line.strip().split(citationsSeparator) + if len(sections) != 3: + continue + title = sections[1] + link = sections[2] + citationsStr += \ + '
  • ' + \ + title + '
  • ' + citationsStr += '
\n' + citationsStr += '
\n' + + dateAndLocation = '' + if endpoint != 'newshare' and \ + endpoint != 'newreport' and \ + endpoint != 'newquestion': + dateAndLocation = '
\n' + + if endpoint == 'newevent': + # event status + dateAndLocation += '
\n' + dateAndLocation += '\n' + dateAndLocation += '
\n' + dateAndLocation += '\n' + dateAndLocation += '
\n' + dateAndLocation += '\n' + dateAndLocation += '
\n' + dateAndLocation += '
\n' + dateAndLocation += '
\n' + # maximum attendees + dateAndLocation += '\n' + dateAndLocation += '\n' + dateAndLocation += '
\n' + dateAndLocation += '
\n' + # event joining options + dateAndLocation += '
\n' + dateAndLocation += '\n' + dateAndLocation += '
\n' + dateAndLocation += '\n' + dateAndLocation += '
\n' + dateAndLocation += '\n' + dateAndLocation += '\n' + dateAndLocation += '
\n' + dateAndLocation += '
\n' + # Event posts don't allow replies - they're just an announcement. + # They also have a few more checkboxes + dateAndLocation += \ + '

\n' + dateAndLocation += \ + '

' + \ + '

\n' + else: + dateAndLocation += \ + '

\n' + + if not inReplyTo and endpoint != 'newevent': + dateAndLocation += \ + '

\n' + + if endpoint != 'newevent': + dateAndLocation += \ + '

\n' + # select a date and time for this post + dateAndLocation += '\n' + dateAndLocation += '\n' + dateAndLocation += '

\n' + else: + dateAndLocation += '
\n' + dateAndLocation += '
\n' + dateAndLocation += \ + '

\n' + # select start time for the event + dateAndLocation += '\n' + dateAndLocation += '\n' + dateAndLocation += '

\n' + # select end time for the event + dateAndLocation += \ + '
\n' + dateAndLocation += '\n' + dateAndLocation += '\n' + dateAndLocation += '\n' + + if endpoint == 'newevent': + dateAndLocation += '
\n' + dateAndLocation += '
\n' + dateAndLocation += '
\n' + dateAndLocation += \ + ' \n' + dateAndLocation += '
\n' + dateAndLocation += '
\n' + dateAndLocation += '
\n' + dateAndLocation += '\n' + if endpoint == 'newevent': + dateAndLocation += '
\n' + dateAndLocation += '\n' + dateAndLocation += '
\n' + dateAndLocation += '\n' + dateAndLocation += '
\n' + + newPostForm = htmlHeader(cssFilename, newPostCSS) + + newPostForm += \ + '\n' + newPostForm += '\n' + + mentionsStr = '' + for m in mentions: + mentionNickname = getNicknameFromActor(m) + if not mentionNickname: + continue + mentionDomain, mentionPort = getDomainFromActor(m) + if not mentionDomain: + continue + if mentionPort: + mentionsHandle = \ + '@' + mentionNickname + '@' + \ + mentionDomain + ':' + str(mentionPort) + else: + mentionsHandle = '@' + mentionNickname + '@' + mentionDomain + if mentionsHandle not in mentionsStr: + mentionsStr += mentionsHandle + ' ' + + # build suffixes so that any replies or mentions are + # preserved when switching between scopes + dropdownNewPostSuffix = '/newpost' + dropdownNewBlogSuffix = '/newblog' + dropdownUnlistedSuffix = '/newunlisted' + dropdownFollowersSuffix = '/newfollowers' + dropdownDMSuffix = '/newdm' + dropdownEventSuffix = '/newevent' + dropdownReminderSuffix = '/newreminder' + dropdownReportSuffix = '/newreport' + if inReplyTo or mentions: + dropdownNewPostSuffix = '' + dropdownNewBlogSuffix = '' + dropdownUnlistedSuffix = '' + dropdownFollowersSuffix = '' + dropdownDMSuffix = '' + dropdownEventSuffix = '' + dropdownReminderSuffix = '' + dropdownReportSuffix = '' + if inReplyTo: + dropdownNewPostSuffix += '?replyto=' + inReplyTo + dropdownNewBlogSuffix += '?replyto=' + inReplyTo + dropdownUnlistedSuffix += '?replyto=' + inReplyTo + dropdownFollowersSuffix += '?replyfollowers=' + inReplyTo + dropdownDMSuffix += '?replydm=' + inReplyTo + for mentionedActor in mentions: + dropdownNewPostSuffix += '?mention=' + mentionedActor + dropdownNewBlogSuffix += '?mention=' + mentionedActor + dropdownUnlistedSuffix += '?mention=' + mentionedActor + dropdownFollowersSuffix += '?mention=' + mentionedActor + dropdownDMSuffix += '?mention=' + mentionedActor + dropdownReportSuffix += '?mention=' + mentionedActor + + dropDownContent = '' + if not reportUrl: + dropDownContent = \ + htmlNewPostDropDown(scopeIcon, scopeDescription, + replyStr, + translate, + iconsDir, + showPublicOnDropdown, + defaultTimeline, + pathBase, + dropdownNewPostSuffix, + dropdownNewBlogSuffix, + dropdownUnlistedSuffix, + dropdownFollowersSuffix, + dropdownDMSuffix, + dropdownReminderSuffix, + dropdownEventSuffix, + dropdownReportSuffix) + else: + mentionsStr = 'Re: ' + reportUrl + '\n\n' + mentionsStr + + newPostForm += \ + '\n' + newPostForm += '
\n' + newPostForm += \ + ' \n' + newPostForm += '
\n' + newPostForm += ' \n' + newPostForm += '\n' + + newPostForm += \ + ' \n' + newPostForm += ' \n' + newPostForm += '
' + dropDownContent + '' + \
+        translate['Search for emoji'] + '
\n' + newPostForm += '
\n' + + newPostForm += '
\n' + + # newPostForm += \ + # ' \n' + + # for a new blog if newswire items exist then add a citations button + if newswire and path.endswith('/newblog'): + newPostForm += \ + ' \n' + + newPostForm += \ + ' \n' + + newPostForm += '
\n' + + newPostForm += replyStr + if mediaInstance and not replyStr: + newPostForm += newPostImageSection + + newPostForm += \ + '
' + newPostForm += ' ' + newPostForm += '' + + selectedStr = ' selected' + if inReplyTo or endpoint == 'newdm': + if inReplyTo: + newPostForm += \ + '
\n' + else: + newPostForm += \ + ' ' \ + ' 📄
\n' + newPostForm += \ + ' \n' + newPostForm += \ + htmlFollowingDataList(baseDir, nickname, domain, domainFull) + newPostForm += '' + selectedStr = '' + + newPostForm += \ + '
' + if mediaInstance: + messageBoxHeight = 200 + + if endpoint == 'newquestion': + messageBoxHeight = 100 + elif endpoint == 'newblog': + messageBoxHeight = 800 + + newPostForm += \ + ' \n' + newPostForm += extraFields + citationsStr + dateAndLocation + if not mediaInstance or replyStr: + newPostForm += newPostImageSection + newPostForm += '
\n' + newPostForm += '\n' + + if not reportUrl: + newPostForm = \ + newPostForm.replace('', '') + + newPostForm += htmlFooter() + return newPostForm diff --git a/webapp_media.py b/webapp_media.py new file mode 100644 index 000000000..e551624d3 --- /dev/null +++ b/webapp_media.py @@ -0,0 +1,224 @@ +__filename__ = "webapp_media.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "1.1.0" +__maintainer__ = "Bob Mottram" +__email__ = "bob@freedombone.net" +__status__ = "Production" + + +def addEmbeddedVideoFromSites(translate: {}, content: str, + width=400, height=300) -> str: + """Adds embedded videos + """ + if '>vimeo.com/' in content: + url = content.split('>vimeo.com/')[1] + if '<' in url: + url = url.split('<')[0] + content = \ + content + "
\n\n
\n" + return content + + videoSite = 'https://www.youtube.com' + if '"' + videoSite in content: + url = content.split('"' + videoSite)[1] + if '"' in url: + url = url.split('"')[0].replace('/watch?v=', '/embed/') + if '&' in url: + url = url.split('&')[0] + content = \ + content + "
\n\n
\n" + return content + + invidiousSites = ('https://invidio.us', + 'https://invidious.snopyta.org', + 'http://c7hqkpkpemu6e7emz5b4vy' + + 'z7idjgdvgaaa3dyimmeojqbgpea3xqjoid.onion', + 'http://axqzx4s6s54s32yentfqojs3x5i7faxza6xo3ehd4' + + 'bzzsg2ii4fv2iid.onion') + for videoSite in invidiousSites: + if '"' + videoSite in content: + url = content.split('"' + videoSite)[1] + if '"' in url: + url = url.split('"')[0].replace('/watch?v=', '/embed/') + if '&' in url: + url = url.split('&')[0] + content = \ + content + "
\n\n
\n" + return content + + videoSite = 'https://media.ccc.de' + if '"' + videoSite in content: + url = content.split('"' + videoSite)[1] + if '"' in url: + url = url.split('"')[0] + if not url.endswith('/oembed'): + url = url + '/oembed' + content = \ + content + "
\n\n
\n" + return content + + if '"https://' in content: + # A selection of the current larger peertube sites, mostly + # French and German language + # These have been chosen based on reported numbers of users + # and the content of each has not been reviewed, so mileage could vary + peerTubeSites = ('peertube.mastodon.host', 'open.tube', 'share.tube', + 'tube.tr4sk.me', 'videos.elbinario.net', + 'hkvideo.live', + 'peertube.snargol.com', 'tube.22decembre.eu', + 'tube.fabrigli.fr', 'libretube.net', 'libre.video', + 'peertube.linuxrocks.online', 'spacepub.space', + 'video.ploud.jp', 'video.omniatv.com', + 'peertube.servebeer.com', + 'tube.tchncs.de', 'tubee.fr', 'video.alternanet.fr', + 'devtube.dev-wiki.de', 'video.samedi.pm', + 'video.irem.univ-paris-diderot.fr', + 'peertube.openstreetmap.fr', 'video.antopie.org', + 'scitech.video', 'tube.4aem.com', 'video.ploud.fr', + 'peervideo.net', 'video.valme.io', + 'videos.pair2jeux.tube', + 'vault.mle.party', 'hostyour.tv', + 'diode.zone', 'visionon.tv', + 'artitube.artifaille.fr', 'peertube.fr', + 'peertube.live', + 'tube.ac-lyon.fr', 'www.yiny.org', 'betamax.video', + 'tube.piweb.be', 'pe.ertu.be', 'peertube.social', + 'videos.lescommuns.org', 'peertube.nogafa.org', + 'skeptikon.fr', 'video.tedomum.net', + 'tube.p2p.legal', + 'sikke.fi', 'exode.me', 'peertube.video') + for site in peerTubeSites: + if '"https://' + site in content: + url = content.split('"https://' + site)[1] + if '"' in url: + url = url.split('"')[0].replace('/watch/', '/embed/') + content = \ + content + "
\n\n
\n" + return content + return content + + +def addEmbeddedAudio(translate: {}, content: str) -> str: + """Adds embedded audio for mp3/ogg + """ + if not ('.mp3' in content or '.ogg' in content): + return content + + if '
\n' + return tlStr + + +def htmlTimeline(cssCache: {}, defaultTimeline: str, + recentPostsCache: {}, maxRecentPosts: int, + translate: {}, pageNumber: int, + itemsPerPage: int, session, baseDir: str, + wfRequest: {}, 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) -> str: + """Show the timeline as html + """ + 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) + + # directory where icons are found + # This changes depending upon theme + iconsDir = getIconsDir(baseDir) + + 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) + + # benchmark 1 + timeDiff = int((time.time() - timelineStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE TIMING ' + boxName + ' 1 = ' + str(timeDiff)) + + profileStyle = getCSS(baseDir, cssFilename, cssCache) + if not profileStyle: + print('ERROR: css file not found ' + cssFilename) + return None + + # replace any https within the css with whatever prefix is needed + if httpPrefix != 'https': + profileStyle = \ + profileStyle.replace('https://', + httpPrefix + '://') + + # 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) + + # benchmark 2 + timeDiff = int((time.time() - timelineStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE TIMING ' + boxName + ' 2 = ' + str(timeDiff)) + + # the appearance of buttons - highlighted or not + inboxButton = 'button' + blogsButton = '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 == '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 = domain + if port != 80 and port != 443: + if ':' not in domain: + fullDomain = domain + ':' + str(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 = \ + '' + \ + '' + \
+                        translate['Approve follow requests'] + \
+                        '\n' + break + + # benchmark 3 + timeDiff = int((time.time() - timelineStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE TIMING ' + boxName + ' 3 = ' + str(timeDiff)) + + # moderation / reports button + moderationButtonStr = '' + if moderator and not minimal: + moderationButtonStr = \ + '' + + # shares, bookmarks and events buttons + sharesButtonStr = '' + bookmarksButtonStr = '' + eventsButtonStr = '' + if not minimal: + sharesButtonStr = \ + '' + + bookmarksButtonStr = \ + '' + + eventsButtonStr = \ + '' + + tlStr = htmlHeader(cssFilename, profileStyle) + + # benchmark 4 + timeDiff = int((time.time() - timelineStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE TIMING ' + boxName + ' 4 = ' + str(timeDiff)) + + # if this is a news instance and we are viewing the news timeline + newsHeader = False + if defaultTimeline == 'tlnews' and boxName == 'tlnews': + newsHeader = True + + newPostButtonStr = '' + # start of headericons div + if not newsHeader: + if not iconsAsButtons: + newPostButtonStr += '
' + + # what screen to go to when a new post is created + if boxName == 'dm': + if not iconsAsButtons: + newPostButtonStr += \ + '| ' + translate['Create a new DM'] + \
+                '\n' + else: + newPostButtonStr += \ + '' + \ + '' + elif boxName == 'tlblogs' or boxName == 'tlnews': + if not iconsAsButtons: + newPostButtonStr += \ + '| ' + \
+                translate['Create a new post'] + \
+                '\n' + else: + newPostButtonStr += \ + '' + \ + '' + elif boxName == 'tlevents': + if not iconsAsButtons: + newPostButtonStr += \ + '| ' + \
+                translate['Create a new event'] + \
+                '\n' + else: + newPostButtonStr += \ + '' + \ + '' + else: + if not manuallyApproveFollowers: + if not iconsAsButtons: + newPostButtonStr += \ + '| ' + \
+                    translate['Create a new post'] + \
+                    '\n' + else: + newPostButtonStr += \ + '' + \ + '' + else: + if not iconsAsButtons: + newPostButtonStr += \ + '| ' + translate['Create a new post'] + \
+                    '\n' + else: + newPostButtonStr += \ + '' + \ + '' + + # This creates a link to the profile page when viewed + # in lynx, but should be invisible in a graphical web browser + tlStr += \ + '\n' + + # banner and row of buttons + tlStr += \ + '\n' + tlStr += '\n' + + if fullWidthTimelineButtonHeader: + tlStr += \ + headerButtonsTimeline(defaultTimeline, boxName, pageNumber, + translate, usersPath, mediaButton, + blogsButton, newsButton, inboxButton, + dmButton, newDM, repliesButton, + newReply, minimal, sentButton, + sharesButtonStr, bookmarksButtonStr, + eventsButtonStr, moderationButtonStr, + newPostButtonStr, baseDir, nickname, + domain, iconsDir, timelineStartTime, + newCalendarEvent, calendarPath, + calendarImage, followApprovals, + iconsAsButtons) + + # start the timeline + tlStr += '\n' + tlStr += ' \n' + tlStr += ' \n' + tlStr += ' \n' + tlStr += ' \n' + tlStr += ' \n' + tlStr += ' \n' + tlStr += ' \n' + + domainFull = domain + if port: + if port != 80 and port != 443: + domainFull = domain + ':' + str(port) + + # left column + leftColumnStr = \ + getLeftColumnContent(baseDir, nickname, domainFull, + httpPrefix, translate, iconsDir, + editor, False, None, rssIconAtTop, + True, False) + tlStr += ' \n' + # center column containing posts + tlStr += ' \n' + + # right column + rightColumnStr = getRightColumnContent(baseDir, nickname, domainFull, + httpPrefix, translate, iconsDir, + moderator, editor, + newswire, positiveVoting, + False, None, True, + showPublishAsIcon, + rssIconAtTop, publishButtonAtTop, + authorized, True) + tlStr += ' \n' + tlStr += ' \n' + + # benchmark 9 + timeDiff = int((time.time() - timelineStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE TIMING ' + boxName + ' 9 = ' + str(timeDiff)) + + tlStr += ' \n' + tlStr += '
' + \ + leftColumnStr + ' \n' + + if not fullWidthTimelineButtonHeader: + tlStr += \ + headerButtonsTimeline(defaultTimeline, boxName, pageNumber, + translate, usersPath, mediaButton, + blogsButton, newsButton, inboxButton, + dmButton, newDM, repliesButton, + newReply, minimal, sentButton, + sharesButtonStr, bookmarksButtonStr, + eventsButtonStr, moderationButtonStr, + newPostButtonStr, baseDir, nickname, + domain, iconsDir, timelineStartTime, + newCalendarEvent, calendarPath, + calendarImage, followApprovals, + iconsAsButtons) + + # second row of buttons for moderator actions + if moderator and boxName == 'moderation': + tlStr += \ + '
' + tlStr += '
\n' + idx = 'Nickname or URL. Block using *@domain or nickname@domain' + tlStr += \ + ' ' + translate[idx] + '
\n' + tlStr += '
\n' + tlStr += \ + ' \n' + tlStr += \ + ' \n' + tlStr += \ + ' \n' + tlStr += \ + ' \n' + tlStr += \ + ' \n' + tlStr += \ + ' \n' + tlStr += '
\n
\n' + + # benchmark 6 + timeDiff = int((time.time() - timelineStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE TIMING ' + boxName + ' 6 = ' + str(timeDiff)) + + if boxName == 'tlshares': + maxSharesPerAccount = itemsPerPage + return (tlStr + + htmlSharesTimeline(translate, pageNumber, itemsPerPage, + baseDir, actor, nickname, domain, port, + maxSharesPerAccount, httpPrefix) + + htmlFooter()) + + # benchmark 7 + timeDiff = int((time.time() - timelineStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE TIMING ' + boxName + ' 7 = ' + str(timeDiff)) + + # benchmark 8 + timeDiff = int((time.time() - timelineStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE TIMING ' + boxName + ' 8 = ' + str(timeDiff)) + + # page up arrow + if pageNumber > 1: + tlStr += \ + '
\n' + \ + ' ' + \
+            translate['Page up'] + '\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']: + timelinePostStartTime = time.time() + + 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(currTlStr, + boxName, + pageNumber) + # benchmark cache post + timeDiff = \ + int((time.time() - + timelinePostStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE POST CACHE TIMING ' + + boxName + ' = ' + str(timeDiff)) + + if not currTlStr: + # benchmark cache post + timeDiff = \ + int((time.time() - + timelinePostStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE POST DISK TIMING START ' + + boxName + ' = ' + str(timeDiff)) + + # read the post from disk + currTlStr = \ + individualPostAsHtml(False, recentPostsCache, + maxRecentPosts, + iconsDir, translate, pageNumber, + baseDir, session, wfRequest, + personCache, + nickname, domain, port, + item, None, True, + allowDeletion, + httpPrefix, projectVersion, + boxName, + YTReplacementDomain, + showPublishedDateOnly, + boxName != 'dm', + showIndividualPostIcons, + manuallyApproveFollowers, + False, True) + # benchmark cache post + timeDiff = \ + int((time.time() - + timelinePostStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE POST DISK TIMING ' + + boxName + ' = ' + str(timeDiff)) + + if currTlStr: + itemCtr += 1 + if separatorStr: + tlStr += separatorStr + tlStr += currTlStr + if boxName == 'tlmedia': + tlStr += '
\n' + + # page down arrow + if itemCtr > 2: + tlStr += \ + '
\n' + \ + ' ' + \
+            translate['Page down'] + '\n' + \ + '
\n' + + # end of column-center + tlStr += '
' + \ + rightColumnStr + '
\n' + tlStr += htmlFooter() + return tlStr + + +def htmlShares(cssCache: {}, defaultTimeline: str, + recentPostsCache: {}, maxRecentPosts: int, + translate: {}, pageNumber: int, itemsPerPage: int, + session, baseDir: str, wfRequest: {}, personCache: {}, + nickname: str, domain: str, port: int, + allowDeletion: bool, + httpPrefix: str, projectVersion: str, + YTReplacementDomain: str, + showPublishedDateOnly: bool, + newswire: {}, positiveVoting: bool, + showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: + """Show the shares timeline as html + """ + manuallyApproveFollowers = \ + followerApprovalActive(baseDir, nickname, domain) + + return htmlTimeline(cssCache, defaultTimeline, + recentPostsCache, maxRecentPosts, + translate, pageNumber, + itemsPerPage, session, baseDir, wfRequest, personCache, + nickname, domain, port, None, + 'tlshares', allowDeletion, + httpPrefix, projectVersion, manuallyApproveFollowers, + False, YTReplacementDomain, + showPublishedDateOnly, + newswire, False, False, + positiveVoting, showPublishAsIcon, + fullWidthTimelineButtonHeader, + iconsAsButtons, rssIconAtTop, publishButtonAtTop, + authorized) + + +def htmlInbox(cssCache: {}, defaultTimeline: str, + recentPostsCache: {}, maxRecentPosts: int, + translate: {}, pageNumber: int, itemsPerPage: int, + session, baseDir: str, wfRequest: {}, personCache: {}, + nickname: str, domain: str, port: int, inboxJson: {}, + allowDeletion: bool, + httpPrefix: str, projectVersion: str, + minimal: bool, YTReplacementDomain: str, + showPublishedDateOnly: bool, + newswire: {}, positiveVoting: bool, + showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: + """Show the inbox as html + """ + manuallyApproveFollowers = \ + followerApprovalActive(baseDir, nickname, domain) + + return htmlTimeline(cssCache, defaultTimeline, + recentPostsCache, maxRecentPosts, + translate, pageNumber, + itemsPerPage, session, baseDir, wfRequest, personCache, + nickname, domain, port, inboxJson, + 'inbox', allowDeletion, + httpPrefix, projectVersion, manuallyApproveFollowers, + minimal, YTReplacementDomain, + showPublishedDateOnly, + newswire, False, False, + positiveVoting, showPublishAsIcon, + fullWidthTimelineButtonHeader, + iconsAsButtons, rssIconAtTop, publishButtonAtTop, + authorized) + + +def htmlBookmarks(cssCache: {}, defaultTimeline: str, + recentPostsCache: {}, maxRecentPosts: int, + translate: {}, pageNumber: int, itemsPerPage: int, + session, baseDir: str, wfRequest: {}, personCache: {}, + nickname: str, domain: str, port: int, bookmarksJson: {}, + allowDeletion: bool, + httpPrefix: str, projectVersion: str, + minimal: bool, YTReplacementDomain: str, + showPublishedDateOnly: bool, + newswire: {}, positiveVoting: bool, + showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: + """Show the bookmarks as html + """ + manuallyApproveFollowers = \ + followerApprovalActive(baseDir, nickname, domain) + + return htmlTimeline(cssCache, defaultTimeline, + recentPostsCache, maxRecentPosts, + translate, pageNumber, + itemsPerPage, session, baseDir, wfRequest, personCache, + nickname, domain, port, bookmarksJson, + 'tlbookmarks', allowDeletion, + httpPrefix, projectVersion, manuallyApproveFollowers, + minimal, YTReplacementDomain, + showPublishedDateOnly, + newswire, False, False, + positiveVoting, showPublishAsIcon, + fullWidthTimelineButtonHeader, + iconsAsButtons, rssIconAtTop, publishButtonAtTop, + authorized) + + +def htmlEvents(cssCache: {}, defaultTimeline: str, + recentPostsCache: {}, maxRecentPosts: int, + translate: {}, pageNumber: int, itemsPerPage: int, + session, baseDir: str, wfRequest: {}, personCache: {}, + nickname: str, domain: str, port: int, bookmarksJson: {}, + allowDeletion: bool, + httpPrefix: str, projectVersion: str, + minimal: bool, YTReplacementDomain: str, + showPublishedDateOnly: bool, + newswire: {}, positiveVoting: bool, + showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: + """Show the events as html + """ + manuallyApproveFollowers = \ + followerApprovalActive(baseDir, nickname, domain) + + return htmlTimeline(cssCache, defaultTimeline, + recentPostsCache, maxRecentPosts, + translate, pageNumber, + itemsPerPage, session, baseDir, wfRequest, personCache, + nickname, domain, port, bookmarksJson, + 'tlevents', allowDeletion, + httpPrefix, projectVersion, manuallyApproveFollowers, + minimal, YTReplacementDomain, + showPublishedDateOnly, + newswire, False, False, + positiveVoting, showPublishAsIcon, + fullWidthTimelineButtonHeader, + iconsAsButtons, rssIconAtTop, publishButtonAtTop, + authorized) + + +def htmlInboxDMs(cssCache: {}, defaultTimeline: str, + recentPostsCache: {}, maxRecentPosts: int, + translate: {}, pageNumber: int, itemsPerPage: int, + session, baseDir: str, wfRequest: {}, personCache: {}, + nickname: str, domain: str, port: int, inboxJson: {}, + allowDeletion: bool, + httpPrefix: str, projectVersion: str, + minimal: bool, YTReplacementDomain: str, + showPublishedDateOnly: bool, + newswire: {}, positiveVoting: bool, + showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: + """Show the DM timeline as html + """ + return htmlTimeline(cssCache, defaultTimeline, + recentPostsCache, maxRecentPosts, + translate, pageNumber, + itemsPerPage, session, baseDir, wfRequest, personCache, + nickname, domain, port, inboxJson, 'dm', allowDeletion, + httpPrefix, projectVersion, False, minimal, + YTReplacementDomain, showPublishedDateOnly, + newswire, False, False, positiveVoting, + showPublishAsIcon, + fullWidthTimelineButtonHeader, + iconsAsButtons, rssIconAtTop, publishButtonAtTop, + authorized) + + +def htmlInboxReplies(cssCache: {}, defaultTimeline: str, + recentPostsCache: {}, maxRecentPosts: int, + translate: {}, pageNumber: int, itemsPerPage: int, + session, baseDir: str, wfRequest: {}, personCache: {}, + nickname: str, domain: str, port: int, inboxJson: {}, + allowDeletion: bool, + httpPrefix: str, projectVersion: str, + minimal: bool, YTReplacementDomain: str, + showPublishedDateOnly: bool, + newswire: {}, positiveVoting: bool, + showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: + """Show the replies timeline as html + """ + return htmlTimeline(cssCache, defaultTimeline, + recentPostsCache, maxRecentPosts, + translate, pageNumber, + itemsPerPage, session, baseDir, wfRequest, personCache, + nickname, domain, port, inboxJson, 'tlreplies', + allowDeletion, httpPrefix, projectVersion, False, + minimal, YTReplacementDomain, + showPublishedDateOnly, + newswire, False, False, + positiveVoting, showPublishAsIcon, + fullWidthTimelineButtonHeader, + iconsAsButtons, rssIconAtTop, publishButtonAtTop, + authorized) + + +def htmlInboxMedia(cssCache: {}, defaultTimeline: str, + recentPostsCache: {}, maxRecentPosts: int, + translate: {}, pageNumber: int, itemsPerPage: int, + session, baseDir: str, wfRequest: {}, personCache: {}, + nickname: str, domain: str, port: int, inboxJson: {}, + allowDeletion: bool, + httpPrefix: str, projectVersion: str, + minimal: bool, YTReplacementDomain: str, + showPublishedDateOnly: bool, + newswire: {}, positiveVoting: bool, + showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: + """Show the media timeline as html + """ + return htmlTimeline(cssCache, defaultTimeline, + recentPostsCache, maxRecentPosts, + translate, pageNumber, + itemsPerPage, session, baseDir, wfRequest, personCache, + nickname, domain, port, inboxJson, 'tlmedia', + allowDeletion, httpPrefix, projectVersion, False, + minimal, YTReplacementDomain, + showPublishedDateOnly, + newswire, False, False, + positiveVoting, showPublishAsIcon, + fullWidthTimelineButtonHeader, + iconsAsButtons, rssIconAtTop, publishButtonAtTop, + authorized) + + +def htmlInboxBlogs(cssCache: {}, defaultTimeline: str, + recentPostsCache: {}, maxRecentPosts: int, + translate: {}, pageNumber: int, itemsPerPage: int, + session, baseDir: str, wfRequest: {}, personCache: {}, + nickname: str, domain: str, port: int, inboxJson: {}, + allowDeletion: bool, + httpPrefix: str, projectVersion: str, + minimal: bool, YTReplacementDomain: str, + showPublishedDateOnly: bool, + newswire: {}, positiveVoting: bool, + showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: + """Show the blogs timeline as html + """ + return htmlTimeline(cssCache, defaultTimeline, + recentPostsCache, maxRecentPosts, + translate, pageNumber, + itemsPerPage, session, baseDir, wfRequest, personCache, + nickname, domain, port, inboxJson, 'tlblogs', + allowDeletion, httpPrefix, projectVersion, False, + minimal, YTReplacementDomain, + showPublishedDateOnly, + newswire, False, False, + positiveVoting, showPublishAsIcon, + fullWidthTimelineButtonHeader, + iconsAsButtons, rssIconAtTop, publishButtonAtTop, + authorized) + + +def htmlInboxNews(cssCache: {}, defaultTimeline: str, + recentPostsCache: {}, maxRecentPosts: int, + translate: {}, pageNumber: int, itemsPerPage: int, + session, baseDir: str, wfRequest: {}, personCache: {}, + nickname: str, domain: str, port: int, inboxJson: {}, + allowDeletion: bool, + httpPrefix: str, projectVersion: str, + minimal: bool, YTReplacementDomain: str, + showPublishedDateOnly: bool, + newswire: {}, moderator: bool, editor: bool, + positiveVoting: bool, showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: + """Show the news timeline as html + """ + return htmlTimeline(cssCache, defaultTimeline, + recentPostsCache, maxRecentPosts, + translate, pageNumber, + itemsPerPage, session, baseDir, wfRequest, personCache, + nickname, domain, port, inboxJson, 'tlnews', + allowDeletion, httpPrefix, projectVersion, False, + minimal, YTReplacementDomain, + showPublishedDateOnly, + newswire, moderator, editor, + positiveVoting, showPublishAsIcon, + fullWidthTimelineButtonHeader, + iconsAsButtons, rssIconAtTop, publishButtonAtTop, + authorized) + + +def htmlModeration(cssCache: {}, defaultTimeline: str, + recentPostsCache: {}, maxRecentPosts: int, + translate: {}, pageNumber: int, itemsPerPage: int, + session, baseDir: str, wfRequest: {}, personCache: {}, + nickname: str, domain: str, port: int, inboxJson: {}, + allowDeletion: bool, + httpPrefix: str, projectVersion: str, + YTReplacementDomain: str, + showPublishedDateOnly: bool, + newswire: {}, positiveVoting: bool, + showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: + """Show the moderation feed as html + """ + return htmlTimeline(cssCache, defaultTimeline, + recentPostsCache, maxRecentPosts, + translate, pageNumber, + itemsPerPage, session, baseDir, wfRequest, personCache, + nickname, domain, port, inboxJson, 'moderation', + allowDeletion, httpPrefix, projectVersion, True, False, + YTReplacementDomain, showPublishedDateOnly, + newswire, False, False, positiveVoting, + showPublishAsIcon, fullWidthTimelineButtonHeader, + iconsAsButtons, rssIconAtTop, publishButtonAtTop, + authorized) + + +def htmlOutbox(cssCache: {}, defaultTimeline: str, + recentPostsCache: {}, maxRecentPosts: int, + translate: {}, pageNumber: int, itemsPerPage: int, + session, baseDir: str, wfRequest: {}, personCache: {}, + nickname: str, domain: str, port: int, outboxJson: {}, + allowDeletion: bool, + httpPrefix: str, projectVersion: str, + minimal: bool, YTReplacementDomain: str, + showPublishedDateOnly: bool, + newswire: {}, positiveVoting: bool, + showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: + """Show the Outbox as html + """ + manuallyApproveFollowers = \ + followerApprovalActive(baseDir, nickname, domain) + return htmlTimeline(cssCache, defaultTimeline, + recentPostsCache, maxRecentPosts, + translate, pageNumber, + itemsPerPage, session, baseDir, wfRequest, personCache, + nickname, domain, port, outboxJson, 'outbox', + allowDeletion, httpPrefix, projectVersion, + manuallyApproveFollowers, minimal, + YTReplacementDomain, showPublishedDateOnly, + newswire, False, False, positiveVoting, + showPublishAsIcon, fullWidthTimelineButtonHeader, + iconsAsButtons, rssIconAtTop, publishButtonAtTop, + authorized) diff --git a/webapp_utils.py b/webapp_utils.py new file mode 100644 index 000000000..2dcf172c0 --- /dev/null +++ b/webapp_utils.py @@ -0,0 +1,812 @@ +__filename__ = "webapp_utils.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "1.1.0" +__maintainer__ = "Bob Mottram" +__email__ = "bob@freedombone.net" +__status__ = "Production" + +import os +from collections import OrderedDict +from session import getJson +from utils import getProtocolPrefixes +from utils import loadJson +from utils import getCachedPostFilename +from utils import getConfigParam +from cache import getPersonFromCache +from cache import storePersonInCache +from content import addHtmlTags +from content import replaceEmojiFromTags + + +def getAltPath(actor: str, domainFull: str, callingDomain: str) -> str: + """Returns alternate path from the actor + eg. https://clearnetdomain/path becomes http://oniondomain/path + """ + postActor = actor + if callingDomain not in actor and domainFull in actor: + if callingDomain.endswith('.onion') or \ + callingDomain.endswith('.i2p'): + postActor = \ + 'http://' + callingDomain + actor.split(domainFull)[1] + print('Changed POST domain from ' + actor + ' to ' + postActor) + return postActor + + +def getContentWarningButton(postID: str, translate: {}, + content: str) -> str: + """Returns the markup for a content warning button + """ + return '
' + \ + translate['SHOW MORE'] + '' + \ + '
' + content + \ + '
\n' + + +def getActorPropertyUrl(actorJson: {}, propertyName: str) -> str: + """Returns a url property from an actor + """ + if not actorJson.get('attachment'): + return '' + propertyName = propertyName.lower() + for propertyValue in actorJson['attachment']: + if not propertyValue.get('name'): + continue + if not propertyValue['name'].lower().startswith(propertyName): + continue + if not propertyValue.get('type'): + continue + if not propertyValue.get('value'): + continue + if propertyValue['type'] != 'PropertyValue': + continue + propertyValue['value'] = propertyValue['value'].strip() + prefixes = getProtocolPrefixes() + prefixFound = False + for prefix in prefixes: + if propertyValue['value'].startswith(prefix): + prefixFound = True + break + if not prefixFound: + continue + if '.' not in propertyValue['value']: + continue + if ' ' in propertyValue['value']: + continue + if ',' in propertyValue['value']: + continue + return propertyValue['value'] + return '' + + +def getBlogAddress(actorJson: {}) -> str: + """Returns blog address for the given actor + """ + return getActorPropertyUrl(actorJson, 'Blog') + + +def setActorPropertyUrl(actorJson: {}, propertyName: str, url: str) -> None: + """Sets a url for the given actor property + """ + if not actorJson.get('attachment'): + actorJson['attachment'] = [] + + propertyNameLower = propertyName.lower() + + # remove any existing value + propertyFound = None + for propertyValue in actorJson['attachment']: + if not propertyValue.get('name'): + continue + if not propertyValue.get('type'): + continue + if not propertyValue['name'].lower().startswith(propertyNameLower): + continue + propertyFound = propertyValue + break + if propertyFound: + actorJson['attachment'].remove(propertyFound) + + prefixes = getProtocolPrefixes() + prefixFound = False + for prefix in prefixes: + if url.startswith(prefix): + prefixFound = True + break + if not prefixFound: + return + if '.' not in url: + return + if ' ' in url: + return + if ',' in url: + return + + for propertyValue in actorJson['attachment']: + if not propertyValue.get('name'): + continue + if not propertyValue.get('type'): + continue + if not propertyValue['name'].lower().startswith(propertyNameLower): + continue + if propertyValue['type'] != 'PropertyValue': + continue + propertyValue['value'] = url + return + + newAddress = { + "name": propertyName, + "type": "PropertyValue", + "value": url + } + actorJson['attachment'].append(newAddress) + + +def setBlogAddress(actorJson: {}, blogAddress: str) -> None: + """Sets an blog address for the given actor + """ + setActorPropertyUrl(actorJson, 'Blog', blogAddress) + + +def updateAvatarImageCache(session, baseDir: str, httpPrefix: str, + actor: str, avatarUrl: str, + personCache: {}, allowDownloads: bool, + force=False) -> str: + """Updates the cached avatar for the given actor + """ + if not avatarUrl: + return None + actorStr = actor.replace('/', '-') + avatarImagePath = baseDir + '/cache/avatars/' + actorStr + if avatarUrl.endswith('.png') or \ + '.png?' in avatarUrl: + sessionHeaders = { + 'Accept': 'image/png' + } + avatarImageFilename = avatarImagePath + '.png' + elif (avatarUrl.endswith('.jpg') or + avatarUrl.endswith('.jpeg') or + '.jpg?' in avatarUrl or + '.jpeg?' in avatarUrl): + sessionHeaders = { + 'Accept': 'image/jpeg' + } + avatarImageFilename = avatarImagePath + '.jpg' + elif avatarUrl.endswith('.gif') or '.gif?' in avatarUrl: + sessionHeaders = { + 'Accept': 'image/gif' + } + avatarImageFilename = avatarImagePath + '.gif' + elif avatarUrl.endswith('.webp') or '.webp?' in avatarUrl: + sessionHeaders = { + 'Accept': 'image/webp' + } + avatarImageFilename = avatarImagePath + '.webp' + elif avatarUrl.endswith('.avif') or '.avif?' in avatarUrl: + sessionHeaders = { + 'Accept': 'image/avif' + } + avatarImageFilename = avatarImagePath + '.avif' + else: + return None + + if (not os.path.isfile(avatarImageFilename) or force) and allowDownloads: + try: + print('avatar image url: ' + avatarUrl) + result = session.get(avatarUrl, + headers=sessionHeaders, + params=None) + if result.status_code < 200 or \ + result.status_code > 202: + print('Avatar image download failed with status ' + + str(result.status_code)) + # remove partial download + if os.path.isfile(avatarImageFilename): + os.remove(avatarImageFilename) + else: + with open(avatarImageFilename, 'wb') as f: + f.write(result.content) + print('avatar image downloaded for ' + actor) + return avatarImageFilename.replace(baseDir + '/cache', '') + except Exception as e: + print('Failed to download avatar image: ' + str(avatarUrl)) + print(e) + prof = 'https://www.w3.org/ns/activitystreams' + if '/channel/' not in actor or '/accounts/' not in actor: + sessionHeaders = { + 'Accept': 'application/activity+json; profile="' + prof + '"' + } + else: + sessionHeaders = { + 'Accept': 'application/ld+json; profile="' + prof + '"' + } + personJson = \ + getJson(session, actor, sessionHeaders, None, __version__, + httpPrefix, None) + if personJson: + if not personJson.get('id'): + return None + if not personJson.get('publicKey'): + return None + if not personJson['publicKey'].get('publicKeyPem'): + return None + if personJson['id'] != actor: + return None + if not personCache.get(actor): + return None + if personCache[actor]['actor']['publicKey']['publicKeyPem'] != \ + personJson['publicKey']['publicKeyPem']: + print("ERROR: " + + "public keys don't match when downloading actor for " + + actor) + return None + storePersonInCache(baseDir, actor, personJson, personCache, + allowDownloads) + return getPersonAvatarUrl(baseDir, actor, personCache, + allowDownloads) + return None + return avatarImageFilename.replace(baseDir + '/cache', '') + + +def getImageExtensions() -> []: + """Returns a list of the possible image file extensions + """ + return ('png', 'jpg', 'jpeg', 'gif', 'webp', 'avif') + + +def getPersonAvatarUrl(baseDir: str, personUrl: str, personCache: {}, + allowDownloads: bool) -> str: + """Returns the avatar url for the person + """ + personJson = \ + getPersonFromCache(baseDir, personUrl, personCache, allowDownloads) + if not personJson: + return None + + # get from locally stored image + actorStr = personJson['id'].replace('/', '-') + avatarImagePath = baseDir + '/cache/avatars/' + actorStr + + imageExtension = getImageExtensions() + for ext in imageExtension: + if os.path.isfile(avatarImagePath + '.' + ext): + return '/avatars/' + actorStr + '.' + ext + elif os.path.isfile(avatarImagePath.lower() + '.' + ext): + return '/avatars/' + actorStr.lower() + '.' + ext + + if personJson.get('icon'): + if personJson['icon'].get('url'): + return personJson['icon']['url'] + return None + + +def getIconsDir(baseDir: str) -> str: + """Returns the directory where icons exist + """ + iconsDir = 'icons' + theme = getConfigParam(baseDir, 'theme') + if theme: + if os.path.isdir(baseDir + '/img/icons/' + theme): + iconsDir = 'icons/' + theme + return iconsDir + + +def scheduledPostsExist(baseDir: str, nickname: str, domain: str) -> bool: + """Returns true if there are posts scheduled to be delivered + """ + scheduleIndexFilename = \ + baseDir + '/accounts/' + nickname + '@' + domain + '/schedule.index' + if not os.path.isfile(scheduleIndexFilename): + return False + if '#users#' in open(scheduleIndexFilename).read(): + return True + return False + + +def sharesTimelineJson(actor: str, pageNumber: int, itemsPerPage: int, + baseDir: str, maxSharesPerAccount: int) -> ({}, bool): + """Get a page on the shared items timeline as json + maxSharesPerAccount helps to avoid one person dominating the timeline + by sharing a large number of things + """ + allSharesJson = {} + for subdir, dirs, files in os.walk(baseDir + '/accounts'): + for handle in dirs: + if '@' in handle: + accountDir = baseDir + '/accounts/' + handle + sharesFilename = accountDir + '/shares.json' + if os.path.isfile(sharesFilename): + sharesJson = loadJson(sharesFilename) + if not sharesJson: + continue + nickname = handle.split('@')[0] + # actor who owns this share + owner = actor.split('/users/')[0] + '/users/' + nickname + ctr = 0 + for itemID, item in sharesJson.items(): + # assign owner to the item + item['actor'] = owner + allSharesJson[str(item['published'])] = item + ctr += 1 + if ctr >= maxSharesPerAccount: + break + # sort the shared items in descending order of publication date + sharesJson = OrderedDict(sorted(allSharesJson.items(), reverse=True)) + lastPage = False + startIndex = itemsPerPage * pageNumber + maxIndex = len(sharesJson.items()) + if maxIndex < itemsPerPage: + lastPage = True + if startIndex >= maxIndex - itemsPerPage: + lastPage = True + startIndex = maxIndex - itemsPerPage + if startIndex < 0: + startIndex = 0 + ctr = 0 + resultJson = {} + for published, item in sharesJson.items(): + if ctr >= startIndex + itemsPerPage: + break + if ctr < startIndex: + ctr += 1 + continue + resultJson[published] = item + ctr += 1 + return resultJson, lastPage + + +def postContainsPublic(postJsonObject: {}) -> bool: + """Does the given post contain #Public + """ + containsPublic = False + if not postJsonObject['object'].get('to'): + return containsPublic + + for toAddress in postJsonObject['object']['to']: + if toAddress.endswith('#Public'): + containsPublic = True + break + if not containsPublic: + if postJsonObject['object'].get('cc'): + for toAddress in postJsonObject['object']['cc']: + if toAddress.endswith('#Public'): + containsPublic = True + break + return containsPublic + + +def getImageFile(baseDir: str, name: str, directory: str, + nickname: str, domain: str) -> (str, str): + """ + returns the filenames for an image with the given name + """ + bannerExtensions = getImageExtensions() + bannerFile = '' + bannerFilename = '' + for ext in bannerExtensions: + bannerFile = name + '.' + ext + bannerFilename = directory + '/' + bannerFile + if os.path.isfile(bannerFilename): + break + return bannerFile, bannerFilename + + +def getBannerFile(baseDir: str, + nickname: str, domain: str) -> (str, str): + return getImageFile(baseDir, 'banner', + baseDir + '/accounts/' + nickname + '@' + domain, + nickname, domain) + + +def getSearchBannerFile(baseDir: str, + nickname: str, domain: str) -> (str, str): + return getImageFile(baseDir, 'search_banner', + baseDir + '/accounts/' + nickname + '@' + domain, + nickname, domain) + + +def getLeftImageFile(baseDir: str, + nickname: str, domain: str) -> (str, str): + return getImageFile(baseDir, 'left_col_image', + baseDir + '/accounts/' + nickname + '@' + domain, + nickname, domain) + + +def getRightImageFile(baseDir: str, + nickname: str, domain: str) -> (str, str): + return getImageFile(baseDir, 'right_col_image', + baseDir + '/accounts/' + nickname + '@' + domain, + nickname, domain) + + +def htmlHeader(cssFilename: str, css: str, lang='en') -> str: + htmlStr = '\n' + htmlStr += '\n' + htmlStr += ' \n' + htmlStr += ' \n' + fontName, fontFormat = getFontFromCss(css) + if fontName: + htmlStr += ' \n' + htmlStr += ' \n' + htmlStr += ' \n' + htmlStr += ' \n' + htmlStr += ' Epicyon\n' + htmlStr += ' \n' + htmlStr += ' \n' + return htmlStr + + +def htmlFooter() -> str: + htmlStr = ' \n' + htmlStr += '\n' + return htmlStr + + +def getFontFromCss(css: str) -> (str, str): + """Returns the font name and format + """ + if ' url(' not in css: + return None, None + fontName = css.split(" url(")[1].split(")")[0].replace("'", '') + fontFormat = css.split(" format('")[1].split("')")[0] + return fontName, fontFormat + + +def loadIndividualPostAsHtmlFromCache(baseDir: str, + nickname: str, domain: str, + postJsonObject: {}) -> str: + """If a cached html version of the given post exists then load it and + return the html text + This is much quicker than generating the html from the json object + """ + cachedPostFilename = \ + getCachedPostFilename(baseDir, nickname, domain, postJsonObject) + + postHtml = '' + if not cachedPostFilename: + return postHtml + + if not os.path.isfile(cachedPostFilename): + return postHtml + + tries = 0 + while tries < 3: + try: + with open(cachedPostFilename, 'r') as file: + postHtml = file.read() + break + except Exception as e: + print(e) + # no sleep + tries += 1 + if postHtml: + return postHtml + + +def addEmojiToDisplayName(baseDir: str, httpPrefix: str, + nickname: str, domain: str, + displayName: str, inProfileName: bool) -> str: + """Adds emoji icons to display names on individual posts + """ + if ':' not in displayName: + return displayName + + displayName = displayName.replace('

', '').replace('

', '') + emojiTags = {} + print('TAG: displayName before tags: ' + displayName) + displayName = \ + addHtmlTags(baseDir, httpPrefix, + nickname, domain, displayName, [], emojiTags) + displayName = displayName.replace('

', '').replace('

', '') + print('TAG: displayName after tags: ' + displayName) + # convert the emoji dictionary to a list + emojiTagsList = [] + for tagName, tag in emojiTags.items(): + emojiTagsList.append(tag) + print('TAG: emoji tags list: ' + str(emojiTagsList)) + if not inProfileName: + displayName = \ + replaceEmojiFromTags(displayName, emojiTagsList, 'post header') + else: + displayName = \ + replaceEmojiFromTags(displayName, emojiTagsList, 'profile') + print('TAG: displayName after tags 2: ' + displayName) + + # remove any stray emoji + while ':' in displayName: + if '://' in displayName: + break + emojiStr = displayName.split(':')[1] + prevDisplayName = displayName + displayName = displayName.replace(':' + emojiStr + ':', '').strip() + if prevDisplayName == displayName: + break + print('TAG: displayName after tags 3: ' + displayName) + print('TAG: displayName after tag replacements: ' + displayName) + + return displayName + + +def getPostAttachmentsAsHtml(postJsonObject: {}, boxName: str, translate: {}, + isMuted: bool, avatarLink: str, + replyStr: str, announceStr: str, likeStr: str, + bookmarkStr: str, deleteStr: str, + muteStr: str) -> (str, str): + """Returns a string representing any attachments + """ + attachmentStr = '' + galleryStr = '' + if not postJsonObject['object'].get('attachment'): + return attachmentStr, galleryStr + + if not isinstance(postJsonObject['object']['attachment'], list): + return attachmentStr, galleryStr + + attachmentCtr = 0 + attachmentStr += '
\n' + for attach in postJsonObject['object']['attachment']: + if not (attach.get('mediaType') and attach.get('url')): + continue + + mediaType = attach['mediaType'] + imageDescription = '' + if attach.get('name'): + imageDescription = attach['name'].replace('"', "'") + if mediaType == 'image/png' or \ + mediaType == 'image/jpeg' or \ + mediaType == 'image/webp' or \ + mediaType == 'image/avif' or \ + mediaType == 'image/gif': + if attach['url'].endswith('.png') or \ + attach['url'].endswith('.jpg') or \ + attach['url'].endswith('.jpeg') or \ + attach['url'].endswith('.webp') or \ + attach['url'].endswith('.avif') or \ + attach['url'].endswith('.gif'): + if attachmentCtr > 0: + attachmentStr += '
' + if boxName == 'tlmedia': + galleryStr += '\n' + + attachmentStr += '' + attachmentStr += \ + '' + imageDescription + '\n' + attachmentCtr += 1 + elif (mediaType == 'video/mp4' or + mediaType == 'video/webm' or + mediaType == 'video/ogv'): + extension = '.mp4' + if attach['url'].endswith('.webm'): + extension = '.webm' + elif attach['url'].endswith('.ogv'): + extension = '.ogv' + if attach['url'].endswith(extension): + if attachmentCtr > 0: + attachmentStr += '
' + if boxName == 'tlmedia': + galleryStr += '\n' + + attachmentStr += \ + '
' + attachmentCtr += 1 + elif (mediaType == 'audio/mpeg' or + mediaType == 'audio/ogg'): + extension = '.mp3' + if attach['url'].endswith('.ogg'): + extension = '.ogg' + if attach['url'].endswith(extension): + if attachmentCtr > 0: + attachmentStr += '
' + if boxName == 'tlmedia': + galleryStr += '\n' + + attachmentStr += '
\n\n
\n' + attachmentCtr += 1 + attachmentStr += '
' + return attachmentStr, galleryStr + + +def htmlPostSeparator(baseDir: str, column: str) -> str: + """Returns the html for a timeline post separator image + """ + iconsDir = getIconsDir(baseDir) + filename = 'separator.png' + if column: + filename = 'separator_' + column + '.png' + separatorImageFilename = baseDir + '/img/' + iconsDir + '/' + filename + separatorStr = '' + if os.path.isfile(separatorImageFilename): + separatorStr = \ + '
' + \ + '' + \ + '
\n' + return separatorStr + + +def headerButtonsFrontScreen(translate: {}, + nickname: str, boxName: str, + authorized: bool, + iconsAsButtons: bool, + iconsDir: bool) -> str: + """Returns the header buttons for the front page of a news instance + """ + headerStr = '' + if nickname == 'news': + buttonFeatures = 'buttonMobile' + buttonNewswire = 'buttonMobile' + buttonLinks = 'buttonMobile' + if boxName == 'features': + buttonFeatures = 'buttonselected' + elif boxName == 'newswire': + buttonNewswire = 'buttonselected' + elif boxName == 'links': + buttonLinks = 'buttonselected' + + headerStr += \ + ' ' + \ + '' + if not authorized: + headerStr += \ + ' ' + \ + '' + if iconsAsButtons: + headerStr += \ + ' ' + \ + '' + headerStr += \ + ' ' + \ + '' + else: + headerStr += \ + ' ' + \ + '| ' + translate['Newswire'] + '\n' + headerStr += \ + ' ' + \ + '| ' + translate['Links'] + '\n' + else: + if not authorized: + headerStr += \ + ' ' + \ + '' + + if headerStr: + headerStr = \ + '\n
\n' + \ + headerStr + \ + '
\n' + return headerStr diff --git a/webinterface.py b/webinterface.py deleted file mode 100644 index a2933611d..000000000 --- a/webinterface.py +++ /dev/null @@ -1,9244 +0,0 @@ -__filename__ = "webinterface.py" -__author__ = "Bob Mottram" -__license__ = "AGPL3+" -__version__ = "1.1.0" -__maintainer__ = "Bob Mottram" -__email__ = "bob@freedombone.net" -__status__ = "Production" - -import time -import os -import urllib.parse -from collections import OrderedDict -from datetime import datetime -from datetime import date -from dateutil.parser import parse -from shutil import copyfile -from pprint import pprint -from person import personBoxJson -from person import isPersonSnoozed -from pgp import getEmailAddress -from pgp import getPGPpubKey -from pgp import getPGPfingerprint -from xmpp import getXmppAddress -from ssb import getSSBAddress -from tox import getToxAddress -from matrix import getMatrixAddress -from donate import getDonationUrl -from utils import getCSS -from utils import isSystemAccount -from utils import removeIdEnding -from utils import getProtocolPrefixes -from utils import searchBoxPosts -from utils import isEventPost -from utils import isBlogPost -from utils import isNewsPost -from utils import updateRecentPostsCache -from utils import getNicknameFromActor -from utils import getDomainFromActor -from utils import locatePost -from utils import noOfAccounts -from utils import isPublicPost -from utils import isPublicPostFromUrl -from utils import getDisplayName -from utils import getCachedPostDirectory -from utils import getCachedPostFilename -from utils import loadJson -from utils import getConfigParam -from utils import votesOnNewswireItem -from utils import removeHtml -from follow import isFollowingActor -from webfinger import webfingerHandle -from posts import isDM -from posts import getPersonBox -from posts import getUserUrl -from posts import parseUserFeed -from posts import populateRepliesJson -from posts import isModerator -from posts import isEditor -from posts import downloadAnnounce -from session import getJson -from auth import createPassword -from like import likedByPerson -from like import noOfLikes -from bookmarks import bookmarkedByPerson -from announce import announcedByPerson -from blocking import isBlocked -from blocking import isBlockedHashtag -from content import htmlReplaceEmailQuote -from content import htmlReplaceQuoteMarks -from content import removeTextFormatting -from content import switchWords -from content import getMentionsFromHtml -from content import addHtmlTags -from content import replaceEmojiFromTags -from content import removeLongWords -from skills import getSkills -from cache import getPersonFromCache -from cache import storePersonInCache -from shares import getValidSharedItemID -from happening import todaysEventsCheck -from happening import thisWeeksEventsCheck -from happening import getCalendarEvents -from happening import getTodaysEvents -from git import isGitPatch -from theme import getThemesList -from petnames import getPetName -from followingCalendar import receivingCalendarEvents -from devices import E2EEdecryptMessageFromDevice - - -def getAltPath(actor: str, domainFull: str, callingDomain: str) -> str: - """Returns alternate path from the actor - eg. https://clearnetdomain/path becomes http://oniondomain/path - """ - postActor = actor - if callingDomain not in actor and domainFull in actor: - if callingDomain.endswith('.onion') or \ - callingDomain.endswith('.i2p'): - postActor = \ - 'http://' + callingDomain + actor.split(domainFull)[1] - print('Changed POST domain from ' + actor + ' to ' + postActor) - return postActor - - -def getContentWarningButton(postID: str, translate: {}, - content: str) -> str: - """Returns the markup for a content warning button - """ - return '
' + \ - translate['SHOW MORE'] + '' + \ - '
' + content + \ - '
\n' - - -def getBlogAddress(actorJson: {}) -> str: - """Returns blog address for the given actor - """ - if not actorJson.get('attachment'): - return '' - for propertyValue in actorJson['attachment']: - if not propertyValue.get('name'): - continue - if not propertyValue['name'].lower().startswith('blog'): - continue - if not propertyValue.get('type'): - continue - if not propertyValue.get('value'): - continue - if propertyValue['type'] != 'PropertyValue': - continue - propertyValue['value'] = propertyValue['value'].strip() - prefixes = getProtocolPrefixes() - prefixFound = False - for prefix in prefixes: - if propertyValue['value'].startswith(prefix): - prefixFound = True - break - if not prefixFound: - continue - if '.' not in propertyValue['value']: - continue - if ' ' in propertyValue['value']: - continue - if ',' in propertyValue['value']: - continue - return propertyValue['value'] - return '' - - -def setBlogAddress(actorJson: {}, blogAddress: str) -> None: - """Sets an blog address for the given actor - """ - if not actorJson.get('attachment'): - actorJson['attachment'] = [] - - # remove any existing value - propertyFound = None - for propertyValue in actorJson['attachment']: - if not propertyValue.get('name'): - continue - if not propertyValue.get('type'): - continue - if not propertyValue['name'].lower().startswith('blog'): - continue - propertyFound = propertyValue - break - if propertyFound: - actorJson['attachment'].remove(propertyFound) - - prefixes = getProtocolPrefixes() - prefixFound = False - for prefix in prefixes: - if blogAddress.startswith(prefix): - prefixFound = True - break - if not prefixFound: - return - if '.' not in blogAddress: - return - if ' ' in blogAddress: - return - if ',' in blogAddress: - return - - for propertyValue in actorJson['attachment']: - if not propertyValue.get('name'): - continue - if not propertyValue.get('type'): - continue - if not propertyValue['name'].lower().startswith('blog'): - continue - if propertyValue['type'] != 'PropertyValue': - continue - propertyValue['value'] = blogAddress - return - - newBlogAddress = { - "name": "Blog", - "type": "PropertyValue", - "value": blogAddress - } - actorJson['attachment'].append(newBlogAddress) - - -def updateAvatarImageCache(session, baseDir: str, httpPrefix: str, - actor: str, avatarUrl: str, - personCache: {}, allowDownloads: bool, - force=False) -> str: - """Updates the cached avatar for the given actor - """ - if not avatarUrl: - return None - actorStr = actor.replace('/', '-') - avatarImagePath = baseDir + '/cache/avatars/' + actorStr - if avatarUrl.endswith('.png') or \ - '.png?' in avatarUrl: - sessionHeaders = { - 'Accept': 'image/png' - } - avatarImageFilename = avatarImagePath + '.png' - elif (avatarUrl.endswith('.jpg') or - avatarUrl.endswith('.jpeg') or - '.jpg?' in avatarUrl or - '.jpeg?' in avatarUrl): - sessionHeaders = { - 'Accept': 'image/jpeg' - } - avatarImageFilename = avatarImagePath + '.jpg' - elif avatarUrl.endswith('.gif') or '.gif?' in avatarUrl: - sessionHeaders = { - 'Accept': 'image/gif' - } - avatarImageFilename = avatarImagePath + '.gif' - elif avatarUrl.endswith('.webp') or '.webp?' in avatarUrl: - sessionHeaders = { - 'Accept': 'image/webp' - } - avatarImageFilename = avatarImagePath + '.webp' - elif avatarUrl.endswith('.avif') or '.avif?' in avatarUrl: - sessionHeaders = { - 'Accept': 'image/avif' - } - avatarImageFilename = avatarImagePath + '.avif' - else: - return None - - if (not os.path.isfile(avatarImageFilename) or force) and allowDownloads: - try: - print('avatar image url: ' + avatarUrl) - result = session.get(avatarUrl, - headers=sessionHeaders, - params=None) - if result.status_code < 200 or \ - result.status_code > 202: - print('Avatar image download failed with status ' + - str(result.status_code)) - # remove partial download - if os.path.isfile(avatarImageFilename): - os.remove(avatarImageFilename) - else: - with open(avatarImageFilename, 'wb') as f: - f.write(result.content) - print('avatar image downloaded for ' + actor) - return avatarImageFilename.replace(baseDir + '/cache', '') - except Exception as e: - print('Failed to download avatar image: ' + str(avatarUrl)) - print(e) - prof = 'https://www.w3.org/ns/activitystreams' - if '/channel/' not in actor or '/accounts/' not in actor: - sessionHeaders = { - 'Accept': 'application/activity+json; profile="' + prof + '"' - } - else: - sessionHeaders = { - 'Accept': 'application/ld+json; profile="' + prof + '"' - } - personJson = \ - getJson(session, actor, sessionHeaders, None, __version__, - httpPrefix, None) - if personJson: - if not personJson.get('id'): - return None - if not personJson.get('publicKey'): - return None - if not personJson['publicKey'].get('publicKeyPem'): - return None - if personJson['id'] != actor: - return None - if not personCache.get(actor): - return None - if personCache[actor]['actor']['publicKey']['publicKeyPem'] != \ - personJson['publicKey']['publicKeyPem']: - print("ERROR: " + - "public keys don't match when downloading actor for " + - actor) - return None - storePersonInCache(baseDir, actor, personJson, personCache, - allowDownloads) - return getPersonAvatarUrl(baseDir, actor, personCache, - allowDownloads) - return None - return avatarImageFilename.replace(baseDir + '/cache', '') - - -def getPersonAvatarUrl(baseDir: str, personUrl: str, personCache: {}, - allowDownloads: bool) -> str: - """Returns the avatar url for the person - """ - personJson = \ - getPersonFromCache(baseDir, personUrl, personCache, allowDownloads) - if not personJson: - return None - - # get from locally stored image - actorStr = personJson['id'].replace('/', '-') - avatarImagePath = baseDir + '/cache/avatars/' + actorStr - - imageExtension = ('png', 'jpg', 'jpeg', 'gif', 'webp', 'avif') - for ext in imageExtension: - if os.path.isfile(avatarImagePath + '.' + ext): - return '/avatars/' + actorStr + '.' + ext - elif os.path.isfile(avatarImagePath.lower() + '.' + ext): - return '/avatars/' + actorStr.lower() + '.' + ext - - if personJson.get('icon'): - if personJson['icon'].get('url'): - return personJson['icon']['url'] - return None - - -def htmlFollowingList(cssCache: {}, baseDir: str, - followingFilename: str) -> str: - """Returns a list of handles being followed - """ - with open(followingFilename, 'r') as followingFile: - msg = followingFile.read() - followingList = msg.split('\n') - followingList.sort() - if followingList: - cssFilename = baseDir + '/epicyon-profile.css' - if os.path.isfile(baseDir + '/epicyon.css'): - cssFilename = baseDir + '/epicyon.css' - - profileCSS = getCSS(baseDir, cssFilename, cssCache) - if profileCSS: - followingListHtml = htmlHeader(cssFilename, profileCSS) - for followingAddress in followingList: - if followingAddress: - followingListHtml += \ - '

@' + followingAddress + '

' - followingListHtml += htmlFooter() - msg = followingListHtml - return msg - return '' - - -def htmlFollowingDataList(baseDir: str, nickname: str, - domain: str, domainFull: str) -> str: - """Returns a datalist of handles being followed - """ - listStr = '\n' - followingFilename = \ - baseDir + '/accounts/' + nickname + '@' + domain + '/following.txt' - if os.path.isfile(followingFilename): - with open(followingFilename, 'r') as followingFile: - msg = followingFile.read() - # add your own handle, so that you can send DMs - # to yourself as reminders - msg += nickname + '@' + domainFull + '\n' - # include petnames - petnamesFilename = \ - baseDir + '/accounts/' + \ - nickname + '@' + domain + '/petnames.txt' - if os.path.isfile(petnamesFilename): - followingList = [] - with open(petnamesFilename, 'r') as petnamesFile: - petStr = petnamesFile.read() - # extract each petname and append it - petnamesList = petStr.split('\n') - for pet in petnamesList: - followingList.append(pet.split(' ')[0]) - # add the following.txt entries - followingList += msg.split('\n') - else: - # no petnames list exists - just use following.txt - followingList = msg.split('\n') - followingList.sort() - if followingList: - for followingAddress in followingList: - if followingAddress: - listStr += \ - '\n' - listStr += '\n' - return listStr - - -def htmlSearchEmoji(cssCache: {}, translate: {}, - baseDir: str, httpPrefix: str, - searchStr: str) -> str: - """Search results for emoji - """ - # emoji.json is generated so that it can be customized and the changes - # will be retained even if default_emoji.json is subsequently updated - if not os.path.isfile(baseDir + '/emoji/emoji.json'): - copyfile(baseDir + '/emoji/default_emoji.json', - baseDir + '/emoji/emoji.json') - - searchStr = searchStr.lower().replace(':', '').strip('\n').strip('\r') - cssFilename = baseDir + '/epicyon-profile.css' - if os.path.isfile(baseDir + '/epicyon.css'): - cssFilename = baseDir + '/epicyon.css' - - emojiCSS = getCSS(baseDir, cssFilename, cssCache) - if emojiCSS: - if httpPrefix != 'https': - emojiCSS = emojiCSS.replace('https://', - httpPrefix + '://') - emojiLookupFilename = baseDir + '/emoji/emoji.json' - - # create header - emojiForm = htmlHeader(cssFilename, emojiCSS) - emojiForm += '

' + \ - translate['Emoji Search'] + \ - '

' - - # does the lookup file exist? - if not os.path.isfile(emojiLookupFilename): - emojiForm += '
' + \ - translate['No results'] + '
' - emojiForm += htmlFooter() - return emojiForm - - emojiJson = loadJson(emojiLookupFilename) - if emojiJson: - results = {} - for emojiName, filename in emojiJson.items(): - if searchStr in emojiName: - results[emojiName] = filename + '.png' - for emojiName, filename in emojiJson.items(): - if emojiName in searchStr: - results[emojiName] = filename + '.png' - headingShown = False - emojiForm += '
' - msgStr1 = translate['Copy the text then paste it into your post'] - msgStr2 = ':' - emojiForm += '
' - - emojiForm += htmlFooter() - return emojiForm - - -def getIconsDir(baseDir: str) -> str: - """Returns the directory where icons exist - """ - iconsDir = 'icons' - theme = getConfigParam(baseDir, 'theme') - if theme: - if os.path.isdir(baseDir + '/img/icons/' + theme): - iconsDir = 'icons/' + theme - return iconsDir - - -def htmlSearchSharedItems(cssCache: {}, translate: {}, - baseDir: str, searchStr: str, - pageNumber: int, - resultsPerPage: int, - httpPrefix: str, - domainFull: str, actor: str, - callingDomain: str) -> str: - """Search results for shared items - """ - iconsDir = getIconsDir(baseDir) - currPage = 1 - ctr = 0 - sharedItemsForm = '' - searchStrLower = urllib.parse.unquote(searchStr) - searchStrLower = searchStrLower.lower().strip('\n').strip('\r') - searchStrLowerList = searchStrLower.split('+') - cssFilename = baseDir + '/epicyon-profile.css' - if os.path.isfile(baseDir + '/epicyon.css'): - cssFilename = baseDir + '/epicyon.css' - - sharedItemsCSS = getCSS(baseDir, cssFilename, cssCache) - if sharedItemsCSS: - if httpPrefix != 'https': - sharedItemsCSS = \ - sharedItemsCSS.replace('https://', - httpPrefix + '://') - sharedItemsForm = htmlHeader(cssFilename, sharedItemsCSS) - sharedItemsForm += \ - '

' + translate['Shared Items Search'] + \ - '

' - resultsExist = False - for subdir, dirs, files in os.walk(baseDir + '/accounts'): - for handle in dirs: - if '@' not in handle: - continue - contactNickname = handle.split('@')[0] - sharesFilename = baseDir + '/accounts/' + handle + \ - '/shares.json' - if not os.path.isfile(sharesFilename): - continue - - sharesJson = loadJson(sharesFilename) - if not sharesJson: - continue - - for name, sharedItem in sharesJson.items(): - matched = True - for searchSubstr in searchStrLowerList: - subStrMatched = False - searchSubstr = searchSubstr.strip() - if searchSubstr in sharedItem['location'].lower(): - subStrMatched = True - elif searchSubstr in sharedItem['summary'].lower(): - subStrMatched = True - elif searchSubstr in sharedItem['displayName'].lower(): - subStrMatched = True - elif searchSubstr in sharedItem['category'].lower(): - subStrMatched = True - if not subStrMatched: - matched = False - break - if matched: - if currPage == pageNumber: - sharedItemsForm += '
\n' - sharedItemsForm += \ - '\n' - if sharedItem.get('imageUrl'): - sharedItemsForm += \ - '\n' - sharedItemsForm += \ - 'Item image\n' - sharedItemsForm += \ - '

' + sharedItem['summary'] + '

\n' - sharedItemsForm += \ - '

' + translate['Type'] + \ - ': ' + sharedItem['itemType'] + ' ' - sharedItemsForm += \ - '' + translate['Category'] + \ - ': ' + sharedItem['category'] + ' ' - sharedItemsForm += \ - '' + translate['Location'] + \ - ': ' + sharedItem['location'] + '

\n' - contactActor = \ - httpPrefix + '://' + domainFull + \ - '/users/' + contactNickname - sharedItemsForm += \ - '

\n' - if actor.endswith('/users/' + contactNickname): - sharedItemsForm += \ - ' \n' - sharedItemsForm += '

\n' - if not resultsExist and currPage > 1: - postActor = \ - getAltPath(actor, domainFull, - callingDomain) - # previous page link, needs to be a POST - sharedItemsForm += \ - '
\n' - sharedItemsForm += \ - ' \n' - sharedItemsForm += \ - '
\n' - sharedItemsForm += \ - '
\n' + \ - ' \n' - sharedItemsForm += \ - ' ' + translate['Page up'] + \
-                                    '\n' - sharedItemsForm += '
\n' - sharedItemsForm += '
\n' - resultsExist = True - ctr += 1 - if ctr >= resultsPerPage: - currPage += 1 - if currPage > pageNumber: - postActor = \ - getAltPath(actor, domainFull, - callingDomain) - # next page link, needs to be a POST - sharedItemsForm += \ - '
\n' - sharedItemsForm += \ - ' \n' - sharedItemsForm += \ - '
\n' - sharedItemsForm += \ - '
\n' + \ - ' \n' - sharedItemsForm += \ - ' ' + translate['Page down'] + \
-                                    '\n' - sharedItemsForm += '
\n' - sharedItemsForm += '
\n' - break - ctr = 0 - if not resultsExist: - sharedItemsForm += \ - '
' + translate['No results'] + '
\n' - sharedItemsForm += htmlFooter() - return sharedItemsForm - - -def htmlModerationInfo(cssCache: {}, translate: {}, - baseDir: str, httpPrefix: str) -> str: - msgStr1 = \ - 'These are globally blocked for all accounts on this instance' - msgStr2 = \ - 'Any blocks or suspensions made by moderators will be shown here.' - infoForm = '' - cssFilename = baseDir + '/epicyon-profile.css' - if os.path.isfile(baseDir + '/epicyon.css'): - cssFilename = baseDir + '/epicyon.css' - - infoCSS = getCSS(baseDir, cssFilename, cssCache) - if infoCSS: - if httpPrefix != 'https': - infoCSS = infoCSS.replace('https://', - httpPrefix + '://') - infoForm = htmlHeader(cssFilename, infoCSS) - - infoForm += \ - '

' + \ - translate['Moderation Information'] + \ - '

' - - infoShown = False - suspendedFilename = baseDir + '/accounts/suspended.txt' - if os.path.isfile(suspendedFilename): - with open(suspendedFilename, "r") as f: - suspendedStr = f.read() - infoForm += '
' - infoForm += '
' + \ - translate['Suspended accounts'] + '' - infoForm += '
' + \ - translate['These are currently suspended'] - infoForm += \ - ' ' - infoForm += '
' - infoShown = True - - blockingFilename = baseDir + '/accounts/blocking.txt' - if os.path.isfile(blockingFilename): - with open(blockingFilename, "r") as f: - blockedStr = f.read() - infoForm += '
' - infoForm += \ - '
' + \ - translate['Blocked accounts and hashtags'] + '' - infoForm += \ - '
' + \ - translate[msgStr1] - infoForm += \ - ' ' - infoForm += '
' - infoShown = True - if not infoShown: - infoForm += \ - '

' + \ - translate[msgStr2] + \ - '

' - infoForm += htmlFooter() - return infoForm - - -def htmlHashtagSearch(cssCache: {}, - nickname: str, domain: str, port: int, - recentPostsCache: {}, maxRecentPosts: int, - translate: {}, - baseDir: str, hashtag: str, pageNumber: int, - postsPerPage: int, - session, wfRequest: {}, personCache: {}, - httpPrefix: str, projectVersion: str, - YTReplacementDomain: str, - showPublishedDateOnly: bool) -> str: - """Show a page containing search results for a hashtag - """ - if hashtag.startswith('#'): - hashtag = hashtag[1:] - hashtag = urllib.parse.unquote(hashtag) - hashtagIndexFile = baseDir + '/tags/' + hashtag + '.txt' - if not os.path.isfile(hashtagIndexFile): - if hashtag != hashtag.lower(): - hashtag = hashtag.lower() - hashtagIndexFile = baseDir + '/tags/' + hashtag + '.txt' - if not os.path.isfile(hashtagIndexFile): - print('WARN: hashtag file not found ' + hashtagIndexFile) - return None - - iconsDir = getIconsDir(baseDir) - separatorStr = htmlPostSeparator(baseDir, None) - - # check that the directory for the nickname exists - if nickname: - if not os.path.isdir(baseDir + '/accounts/' + - nickname + '@' + domain): - nickname = None - - # read the index - with open(hashtagIndexFile, "r") as f: - lines = f.readlines() - - # read the css - cssFilename = baseDir + '/epicyon-profile.css' - if os.path.isfile(baseDir + '/epicyon.css'): - cssFilename = baseDir + '/epicyon.css' - - hashtagSearchCSS = getCSS(baseDir, cssFilename, cssCache) - if hashtagSearchCSS: - if httpPrefix != 'https': - hashtagSearchCSS = \ - hashtagSearchCSS.replace('https://', - httpPrefix + '://') - - # ensure that the page number is in bounds - if not pageNumber: - pageNumber = 1 - elif pageNumber < 1: - pageNumber = 1 - - # get the start end end within the index file - startIndex = int((pageNumber - 1) * postsPerPage) - endIndex = startIndex + postsPerPage - noOfLines = len(lines) - if endIndex >= noOfLines and noOfLines > 0: - endIndex = noOfLines - 1 - - # add the page title - hashtagSearchForm = htmlHeader(cssFilename, hashtagSearchCSS) - if nickname: - hashtagSearchForm += '
\n' + \ - '

#' + \ - hashtag + '

\n' + '
\n' - else: - hashtagSearchForm += '
\n' + \ - '

#' + hashtag + '

\n' + '
\n' - - # RSS link for hashtag feed - hashtagSearchForm += '
' - hashtagSearchForm += \ - 'RSS 2.0
' - - if startIndex > 0: - # previous page link - hashtagSearchForm += \ - '
\n' + \ - ' ' + translate['Page up'] + \
-            '\n
\n' - index = startIndex - while index <= endIndex: - postId = lines[index].strip('\n').strip('\r') - if ' ' not in postId: - nickname = getNicknameFromActor(postId) - if not nickname: - index += 1 - continue - else: - postFields = postId.split(' ') - if len(postFields) != 3: - index += 1 - continue - nickname = postFields[1] - postId = postFields[2] - postFilename = locatePost(baseDir, nickname, domain, postId) - if not postFilename: - index += 1 - continue - postJsonObject = loadJson(postFilename) - if postJsonObject: - if not isPublicPost(postJsonObject): - index += 1 - continue - showIndividualPostIcons = False - if nickname: - showIndividualPostIcons = True - allowDeletion = False - hashtagSearchForm += separatorStr + \ - individualPostAsHtml(True, recentPostsCache, - maxRecentPosts, - iconsDir, translate, None, - baseDir, session, wfRequest, - personCache, - nickname, domain, port, - postJsonObject, - None, True, allowDeletion, - httpPrefix, projectVersion, - 'search', - YTReplacementDomain, - showPublishedDateOnly, - showIndividualPostIcons, - showIndividualPostIcons, - False, False, False) - index += 1 - - if endIndex < noOfLines - 1: - # next page link - hashtagSearchForm += \ - '
\n' + \ - ' ' + translate['Page down'] + '' + \ - '
' - hashtagSearchForm += htmlFooter() - return hashtagSearchForm - - -def rss2TagHeader(hashtag: str, httpPrefix: str, domainFull: str) -> str: - rssStr = "" - rssStr += "" - rssStr += '' - rssStr += ' #' + hashtag + '' - rssStr += ' ' + httpPrefix + '://' + domainFull + \ - '/tags/rss2/' + hashtag + '' - return rssStr - - -def rss2TagFooter() -> str: - rssStr = '' - rssStr += '' - return rssStr - - -def rssHashtagSearch(nickname: str, domain: str, port: int, - recentPostsCache: {}, maxRecentPosts: int, - translate: {}, - baseDir: str, hashtag: str, - postsPerPage: int, - session, wfRequest: {}, personCache: {}, - httpPrefix: str, projectVersion: str, - YTReplacementDomain: str) -> str: - """Show an rss feed for a hashtag - """ - if hashtag.startswith('#'): - hashtag = hashtag[1:] - hashtag = urllib.parse.unquote(hashtag) - hashtagIndexFile = baseDir + '/tags/' + hashtag + '.txt' - if not os.path.isfile(hashtagIndexFile): - if hashtag != hashtag.lower(): - hashtag = hashtag.lower() - hashtagIndexFile = baseDir + '/tags/' + hashtag + '.txt' - if not os.path.isfile(hashtagIndexFile): - print('WARN: hashtag file not found ' + hashtagIndexFile) - return None - - # check that the directory for the nickname exists - if nickname: - if not os.path.isdir(baseDir + '/accounts/' + - nickname + '@' + domain): - nickname = None - - # read the index - lines = [] - with open(hashtagIndexFile, "r") as f: - lines = f.readlines() - if not lines: - return None - - domainFull = domain - if port: - if port != 80 and port != 443: - domainFull = domain + ':' + str(port) - - maxFeedLength = 10 - hashtagFeed = \ - rss2TagHeader(hashtag, httpPrefix, domainFull) - for index in range(len(lines)): - postId = lines[index].strip('\n').strip('\r') - if ' ' not in postId: - nickname = getNicknameFromActor(postId) - if not nickname: - index += 1 - if index >= maxFeedLength: - break - continue - else: - postFields = postId.split(' ') - if len(postFields) != 3: - index += 1 - if index >= maxFeedLength: - break - continue - nickname = postFields[1] - postId = postFields[2] - postFilename = locatePost(baseDir, nickname, domain, postId) - if not postFilename: - index += 1 - if index >= maxFeedLength: - break - continue - postJsonObject = loadJson(postFilename) - if postJsonObject: - if not isPublicPost(postJsonObject): - index += 1 - if index >= maxFeedLength: - break - continue - # add to feed - if postJsonObject['object'].get('content') and \ - postJsonObject['object'].get('attributedTo') and \ - postJsonObject['object'].get('published'): - published = postJsonObject['object']['published'] - pubDate = datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ") - rssDateStr = pubDate.strftime("%a, %d %b %Y %H:%M:%S UT") - hashtagFeed += ' ' - hashtagFeed += \ - ' ' + \ - postJsonObject['object']['attributedTo'] + \ - '' - if postJsonObject['object'].get('summary'): - hashtagFeed += \ - ' ' + \ - postJsonObject['object']['summary'] + \ - '' - hashtagFeed += \ - ' ' - hashtagFeed += \ - ' ' + rssDateStr + '' - if postJsonObject['object'].get('attachment'): - for attach in postJsonObject['object']['attachment']: - if not attach.get('url'): - continue - hashtagFeed += \ - ' ' + attach['url'] + '' - hashtagFeed += ' ' - index += 1 - if index >= maxFeedLength: - break - - return hashtagFeed + rss2TagFooter() - - -def htmlSkillsSearch(cssCache: {}, translate: {}, baseDir: str, - httpPrefix: str, - skillsearch: str, instanceOnly: bool, - postsPerPage: int) -> str: - """Show a page containing search results for a skill - """ - if skillsearch.startswith('*'): - skillsearch = skillsearch[1:].strip() - - skillsearch = skillsearch.lower().strip('\n').strip('\r') - - results = [] - # search instance accounts - for subdir, dirs, files in os.walk(baseDir + '/accounts/'): - for f in files: - if not f.endswith('.json'): - continue - if '@' not in f: - continue - if f.startswith('inbox@'): - continue - actorFilename = os.path.join(subdir, f) - actorJson = loadJson(actorFilename) - if actorJson: - if actorJson.get('id') and \ - actorJson.get('skills') and \ - actorJson.get('name') and \ - actorJson.get('icon'): - actor = actorJson['id'] - for skillName, skillLevel in actorJson['skills'].items(): - skillName = skillName.lower() - if not (skillName in skillsearch or - skillsearch in skillName): - continue - skillLevelStr = str(skillLevel) - if skillLevel < 100: - skillLevelStr = '0' + skillLevelStr - if skillLevel < 10: - skillLevelStr = '0' + skillLevelStr - indexStr = \ - skillLevelStr + ';' + actor + ';' + \ - actorJson['name'] + \ - ';' + actorJson['icon']['url'] - if indexStr not in results: - results.append(indexStr) - if not instanceOnly: - # search actor cache - for subdir, dirs, files in os.walk(baseDir + '/cache/actors/'): - for f in files: - if not f.endswith('.json'): - continue - if '@' not in f: - continue - if f.startswith('inbox@'): - continue - actorFilename = os.path.join(subdir, f) - cachedActorJson = loadJson(actorFilename) - if cachedActorJson: - if cachedActorJson.get('actor'): - actorJson = cachedActorJson['actor'] - if actorJson.get('id') and \ - actorJson.get('skills') and \ - actorJson.get('name') and \ - actorJson.get('icon'): - actor = actorJson['id'] - for skillName, skillLevel in \ - actorJson['skills'].items(): - skillName = skillName.lower() - if not (skillName in skillsearch or - skillsearch in skillName): - continue - skillLevelStr = str(skillLevel) - if skillLevel < 100: - skillLevelStr = '0' + skillLevelStr - if skillLevel < 10: - skillLevelStr = '0' + skillLevelStr - indexStr = \ - skillLevelStr + ';' + actor + ';' + \ - actorJson['name'] + \ - ';' + actorJson['icon']['url'] - if indexStr not in results: - results.append(indexStr) - - results.sort(reverse=True) - - cssFilename = baseDir + '/epicyon-profile.css' - if os.path.isfile(baseDir + '/epicyon.css'): - cssFilename = baseDir + '/epicyon.css' - - skillSearchCSS = getCSS(baseDir, cssFilename, cssCache) - if skillSearchCSS: - if httpPrefix != 'https': - skillSearchCSS = \ - skillSearchCSS.replace('https://', - httpPrefix + '://') - skillSearchForm = htmlHeader(cssFilename, skillSearchCSS) - skillSearchForm += \ - '

' + translate['Skills search'] + ': ' + \ - skillsearch + '

' - - if len(results) == 0: - skillSearchForm += \ - '
' + translate['No results'] + \ - '
' - else: - skillSearchForm += '
' - ctr = 0 - for skillMatch in results: - skillMatchFields = skillMatch.split(';') - if len(skillMatchFields) != 4: - continue - actor = skillMatchFields[1] - actorName = skillMatchFields[2] - avatarUrl = skillMatchFields[3] - skillSearchForm += \ - '' - ctr += 1 - if ctr >= postsPerPage: - break - skillSearchForm += '
' - skillSearchForm += htmlFooter() - return skillSearchForm - - -def htmlHistorySearch(cssCache: {}, translate: {}, baseDir: str, - httpPrefix: str, - nickname: str, domain: str, - historysearch: str, - postsPerPage: int, pageNumber: int, - projectVersion: str, - recentPostsCache: {}, - maxRecentPosts: int, - session, - wfRequest, - personCache: {}, - port: int, - YTReplacementDomain: str, - showPublishedDateOnly: bool) -> str: - """Show a page containing search results for your post history - """ - if historysearch.startswith('!'): - historysearch = historysearch[1:].strip() - - historysearch = historysearch.lower().strip('\n').strip('\r') - - boxFilenames = \ - searchBoxPosts(baseDir, nickname, domain, - historysearch, postsPerPage) - - cssFilename = baseDir + '/epicyon-profile.css' - if os.path.isfile(baseDir + '/epicyon.css'): - cssFilename = baseDir + '/epicyon.css' - - historySearchCSS = getCSS(baseDir, cssFilename, cssCache) - if historySearchCSS: - if httpPrefix != 'https': - historySearchCSS = \ - historySearchCSS.replace('https://', - httpPrefix + '://') - historySearchForm = htmlHeader(cssFilename, historySearchCSS) - - # add the page title - historySearchForm += \ - '

' + translate['Your Posts'] + '

' - - if len(boxFilenames) == 0: - historySearchForm += \ - '
' + translate['No results'] + \ - '
' - return historySearchForm - - iconsDir = getIconsDir(baseDir) - separatorStr = htmlPostSeparator(baseDir, None) - - # ensure that the page number is in bounds - if not pageNumber: - pageNumber = 1 - elif pageNumber < 1: - pageNumber = 1 - - # get the start end end within the index file - startIndex = int((pageNumber - 1) * postsPerPage) - endIndex = startIndex + postsPerPage - noOfBoxFilenames = len(boxFilenames) - if endIndex >= noOfBoxFilenames and noOfBoxFilenames > 0: - endIndex = noOfBoxFilenames - 1 - - index = startIndex - while index <= endIndex: - postFilename = boxFilenames[index] - if not postFilename: - index += 1 - continue - postJsonObject = loadJson(postFilename) - if not postJsonObject: - index += 1 - continue - showIndividualPostIcons = True - allowDeletion = False - historySearchForm += separatorStr + \ - individualPostAsHtml(True, recentPostsCache, - maxRecentPosts, - iconsDir, translate, None, - baseDir, session, wfRequest, - personCache, - nickname, domain, port, - postJsonObject, - None, True, allowDeletion, - httpPrefix, projectVersion, - 'search', - YTReplacementDomain, - showPublishedDateOnly, - showIndividualPostIcons, - showIndividualPostIcons, - False, False, False) - index += 1 - - historySearchForm += htmlFooter() - return historySearchForm - - -def scheduledPostsExist(baseDir: str, nickname: str, domain: str) -> bool: - """Returns true if there are posts scheduled to be delivered - """ - scheduleIndexFilename = \ - baseDir + '/accounts/' + nickname + '@' + domain + '/schedule.index' - if not os.path.isfile(scheduleIndexFilename): - return False - if '#users#' in open(scheduleIndexFilename).read(): - return True - return False - - -def htmlEditLinks(cssCache: {}, translate: {}, baseDir: str, path: str, - domain: str, port: int, httpPrefix: str, - defaultTimeline: str) -> str: - """Shows the edit links screen - """ - if '/users/' not in path: - return '' - path = path.replace('/inbox', '').replace('/outbox', '') - path = path.replace('/shares', '') - - nickname = getNicknameFromActor(path) - if not nickname: - return '' - - # is the user a moderator? - if not isEditor(baseDir, nickname): - return '' - - cssFilename = baseDir + '/epicyon-links.css' - if os.path.isfile(baseDir + '/links.css'): - cssFilename = baseDir + '/links.css' - - editCSS = getCSS(baseDir, cssFilename, cssCache) - if editCSS: - if httpPrefix != 'https': - editCSS = \ - editCSS.replace('https://', httpPrefix + '://') - - # filename of the banner shown at the top - bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) - - editLinksForm = htmlHeader(cssFilename, editCSS) - - # top banner - editLinksForm += \ - '\n' - editLinksForm += '\n' - - editLinksForm += \ - '
\n' - editLinksForm += \ - '
\n' - editLinksForm += \ - '

' + translate['Edit Links'] + '

' - editLinksForm += \ - '
\n' - # editLinksForm += \ - # ' \n' - editLinksForm += \ - '
\n' + \ - ' \n' + \ - '
\n' - editLinksForm += \ - '
\n' - - linksFilename = baseDir + '/accounts/links.txt' - linksStr = '' - if os.path.isfile(linksFilename): - with open(linksFilename, 'r') as fp: - linksStr = fp.read() - - editLinksForm += \ - '
' - editLinksForm += \ - ' ' + \ - translate['One link per line. Description followed by the link.'] + \ - '
' - editLinksForm += \ - ' ' - editLinksForm += \ - '
' - - editLinksForm += htmlFooter() - return editLinksForm - - -def htmlEditNewswire(cssCache: {}, translate: {}, baseDir: str, path: str, - domain: str, port: int, httpPrefix: str, - defaultTimeline: str) -> str: - """Shows the edit newswire screen - """ - if '/users/' not in path: - return '' - path = path.replace('/inbox', '').replace('/outbox', '') - path = path.replace('/shares', '') - - nickname = getNicknameFromActor(path) - if not nickname: - return '' - - # is the user a moderator? - if not isModerator(baseDir, nickname): - return '' - - cssFilename = baseDir + '/epicyon-links.css' - if os.path.isfile(baseDir + '/links.css'): - cssFilename = baseDir + '/links.css' - - editCSS = getCSS(baseDir, cssFilename, cssCache) - if editCSS: - if httpPrefix != 'https': - editCSS = \ - editCSS.replace('https://', httpPrefix + '://') - - # filename of the banner shown at the top - bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) - - editNewswireForm = htmlHeader(cssFilename, editCSS) - - # top banner - editNewswireForm += \ - '\n' - editNewswireForm += '\n' - - editNewswireForm += \ - '\n' - editNewswireForm += \ - '
\n' - editNewswireForm += \ - '

' + translate['Edit newswire'] + '

' - editNewswireForm += \ - '
\n' - # editNewswireForm += \ - # ' \n' - editNewswireForm += \ - '
\n' + \ - ' \n' + \ - '
\n' - editNewswireForm += \ - '
\n' - - newswireFilename = baseDir + '/accounts/newswire.txt' - newswireStr = '' - if os.path.isfile(newswireFilename): - with open(newswireFilename, 'r') as fp: - newswireStr = fp.read() - - editNewswireForm += \ - '
' - - editNewswireForm += \ - ' ' + \ - translate['Add RSS feed links below.'] + \ - '
' - editNewswireForm += \ - ' ' - - filterStr = '' - filterFilename = \ - baseDir + '/accounts/news@' + domain + '/filters.txt' - if os.path.isfile(filterFilename): - with open(filterFilename, 'r') as filterfile: - filterStr = filterfile.read() - - editNewswireForm += \ - '
\n' - editNewswireForm += '
' - editNewswireForm += ' \n' - - hashtagRulesStr = '' - hashtagRulesFilename = \ - baseDir + '/accounts/hashtagrules.txt' - if os.path.isfile(hashtagRulesFilename): - with open(hashtagRulesFilename, 'r') as rulesfile: - hashtagRulesStr = rulesfile.read() - - editNewswireForm += \ - '
\n' - editNewswireForm += '
\n' - editNewswireForm += \ - ' ' + translate['See instructions'] + '\n' - editNewswireForm += ' \n' - - editNewswireForm += \ - '
' - - editNewswireForm += htmlFooter() - return editNewswireForm - - -def htmlEditNewsPost(cssCache: {}, translate: {}, baseDir: str, path: str, - domain: str, port: int, - httpPrefix: str, postUrl: str) -> str: - """Edits a news post - """ - if '/users/' not in path: - return '' - pathOriginal = path - - nickname = getNicknameFromActor(path) - if not nickname: - return '' - - # is the user an editor? - if not isEditor(baseDir, nickname): - return '' - - postUrl = postUrl.replace('/', '#') - postFilename = locatePost(baseDir, nickname, domain, postUrl) - if not postFilename: - return '' - postJsonObject = loadJson(postFilename) - if not postJsonObject: - return '' - - cssFilename = baseDir + '/epicyon-links.css' - if os.path.isfile(baseDir + '/links.css'): - cssFilename = baseDir + '/links.css' - - editCSS = getCSS(baseDir, cssFilename, cssCache) - if editCSS: - if httpPrefix != 'https': - editCSS = \ - editCSS.replace('https://', httpPrefix + '://') - - editNewsPostForm = htmlHeader(cssFilename, editCSS) - editNewsPostForm += \ - '\n' - editNewsPostForm += \ - '
\n' - editNewsPostForm += \ - '

' + translate['Edit News Post'] + '

' - editNewsPostForm += \ - '
\n' - editNewsPostForm += \ - ' ' + \ - '\n' - editNewsPostForm += \ - ' \n' - editNewsPostForm += \ - '
\n' - - editNewsPostForm += \ - '
' - - editNewsPostForm += \ - ' \n' - - newsPostTitle = postJsonObject['object']['summary'] - editNewsPostForm += \ - '
\n' - - newsPostContent = postJsonObject['object']['content'] - editNewsPostForm += \ - ' ' - - editNewsPostForm += \ - '
' - - editNewsPostForm += htmlFooter() - return editNewsPostForm - - -def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str, - domain: str, port: int, httpPrefix: str, - defaultTimeline: str) -> str: - """Shows the edit profile screen - """ - imageFormats = '.png, .jpg, .jpeg, .gif, .webp, .avif' - path = path.replace('/inbox', '').replace('/outbox', '') - path = path.replace('/shares', '') - nickname = getNicknameFromActor(path) - if not nickname: - return '' - domainFull = domain - if port: - if port != 80 and port != 443: - if ':' not in domain: - domainFull = domain + ':' + str(port) - - actorFilename = \ - baseDir + '/accounts/' + nickname + '@' + domain + '.json' - if not os.path.isfile(actorFilename): - return '' - - # filename of the banner shown at the top - bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) - - isBot = '' - isGroup = '' - followDMs = '' - removeTwitter = '' - notifyLikes = '' - hideLikeButton = '' - mediaInstanceStr = '' - blogsInstanceStr = '' - newsInstanceStr = '' - displayNickname = nickname - bioStr = '' - donateUrl = '' - emailAddress = '' - PGPpubKey = '' - PGPfingerprint = '' - xmppAddress = '' - matrixAddress = '' - ssbAddress = '' - blogAddress = '' - toxAddress = '' - manuallyApprovesFollowers = '' - actorJson = loadJson(actorFilename) - if actorJson: - donateUrl = getDonationUrl(actorJson) - xmppAddress = getXmppAddress(actorJson) - matrixAddress = getMatrixAddress(actorJson) - ssbAddress = getSSBAddress(actorJson) - blogAddress = getBlogAddress(actorJson) - toxAddress = getToxAddress(actorJson) - emailAddress = getEmailAddress(actorJson) - PGPpubKey = getPGPpubKey(actorJson) - PGPfingerprint = getPGPfingerprint(actorJson) - if actorJson.get('name'): - displayNickname = actorJson['name'] - if actorJson.get('summary'): - bioStr = \ - actorJson['summary'].replace('

', '').replace('

', '') - if actorJson.get('manuallyApprovesFollowers'): - if actorJson['manuallyApprovesFollowers']: - manuallyApprovesFollowers = 'checked' - else: - manuallyApprovesFollowers = '' - if actorJson.get('type'): - if actorJson['type'] == 'Service': - isBot = 'checked' - isGroup = '' - elif actorJson['type'] == 'Group': - isGroup = 'checked' - isBot = '' - if os.path.isfile(baseDir + '/accounts/' + - nickname + '@' + domain + '/.followDMs'): - followDMs = 'checked' - if os.path.isfile(baseDir + '/accounts/' + - nickname + '@' + domain + '/.removeTwitter'): - removeTwitter = 'checked' - if os.path.isfile(baseDir + '/accounts/' + - nickname + '@' + domain + '/.notifyLikes'): - notifyLikes = 'checked' - if os.path.isfile(baseDir + '/accounts/' + - nickname + '@' + domain + '/.hideLikeButton'): - hideLikeButton = 'checked' - - mediaInstance = getConfigParam(baseDir, "mediaInstance") - if mediaInstance: - if mediaInstance is True: - mediaInstanceStr = 'checked' - blogsInstanceStr = '' - newsInstanceStr = '' - - newsInstance = getConfigParam(baseDir, "newsInstance") - if newsInstance: - if newsInstance is True: - newsInstanceStr = 'checked' - blogsInstanceStr = '' - mediaInstanceStr = '' - - blogsInstance = getConfigParam(baseDir, "blogsInstance") - if blogsInstance: - if blogsInstance is True: - blogsInstanceStr = 'checked' - mediaInstanceStr = '' - newsInstanceStr = '' - - filterStr = '' - filterFilename = \ - baseDir + '/accounts/' + nickname + '@' + domain + '/filters.txt' - if os.path.isfile(filterFilename): - with open(filterFilename, 'r') as filterfile: - filterStr = filterfile.read() - - switchStr = '' - switchFilename = \ - baseDir + '/accounts/' + \ - nickname + '@' + domain + '/replacewords.txt' - if os.path.isfile(switchFilename): - with open(switchFilename, 'r') as switchfile: - switchStr = switchfile.read() - - autoTags = '' - autoTagsFilename = \ - baseDir + '/accounts/' + \ - nickname + '@' + domain + '/autotags.txt' - if os.path.isfile(autoTagsFilename): - with open(autoTagsFilename, 'r') as autoTagsFile: - autoTags = autoTagsFile.read() - - autoCW = '' - autoCWFilename = \ - baseDir + '/accounts/' + \ - nickname + '@' + domain + '/autocw.txt' - if os.path.isfile(autoCWFilename): - with open(autoCWFilename, 'r') as autoCWFile: - autoCW = autoCWFile.read() - - blockedStr = '' - blockedFilename = \ - baseDir + '/accounts/' + \ - nickname + '@' + domain + '/blocking.txt' - if os.path.isfile(blockedFilename): - with open(blockedFilename, 'r') as blockedfile: - blockedStr = blockedfile.read() - - allowedInstancesStr = '' - allowedInstancesFilename = \ - baseDir + '/accounts/' + \ - nickname + '@' + domain + '/allowedinstances.txt' - if os.path.isfile(allowedInstancesFilename): - with open(allowedInstancesFilename, 'r') as allowedInstancesFile: - allowedInstancesStr = allowedInstancesFile.read() - - gitProjectsStr = '' - gitProjectsFilename = \ - baseDir + '/accounts/' + \ - nickname + '@' + domain + '/gitprojects.txt' - if os.path.isfile(gitProjectsFilename): - with open(gitProjectsFilename, 'r') as gitProjectsFile: - gitProjectsStr = gitProjectsFile.read() - - skills = getSkills(baseDir, nickname, domain) - skillsStr = '' - skillCtr = 1 - if skills: - for skillDesc, skillValue in skills.items(): - skillsStr += \ - '

' - skillsStr += \ - '

' - skillCtr += 1 - - skillsStr += \ - '

' - skillsStr += \ - '

' - - cssFilename = baseDir + '/epicyon-profile.css' - if os.path.isfile(baseDir + '/epicyon.css'): - cssFilename = baseDir + '/epicyon.css' - - editProfileCSS = getCSS(baseDir, cssFilename, cssCache) - if editProfileCSS: - if httpPrefix != 'https': - editProfileCSS = \ - editProfileCSS.replace('https://', httpPrefix + '://') - - moderatorsStr = '' - themesDropdown = '' - instanceStr = '' - - adminNickname = getConfigParam(baseDir, 'admin') - if adminNickname: - if path.startswith('/users/' + adminNickname + '/'): - instanceDescription = \ - getConfigParam(baseDir, 'instanceDescription') - instanceDescriptionShort = \ - getConfigParam(baseDir, 'instanceDescriptionShort') - instanceTitle = \ - getConfigParam(baseDir, 'instanceTitle') - instanceStr += '
' - instanceStr += \ - ' ' - if instanceTitle: - instanceStr += \ - '
' - else: - instanceStr += \ - '
' - instanceStr += \ - ' ' - if instanceDescriptionShort: - instanceStr += \ - '
' - else: - instanceStr += \ - '
' - instanceStr += \ - ' ' - if instanceDescription: - instanceStr += \ - ' ' - else: - instanceStr += \ - ' ' - instanceStr += \ - ' ' - instanceStr += \ - ' ' - instanceStr += '
' - - moderators = '' - moderatorsFile = baseDir + '/accounts/moderators.txt' - if os.path.isfile(moderatorsFile): - with open(moderatorsFile, "r") as f: - moderators = f.read() - moderatorsStr = '
' - moderatorsStr += ' ' + translate['Moderators'] + '
' - moderatorsStr += ' ' + \ - translate['A list of moderator nicknames. One per line.'] - moderatorsStr += \ - ' ' - moderatorsStr += '
' - - editors = '' - editorsFile = baseDir + '/accounts/editors.txt' - if os.path.isfile(editorsFile): - with open(editorsFile, "r") as f: - editors = f.read() - editorsStr = '
' - editorsStr += ' ' + translate['Site Editors'] + '
' - editorsStr += ' ' + \ - translate['A list of editor nicknames. One per line.'] - editorsStr += \ - ' ' - editorsStr += '
' - - themes = getThemesList() - themesDropdown = '
' - themesDropdown += ' ' + translate['Theme'] + '
' - grayscaleFilename = \ - baseDir + '/accounts/.grayscale' - grayscale = '' - if os.path.isfile(grayscaleFilename): - grayscale = 'checked' - themesDropdown += \ - ' ' + translate['Grayscale'] + '
' - themesDropdown += '
' - if os.path.isfile(baseDir + '/fonts/custom.woff') or \ - os.path.isfile(baseDir + '/fonts/custom.woff2') or \ - os.path.isfile(baseDir + '/fonts/custom.otf') or \ - os.path.isfile(baseDir + '/fonts/custom.ttf'): - themesDropdown += \ - ' ' + \ - translate['Remove the custom font'] + '
' - themesDropdown += '
' - themeName = getConfigParam(baseDir, 'theme') - themesDropdown = \ - themesDropdown.replace('\n' - editProfileForm += htmlFooter() - return editProfileForm - - -def htmlGetLoginCredentials(loginParams: str, - lastLoginTime: int) -> (str, str, bool): - """Receives login credentials via HTTPServer POST - """ - if not loginParams.startswith('username='): - return None, None, None - # minimum time between login attempts - currTime = int(time.time()) - if currTime < lastLoginTime+10: - return None, None, None - if '&' not in loginParams: - return None, None, None - loginArgs = loginParams.split('&') - nickname = None - password = None - register = False - for arg in loginArgs: - if '=' in arg: - if arg.split('=', 1)[0] == 'username': - nickname = arg.split('=', 1)[1] - elif arg.split('=', 1)[0] == 'password': - password = arg.split('=', 1)[1] - elif arg.split('=', 1)[0] == 'register': - register = True - return nickname, password, register - - -def htmlLogin(cssCache: {}, translate: {}, - baseDir: str, autocomplete=True) -> str: - """Shows the login screen - """ - accounts = noOfAccounts(baseDir) - - loginImage = 'login.png' - loginImageFilename = None - if os.path.isfile(baseDir + '/accounts/' + loginImage): - loginImageFilename = baseDir + '/accounts/' + loginImage - elif os.path.isfile(baseDir + '/accounts/login.jpg'): - loginImage = 'login.jpg' - loginImageFilename = baseDir + '/accounts/' + loginImage - elif os.path.isfile(baseDir + '/accounts/login.jpeg'): - loginImage = 'login.jpeg' - loginImageFilename = baseDir + '/accounts/' + loginImage - elif os.path.isfile(baseDir + '/accounts/login.gif'): - loginImage = 'login.gif' - loginImageFilename = baseDir + '/accounts/' + loginImage - elif os.path.isfile(baseDir + '/accounts/login.webp'): - loginImage = 'login.webp' - loginImageFilename = baseDir + '/accounts/' + loginImage - elif os.path.isfile(baseDir + '/accounts/login.avif'): - loginImage = 'login.avif' - loginImageFilename = baseDir + '/accounts/' + loginImage - - if not loginImageFilename: - loginImageFilename = baseDir + '/accounts/' + loginImage - copyfile(baseDir + '/img/login.png', loginImageFilename) - - if os.path.isfile(baseDir + '/accounts/login-background-custom.jpg'): - if not os.path.isfile(baseDir + '/accounts/login-background.jpg'): - copyfile(baseDir + '/accounts/login-background-custom.jpg', - baseDir + '/accounts/login-background.jpg') - - if accounts > 0: - loginText = \ - '' - else: - loginText = \ - '' - loginText += \ - '' - if os.path.isfile(baseDir + '/accounts/login.txt'): - # custom login message - with open(baseDir + '/accounts/login.txt', 'r') as file: - loginText = '' - - cssFilename = baseDir + '/epicyon-login.css' - if os.path.isfile(baseDir + '/login.css'): - cssFilename = baseDir + '/login.css' - - loginCSS = getCSS(baseDir, cssFilename, cssCache) - if not loginCSS: - print('ERROR: login css file missing ' + cssFilename) - return None - - # show the register button - registerButtonStr = '' - if getConfigParam(baseDir, 'registration') == 'open': - if int(getConfigParam(baseDir, 'registrationsRemaining')) > 0: - if accounts > 0: - idx = 'Welcome. Please login or register a new account.' - loginText = \ - '' - registerButtonStr = \ - '' - - TOSstr = \ - '' - TOSstr += \ - '' - - loginButtonStr = '' - if accounts > 0: - loginButtonStr = \ - '' - - autocompleteStr = '' - if not autocomplete: - autocompleteStr = 'autocomplete="off" value=""' - - loginForm = htmlHeader(cssFilename, loginCSS) - loginForm += '
\n' - loginForm += '
\n' - loginForm += '
\n' - loginForm += \ - ' login image\n' - loginForm += loginText + TOSstr + '\n' - loginForm += '
\n' - loginForm += '\n' - loginForm += '
\n' - loginForm += ' \n' - loginForm += \ - ' \n' - loginForm += '\n' - loginForm += ' \n' - loginForm += \ - ' \n' - loginForm += loginButtonStr + registerButtonStr + '\n' - loginForm += '
\n' - loginForm += '
\n' - loginForm += \ - '' + \ - '' + \
-        translate['Get the source code'] + '\n' - loginForm += htmlFooter() - return loginForm - - -def htmlTermsOfService(cssCache: {}, baseDir: str, - httpPrefix: str, domainFull: str) -> str: - """Show the terms of service screen - """ - adminNickname = getConfigParam(baseDir, 'admin') - if not os.path.isfile(baseDir + '/accounts/tos.txt'): - copyfile(baseDir + '/default_tos.txt', - baseDir + '/accounts/tos.txt') - - if os.path.isfile(baseDir + '/accounts/login-background-custom.jpg'): - if not os.path.isfile(baseDir + '/accounts/login-background.jpg'): - copyfile(baseDir + '/accounts/login-background-custom.jpg', - baseDir + '/accounts/login-background.jpg') - - TOSText = 'Terms of Service go here.' - if os.path.isfile(baseDir + '/accounts/tos.txt'): - with open(baseDir + '/accounts/tos.txt', 'r') as file: - TOSText = file.read() - - TOSForm = '' - cssFilename = baseDir + '/epicyon-profile.css' - if os.path.isfile(baseDir + '/epicyon.css'): - cssFilename = baseDir + '/epicyon.css' - - termsCSS = getCSS(baseDir, cssFilename, cssCache) - if termsCSS: - if httpPrefix != 'https': - termsCSS = termsCSS.replace('https://', httpPrefix+'://') - - TOSForm = htmlHeader(cssFilename, termsCSS) - TOSForm += '
' + TOSText + '
\n' - if adminNickname: - adminActor = httpPrefix + '://' + domainFull + \ - '/users/' + adminNickname - TOSForm += \ - '
\n' + \ - '

Administered by ' + adminNickname + '

\n' + \ - '
\n' - TOSForm += htmlFooter() - return TOSForm - - -def htmlAbout(cssCache: {}, baseDir: str, httpPrefix: str, - domainFull: str, onionDomain: str) -> str: - """Show the about screen - """ - adminNickname = getConfigParam(baseDir, 'admin') - if not os.path.isfile(baseDir + '/accounts/about.txt'): - copyfile(baseDir + '/default_about.txt', - baseDir + '/accounts/about.txt') - - if os.path.isfile(baseDir + '/accounts/login-background-custom.jpg'): - if not os.path.isfile(baseDir + '/accounts/login-background.jpg'): - copyfile(baseDir + '/accounts/login-background-custom.jpg', - baseDir + '/accounts/login-background.jpg') - - aboutText = 'Information about this instance goes here.' - if os.path.isfile(baseDir + '/accounts/about.txt'): - with open(baseDir + '/accounts/about.txt', 'r') as aboutFile: - aboutText = aboutFile.read() - - aboutForm = '' - cssFilename = baseDir + '/epicyon-profile.css' - if os.path.isfile(baseDir + '/epicyon.css'): - cssFilename = baseDir + '/epicyon.css' - - aboutCSS = getCSS(baseDir, cssFilename, cssCache) - if aboutCSS: - if httpPrefix != 'http': - aboutCSS = aboutCSS.replace('https://', - httpPrefix + '://') - - aboutForm = htmlHeader(cssFilename, aboutCSS) - aboutForm += '
' + aboutText + '
' - if onionDomain: - aboutForm += \ - '
\n' + \ - '

' + \ - 'http://' + onionDomain + '

\n
\n' - if adminNickname: - adminActor = '/users/' + adminNickname - aboutForm += \ - '
\n' + \ - '

Administered by ' + adminNickname + '

\n' + \ - '
\n' - aboutForm += htmlFooter() - return aboutForm - - -def htmlHashtagBlocked(cssCache: {}, baseDir: str, translate: {}) -> str: - """Show the screen for a blocked hashtag - """ - blockedHashtagForm = '' - cssFilename = baseDir + '/epicyon-suspended.css' - if os.path.isfile(baseDir + '/suspended.css'): - cssFilename = baseDir + '/suspended.css' - - blockedHashtagCSS = getCSS(baseDir, cssFilename, cssCache) - if blockedHashtagCSS: - blockedHashtagForm = htmlHeader(cssFilename, blockedHashtagCSS) - blockedHashtagForm += '
\n' - blockedHashtagForm += \ - '

' + \ - translate['Hashtag Blocked'] + '

\n' - blockedHashtagForm += \ - '

See ' + \ - translate['Terms of Service'] + '

\n' - blockedHashtagForm += '
\n' - blockedHashtagForm += htmlFooter() - return blockedHashtagForm - - -def htmlSuspended(cssCache: {}, baseDir: str) -> str: - """Show the screen for suspended accounts - """ - suspendedForm = '' - cssFilename = baseDir + '/epicyon-suspended.css' - if os.path.isfile(baseDir + '/suspended.css'): - cssFilename = baseDir + '/suspended.css' - - suspendedCSS = getCSS(baseDir, cssFilename, cssCache) - if suspendedCSS: - suspendedForm = htmlHeader(cssFilename, suspendedCSS) - suspendedForm += '
\n' - suspendedForm += '

Account Suspended

\n' - suspendedForm += '

See Terms of Service

\n' - suspendedForm += '
\n' - suspendedForm += htmlFooter() - return suspendedForm - - -def htmlNewPostDropDown(scopeIcon: str, scopeDescription: str, - replyStr: str, - translate: {}, - iconsDir: str, - showPublicOnDropdown: bool, - defaultTimeline: str, - pathBase: str, - dropdownNewPostSuffix: str, - dropdownNewBlogSuffix: str, - dropdownUnlistedSuffix: str, - dropdownFollowersSuffix: str, - dropdownDMSuffix: str, - dropdownReminderSuffix: str, - dropdownEventSuffix: str, - dropdownReportSuffix: str) -> str: - """Returns the html for a drop down list of new post types - """ - dropDownContent = '
\n' - dropDownContent += ' \n' - dropDownContent += ' \n' - dropDownContent += ' \n' - dropDownContent += '
\n' - return dropDownContent - - -def htmlNewPost(cssCache: {}, mediaInstance: bool, translate: {}, - baseDir: str, httpPrefix: str, - path: str, inReplyTo: str, - mentions: [], - reportUrl: str, pageNumber: int, - nickname: str, domain: str, - domainFull: str, - defaultTimeline: str, newswire: {}) -> str: - """New post screen - """ - iconsDir = getIconsDir(baseDir) - replyStr = '' - - showPublicOnDropdown = True - messageBoxHeight = 400 - - # filename of the banner shown at the top - bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) - - if not path.endswith('/newshare'): - if not path.endswith('/newreport'): - if not inReplyTo or path.endswith('/newreminder'): - newPostText = '

' + \ - translate['Write your post text below.'] + '

\n' - else: - newPostText = \ - '

' + \ - translate['Write your reply to'] + \ - ' ' + \ - translate['this post'] + '

\n' - replyStr = '\n' - - # if replying to a non-public post then also make - # this post non-public - if not isPublicPostFromUrl(baseDir, nickname, domain, - inReplyTo): - newPostPath = path - if '?' in newPostPath: - newPostPath = newPostPath.split('?')[0] - if newPostPath.endswith('/newpost'): - path = path.replace('/newpost', '/newfollowers') - elif newPostPath.endswith('/newunlisted'): - path = path.replace('/newunlisted', '/newfollowers') - showPublicOnDropdown = False - else: - newPostText = \ - '

' + \ - translate['Write your report below.'] + '

\n' - - # custom report header with any additional instructions - if os.path.isfile(baseDir + '/accounts/report.txt'): - with open(baseDir + '/accounts/report.txt', 'r') as file: - customReportText = file.read() - if '

' not in customReportText: - customReportText = \ - '\n' - repStr = '

', repStr) - newPostText += customReportText - - idx = 'This message only goes to moderators, even if it ' + \ - 'mentions other fediverse addresses.' - newPostText += \ - '

' + translate[idx] + '

\n' + \ - '

' + translate['Also see'] + \ - ' ' + \ - translate['Terms of Service'] + '

\n' - else: - newPostText = \ - '

' + \ - translate['Enter the details for your shared item below.'] + \ - '

\n' - - if path.endswith('/newquestion'): - newPostText = \ - '

' + \ - translate['Enter the choices for your question below.'] + \ - '

\n' - - if os.path.isfile(baseDir + '/accounts/newpost.txt'): - with open(baseDir + '/accounts/newpost.txt', 'r') as file: - newPostText = \ - '

' + file.read() + '

\n' - - cssFilename = baseDir + '/epicyon-profile.css' - if os.path.isfile(baseDir + '/epicyon.css'): - cssFilename = baseDir + '/epicyon.css' - - newPostCSS = getCSS(baseDir, cssFilename, cssCache) - if newPostCSS: - if httpPrefix != 'https': - newPostCSS = newPostCSS.replace('https://', - httpPrefix + '://') - - if '?' in path: - path = path.split('?')[0] - pathBase = path.replace('/newreport', '').replace('/newpost', '') - pathBase = pathBase.replace('/newblog', '').replace('/newshare', '') - pathBase = pathBase.replace('/newunlisted', '') - pathBase = pathBase.replace('/newevent', '') - pathBase = pathBase.replace('/newreminder', '') - pathBase = pathBase.replace('/newfollowers', '').replace('/newdm', '') - - newPostImageSection = '
' - if not path.endswith('/newevent'): - newPostImageSection += \ - ' \n' - else: - newPostImageSection += \ - ' \n' - newPostImageSection += \ - ' \n' - - if path.endswith('/newevent'): - newPostImageSection += \ - ' \n' - newPostImageSection += \ - ' \n' - else: - newPostImageSection += \ - ' \n' - newPostImageSection += '
\n' - - scopeIcon = 'scope_public.png' - scopeDescription = translate['Public'] - placeholderSubject = \ - translate['Subject or Content Warning (optional)'] + '...' - placeholderMentions = '' - if inReplyTo: - # mentionsAndContent = getMentionsString(content) - placeholderMentions = \ - translate['Replying to'] + '...' - placeholderMessage = translate['Write something'] + '...' - extraFields = '' - endpoint = 'newpost' - if path.endswith('/newblog'): - placeholderSubject = translate['Title'] - scopeIcon = 'scope_blog.png' - if defaultTimeline != 'tlnews': - scopeDescription = translate['Blog'] - else: - scopeDescription = translate['Article'] - endpoint = 'newblog' - elif path.endswith('/newunlisted'): - scopeIcon = 'scope_unlisted.png' - scopeDescription = translate['Unlisted'] - endpoint = 'newunlisted' - elif path.endswith('/newfollowers'): - scopeIcon = 'scope_followers.png' - scopeDescription = translate['Followers'] - endpoint = 'newfollowers' - elif path.endswith('/newdm'): - scopeIcon = 'scope_dm.png' - scopeDescription = translate['DM'] - endpoint = 'newdm' - elif path.endswith('/newreminder'): - scopeIcon = 'scope_reminder.png' - scopeDescription = translate['Reminder'] - endpoint = 'newreminder' - elif path.endswith('/newevent'): - scopeIcon = 'scope_event.png' - scopeDescription = translate['Event'] - endpoint = 'newevent' - placeholderSubject = translate['Event name'] - placeholderMessage = translate['Describe the event'] + '...' - elif path.endswith('/newreport'): - scopeIcon = 'scope_report.png' - scopeDescription = translate['Report'] - endpoint = 'newreport' - elif path.endswith('/newquestion'): - scopeIcon = 'scope_question.png' - scopeDescription = translate['Question'] - placeholderMessage = translate['Enter your question'] + '...' - endpoint = 'newquestion' - extraFields = '
\n' - extraFields += '
\n' - for questionCtr in range(8): - extraFields += \ - '
\n' - extraFields += \ - '
\n' - extraFields += '
' - elif path.endswith('/newshare'): - scopeIcon = 'scope_share.png' - scopeDescription = translate['Shared Item'] - placeholderSubject = translate['Name of the shared item'] + '...' - placeholderMessage = \ - translate['Description of the item being shared'] + '...' - endpoint = 'newshare' - extraFields = '
\n' - extraFields += \ - ' \n' - extraFields += \ - ' \n' - extraFields += \ - '
\n' - extraFields += \ - ' \n' - extraFields += \ - '
\n' - extraFields += ' \n' - extraFields += '
\n' - extraFields += '
\n' - extraFields += \ - '\n' - extraFields += '\n' - extraFields += '
\n' - - citationsStr = '' - if endpoint == 'newblog': - citationsFilename = \ - baseDir + '/accounts/' + \ - nickname + '@' + domain + '/.citations.txt' - if os.path.isfile(citationsFilename): - citationsStr = '
\n' - citationsStr += '

\n' - citationsStr += '
    \n' - citationsSeparator = '#####' - with open(citationsFilename, "r") as f: - citations = f.readlines() - for line in citations: - if citationsSeparator not in line: - continue - sections = line.strip().split(citationsSeparator) - if len(sections) != 3: - continue - title = sections[1] - link = sections[2] - citationsStr += \ - '
  • ' + \ - title + '
  • ' - citationsStr += '
\n' - citationsStr += '
\n' - - dateAndLocation = '' - if endpoint != 'newshare' and \ - endpoint != 'newreport' and \ - endpoint != 'newquestion': - dateAndLocation = '
\n' - - if endpoint == 'newevent': - # event status - dateAndLocation += '
\n' - dateAndLocation += '\n' - dateAndLocation += '
\n' - dateAndLocation += '\n' - dateAndLocation += '
\n' - dateAndLocation += '\n' - dateAndLocation += '
\n' - dateAndLocation += '
\n' - dateAndLocation += '
\n' - # maximum attendees - dateAndLocation += '\n' - dateAndLocation += '\n' - dateAndLocation += '
\n' - dateAndLocation += '
\n' - # event joining options - dateAndLocation += '
\n' - dateAndLocation += '\n' - dateAndLocation += '
\n' - dateAndLocation += '\n' - dateAndLocation += '
\n' - dateAndLocation += '\n' - dateAndLocation += '\n' - dateAndLocation += '
\n' - dateAndLocation += '
\n' - # Event posts don't allow replies - they're just an announcement. - # They also have a few more checkboxes - dateAndLocation += \ - '

\n' - dateAndLocation += \ - '

' + \ - '

\n' - else: - dateAndLocation += \ - '

\n' - - if not inReplyTo and endpoint != 'newevent': - dateAndLocation += \ - '

\n' - - if endpoint != 'newevent': - dateAndLocation += \ - '

\n' - # select a date and time for this post - dateAndLocation += '\n' - dateAndLocation += '\n' - dateAndLocation += '

\n' - else: - dateAndLocation += '
\n' - dateAndLocation += '
\n' - dateAndLocation += \ - '

\n' - # select start time for the event - dateAndLocation += '\n' - dateAndLocation += '\n' - dateAndLocation += '

\n' - # select end time for the event - dateAndLocation += \ - '
\n' - dateAndLocation += '\n' - dateAndLocation += '\n' - dateAndLocation += '\n' - - if endpoint == 'newevent': - dateAndLocation += '
\n' - dateAndLocation += '
\n' - dateAndLocation += '
\n' - dateAndLocation += \ - ' \n' - dateAndLocation += '
\n' - dateAndLocation += '
\n' - dateAndLocation += '
\n' - dateAndLocation += '\n' - if endpoint == 'newevent': - dateAndLocation += '
\n' - dateAndLocation += '\n' - dateAndLocation += '
\n' - dateAndLocation += '\n' - dateAndLocation += '
\n' - - newPostForm = htmlHeader(cssFilename, newPostCSS) - - newPostForm += \ - '\n' - newPostForm += '\n' - - mentionsStr = '' - for m in mentions: - mentionNickname = getNicknameFromActor(m) - if not mentionNickname: - continue - mentionDomain, mentionPort = getDomainFromActor(m) - if not mentionDomain: - continue - if mentionPort: - mentionsHandle = \ - '@' + mentionNickname + '@' + \ - mentionDomain + ':' + str(mentionPort) - else: - mentionsHandle = '@' + mentionNickname + '@' + mentionDomain - if mentionsHandle not in mentionsStr: - mentionsStr += mentionsHandle + ' ' - - # build suffixes so that any replies or mentions are - # preserved when switching between scopes - dropdownNewPostSuffix = '/newpost' - dropdownNewBlogSuffix = '/newblog' - dropdownUnlistedSuffix = '/newunlisted' - dropdownFollowersSuffix = '/newfollowers' - dropdownDMSuffix = '/newdm' - dropdownEventSuffix = '/newevent' - dropdownReminderSuffix = '/newreminder' - dropdownReportSuffix = '/newreport' - if inReplyTo or mentions: - dropdownNewPostSuffix = '' - dropdownNewBlogSuffix = '' - dropdownUnlistedSuffix = '' - dropdownFollowersSuffix = '' - dropdownDMSuffix = '' - dropdownEventSuffix = '' - dropdownReminderSuffix = '' - dropdownReportSuffix = '' - if inReplyTo: - dropdownNewPostSuffix += '?replyto=' + inReplyTo - dropdownNewBlogSuffix += '?replyto=' + inReplyTo - dropdownUnlistedSuffix += '?replyto=' + inReplyTo - dropdownFollowersSuffix += '?replyfollowers=' + inReplyTo - dropdownDMSuffix += '?replydm=' + inReplyTo - for mentionedActor in mentions: - dropdownNewPostSuffix += '?mention=' + mentionedActor - dropdownNewBlogSuffix += '?mention=' + mentionedActor - dropdownUnlistedSuffix += '?mention=' + mentionedActor - dropdownFollowersSuffix += '?mention=' + mentionedActor - dropdownDMSuffix += '?mention=' + mentionedActor - dropdownReportSuffix += '?mention=' + mentionedActor - - dropDownContent = '' - if not reportUrl: - dropDownContent = \ - htmlNewPostDropDown(scopeIcon, scopeDescription, - replyStr, - translate, - iconsDir, - showPublicOnDropdown, - defaultTimeline, - pathBase, - dropdownNewPostSuffix, - dropdownNewBlogSuffix, - dropdownUnlistedSuffix, - dropdownFollowersSuffix, - dropdownDMSuffix, - dropdownReminderSuffix, - dropdownEventSuffix, - dropdownReportSuffix) - else: - mentionsStr = 'Re: ' + reportUrl + '\n\n' + mentionsStr - - newPostForm += \ - '
\n' - newPostForm += '
\n' - newPostForm += \ - ' \n' - newPostForm += '
\n' - newPostForm += ' \n' - newPostForm += '\n' - - newPostForm += \ - ' \n' - newPostForm += ' \n' - newPostForm += '
' + dropDownContent + '' + \
-        translate['Search for emoji'] + '
\n' - newPostForm += '
\n' - - newPostForm += '
\n' - - # newPostForm += \ - # ' \n' - - # for a new blog if newswire items exist then add a citations button - if newswire and path.endswith('/newblog'): - newPostForm += \ - ' \n' - - newPostForm += \ - ' \n' - - newPostForm += '
\n' - - newPostForm += replyStr - if mediaInstance and not replyStr: - newPostForm += newPostImageSection - - newPostForm += \ - '
' - newPostForm += ' ' - newPostForm += '' - - selectedStr = ' selected' - if inReplyTo or endpoint == 'newdm': - if inReplyTo: - newPostForm += \ - '
\n' - else: - newPostForm += \ - ' ' \ - ' 📄
\n' - newPostForm += \ - ' \n' - newPostForm += \ - htmlFollowingDataList(baseDir, nickname, domain, domainFull) - newPostForm += '' - selectedStr = '' - - newPostForm += \ - '
' - if mediaInstance: - messageBoxHeight = 200 - - if endpoint == 'newquestion': - messageBoxHeight = 100 - elif endpoint == 'newblog': - messageBoxHeight = 800 - - newPostForm += \ - ' \n' - newPostForm += extraFields + citationsStr + dateAndLocation - if not mediaInstance or replyStr: - newPostForm += newPostImageSection - newPostForm += '
\n' - newPostForm += '
\n' - - if not reportUrl: - newPostForm = \ - newPostForm.replace('', '') - - newPostForm += htmlFooter() - return newPostForm - - -def getFontFromCss(css: str) -> (str, str): - """Returns the font name and format - """ - if ' url(' not in css: - return None, None - fontName = css.split(" url(")[1].split(")")[0].replace("'", '') - fontFormat = css.split(" format('")[1].split("')")[0] - return fontName, fontFormat - - -def htmlHeader(cssFilename: str, css: str, lang='en') -> str: - htmlStr = '\n' - htmlStr += '\n' - htmlStr += ' \n' - htmlStr += ' \n' - fontName, fontFormat = getFontFromCss(css) - if fontName: - htmlStr += ' \n' - htmlStr += ' \n' - htmlStr += ' \n' - htmlStr += ' \n' - htmlStr += ' Epicyon\n' - htmlStr += ' \n' - htmlStr += ' \n' - return htmlStr - - -def htmlFooter() -> str: - htmlStr = ' \n' - htmlStr += '\n' - return htmlStr - - -def htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int, - translate: {}, - baseDir: str, httpPrefix: str, - authorized: bool, - nickname: str, domain: str, port: int, - session, wfRequest: {}, personCache: {}, - projectVersion: str, - YTReplacementDomain: str, - showPublishedDateOnly: bool) -> str: - """Shows posts on the profile screen - These should only be public posts - """ - iconsDir = getIconsDir(baseDir) - separatorStr = htmlPostSeparator(baseDir, None) - profileStr = '' - maxItems = 4 - ctr = 0 - currPage = 1 - while ctr < maxItems and currPage < 4: - outboxFeed = \ - personBoxJson({}, session, baseDir, domain, - port, - '/users/' + nickname + '/outbox?page=' + - str(currPage), - httpPrefix, - 10, 'outbox', - authorized, 0, False, 0) - if not outboxFeed: - break - if len(outboxFeed['orderedItems']) == 0: - break - for item in outboxFeed['orderedItems']: - if item['type'] == 'Create': - postStr = \ - individualPostAsHtml(True, recentPostsCache, - maxRecentPosts, - iconsDir, translate, None, - baseDir, session, wfRequest, - personCache, - nickname, domain, port, item, - None, True, False, - httpPrefix, projectVersion, 'inbox', - YTReplacementDomain, - showPublishedDateOnly, - False, False, False, True, False) - if postStr: - profileStr += separatorStr + postStr - ctr += 1 - if ctr >= maxItems: - break - currPage += 1 - return profileStr - - -def htmlProfileFollowing(translate: {}, baseDir: str, httpPrefix: str, - authorized: bool, - nickname: str, domain: str, port: int, - session, wfRequest: {}, personCache: {}, - followingJson: {}, projectVersion: str, - buttons: [], - feedName: str, actor: str, - pageNumber: int, - maxItemsPerPage: int) -> str: - """Shows following on the profile screen - """ - profileStr = '' - - iconsDir = getIconsDir(baseDir) - if authorized and pageNumber: - if authorized and pageNumber > 1: - # page up arrow - profileStr += \ - '
\n' + \ - ' ' + \
-                translate['Page up'] + '\n' + \ - '
\n' - - for item in followingJson['orderedItems']: - profileStr += \ - individualFollowAsHtml(translate, baseDir, session, - wfRequest, personCache, - domain, item, authorized, nickname, - httpPrefix, projectVersion, - buttons) - if authorized and maxItemsPerPage and pageNumber: - if len(followingJson['orderedItems']) >= maxItemsPerPage: - # page down arrow - profileStr += \ - '
\n' + \ - ' ' + \
-                translate['Page down'] + '\n' + \ - '
\n' - return profileStr - - -def htmlProfileRoles(translate: {}, nickname: str, domain: str, - rolesJson: {}) -> str: - """Shows roles on the profile screen - """ - profileStr = '' - for project, rolesList in rolesJson.items(): - profileStr += \ - '
\n

' + project + \ - '

\n
\n' - for role in rolesList: - profileStr += '

' + role + '

\n' - profileStr += '
\n' - if len(profileStr) == 0: - profileStr += \ - '

@' + nickname + '@' + domain + ' has no roles assigned

\n' - else: - profileStr = '
' + profileStr + '
\n' - return profileStr - - -def htmlProfileSkills(translate: {}, nickname: str, domain: str, - skillsJson: {}) -> str: - """Shows skills on the profile screen - """ - profileStr = '' - for skill, level in skillsJson.items(): - profileStr += \ - '
' + skill + \ - '
\n
\n' - if len(profileStr) > 0: - profileStr = '
' + \ - profileStr + '
\n' - return profileStr - - -def htmlIndividualShare(actor: str, item: {}, translate: {}, - showContact: bool, removeButton: bool) -> str: - """Returns an individual shared item as html - """ - profileStr = '
\n' - profileStr += '\n' - if item.get('imageUrl'): - profileStr += '\n' - profileStr += \ - '' + translate['Item image'] + '\n\n' - profileStr += '

' + item['summary'] + '

\n' - profileStr += \ - '

' + translate['Type'] + ': ' + item['itemType'] + ' ' - profileStr += \ - '' + translate['Category'] + ': ' + item['category'] + ' ' - profileStr += \ - '' + translate['Location'] + ': ' + item['location'] + '

\n' - if showContact: - contactActor = item['actor'] - profileStr += \ - '

\n' - if removeButton: - profileStr += \ - ' \n' - profileStr += '

\n' - return profileStr - - -def htmlProfileShares(actor: str, translate: {}, - nickname: str, domain: str, sharesJson: {}) -> str: - """Shows shares on the profile screen - """ - profileStr = '' - for item in sharesJson['orderedItems']: - profileStr += htmlIndividualShare(actor, item, translate, False, False) - if len(profileStr) > 0: - profileStr = '\n' - return profileStr - - -def sharesTimelineJson(actor: str, pageNumber: int, itemsPerPage: int, - baseDir: str, maxSharesPerAccount: int) -> ({}, bool): - """Get a page on the shared items timeline as json - maxSharesPerAccount helps to avoid one person dominating the timeline - by sharing a large number of things - """ - allSharesJson = {} - for subdir, dirs, files in os.walk(baseDir + '/accounts'): - for handle in dirs: - if '@' in handle: - accountDir = baseDir + '/accounts/' + handle - sharesFilename = accountDir + '/shares.json' - if os.path.isfile(sharesFilename): - sharesJson = loadJson(sharesFilename) - if not sharesJson: - continue - nickname = handle.split('@')[0] - # actor who owns this share - owner = actor.split('/users/')[0] + '/users/' + nickname - ctr = 0 - for itemID, item in sharesJson.items(): - # assign owner to the item - item['actor'] = owner - allSharesJson[str(item['published'])] = item - ctr += 1 - if ctr >= maxSharesPerAccount: - break - # sort the shared items in descending order of publication date - sharesJson = OrderedDict(sorted(allSharesJson.items(), reverse=True)) - lastPage = False - startIndex = itemsPerPage*pageNumber - maxIndex = len(sharesJson.items()) - if maxIndex < itemsPerPage: - lastPage = True - if startIndex >= maxIndex - itemsPerPage: - lastPage = True - startIndex = maxIndex - itemsPerPage - if startIndex < 0: - startIndex = 0 - ctr = 0 - resultJson = {} - for published, item in sharesJson.items(): - if ctr >= startIndex + itemsPerPage: - break - if ctr < startIndex: - ctr += 1 - continue - resultJson[published] = item - ctr += 1 - return resultJson, lastPage - - -def htmlSharesTimeline(translate: {}, pageNumber: int, itemsPerPage: int, - baseDir: str, actor: str, - nickname: str, domain: str, port: int, - maxSharesPerAccount: int, httpPrefix: str) -> str: - """Show shared items timeline as html - """ - sharesJson, lastPage = \ - sharesTimelineJson(actor, pageNumber, itemsPerPage, - baseDir, maxSharesPerAccount) - domainFull = domain - if port != 80 and port != 443: - if ':' not in domain: - domainFull = domain + ':' + str(port) - actor = httpPrefix + '://' + domainFull + '/users/' + nickname - timelineStr = '' - - if pageNumber > 1: - iconsDir = getIconsDir(baseDir) - timelineStr += \ - '
\n' + \ - ' ' + translate['Page up'] + '\n' + \ - '
\n' - - separatorStr = htmlPostSeparator(baseDir, None) - for published, item in sharesJson.items(): - showContactButton = False - if item['actor'] != actor: - showContactButton = True - showRemoveButton = False - if item['actor'] == actor: - showRemoveButton = True - timelineStr += separatorStr + \ - htmlIndividualShare(actor, item, translate, - showContactButton, showRemoveButton) - - if not lastPage: - iconsDir = getIconsDir(baseDir) - timelineStr += \ - '
\n' + \ - ' ' + translate['Page down'] + '\n' + \ - '
\n' - - return timelineStr - - -def htmlProfile(rssIconAtTop: bool, - cssCache: {}, iconsAsButtons: bool, - defaultTimeline: str, - recentPostsCache: {}, maxRecentPosts: int, - translate: {}, projectVersion: str, - baseDir: str, httpPrefix: str, authorized: bool, - profileJson: {}, selected: str, - session, wfRequest: {}, personCache: {}, - YTReplacementDomain: str, - showPublishedDateOnly: bool, - newswire: {}, extraJson=None, - pageNumber=None, maxItemsPerPage=None) -> str: - """Show the profile page as html - """ - nickname = profileJson['preferredUsername'] - if not nickname: - return "" - domain, port = getDomainFromActor(profileJson['id']) - if not domain: - return "" - displayName = \ - addEmojiToDisplayName(baseDir, httpPrefix, - nickname, domain, - profileJson['name'], True) - domainFull = domain - if port: - domainFull = domain + ':' + str(port) - profileDescription = \ - addEmojiToDisplayName(baseDir, httpPrefix, - nickname, domain, - profileJson['summary'], False) - postsButton = 'button' - followingButton = 'button' - followersButton = 'button' - rolesButton = 'button' - skillsButton = 'button' - sharesButton = 'button' - if selected == 'posts': - postsButton = 'buttonselected' - elif selected == 'following': - followingButton = 'buttonselected' - elif selected == 'followers': - followersButton = 'buttonselected' - elif selected == 'roles': - rolesButton = 'buttonselected' - elif selected == 'skills': - skillsButton = 'buttonselected' - elif selected == 'shares': - sharesButton = 'buttonselected' - loginButton = '' - - followApprovalsSection = '' - followApprovals = False - linkToTimelineStart = '' - linkToTimelineEnd = '' - editProfileStr = '' - logoutStr = '' - actor = profileJson['id'] - usersPath = '/users/' + actor.split('/users/')[1] - - donateSection = '' - donateUrl = getDonationUrl(profileJson) - PGPpubKey = getPGPpubKey(profileJson) - PGPfingerprint = getPGPfingerprint(profileJson) - emailAddress = getEmailAddress(profileJson) - xmppAddress = getXmppAddress(profileJson) - matrixAddress = getMatrixAddress(profileJson) - ssbAddress = getSSBAddress(profileJson) - toxAddress = getToxAddress(profileJson) - if donateUrl or xmppAddress or matrixAddress or \ - ssbAddress or toxAddress or PGPpubKey or \ - PGPfingerprint or emailAddress: - donateSection = '
\n' - donateSection += '
\n' - if donateUrl and not isSystemAccount(nickname): - donateSection += \ - '

\n' - if emailAddress: - donateSection += \ - '

' + translate['Email'] + ': ' + emailAddress + '

\n' - if xmppAddress: - donateSection += \ - '

' + translate['XMPP'] + ': '+xmppAddress + '

\n' - if matrixAddress: - donateSection += \ - '

' + translate['Matrix'] + ': ' + matrixAddress + '

\n' - if ssbAddress: - donateSection += \ - '

SSB:

\n' - if toxAddress: - donateSection += \ - '

Tox:

\n' - if PGPfingerprint: - donateSection += \ - '

PGP: ' + \ - PGPfingerprint.replace('\n', '
') + '

\n' - if PGPpubKey: - donateSection += \ - '

' + PGPpubKey.replace('\n', '
') + '

\n' - donateSection += '
\n' - donateSection += '
\n' - - iconsDir = getIconsDir(baseDir) - if not authorized: - loginButton = headerButtonsFrontScreen(translate, nickname, - 'features', authorized, - iconsAsButtons, iconsDir) - else: - editProfileStr = \ - '' + \ - '| ' + translate['Edit'] + '\n' - - logoutStr = \ - '' + \ - '| ' + translate['Logout'] + \
-            '\n' - - linkToTimelineStart = \ - '' - linkToTimelineStart += \ - '' - linkToTimelineEnd = '' - # are there any follow requests? - 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: - followApprovals = True - followersButton = 'buttonhighlighted' - if selected == 'followers': - followersButton = 'buttonselectedhighlighted' - break - if selected == 'followers': - if followApprovals: - with open(followRequestsFilename, 'r') as f: - for followerHandle in f: - if len(line) > 0: - if '://' in followerHandle: - followerActor = followerHandle - else: - followerActor = \ - httpPrefix + '://' + \ - followerHandle.split('@')[1] + \ - '/users/' + followerHandle.split('@')[0] - basePath = '/users/' + nickname - followApprovalsSection += '
' - followApprovalsSection += \ - '' - followApprovalsSection += \ - '' + \ - followerHandle + '' - followApprovalsSection += \ - '' - followApprovalsSection += \ - '

' - followApprovalsSection += \ - '' - followApprovalsSection += \ - '' - followApprovalsSection += '
' - - profileDescriptionShort = profileDescription - if '\n' in profileDescription: - if len(profileDescription.split('\n')) > 2: - profileDescriptionShort = '' - else: - if '
' in profileDescription: - if len(profileDescription.split('
')) > 2: - profileDescriptionShort = '' - profileDescription = profileDescription.replace('
', '\n') - # keep the profile description short - if len(profileDescriptionShort) > 256: - profileDescriptionShort = '' - # remove formatting from profile description used on title - avatarDescription = '' - if profileJson.get('summary'): - avatarDescription = profileJson['summary'].replace('
', '\n') - avatarDescription = avatarDescription.replace('

', '') - avatarDescription = avatarDescription.replace('

', '') - - # If this is the news account then show a different banner - if isSystemAccount(nickname): - bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) - profileHeaderStr = \ - '\n' - if loginButton: - profileHeaderStr += '
' + loginButton + '
\n' - - profileHeaderStr += '\n' - profileHeaderStr += ' \n' - profileHeaderStr += ' \n' - profileHeaderStr += ' \n' - profileHeaderStr += ' \n' - profileHeaderStr += ' \n' - profileHeaderStr += ' \n' - profileHeaderStr += ' \n' - profileHeaderStr += ' \n' - profileHeaderStr += ' \n' - profileFooterStr += ' \n' - profileFooterStr += ' \n' - profileFooterStr += ' \n' - profileFooterStr += '
\n' - iconsDir = getIconsDir(baseDir) - profileHeaderStr += \ - getLeftColumnContent(baseDir, 'news', domainFull, - httpPrefix, translate, - iconsDir, False, - False, None, rssIconAtTop, True) - profileHeaderStr += ' \n' - else: - profileHeaderStr = '
\n' - profileHeaderStr += '
\n' - profileHeaderStr += \ - ' ' + \
-            avatarDescription + '\n' - profileHeaderStr += '

' + displayName + '

\n' - iconsDir = getIconsDir(baseDir) - profileHeaderStr += \ - '

@' + nickname + '@' + domainFull + '
' - profileHeaderStr += \ - '' + \ - '

\n' - profileHeaderStr += '

' + profileDescriptionShort + '

\n' - profileHeaderStr += loginButton - profileHeaderStr += '
\n' - profileHeaderStr += '
\n' - - profileStr = \ - linkToTimelineStart + profileHeaderStr + \ - linkToTimelineEnd + donateSection - if not isSystemAccount(nickname): - profileStr += '
\n' - profileStr += '
' - profileStr += \ - ' ' - profileStr += \ - ' ' + \ - '' - profileStr += \ - ' ' + \ - '' - profileStr += \ - ' ' + \ - '' - profileStr += \ - ' ' + \ - '' - profileStr += \ - ' ' + \ - '' - profileStr += logoutStr + editProfileStr - profileStr += '
' - profileStr += '
' - - profileStr += followApprovalsSection - - cssFilename = baseDir + '/epicyon-profile.css' - if os.path.isfile(baseDir + '/epicyon.css'): - cssFilename = baseDir + '/epicyon.css' - - profileStyle = getCSS(baseDir, cssFilename, cssCache) - if profileStyle: - profileStyle = \ - profileStyle.replace('image.png', - profileJson['image']['url']) - if isSystemAccount(nickname): - bannerFile, bannerFilename = \ - getBannerFile(baseDir, nickname, domain) - profileStyle = \ - profileStyle.replace('banner.png', - '/users/' + nickname + '/' + bannerFile) - - licenseStr = \ - '' + \ - '' + \
-            translate['Get the source code'] + '' - - if selected == 'posts': - profileStr += \ - htmlProfilePosts(recentPostsCache, maxRecentPosts, - translate, - baseDir, httpPrefix, authorized, - nickname, domain, port, - session, wfRequest, personCache, - projectVersion, - YTReplacementDomain, - showPublishedDateOnly) + licenseStr - if selected == 'following': - profileStr += \ - htmlProfileFollowing(translate, baseDir, httpPrefix, - authorized, nickname, - domain, port, session, - wfRequest, personCache, extraJson, - projectVersion, ["unfollow"], selected, - usersPath, pageNumber, maxItemsPerPage) - if selected == 'followers': - profileStr += \ - htmlProfileFollowing(translate, baseDir, httpPrefix, - authorized, nickname, - domain, port, session, - wfRequest, personCache, extraJson, - projectVersion, ["block"], - selected, usersPath, pageNumber, - maxItemsPerPage) - if selected == 'roles': - profileStr += \ - htmlProfileRoles(translate, nickname, domainFull, extraJson) - if selected == 'skills': - profileStr += \ - htmlProfileSkills(translate, nickname, domainFull, extraJson) - if selected == 'shares': - profileStr += \ - htmlProfileShares(actor, translate, - nickname, domainFull, - extraJson) + licenseStr - - # Footer which is only used for system accounts - profileFooterStr = '' - if isSystemAccount(nickname): - profileFooterStr = '
\n' - iconsDir = getIconsDir(baseDir) - profileFooterStr += \ - getRightColumnContent(baseDir, 'news', domainFull, - httpPrefix, translate, - iconsDir, False, False, - newswire, False, - False, None, False, False, - False, True, authorized, True) - profileFooterStr += '
\n' - - profileStr = \ - htmlHeader(cssFilename, profileStyle) + \ - profileStr + profileFooterStr + htmlFooter() - return profileStr - - -def individualFollowAsHtml(translate: {}, - baseDir: str, session, wfRequest: {}, - personCache: {}, domain: str, - followUrl: str, - authorized: bool, - actorNickname: str, - httpPrefix: str, - projectVersion: str, - buttons=[]) -> str: - """An individual follow entry on the profile screen - """ - nickname = getNicknameFromActor(followUrl) - domain, port = getDomainFromActor(followUrl) - titleStr = '@' + nickname + '@' + domain - avatarUrl = getPersonAvatarUrl(baseDir, followUrl, personCache, True) - if not avatarUrl: - avatarUrl = followUrl + '/avatar.png' - if domain not in followUrl: - (inboxUrl, pubKeyId, pubKey, - fromPersonId, sharedInbox, - avatarUrl2, displayName) = getPersonBox(baseDir, session, wfRequest, - personCache, projectVersion, - httpPrefix, nickname, - domain, 'outbox') - if avatarUrl2: - avatarUrl = avatarUrl2 - if displayName: - titleStr = displayName + ' ' + titleStr - - buttonsStr = '' - if authorized: - for b in buttons: - if b == 'block': - buttonsStr += \ - '\n' - if b == 'unfollow': - buttonsStr += \ - '\n' - - resultStr = '
\n' - resultStr += \ - '\n' - resultStr += '

 ' - resultStr += titleStr + '' + buttonsStr + '

\n' - resultStr += '
\n' - return resultStr - - -def addEmbeddedAudio(translate: {}, content: str) -> str: - """Adds embedded audio for mp3/ogg - """ - if not ('.mp3' in content or '.ogg' in content): - return content - - if '
\n' - return tlStr - - -def htmlPostSeparator(baseDir: str, column: str) -> str: - """Returns the html for a timeline post separator image - """ - iconsDir = getIconsDir(baseDir) - filename = 'separator.png' - if column: - filename = 'separator_' + column + '.png' - separatorImageFilename = baseDir + '/img/' + iconsDir + '/' + filename - separatorStr = '' - if os.path.isfile(separatorImageFilename): - separatorStr = \ - '
' + \ - '' + \ - '
\n' - return separatorStr - - -def htmlTimeline(cssCache: {}, defaultTimeline: str, - recentPostsCache: {}, maxRecentPosts: int, - translate: {}, pageNumber: int, - itemsPerPage: int, session, baseDir: str, - wfRequest: {}, 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) -> str: - """Show the timeline as html - """ - 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) - - # directory where icons are found - # This changes depending upon theme - iconsDir = getIconsDir(baseDir) - - 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) - - # benchmark 1 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 1 = ' + str(timeDiff)) - - profileStyle = getCSS(baseDir, cssFilename, cssCache) - if not profileStyle: - print('ERROR: css file not found ' + cssFilename) - return None - - # replace any https within the css with whatever prefix is needed - if httpPrefix != 'https': - profileStyle = \ - profileStyle.replace('https://', - httpPrefix + '://') - - # 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) - - # benchmark 2 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 2 = ' + str(timeDiff)) - - # the appearance of buttons - highlighted or not - inboxButton = 'button' - blogsButton = '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 == '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 = domain - if port != 80 and port != 443: - if ':' not in domain: - fullDomain = domain + ':' + str(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 = \ - '' + \ - '' + \
-                        translate['Approve follow requests'] + \
-                        '\n' - break - - # benchmark 3 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 3 = ' + str(timeDiff)) - - # moderation / reports button - moderationButtonStr = '' - if moderator and not minimal: - moderationButtonStr = \ - '' - - # shares, bookmarks and events buttons - sharesButtonStr = '' - bookmarksButtonStr = '' - eventsButtonStr = '' - if not minimal: - sharesButtonStr = \ - '' - - bookmarksButtonStr = \ - '' - - eventsButtonStr = \ - '' - - tlStr = htmlHeader(cssFilename, profileStyle) - - # benchmark 4 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 4 = ' + str(timeDiff)) - - # if this is a news instance and we are viewing the news timeline - newsHeader = False - if defaultTimeline == 'tlnews' and boxName == 'tlnews': - newsHeader = True - - newPostButtonStr = '' - # start of headericons div - if not newsHeader: - if not iconsAsButtons: - newPostButtonStr += '
' - - # what screen to go to when a new post is created - if boxName == 'dm': - if not iconsAsButtons: - newPostButtonStr += \ - '| ' + translate['Create a new DM'] + \
-                '\n' - else: - newPostButtonStr += \ - '' + \ - '' - elif boxName == 'tlblogs' or boxName == 'tlnews': - if not iconsAsButtons: - newPostButtonStr += \ - '| ' + \
-                translate['Create a new post'] + \
-                '\n' - else: - newPostButtonStr += \ - '' + \ - '' - elif boxName == 'tlevents': - if not iconsAsButtons: - newPostButtonStr += \ - '| ' + \
-                translate['Create a new event'] + \
-                '\n' - else: - newPostButtonStr += \ - '' + \ - '' - else: - if not manuallyApproveFollowers: - if not iconsAsButtons: - newPostButtonStr += \ - '| ' + \
-                    translate['Create a new post'] + \
-                    '\n' - else: - newPostButtonStr += \ - '' + \ - '' - else: - if not iconsAsButtons: - newPostButtonStr += \ - '| ' + translate['Create a new post'] + \
-                    '\n' - else: - newPostButtonStr += \ - '' + \ - '' - - # This creates a link to the profile page when viewed - # in lynx, but should be invisible in a graphical web browser - tlStr += \ - '\n' - - # banner and row of buttons - tlStr += \ - '\n' - tlStr += '\n' - - if fullWidthTimelineButtonHeader: - tlStr += \ - headerButtonsTimeline(defaultTimeline, boxName, pageNumber, - translate, usersPath, mediaButton, - blogsButton, newsButton, inboxButton, - dmButton, newDM, repliesButton, - newReply, minimal, sentButton, - sharesButtonStr, bookmarksButtonStr, - eventsButtonStr, moderationButtonStr, - newPostButtonStr, baseDir, nickname, - domain, iconsDir, timelineStartTime, - newCalendarEvent, calendarPath, - calendarImage, followApprovals, - iconsAsButtons) - - # start the timeline - tlStr += '\n' - tlStr += ' \n' - tlStr += ' \n' - tlStr += ' \n' - tlStr += ' \n' - tlStr += ' \n' - tlStr += ' \n' - tlStr += ' \n' - - domainFull = domain - if port: - if port != 80 and port != 443: - domainFull = domain + ':' + str(port) - - # left column - leftColumnStr = \ - getLeftColumnContent(baseDir, nickname, domainFull, - httpPrefix, translate, iconsDir, - editor, False, None, rssIconAtTop, - True) - tlStr += ' \n' - # center column containing posts - tlStr += ' \n' - - # right column - rightColumnStr = getRightColumnContent(baseDir, nickname, domainFull, - httpPrefix, translate, iconsDir, - moderator, editor, - newswire, positiveVoting, - False, None, True, - showPublishAsIcon, - rssIconAtTop, publishButtonAtTop, - authorized, True) - tlStr += ' \n' - tlStr += ' \n' - - # benchmark 9 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 9 = ' + str(timeDiff)) - - tlStr += ' \n' - tlStr += '
' + \ - leftColumnStr + ' \n' - - if not fullWidthTimelineButtonHeader: - tlStr += \ - headerButtonsTimeline(defaultTimeline, boxName, pageNumber, - translate, usersPath, mediaButton, - blogsButton, newsButton, inboxButton, - dmButton, newDM, repliesButton, - newReply, minimal, sentButton, - sharesButtonStr, bookmarksButtonStr, - eventsButtonStr, moderationButtonStr, - newPostButtonStr, baseDir, nickname, - domain, iconsDir, timelineStartTime, - newCalendarEvent, calendarPath, - calendarImage, followApprovals, - iconsAsButtons) - - # second row of buttons for moderator actions - if moderator and boxName == 'moderation': - tlStr += \ - '
' - tlStr += '
\n' - idx = 'Nickname or URL. Block using *@domain or nickname@domain' - tlStr += \ - ' ' + translate[idx] + '
\n' - tlStr += '
\n' - tlStr += \ - ' \n' - tlStr += \ - ' \n' - tlStr += \ - ' \n' - tlStr += \ - ' \n' - tlStr += \ - ' \n' - tlStr += \ - ' \n' - tlStr += '
\n
\n' - - # benchmark 6 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 6 = ' + str(timeDiff)) - - if boxName == 'tlshares': - maxSharesPerAccount = itemsPerPage - return (tlStr + - htmlSharesTimeline(translate, pageNumber, itemsPerPage, - baseDir, actor, nickname, domain, port, - maxSharesPerAccount, httpPrefix) + - htmlFooter()) - - # benchmark 7 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 7 = ' + str(timeDiff)) - - # benchmark 8 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 8 = ' + str(timeDiff)) - - # page up arrow - if pageNumber > 1: - tlStr += \ - '
\n' + \ - ' ' + \
-            translate['Page up'] + '\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']: - timelinePostStartTime = time.time() - - 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(currTlStr, - boxName, - pageNumber) - # benchmark cache post - timeDiff = \ - int((time.time() - - timelinePostStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE POST CACHE TIMING ' + - boxName + ' = ' + str(timeDiff)) - - if not currTlStr: - # benchmark cache post - timeDiff = \ - int((time.time() - - timelinePostStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE POST DISK TIMING START ' + - boxName + ' = ' + str(timeDiff)) - - # read the post from disk - currTlStr = \ - individualPostAsHtml(False, recentPostsCache, - maxRecentPosts, - iconsDir, translate, pageNumber, - baseDir, session, wfRequest, - personCache, - nickname, domain, port, - item, None, True, - allowDeletion, - httpPrefix, projectVersion, - boxName, - YTReplacementDomain, - showPublishedDateOnly, - boxName != 'dm', - showIndividualPostIcons, - manuallyApproveFollowers, - False, True) - # benchmark cache post - timeDiff = \ - int((time.time() - - timelinePostStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE POST DISK TIMING ' + - boxName + ' = ' + str(timeDiff)) - - if currTlStr: - itemCtr += 1 - if separatorStr: - tlStr += separatorStr - tlStr += currTlStr - if boxName == 'tlmedia': - tlStr += '
\n' - - # page down arrow - if itemCtr > 2: - tlStr += \ - '
\n' + \ - ' ' + \
-            translate['Page down'] + '\n' + \ - '
\n' - - # end of column-center - tlStr += '
' + \ - rightColumnStr + '
\n' - tlStr += htmlFooter() - return tlStr - - -def htmlShares(cssCache: {}, defaultTimeline: str, - recentPostsCache: {}, maxRecentPosts: int, - translate: {}, pageNumber: int, itemsPerPage: int, - session, baseDir: str, wfRequest: {}, personCache: {}, - nickname: str, domain: str, port: int, - allowDeletion: bool, - httpPrefix: str, projectVersion: str, - YTReplacementDomain: str, - showPublishedDateOnly: bool, - newswire: {}, positiveVoting: bool, - showPublishAsIcon: bool, - fullWidthTimelineButtonHeader: bool, - iconsAsButtons: bool, - rssIconAtTop: bool, - publishButtonAtTop: bool, - authorized: bool) -> str: - """Show the shares timeline as html - """ - manuallyApproveFollowers = \ - followerApprovalActive(baseDir, nickname, domain) - - return htmlTimeline(cssCache, defaultTimeline, - recentPostsCache, maxRecentPosts, - translate, pageNumber, - itemsPerPage, session, baseDir, wfRequest, personCache, - nickname, domain, port, None, - 'tlshares', allowDeletion, - httpPrefix, projectVersion, manuallyApproveFollowers, - False, YTReplacementDomain, - showPublishedDateOnly, - newswire, False, False, - positiveVoting, showPublishAsIcon, - fullWidthTimelineButtonHeader, - iconsAsButtons, rssIconAtTop, publishButtonAtTop, - authorized) - - -def htmlInbox(cssCache: {}, defaultTimeline: str, - recentPostsCache: {}, maxRecentPosts: int, - translate: {}, pageNumber: int, itemsPerPage: int, - session, baseDir: str, wfRequest: {}, personCache: {}, - nickname: str, domain: str, port: int, inboxJson: {}, - allowDeletion: bool, - httpPrefix: str, projectVersion: str, - minimal: bool, YTReplacementDomain: str, - showPublishedDateOnly: bool, - newswire: {}, positiveVoting: bool, - showPublishAsIcon: bool, - fullWidthTimelineButtonHeader: bool, - iconsAsButtons: bool, - rssIconAtTop: bool, - publishButtonAtTop: bool, - authorized: bool) -> str: - """Show the inbox as html - """ - manuallyApproveFollowers = \ - followerApprovalActive(baseDir, nickname, domain) - - return htmlTimeline(cssCache, defaultTimeline, - recentPostsCache, maxRecentPosts, - translate, pageNumber, - itemsPerPage, session, baseDir, wfRequest, personCache, - nickname, domain, port, inboxJson, - 'inbox', allowDeletion, - httpPrefix, projectVersion, manuallyApproveFollowers, - minimal, YTReplacementDomain, - showPublishedDateOnly, - newswire, False, False, - positiveVoting, showPublishAsIcon, - fullWidthTimelineButtonHeader, - iconsAsButtons, rssIconAtTop, publishButtonAtTop, - authorized) - - -def htmlBookmarks(cssCache: {}, defaultTimeline: str, - recentPostsCache: {}, maxRecentPosts: int, - translate: {}, pageNumber: int, itemsPerPage: int, - session, baseDir: str, wfRequest: {}, personCache: {}, - nickname: str, domain: str, port: int, bookmarksJson: {}, - allowDeletion: bool, - httpPrefix: str, projectVersion: str, - minimal: bool, YTReplacementDomain: str, - showPublishedDateOnly: bool, - newswire: {}, positiveVoting: bool, - showPublishAsIcon: bool, - fullWidthTimelineButtonHeader: bool, - iconsAsButtons: bool, - rssIconAtTop: bool, - publishButtonAtTop: bool, - authorized: bool) -> str: - """Show the bookmarks as html - """ - manuallyApproveFollowers = \ - followerApprovalActive(baseDir, nickname, domain) - - return htmlTimeline(cssCache, defaultTimeline, - recentPostsCache, maxRecentPosts, - translate, pageNumber, - itemsPerPage, session, baseDir, wfRequest, personCache, - nickname, domain, port, bookmarksJson, - 'tlbookmarks', allowDeletion, - httpPrefix, projectVersion, manuallyApproveFollowers, - minimal, YTReplacementDomain, - showPublishedDateOnly, - newswire, False, False, - positiveVoting, showPublishAsIcon, - fullWidthTimelineButtonHeader, - iconsAsButtons, rssIconAtTop, publishButtonAtTop, - authorized) - - -def htmlEvents(cssCache: {}, defaultTimeline: str, - recentPostsCache: {}, maxRecentPosts: int, - translate: {}, pageNumber: int, itemsPerPage: int, - session, baseDir: str, wfRequest: {}, personCache: {}, - nickname: str, domain: str, port: int, bookmarksJson: {}, - allowDeletion: bool, - httpPrefix: str, projectVersion: str, - minimal: bool, YTReplacementDomain: str, - showPublishedDateOnly: bool, - newswire: {}, positiveVoting: bool, - showPublishAsIcon: bool, - fullWidthTimelineButtonHeader: bool, - iconsAsButtons: bool, - rssIconAtTop: bool, - publishButtonAtTop: bool, - authorized: bool) -> str: - """Show the events as html - """ - manuallyApproveFollowers = \ - followerApprovalActive(baseDir, nickname, domain) - - return htmlTimeline(cssCache, defaultTimeline, - recentPostsCache, maxRecentPosts, - translate, pageNumber, - itemsPerPage, session, baseDir, wfRequest, personCache, - nickname, domain, port, bookmarksJson, - 'tlevents', allowDeletion, - httpPrefix, projectVersion, manuallyApproveFollowers, - minimal, YTReplacementDomain, - showPublishedDateOnly, - newswire, False, False, - positiveVoting, showPublishAsIcon, - fullWidthTimelineButtonHeader, - iconsAsButtons, rssIconAtTop, publishButtonAtTop, - authorized) - - -def htmlInboxDMs(cssCache: {}, defaultTimeline: str, - recentPostsCache: {}, maxRecentPosts: int, - translate: {}, pageNumber: int, itemsPerPage: int, - session, baseDir: str, wfRequest: {}, personCache: {}, - nickname: str, domain: str, port: int, inboxJson: {}, - allowDeletion: bool, - httpPrefix: str, projectVersion: str, - minimal: bool, YTReplacementDomain: str, - showPublishedDateOnly: bool, - newswire: {}, positiveVoting: bool, - showPublishAsIcon: bool, - fullWidthTimelineButtonHeader: bool, - iconsAsButtons: bool, - rssIconAtTop: bool, - publishButtonAtTop: bool, - authorized: bool) -> str: - """Show the DM timeline as html - """ - return htmlTimeline(cssCache, defaultTimeline, - recentPostsCache, maxRecentPosts, - translate, pageNumber, - itemsPerPage, session, baseDir, wfRequest, personCache, - nickname, domain, port, inboxJson, 'dm', allowDeletion, - httpPrefix, projectVersion, False, minimal, - YTReplacementDomain, showPublishedDateOnly, - newswire, False, False, positiveVoting, - showPublishAsIcon, - fullWidthTimelineButtonHeader, - iconsAsButtons, rssIconAtTop, publishButtonAtTop, - authorized) - - -def htmlInboxReplies(cssCache: {}, defaultTimeline: str, - recentPostsCache: {}, maxRecentPosts: int, - translate: {}, pageNumber: int, itemsPerPage: int, - session, baseDir: str, wfRequest: {}, personCache: {}, - nickname: str, domain: str, port: int, inboxJson: {}, - allowDeletion: bool, - httpPrefix: str, projectVersion: str, - minimal: bool, YTReplacementDomain: str, - showPublishedDateOnly: bool, - newswire: {}, positiveVoting: bool, - showPublishAsIcon: bool, - fullWidthTimelineButtonHeader: bool, - iconsAsButtons: bool, - rssIconAtTop: bool, - publishButtonAtTop: bool, - authorized: bool) -> str: - """Show the replies timeline as html - """ - return htmlTimeline(cssCache, defaultTimeline, - recentPostsCache, maxRecentPosts, - translate, pageNumber, - itemsPerPage, session, baseDir, wfRequest, personCache, - nickname, domain, port, inboxJson, 'tlreplies', - allowDeletion, httpPrefix, projectVersion, False, - minimal, YTReplacementDomain, - showPublishedDateOnly, - newswire, False, False, - positiveVoting, showPublishAsIcon, - fullWidthTimelineButtonHeader, - iconsAsButtons, rssIconAtTop, publishButtonAtTop, - authorized) - - -def htmlInboxMedia(cssCache: {}, defaultTimeline: str, - recentPostsCache: {}, maxRecentPosts: int, - translate: {}, pageNumber: int, itemsPerPage: int, - session, baseDir: str, wfRequest: {}, personCache: {}, - nickname: str, domain: str, port: int, inboxJson: {}, - allowDeletion: bool, - httpPrefix: str, projectVersion: str, - minimal: bool, YTReplacementDomain: str, - showPublishedDateOnly: bool, - newswire: {}, positiveVoting: bool, - showPublishAsIcon: bool, - fullWidthTimelineButtonHeader: bool, - iconsAsButtons: bool, - rssIconAtTop: bool, - publishButtonAtTop: bool, - authorized: bool) -> str: - """Show the media timeline as html - """ - return htmlTimeline(cssCache, defaultTimeline, - recentPostsCache, maxRecentPosts, - translate, pageNumber, - itemsPerPage, session, baseDir, wfRequest, personCache, - nickname, domain, port, inboxJson, 'tlmedia', - allowDeletion, httpPrefix, projectVersion, False, - minimal, YTReplacementDomain, - showPublishedDateOnly, - newswire, False, False, - positiveVoting, showPublishAsIcon, - fullWidthTimelineButtonHeader, - iconsAsButtons, rssIconAtTop, publishButtonAtTop, - authorized) - - -def htmlInboxBlogs(cssCache: {}, defaultTimeline: str, - recentPostsCache: {}, maxRecentPosts: int, - translate: {}, pageNumber: int, itemsPerPage: int, - session, baseDir: str, wfRequest: {}, personCache: {}, - nickname: str, domain: str, port: int, inboxJson: {}, - allowDeletion: bool, - httpPrefix: str, projectVersion: str, - minimal: bool, YTReplacementDomain: str, - showPublishedDateOnly: bool, - newswire: {}, positiveVoting: bool, - showPublishAsIcon: bool, - fullWidthTimelineButtonHeader: bool, - iconsAsButtons: bool, - rssIconAtTop: bool, - publishButtonAtTop: bool, - authorized: bool) -> str: - """Show the blogs timeline as html - """ - return htmlTimeline(cssCache, defaultTimeline, - recentPostsCache, maxRecentPosts, - translate, pageNumber, - itemsPerPage, session, baseDir, wfRequest, personCache, - nickname, domain, port, inboxJson, 'tlblogs', - allowDeletion, httpPrefix, projectVersion, False, - minimal, YTReplacementDomain, - showPublishedDateOnly, - newswire, False, False, - positiveVoting, showPublishAsIcon, - fullWidthTimelineButtonHeader, - iconsAsButtons, rssIconAtTop, publishButtonAtTop, - authorized) - - -def htmlInboxNews(cssCache: {}, defaultTimeline: str, - recentPostsCache: {}, maxRecentPosts: int, - translate: {}, pageNumber: int, itemsPerPage: int, - session, baseDir: str, wfRequest: {}, personCache: {}, - nickname: str, domain: str, port: int, inboxJson: {}, - allowDeletion: bool, - httpPrefix: str, projectVersion: str, - minimal: bool, YTReplacementDomain: str, - showPublishedDateOnly: bool, - newswire: {}, moderator: bool, editor: bool, - positiveVoting: bool, showPublishAsIcon: bool, - fullWidthTimelineButtonHeader: bool, - iconsAsButtons: bool, - rssIconAtTop: bool, - publishButtonAtTop: bool, - authorized: bool) -> str: - """Show the news timeline as html - """ - return htmlTimeline(cssCache, defaultTimeline, - recentPostsCache, maxRecentPosts, - translate, pageNumber, - itemsPerPage, session, baseDir, wfRequest, personCache, - nickname, domain, port, inboxJson, 'tlnews', - allowDeletion, httpPrefix, projectVersion, False, - minimal, YTReplacementDomain, - showPublishedDateOnly, - newswire, moderator, editor, - positiveVoting, showPublishAsIcon, - fullWidthTimelineButtonHeader, - iconsAsButtons, rssIconAtTop, publishButtonAtTop, - authorized) - - -def htmlModeration(cssCache: {}, defaultTimeline: str, - recentPostsCache: {}, maxRecentPosts: int, - translate: {}, pageNumber: int, itemsPerPage: int, - session, baseDir: str, wfRequest: {}, personCache: {}, - nickname: str, domain: str, port: int, inboxJson: {}, - allowDeletion: bool, - httpPrefix: str, projectVersion: str, - YTReplacementDomain: str, - showPublishedDateOnly: bool, - newswire: {}, positiveVoting: bool, - showPublishAsIcon: bool, - fullWidthTimelineButtonHeader: bool, - iconsAsButtons: bool, - rssIconAtTop: bool, - publishButtonAtTop: bool, - authorized: bool) -> str: - """Show the moderation feed as html - """ - return htmlTimeline(cssCache, defaultTimeline, - recentPostsCache, maxRecentPosts, - translate, pageNumber, - itemsPerPage, session, baseDir, wfRequest, personCache, - nickname, domain, port, inboxJson, 'moderation', - allowDeletion, httpPrefix, projectVersion, True, False, - YTReplacementDomain, showPublishedDateOnly, - newswire, False, False, positiveVoting, - showPublishAsIcon, fullWidthTimelineButtonHeader, - iconsAsButtons, rssIconAtTop, publishButtonAtTop, - authorized) - - -def htmlOutbox(cssCache: {}, defaultTimeline: str, - recentPostsCache: {}, maxRecentPosts: int, - translate: {}, pageNumber: int, itemsPerPage: int, - session, baseDir: str, wfRequest: {}, personCache: {}, - nickname: str, domain: str, port: int, outboxJson: {}, - allowDeletion: bool, - httpPrefix: str, projectVersion: str, - minimal: bool, YTReplacementDomain: str, - showPublishedDateOnly: bool, - newswire: {}, positiveVoting: bool, - showPublishAsIcon: bool, - fullWidthTimelineButtonHeader: bool, - iconsAsButtons: bool, - rssIconAtTop: bool, - publishButtonAtTop: bool, - authorized: bool) -> str: - """Show the Outbox as html - """ - manuallyApproveFollowers = \ - followerApprovalActive(baseDir, nickname, domain) - return htmlTimeline(cssCache, defaultTimeline, - recentPostsCache, maxRecentPosts, - translate, pageNumber, - itemsPerPage, session, baseDir, wfRequest, personCache, - nickname, domain, port, outboxJson, 'outbox', - allowDeletion, httpPrefix, projectVersion, - manuallyApproveFollowers, minimal, - YTReplacementDomain, showPublishedDateOnly, - newswire, False, False, positiveVoting, - showPublishAsIcon, fullWidthTimelineButtonHeader, - iconsAsButtons, rssIconAtTop, publishButtonAtTop, - authorized) - - -def htmlIndividualPost(cssCache: {}, - recentPostsCache: {}, maxRecentPosts: int, - translate: {}, - baseDir: str, session, wfRequest: {}, personCache: {}, - nickname: str, domain: str, port: int, authorized: bool, - postJsonObject: {}, httpPrefix: str, - projectVersion: str, likedBy: str, - YTReplacementDomain: str, - showPublishedDateOnly: bool) -> str: - """Show an individual post as html - """ - iconsDir = getIconsDir(baseDir) - postStr = '' - if likedBy: - likedByNickname = getNicknameFromActor(likedBy) - likedByDomain, likedByPort = getDomainFromActor(likedBy) - if likedByPort: - if likedByPort != 80 and likedByPort != 443: - likedByDomain += ':' + str(likedByPort) - likedByHandle = likedByNickname + '@' + likedByDomain - postStr += \ - '

' + translate['Liked by'] + \ - ' @' + \ - likedByHandle + '\n' - - domainFull = domain - if port: - if port != 80 and port != 443: - domainFull = domain + ':' + str(port) - actor = '/users/' + nickname - followStr = '

\n' - followStr += \ - ' \n' - followStr += \ - ' \n' - if not isFollowingActor(baseDir, nickname, domainFull, likedBy): - followStr += ' \n' - followStr += ' \n' - followStr += '
\n' - postStr += followStr + '

\n' - - postStr += \ - individualPostAsHtml(True, recentPostsCache, maxRecentPosts, - iconsDir, translate, None, - baseDir, session, wfRequest, personCache, - nickname, domain, port, postJsonObject, - None, True, False, - httpPrefix, projectVersion, 'inbox', - YTReplacementDomain, - showPublishedDateOnly, - False, authorized, False, False, False) - messageId = removeIdEnding(postJsonObject['id']) - - # show the previous posts - if isinstance(postJsonObject['object'], dict): - while postJsonObject['object'].get('inReplyTo'): - postFilename = \ - locatePost(baseDir, nickname, domain, - postJsonObject['object']['inReplyTo']) - if not postFilename: - break - postJsonObject = loadJson(postFilename) - if postJsonObject: - postStr = \ - individualPostAsHtml(True, recentPostsCache, - maxRecentPosts, - iconsDir, translate, None, - baseDir, session, wfRequest, - personCache, - nickname, domain, port, - postJsonObject, - None, True, False, - httpPrefix, projectVersion, 'inbox', - YTReplacementDomain, - showPublishedDateOnly, - False, authorized, - False, False, False) + postStr - - # show the following posts - postFilename = locatePost(baseDir, nickname, domain, messageId) - if postFilename: - # is there a replies file for this post? - repliesFilename = postFilename.replace('.json', '.replies') - if os.path.isfile(repliesFilename): - # get items from the replies file - repliesJson = { - 'orderedItems': [] - } - populateRepliesJson(baseDir, nickname, domain, - repliesFilename, authorized, repliesJson) - # add items to the html output - for item in repliesJson['orderedItems']: - postStr += \ - individualPostAsHtml(True, recentPostsCache, - maxRecentPosts, - iconsDir, translate, None, - baseDir, session, wfRequest, - personCache, - nickname, domain, port, item, - None, True, False, - httpPrefix, projectVersion, 'inbox', - YTReplacementDomain, - showPublishedDateOnly, - False, authorized, - False, False, False) - cssFilename = baseDir + '/epicyon-profile.css' - if os.path.isfile(baseDir + '/epicyon.css'): - cssFilename = baseDir + '/epicyon.css' - - postsCSS = getCSS(baseDir, cssFilename, cssCache) - if postsCSS: - if httpPrefix != 'https': - postsCSS = postsCSS.replace('https://', - httpPrefix + '://') - return htmlHeader(cssFilename, postsCSS) + postStr + htmlFooter() - - -def htmlPostReplies(cssCache: {}, - recentPostsCache: {}, maxRecentPosts: int, - translate: {}, baseDir: str, - session, wfRequest: {}, personCache: {}, - nickname: str, domain: str, port: int, repliesJson: {}, - httpPrefix: str, projectVersion: str, - YTReplacementDomain: str, - showPublishedDateOnly: bool) -> str: - """Show the replies to an individual post as html - """ - iconsDir = getIconsDir(baseDir) - repliesStr = '' - if repliesJson.get('orderedItems'): - for item in repliesJson['orderedItems']: - repliesStr += \ - individualPostAsHtml(True, recentPostsCache, - maxRecentPosts, - iconsDir, translate, None, - baseDir, session, wfRequest, personCache, - nickname, domain, port, item, - None, True, False, - httpPrefix, projectVersion, 'inbox', - YTReplacementDomain, - showPublishedDateOnly, - False, False, False, False, False) - - cssFilename = baseDir + '/epicyon-profile.css' - if os.path.isfile(baseDir + '/epicyon.css'): - cssFilename = baseDir + '/epicyon.css' - - postsCSS = getCSS(baseDir, cssFilename, cssCache) - if postsCSS: - if httpPrefix != 'https': - postsCSS = postsCSS.replace('https://', - httpPrefix + '://') - return htmlHeader(cssFilename, postsCSS) + repliesStr + htmlFooter() - - -def htmlRemoveSharedItem(cssCache: {}, translate: {}, baseDir: str, - actor: str, shareName: str, - callingDomain: str) -> str: - """Shows a screen asking to confirm the removal of a shared item - """ - itemID = getValidSharedItemID(shareName) - nickname = getNicknameFromActor(actor) - domain, port = getDomainFromActor(actor) - domainFull = domain - if port: - if port != 80 and port != 443: - domainFull = domain + ':' + str(port) - sharesFile = baseDir + '/accounts/' + \ - nickname + '@' + domain + '/shares.json' - if not os.path.isfile(sharesFile): - print('ERROR: no shares file ' + sharesFile) - return None - sharesJson = loadJson(sharesFile) - if not sharesJson: - print('ERROR: unable to load shares.json') - return None - if not sharesJson.get(itemID): - print('ERROR: share named "' + itemID + '" is not in ' + sharesFile) - return None - sharedItemDisplayName = sharesJson[itemID]['displayName'] - sharedItemImageUrl = None - if sharesJson[itemID].get('imageUrl'): - sharedItemImageUrl = sharesJson[itemID]['imageUrl'] - - if os.path.isfile(baseDir + '/img/shares-background.png'): - if not os.path.isfile(baseDir + '/accounts/shares-background.png'): - copyfile(baseDir + '/img/shares-background.png', - baseDir + '/accounts/shares-background.png') - - cssFilename = baseDir + '/epicyon-follow.css' - if os.path.isfile(baseDir + '/follow.css'): - cssFilename = baseDir + '/follow.css' - - profileStyle = getCSS(baseDir, cssFilename, cssCache) - sharesStr = htmlHeader(cssFilename, profileStyle) - sharesStr += '\n' - sharesStr += htmlFooter() - return sharesStr - - -def htmlDeletePost(cssCache: {}, - recentPostsCache: {}, maxRecentPosts: int, - translate, pageNumber: int, - session, baseDir: str, messageId: str, - httpPrefix: str, projectVersion: str, - wfRequest: {}, personCache: {}, - callingDomain: str, - YTReplacementDomain: str, - showPublishedDateOnly: bool) -> str: - """Shows a screen asking to confirm the deletion of a post - """ - if '/statuses/' not in messageId: - return None - iconsDir = getIconsDir(baseDir) - actor = messageId.split('/statuses/')[0] - nickname = getNicknameFromActor(actor) - domain, port = getDomainFromActor(actor) - domainFull = domain - if port: - if port != 80 and port != 443: - domainFull = domain + ':' + str(port) - - postFilename = locatePost(baseDir, nickname, domain, messageId) - if not postFilename: - return None - - postJsonObject = loadJson(postFilename) - if not postJsonObject: - return None - - if os.path.isfile(baseDir + '/img/delete-background.png'): - if not os.path.isfile(baseDir + '/accounts/delete-background.png'): - copyfile(baseDir + '/img/delete-background.png', - baseDir + '/accounts/delete-background.png') - - deletePostStr = None - cssFilename = baseDir + '/epicyon-profile.css' - if os.path.isfile(baseDir + '/epicyon.css'): - cssFilename = baseDir + '/epicyon.css' - - profileStyle = getCSS(baseDir, cssFilename, cssCache) - if profileStyle: - if httpPrefix != 'https': - profileStyle = profileStyle.replace('https://', - httpPrefix + '://') - deletePostStr = htmlHeader(cssFilename, profileStyle) - deletePostStr += \ - individualPostAsHtml(True, recentPostsCache, maxRecentPosts, - iconsDir, translate, pageNumber, - baseDir, session, wfRequest, personCache, - nickname, domain, port, postJsonObject, - None, True, False, - httpPrefix, projectVersion, 'outbox', - YTReplacementDomain, - showPublishedDateOnly, - False, False, False, False, False) - deletePostStr += '
' - deletePostStr += \ - '

' + \ - translate['Delete this post?'] + '

' - - postActor = getAltPath(actor, domainFull, callingDomain) - deletePostStr += \ - '
\n' - deletePostStr += \ - ' \n' - deletePostStr += \ - ' \n' - deletePostStr += \ - ' \n' - deletePostStr += \ - ' \n' - deletePostStr += '
\n' - deletePostStr += '
\n' - deletePostStr += htmlFooter() - return deletePostStr - - -def htmlCalendarDeleteConfirm(cssCache: {}, translate: {}, baseDir: str, - path: str, httpPrefix: str, - domainFull: str, postId: str, postTime: str, - year: int, monthNumber: int, - dayNumber: int, callingDomain: str) -> str: - """Shows a screen asking to confirm the deletion of a calendar event - """ - nickname = getNicknameFromActor(path) - actor = httpPrefix + '://' + domainFull + '/users/' + nickname - domain, port = getDomainFromActor(actor) - messageId = actor + '/statuses/' + postId - - postFilename = locatePost(baseDir, nickname, domain, messageId) - if not postFilename: - return None - - postJsonObject = loadJson(postFilename) - if not postJsonObject: - return None - - if os.path.isfile(baseDir + '/img/delete-background.png'): - if not os.path.isfile(baseDir + '/accounts/delete-background.png'): - copyfile(baseDir + '/img/delete-background.png', - baseDir + '/accounts/delete-background.png') - - deletePostStr = None - cssFilename = baseDir + '/epicyon-profile.css' - if os.path.isfile(baseDir + '/epicyon.css'): - cssFilename = baseDir + '/epicyon.css' - - profileStyle = getCSS(baseDir, cssFilename, cssCache) - if profileStyle: - if httpPrefix != 'https': - profileStyle = profileStyle.replace('https://', - httpPrefix + '://') - deletePostStr = htmlHeader(cssFilename, profileStyle) - deletePostStr += \ - '

' + postTime + ' ' + str(year) + '/' + \ - str(monthNumber) + \ - '/' + str(dayNumber) + '

' - deletePostStr += '
' - deletePostStr += '

' + \ - translate['Delete this event'] + '

' - - postActor = getAltPath(actor, domainFull, callingDomain) - deletePostStr += \ - '
\n' - deletePostStr += ' \n' - deletePostStr += ' \n' - deletePostStr += ' \n' - deletePostStr += \ - ' \n' - deletePostStr += \ - ' \n' - deletePostStr += \ - ' \n' - deletePostStr += \ - ' \n' - deletePostStr += '
\n' - deletePostStr += '
\n' - deletePostStr += htmlFooter() - return deletePostStr - - -def htmlFollowConfirm(cssCache: {}, translate: {}, baseDir: str, - originPathStr: str, - followActor: str, - followProfileUrl: str) -> str: - """Asks to confirm a follow - """ - followDomain, port = getDomainFromActor(followActor) - - if os.path.isfile(baseDir + '/accounts/follow-background-custom.jpg'): - if not os.path.isfile(baseDir + '/accounts/follow-background.jpg'): - copyfile(baseDir + '/accounts/follow-background-custom.jpg', - baseDir + '/accounts/follow-background.jpg') - - cssFilename = baseDir + '/epicyon-follow.css' - if os.path.isfile(baseDir + '/follow.css'): - cssFilename = baseDir + '/follow.css' - - profileStyle = getCSS(baseDir, cssFilename, cssCache) - followStr = htmlHeader(cssFilename, profileStyle) - followStr += '\n' - followStr += htmlFooter() - return followStr - - -def htmlUnfollowConfirm(cssCache: {}, translate: {}, baseDir: str, - originPathStr: str, - followActor: str, - followProfileUrl: str) -> str: - """Asks to confirm unfollowing an actor - """ - followDomain, port = getDomainFromActor(followActor) - - if os.path.isfile(baseDir + '/accounts/follow-background-custom.jpg'): - if not os.path.isfile(baseDir + '/accounts/follow-background.jpg'): - copyfile(baseDir + '/accounts/follow-background-custom.jpg', - baseDir + '/accounts/follow-background.jpg') - - cssFilename = baseDir + '/epicyon-follow.css' - if os.path.isfile(baseDir + '/follow.css'): - cssFilename = baseDir + '/follow.css' - - profileStyle = getCSS(baseDir, cssFilename, cssCache) - - followStr = htmlHeader(cssFilename, profileStyle) - followStr += '\n' - followStr += htmlFooter() - return followStr - - -def htmlPersonOptions(cssCache: {}, translate: {}, baseDir: str, - domain: str, domainFull: str, - originPathStr: str, - optionsActor: str, - optionsProfileUrl: str, - optionsLink: str, - pageNumber: int, - donateUrl: str, - xmppAddress: str, - matrixAddress: str, - ssbAddress: str, - blogAddress: str, - toxAddress: str, - PGPpubKey: str, - PGPfingerprint: str, - emailAddress) -> str: - """Show options for a person: view/follow/block/report - """ - optionsDomain, optionsPort = getDomainFromActor(optionsActor) - optionsDomainFull = optionsDomain - if optionsPort: - if optionsPort != 80 and optionsPort != 443: - optionsDomainFull = optionsDomain + ':' + str(optionsPort) - - if os.path.isfile(baseDir + '/accounts/options-background-custom.jpg'): - if not os.path.isfile(baseDir + '/accounts/options-background.jpg'): - copyfile(baseDir + '/accounts/options-background.jpg', - baseDir + '/accounts/options-background.jpg') - - followStr = 'Follow' - blockStr = 'Block' - nickname = None - optionsNickname = None - if originPathStr.startswith('/users/'): - nickname = originPathStr.split('/users/')[1] - if '/' in nickname: - nickname = nickname.split('/')[0] - if '?' in nickname: - nickname = nickname.split('?')[0] - followerDomain, followerPort = getDomainFromActor(optionsActor) - if isFollowingActor(baseDir, nickname, domain, optionsActor): - followStr = 'Unfollow' - - optionsNickname = getNicknameFromActor(optionsActor) - optionsDomainFull = optionsDomain - if optionsPort: - if optionsPort != 80 and optionsPort != 443: - optionsDomainFull = optionsDomain + ':' + str(optionsPort) - if isBlocked(baseDir, nickname, domain, - optionsNickname, optionsDomainFull): - blockStr = 'Block' - - optionsLinkStr = '' - if optionsLink: - optionsLinkStr = \ - ' \n' - cssFilename = baseDir + '/epicyon-options.css' - if os.path.isfile(baseDir + '/options.css'): - cssFilename = baseDir + '/options.css' - - profileStyle = getCSS(baseDir, cssFilename, cssCache) - if profileStyle: - profileStyle = \ - profileStyle.replace('--follow-text-entry-width: 90%;', - '--follow-text-entry-width: 20%;') - - if not os.path.isfile(baseDir + '/accounts/' + - 'options-background.jpg'): - profileStyle = \ - profileStyle.replace('background-image: ' + - 'url("options-background.jpg");', - 'background-image: none;') - - # To snooze, or not to snooze? That is the question - snoozeButtonStr = 'Snooze' - if nickname: - if isPersonSnoozed(baseDir, nickname, domain, optionsActor): - snoozeButtonStr = 'Unsnooze' - - donateStr = '' - if donateUrl: - donateStr = \ - ' \n' - - optionsStr = htmlHeader(cssFilename, profileStyle) - optionsStr += '

\n' - optionsStr += '
\n' - optionsStr += '
\n' - optionsStr += '
\n' - optionsStr += ' \n' - optionsStr += ' \n' - handle = getNicknameFromActor(optionsActor) + '@' + optionsDomain - optionsStr += \ - '

' + translate['Options for'] + \ - ' @' + handle + '

\n' - if emailAddress: - optionsStr += \ - '

' + translate['Email'] + \ - ': ' + emailAddress + '

\n' - if xmppAddress: - optionsStr += \ - '

' + translate['XMPP'] + \ - ': ' + \ - xmppAddress + '

\n' - if matrixAddress: - optionsStr += \ - '

' + translate['Matrix'] + ': ' + \ - matrixAddress + '

\n' - if ssbAddress: - optionsStr += \ - '

SSB: ' + ssbAddress + '

\n' - if blogAddress: - optionsStr += \ - '

Blog: ' + \ - blogAddress + '

\n' - if toxAddress: - optionsStr += \ - '

Tox: ' + toxAddress + '

\n' - if PGPfingerprint: - optionsStr += '

PGP: ' + \ - PGPfingerprint.replace('\n', '
') + '

\n' - if PGPpubKey: - optionsStr += '

' + \ - PGPpubKey.replace('\n', '
') + '

\n' - optionsStr += '
\n' - optionsStr += ' \n' - optionsStr += ' \n' - optionsStr += ' \n' - if optionsNickname: - handle = optionsNickname + '@' + optionsDomainFull - petname = getPetName(baseDir, nickname, domain, handle) - optionsStr += \ - ' ' + translate['Petname'] + ': \n' + \ - ' \n' \ - '
\n' - - # checkbox for receiving calendar events - if isFollowingActor(baseDir, nickname, domain, optionsActor): - checkboxStr = \ - ' ' + \ - translate['Receive calendar events from this account'] + \ - '\n
\n' - if not receivingCalendarEvents(baseDir, nickname, domain, - optionsNickname, optionsDomainFull): - checkboxStr = checkboxStr.replace(' checked>', '>') - optionsStr += checkboxStr - - # checkbox for permission to post to newswire - if optionsDomainFull == domainFull: - if isModerator(baseDir, nickname) and \ - not isModerator(baseDir, optionsNickname): - newswireBlockedFilename = \ - baseDir + '/accounts/' + \ - optionsNickname + '@' + optionsDomain + '/.nonewswire' - checkboxStr = \ - ' ' + \ - translate['Allow news posts'] + \ - '\n
\n' - if os.path.isfile(newswireBlockedFilename): - checkboxStr = checkboxStr.replace(' checked>', '>') - optionsStr += checkboxStr - - optionsStr += optionsLinkStr - optionsStr += \ - ' ' - optionsStr += \ - ' ' - optionsStr += donateStr - optionsStr += \ - ' ' - optionsStr += \ - ' ' - optionsStr += \ - ' ' - optionsStr += \ - ' ' - optionsStr += \ - ' ' - - personNotes = '' - personNotesFilename = \ - baseDir + '/accounts/' + nickname + '@' + domain + \ - '/notes/' + handle + '.txt' - if os.path.isfile(personNotesFilename): - with open(personNotesFilename, 'r') as fp: - personNotes = fp.read() - - optionsStr += \ - '

' + translate['Notes'] + ': \n' - optionsStr += '
\n' - optionsStr += \ - ' \n' - - optionsStr += '
\n' - optionsStr += '
\n' - optionsStr += '
\n' - optionsStr += '
\n' - optionsStr += htmlFooter() - return optionsStr - - -def htmlUnblockConfirm(cssCache: {}, translate: {}, baseDir: str, - originPathStr: str, - blockActor: str, - blockProfileUrl: str) -> str: - """Asks to confirm unblocking an actor - """ - blockDomain, port = getDomainFromActor(blockActor) - - if os.path.isfile(baseDir + '/img/block-background.png'): - if not os.path.isfile(baseDir + '/accounts/block-background.png'): - copyfile(baseDir + '/img/block-background.png', - baseDir + '/accounts/block-background.png') - - cssFilename = baseDir + '/epicyon-follow.css' - if os.path.isfile(baseDir + '/follow.css'): - cssFilename = baseDir + '/follow.css' - - profileStyle = getCSS(baseDir, cssFilename, cssCache) - - blockStr = htmlHeader(cssFilename, profileStyle) - blockStr += '
\n' - blockStr += '
\n' - blockStr += '
\n' - blockStr += ' \n' - blockStr += ' \n' - blockStr += \ - '

' + translate['Stop blocking'] + ' ' + \ - getNicknameFromActor(blockActor) + '@' + blockDomain + ' ?

\n' - blockStr += '
\n' - blockStr += ' \n' - blockStr += \ - ' \n' - blockStr += \ - ' \n' - blockStr += '
\n' - blockStr += '
\n' - blockStr += '
\n' - blockStr += '
\n' - blockStr += htmlFooter() - return blockStr - - -def htmlSearchEmojiTextEntry(cssCache: {}, translate: {}, - baseDir: str, path: str) -> str: - """Search for an emoji by name - """ - # emoji.json is generated so that it can be customized and the changes - # will be retained even if default_emoji.json is subsequently updated - if not os.path.isfile(baseDir + '/emoji/emoji.json'): - copyfile(baseDir + '/emoji/default_emoji.json', - baseDir + '/emoji/emoji.json') - - actor = path.replace('/search', '') - domain, port = getDomainFromActor(actor) - - if os.path.isfile(baseDir + '/img/search-background.png'): - if not os.path.isfile(baseDir + '/accounts/search-background.png'): - copyfile(baseDir + '/img/search-background.png', - baseDir + '/accounts/search-background.png') - - cssFilename = baseDir + '/epicyon-follow.css' - if os.path.isfile(baseDir + '/follow.css'): - cssFilename = baseDir + '/follow.css' - - profileStyle = getCSS(baseDir, cssFilename, cssCache) - - emojiStr = htmlHeader(cssFilename, profileStyle) - emojiStr += '\n' - emojiStr += htmlFooter() - return emojiStr - - -def weekDayOfMonthStart(monthNumber: int, year: int) -> int: - """Gets the day number of the first day of the month - 1=sun, 7=sat - """ - firstDayOfMonth = datetime(year, monthNumber, 1, 0, 0) - return int(firstDayOfMonth.strftime("%w")) + 1 - - -def htmlCalendarDay(cssCache: {}, translate: {}, - baseDir: str, path: str, - year: int, monthNumber: int, dayNumber: int, - nickname: str, domain: str, dayEvents: [], - monthName: str, actor: str) -> str: - """Show a day within the calendar - """ - accountDir = baseDir + '/accounts/' + nickname + '@' + domain - calendarFile = accountDir + '/.newCalendar' - if os.path.isfile(calendarFile): - os.remove(calendarFile) - - cssFilename = baseDir + '/epicyon-calendar.css' - if os.path.isfile(baseDir + '/calendar.css'): - cssFilename = baseDir + '/calendar.css' - - calendarStyle = getCSS(baseDir, cssFilename, cssCache) - - calActor = actor - if '/users/' in actor: - calActor = '/users/' + actor.split('/users/')[1] - - calendarStr = htmlHeader(cssFilename, calendarStyle) - calendarStr += '
\n' - calendarStr += '\n' - calendarStr += '\n' - - iconsDir = getIconsDir(baseDir) - - if dayEvents: - for eventPost in dayEvents: - eventTime = None - eventDescription = None - eventPlace = None - postId = None - # get the time place and description - for ev in eventPost: - if ev['type'] == 'Event': - if ev.get('postId'): - postId = ev['postId'] - if ev.get('startTime'): - eventDate = \ - datetime.strptime(ev['startTime'], - "%Y-%m-%dT%H:%M:%S%z") - eventTime = eventDate.strftime("%H:%M").strip() - if ev.get('name'): - eventDescription = ev['name'].strip() - elif ev['type'] == 'Place': - if ev.get('name'): - eventPlace = ev['name'] - - deleteButtonStr = '' - if postId: - deleteButtonStr = \ - '\n' - - if eventTime and eventDescription and eventPlace: - calendarStr += \ - '' + deleteButtonStr + '\n' - elif eventTime and eventDescription and not eventPlace: - calendarStr += \ - '' + deleteButtonStr + '\n' - elif not eventTime and eventDescription and not eventPlace: - calendarStr += \ - '' + deleteButtonStr + '\n' - elif not eventTime and eventDescription and eventPlace: - calendarStr += \ - '' + \ - '' + deleteButtonStr + '\n' - elif eventTime and not eventDescription and eventPlace: - calendarStr += \ - '' + \ - deleteButtonStr + '\n' - - calendarStr += '\n' - calendarStr += '
\n' - calendarStr += \ - ' \n' - calendarStr += \ - '

' + str(dayNumber) + ' ' + monthName + \ - '


' + str(year) + '\n' - calendarStr += '
\n' + \
-                    translate['Delete this event'] + ' |
' + eventTime + \ - '' + \ - '' + \ - eventPlace + '
' + eventDescription + \ - '
' + eventTime + \ - '' + \ - eventDescription + '
' + \ - '' + \ - eventDescription + '
' + \ - eventPlace + '
' + eventDescription + \ - '
' + eventTime + \ - '' + \ - '' + \ - eventPlace + '
\n' - calendarStr += htmlFooter() - - return calendarStr - - -def htmlCalendar(cssCache: {}, translate: {}, - baseDir: str, path: str, - httpPrefix: str, domainFull: str) -> str: - """Show the calendar for a person - """ - iconsDir = getIconsDir(baseDir) - domain = domainFull - if ':' in domainFull: - domain = domainFull.split(':')[0] - - monthNumber = 0 - dayNumber = None - year = 1970 - actor = httpPrefix + '://' + domainFull + path.replace('/calendar', '') - if '?' in actor: - first = True - for p in actor.split('?'): - if not first: - if '=' in p: - if p.split('=')[0] == 'year': - numStr = p.split('=')[1] - if numStr.isdigit(): - year = int(numStr) - elif p.split('=')[0] == 'month': - numStr = p.split('=')[1] - if numStr.isdigit(): - monthNumber = int(numStr) - elif p.split('=')[0] == 'day': - numStr = p.split('=')[1] - if numStr.isdigit(): - dayNumber = int(numStr) - first = False - actor = actor.split('?')[0] - - currDate = datetime.now() - if year == 1970 and monthNumber == 0: - year = currDate.year - monthNumber = currDate.month - - nickname = getNicknameFromActor(actor) - - if os.path.isfile(baseDir + '/img/calendar-background.png'): - if not os.path.isfile(baseDir + '/accounts/calendar-background.png'): - copyfile(baseDir + '/img/calendar-background.png', - baseDir + '/accounts/calendar-background.png') - - months = ('January', 'February', 'March', 'April', - 'May', 'June', 'July', 'August', 'September', - 'October', 'November', 'December') - monthName = translate[months[monthNumber - 1]] - - if dayNumber: - dayEvents = None - events = \ - getTodaysEvents(baseDir, nickname, domain, - year, monthNumber, dayNumber) - if events: - if events.get(str(dayNumber)): - dayEvents = events[str(dayNumber)] - return htmlCalendarDay(cssCache, translate, baseDir, path, - year, monthNumber, dayNumber, - nickname, domain, dayEvents, - monthName, actor) - - events = \ - getCalendarEvents(baseDir, nickname, domain, year, monthNumber) - - prevYear = year - prevMonthNumber = monthNumber - 1 - if prevMonthNumber < 1: - prevMonthNumber = 12 - prevYear = year - 1 - - nextYear = year - nextMonthNumber = monthNumber + 1 - if nextMonthNumber > 12: - nextMonthNumber = 1 - nextYear = year + 1 - - print('Calendar year=' + str(year) + ' month=' + str(monthNumber) + - ' ' + str(weekDayOfMonthStart(monthNumber, year))) - - if monthNumber < 12: - daysInMonth = \ - (date(year, monthNumber + 1, 1) - date(year, monthNumber, 1)).days - else: - daysInMonth = \ - (date(year + 1, 1, 1) - date(year, monthNumber, 1)).days - # print('daysInMonth ' + str(monthNumber) + ': ' + str(daysInMonth)) - - cssFilename = baseDir + '/epicyon-calendar.css' - if os.path.isfile(baseDir + '/calendar.css'): - cssFilename = baseDir + '/calendar.css' - - calendarStyle = getCSS(baseDir, cssFilename, cssCache) - - calActor = actor - if '/users/' in actor: - calActor = '/users/' + actor.split('/users/')[1] - - calendarStr = htmlHeader(cssFilename, calendarStyle) - calendarStr += '
\n' - calendarStr += '\n' - calendarStr += '\n' - calendarStr += '\n' - calendarStr += ' \n' - calendarStr += ' \n' - calendarStr += ' \n' - calendarStr += ' \n' - calendarStr += ' \n' - calendarStr += ' \n' - calendarStr += ' \n' - calendarStr += '\n' - calendarStr += '\n' - calendarStr += '\n' - - dayOfMonth = 0 - dow = weekDayOfMonthStart(monthNumber, year) - for weekOfMonth in range(1, 7): - if dayOfMonth == daysInMonth: - continue - calendarStr += ' \n' - for dayNumber in range(1, 8): - if (weekOfMonth > 1 and dayOfMonth < daysInMonth) or \ - (weekOfMonth == 1 and dayNumber >= dow): - dayOfMonth += 1 - - isToday = False - if year == currDate.year: - if currDate.month == monthNumber: - if dayOfMonth == currDate.day: - isToday = True - if events.get(str(dayOfMonth)): - url = calActor + '/calendar?year=' + \ - str(year) + '?month=' + \ - str(monthNumber) + '?day=' + str(dayOfMonth) - dayLink = '' + \ - str(dayOfMonth) + '' - # there are events for this day - if not isToday: - calendarStr += \ - ' \n' - else: - calendarStr += \ - ' \n' - else: - # No events today - if not isToday: - calendarStr += \ - ' \n' - else: - calendarStr += \ - ' \n' - else: - calendarStr += ' \n' - calendarStr += ' \n' - - calendarStr += '\n' - calendarStr += '
\n' - calendarStr += \ - ' ' - calendarStr += \ - ' ' + translate['Previous month'] + \
-        '\n' - calendarStr += ' ' - calendarStr += '

' + monthName + '

\n' - calendarStr += \ - ' ' - calendarStr += \ - ' ' + translate['Next month'] + \
-        '\n' - calendarStr += '
' + \ - translate['Sun'] + '' + \ - translate['Mon'] + '' + \ - translate['Tue'] + '' + \ - translate['Wed'] + '' + \ - translate['Thu'] + '' + \ - translate['Fri'] + '' + \ - translate['Sat'] + '
' + \ - dayLink + '' + \ - dayLink + '' + \ - str(dayOfMonth) + '' + str(dayOfMonth) + '
\n' - calendarStr += htmlFooter() - return calendarStr - - -def removeOldHashtags(baseDir: str, maxMonths: int) -> str: - """Remove old hashtags - """ - if maxMonths > 11: - maxMonths = 11 - maxDaysSinceEpoch = \ - (datetime.utcnow() - datetime(1970, 1 + maxMonths, 1)).days - removeHashtags = [] - - for subdir, dirs, files in os.walk(baseDir + '/tags'): - for f in files: - tagsFilename = os.path.join(baseDir + '/tags', f) - if not os.path.isfile(tagsFilename): - continue - # get last modified datetime - modTimesinceEpoc = os.path.getmtime(tagsFilename) - lastModifiedDate = datetime.fromtimestamp(modTimesinceEpoc) - fileDaysSinceEpoch = (lastModifiedDate - datetime(1970, 1, 1)).days - - # check of the file is too old - if fileDaysSinceEpoch < maxDaysSinceEpoch: - removeHashtags.append(tagsFilename) - - for removeFilename in removeHashtags: - try: - os.remove(removeFilename) - except BaseException: - pass - - -def htmlHashTagSwarm(baseDir: str, actor: str) -> str: - """Returns a tag swarm of today's hashtags - """ - currTime = datetime.utcnow() - daysSinceEpoch = (currTime - datetime(1970, 1, 1)).days - daysSinceEpochStr = str(daysSinceEpoch) + ' ' - tagSwarm = [] - - for subdir, dirs, files in os.walk(baseDir + '/tags'): - for f in files: - tagsFilename = os.path.join(baseDir + '/tags', f) - if not os.path.isfile(tagsFilename): - continue - # get last modified datetime - modTimesinceEpoc = os.path.getmtime(tagsFilename) - lastModifiedDate = datetime.fromtimestamp(modTimesinceEpoc) - fileDaysSinceEpoch = (lastModifiedDate - datetime(1970, 1, 1)).days - # check if the file was last modified today - if fileDaysSinceEpoch != daysSinceEpoch: - continue - - hashTagName = f.split('.')[0] - if isBlockedHashtag(baseDir, hashTagName): - continue - if daysSinceEpochStr not in open(tagsFilename).read(): - continue - with open(tagsFilename, 'r') as tagsFile: - line = tagsFile.readline() - lineCtr = 1 - tagCtr = 0 - maxLineCtr = 1 - while line: - if ' ' not in line: - line = tagsFile.readline() - lineCtr += 1 - # don't read too many lines - if lineCtr >= maxLineCtr: - break - continue - postDaysSinceEpochStr = line.split(' ')[0] - if not postDaysSinceEpochStr.isdigit(): - line = tagsFile.readline() - lineCtr += 1 - # don't read too many lines - if lineCtr >= maxLineCtr: - break - continue - postDaysSinceEpoch = int(postDaysSinceEpochStr) - if postDaysSinceEpoch < daysSinceEpoch: - break - if postDaysSinceEpoch == daysSinceEpoch: - if tagCtr == 0: - tagSwarm.append(hashTagName) - tagCtr += 1 - - line = tagsFile.readline() - lineCtr += 1 - # don't read too many lines - if lineCtr >= maxLineCtr: - break - - if not tagSwarm: - return '' - tagSwarm.sort() - tagSwarmStr = '' - ctr = 0 - for tagName in tagSwarm: - tagSwarmStr += \ - '' + tagName + '\n' - ctr += 1 - tagSwarmHtml = tagSwarmStr.strip() + '\n' - return tagSwarmHtml - - -def htmlSearch(cssCache: {}, translate: {}, - baseDir: str, path: str, domain: str, - defaultTimeline: str) -> str: - """Search called from the timeline icon - """ - actor = path.replace('/search', '') - searchNickname = getNicknameFromActor(actor) - - if os.path.isfile(baseDir + '/img/search-background.png'): - if not os.path.isfile(baseDir + '/accounts/search-background.png'): - copyfile(baseDir + '/img/search-background.png', - baseDir + '/accounts/search-background.png') - - cssFilename = baseDir + '/epicyon-search.css' - if os.path.isfile(baseDir + '/search.css'): - cssFilename = baseDir + '/search.css' - - profileStyle = getCSS(baseDir, cssFilename, cssCache) - - if not os.path.isfile(baseDir + '/accounts/' + - 'follow-background.jpg'): - profileStyle = \ - profileStyle.replace('background-image: ' + - 'url("follow-background.jpg");', - 'background-image: none;') - - followStr = htmlHeader(cssFilename, profileStyle) - - # show a banner above the search box - searchBannerFile, searchBannerFilename = \ - getSearchBannerFile(baseDir, searchNickname, domain) - if not os.path.isfile(searchBannerFilename): - # get the default search banner for the theme - theme = getConfigParam(baseDir, 'theme').lower() - if theme == 'default': - theme = '' - else: - theme = '_' + theme - themeSearchImageFile, themeSearchBannerFilename = \ - getImageFile(baseDir, 'search_banner', baseDir + '/img', - searchNickname, domain) - if os.path.isfile(themeSearchBannerFilename): - searchBannerFilename = \ - baseDir + '/accounts/' + \ - searchNickname + '@' + domain + '/' + themeSearchImageFile - copyfile(themeSearchBannerFilename, - searchBannerFilename) - searchBannerFile = themeSearchImageFile - - if os.path.isfile(searchBannerFilename): - usersPath = '/users/' + searchNickname - followStr += \ - '\n' - followStr += '\n' - - # show the search box - followStr += '\n' - followStr += htmlFooter() - return followStr - - -def htmlProfileAfterSearch(cssCache: {}, - recentPostsCache: {}, maxRecentPosts: int, - translate: {}, - baseDir: str, path: str, httpPrefix: str, - nickname: str, domain: str, port: int, - profileHandle: str, - session, cachedWebfingers: {}, personCache: {}, - debug: bool, projectVersion: str, - YTReplacementDomain: str, - showPublishedDateOnly: bool) -> str: - """Show a profile page after a search for a fediverse address - """ - if '/users/' in profileHandle or \ - '/accounts/' in profileHandle or \ - '/channel/' in profileHandle or \ - '/profile/' in profileHandle or \ - '/@' in profileHandle: - searchNickname = getNicknameFromActor(profileHandle) - searchDomain, searchPort = getDomainFromActor(profileHandle) - else: - if '@' not in profileHandle: - print('DEBUG: no @ in ' + profileHandle) - return None - if profileHandle.startswith('@'): - profileHandle = profileHandle[1:] - if '@' not in profileHandle: - print('DEBUG: no @ in ' + profileHandle) - return None - searchNickname = profileHandle.split('@')[0] - searchDomain = profileHandle.split('@')[1] - searchPort = None - if ':' in searchDomain: - searchPortStr = searchDomain.split(':')[1] - if searchPortStr.isdigit(): - searchPort = int(searchPortStr) - searchDomain = searchDomain.split(':')[0] - if searchPort: - print('DEBUG: Search for handle ' + - str(searchNickname) + '@' + str(searchDomain) + ':' + - str(searchPort)) - else: - print('DEBUG: Search for handle ' + - str(searchNickname) + '@' + str(searchDomain)) - if not searchNickname: - print('DEBUG: No nickname found in ' + profileHandle) - return None - if not searchDomain: - print('DEBUG: No domain found in ' + profileHandle) - return None - - searchDomainFull = searchDomain - if searchPort: - if searchPort != 80 and searchPort != 443: - if ':' not in searchDomain: - searchDomainFull = searchDomain + ':' + str(searchPort) - - profileStr = '' - cssFilename = baseDir + '/epicyon-profile.css' - if os.path.isfile(baseDir + '/epicyon.css'): - cssFilename = baseDir + '/epicyon.css' - - profileStyle = getCSS(baseDir, cssFilename, cssCache) - if profileStyle: - wf = \ - webfingerHandle(session, - searchNickname + '@' + searchDomainFull, - httpPrefix, cachedWebfingers, - domain, projectVersion) - if not wf: - print('DEBUG: Unable to webfinger ' + - searchNickname + '@' + searchDomainFull) - print('DEBUG: cachedWebfingers ' + str(cachedWebfingers)) - print('DEBUG: httpPrefix ' + httpPrefix) - print('DEBUG: domain ' + domain) - return None - if not isinstance(wf, dict): - print('WARN: Webfinger search for ' + - searchNickname + '@' + searchDomainFull + - ' did not return a dict. ' + - str(wf)) - return None - - personUrl = None - if wf.get('errors'): - personUrl = httpPrefix + '://' + \ - searchDomainFull + '/users/' + searchNickname - - profileStr = 'https://www.w3.org/ns/activitystreams' - asHeader = { - 'Accept': 'application/activity+json; profile="' + profileStr + '"' - } - if not personUrl: - personUrl = getUserUrl(wf) - if not personUrl: - # try single user instance - asHeader = { - 'Accept': 'application/ld+json; profile="' + profileStr + '"' - } - personUrl = httpPrefix + '://' + searchDomainFull - profileJson = \ - getJson(session, personUrl, asHeader, None, - projectVersion, httpPrefix, domain) - if not profileJson: - asHeader = { - 'Accept': 'application/ld+json; profile="' + profileStr + '"' - } - profileJson = \ - getJson(session, personUrl, asHeader, None, - projectVersion, httpPrefix, domain) - if not profileJson: - print('DEBUG: No actor returned from ' + personUrl) - return None - avatarUrl = '' - if profileJson.get('icon'): - if profileJson['icon'].get('url'): - avatarUrl = profileJson['icon']['url'] - if not avatarUrl: - avatarUrl = getPersonAvatarUrl(baseDir, personUrl, - personCache, True) - displayName = searchNickname - if profileJson.get('name'): - displayName = profileJson['name'] - profileDescription = '' - if profileJson.get('summary'): - profileDescription = profileJson['summary'] - outboxUrl = None - if not profileJson.get('outbox'): - if debug: - pprint(profileJson) - print('DEBUG: No outbox found') - return None - outboxUrl = profileJson['outbox'] - profileBackgroundImage = '' - if profileJson.get('image'): - if profileJson['image'].get('url'): - profileBackgroundImage = profileJson['image']['url'] - - profileStyle = profileStyle.replace('image.png', - profileBackgroundImage) - if httpPrefix != 'https': - profileStyle = profileStyle.replace('https://', - httpPrefix + '://') - # url to return to - backUrl = path - if not backUrl.endswith('/inbox'): - backUrl += '/inbox' - - profileDescriptionShort = profileDescription - if '\n' in profileDescription: - if len(profileDescription.split('\n')) > 2: - profileDescriptionShort = '' - else: - if '
' in profileDescription: - if len(profileDescription.split('
')) > 2: - profileDescriptionShort = '' - # keep the profile description short - if len(profileDescriptionShort) > 256: - profileDescriptionShort = '' - # remove formatting from profile description used on title - avatarDescription = '' - if profileJson.get('summary'): - if isinstance(profileJson['summary'], str): - avatarDescription = \ - profileJson['summary'].replace('
', '\n') - avatarDescription = avatarDescription.replace('

', '') - avatarDescription = avatarDescription.replace('

', '') - if '<' in avatarDescription: - avatarDescription = removeHtml(avatarDescription) - profileStr = '
\n' - profileStr += '
\n' - if avatarUrl: - profileStr += \ - ' ' + avatarDescription + '\n' - profileStr += '

' + displayName + '

\n' - profileStr += '

@' + searchNickname + '@' + \ - searchDomainFull + '

\n' - profileStr += '

' + profileDescriptionShort + '

\n' - profileStr += '
\n' - profileStr += '
\n' - profileStr += '
\n' - profileStr += '
\n' - profileStr += '
\n' - profileStr += \ - ' \n' - profileStr += \ - ' \n' - profileStr += \ - ' \n' - profileStr += \ - ' \n' - profileStr += '
\n' - profileStr += '
\n' - profileStr += '
\n' - - iconsDir = getIconsDir(baseDir) - i = 0 - for item in parseUserFeed(session, outboxUrl, asHeader, - projectVersion, httpPrefix, domain): - if not item.get('type'): - continue - if item['type'] != 'Create' and item['type'] != 'Announce': - continue - if not item.get('object'): - continue - profileStr += \ - individualPostAsHtml(True, recentPostsCache, maxRecentPosts, - iconsDir, translate, None, baseDir, - session, cachedWebfingers, personCache, - nickname, domain, port, - item, avatarUrl, False, False, - httpPrefix, projectVersion, 'inbox', - YTReplacementDomain, - showPublishedDateOnly, - False, False, False, False, False) - i += 1 - if i >= 20: - break - - return htmlHeader(cssFilename, profileStyle) + profileStr + htmlFooter()