Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon into main

merge-requests/30/head
Bob Mottram 2020-11-09 23:29:10 +00:00
commit 8193d8ab7c
41 changed files with 9681 additions and 9327 deletions

27
blog.py
View File

@ -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 += '<h1><a href="' + messageLink + '">' + \
blogStr += '<article><h1><a href="' + messageLink + '">' + \
titleStr + '</a></h1>\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 += '<br>' + contentStr + '\n'
if articleAdded:
blogStr += '<br>' + contentStr + '</article>\n'
else:
blogStr += '<br><article>' + contentStr + '</article>\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 = ' <item>'
rssStr += ' <title>' + titleStr + '</title>'
rssStr += ' <link>' + messageLink + '</link>'
rssStr += \
' <description>' + description + '</description>'
rssStr += ' <pubDate>' + rssDateStr + '</pubDate>'
rssStr += ' </item>'
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

101
daemon.py
View File

@ -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'

View File

@ -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

23
feeds.py 100644
View File

@ -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 = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"
rssStr += "<rss version=\"2.0\">"
rssStr += '<channel>'
rssStr += ' <title>#' + hashtag + '</title>'
rssStr += ' <link>' + httpPrefix + '://' + domainFull + \
'/tags/rss2/' + hashtag + '</link>'
return rssStr
def rss2TagFooter() -> str:
rssStr = '</channel>'
rssStr += '</rss>'
return rssStr

View File

@ -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

View File

@ -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:

View File

@ -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('<![CDATA['):
rssDescription = rssDescription.replace('<![CDATA[', '')
rssDescription = rssDescription.replace(']]>', '')
rssDescription = rssDescription.replace(']]', '')
if '&' in rssDescription:
rssDescription = html.unescape(rssDescription)
rssDescription = '<p>' + rssDescription + '<p>'
@ -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

View File

@ -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 += '<item>\n'
rssStr += ' <title>' + fields[0] + '</title>\n'
description = firstParagraphFromString(fields[4])
rssStr += ' <description>' + description + '</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 += ' <link>' + url + '</link>\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'],

View File

@ -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

View File

@ -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

View File

@ -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 = \
'<p><a href="https://somesite.com/somepath">This is a test</a></p>' + \
'<p>This is another paragraph</p>'
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()

View File

@ -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 لمدونتك"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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 dalt"
"Choose newswire items referenced in your article": "Roghnaigh míreanna sreanga nuachta dá dtagraítear i dalt",
"RSS feed for your blog": "Fotha RSS do do bhlag"
}

View File

@ -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 आपके ब्लॉग के लिए फ़ीड करता है"
}

View File

@ -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"
}

View File

@ -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フィード"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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-канал для вашего блога"
}

View File

@ -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供稿"
}

View File

@ -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 '<p>' not in content or '</p>' not in content:
return removeHtml(content)
paragraph = content.split('<p>')[1]
if '</p>' in paragraph:
paragraph = paragraph.split('</p>')[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

771
webapp.py 100644
View File

@ -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 += \
'<h3>@' + followingAddress + '</h3>'
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 += \
'<center><h1>' + \
translate['Moderation Information'] + \
'</h1></center>'
infoShown = False
suspendedFilename = baseDir + '/accounts/suspended.txt'
if os.path.isfile(suspendedFilename):
with open(suspendedFilename, "r") as f:
suspendedStr = f.read()
infoForm += '<div class="container">'
infoForm += ' <br><b>' + \
translate['Suspended accounts'] + '</b>'
infoForm += ' <br>' + \
translate['These are currently suspended']
infoForm += \
' <textarea id="message" ' + \
'name="suspended" style="height:200px">' + \
suspendedStr + '</textarea>'
infoForm += '</div>'
infoShown = True
blockingFilename = baseDir + '/accounts/blocking.txt'
if os.path.isfile(blockingFilename):
with open(blockingFilename, "r") as f:
blockedStr = f.read()
infoForm += '<div class="container">'
infoForm += \
' <br><b>' + \
translate['Blocked accounts and hashtags'] + '</b>'
infoForm += \
' <br>' + \
translate[msgStr1]
infoForm += \
' <textarea id="message" ' + \
'name="blocked" style="height:700px">' + \
blockedStr + '</textarea>'
infoForm += '</div>'
infoShown = True
if not infoShown:
infoForm += \
'<center><p>' + \
translate[msgStr2] + \
'</p></center>'
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 += \
'<form enctype="multipart/form-data" method="POST" ' + \
'accept-charset="UTF-8" action="' + path + '/newseditdata">\n'
editNewsPostForm += \
' <div class="vertical-center">\n'
editNewsPostForm += \
' <p class="new-post-text">' + translate['Edit News Post'] + '</p>'
editNewsPostForm += \
' <div class="container">\n'
editNewsPostForm += \
' <a href="' + pathOriginal + '/tlnews">' + \
'<button class="cancelbtn">' + translate['Go Back'] + '</button></a>\n'
editNewsPostForm += \
' <input type="submit" name="submitEditedNewsPost" value="' + \
translate['Submit'] + '">\n'
editNewsPostForm += \
' </div>\n'
editNewsPostForm += \
'<div class="container">'
editNewsPostForm += \
' <input type="hidden" name="newsPostUrl" value="' + \
postUrl + '">\n'
newsPostTitle = postJsonObject['object']['summary']
editNewsPostForm += \
' <input type="text" name="newsPostTitle" value="' + \
newsPostTitle + '"><br>\n'
newsPostContent = postJsonObject['object']['content']
editNewsPostForm += \
' <textarea id="message" name="editedNewsPost" ' + \
'style="height:600px">' + newsPostContent + '</textarea>'
editNewsPostForm += \
'</div>'
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 = \
'<p class="login-text">' + \
translate['Welcome. Please enter your login details below.'] + \
'</p>'
else:
loginText = \
'<p class="login-text">' + \
translate['Please enter some credentials'] + '</p>'
loginText += \
'<p class="login-text">' + \
translate['You will become the admin of this site.'] + \
'</p>'
if os.path.isfile(baseDir + '/accounts/login.txt'):
# custom login message
with open(baseDir + '/accounts/login.txt', 'r') as file:
loginText = '<p class="login-text">' + file.read() + '</p>'
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 = \
'<p class="login-text">' + \
translate[idx] + \
'</p>'
registerButtonStr = \
'<button type="submit" name="register">Register</button>'
TOSstr = \
'<p class="login-text"><a href="/terms">' + \
translate['Terms of Service'] + '</a></p>'
TOSstr += \
'<p class="login-text"><a href="/about">' + \
translate['About this Instance'] + '</a></p>'
loginButtonStr = ''
if accounts > 0:
loginButtonStr = \
'<button type="submit" name="submit">' + \
translate['Login'] + '</button>'
autocompleteStr = ''
if not autocomplete:
autocompleteStr = 'autocomplete="off" value=""'
loginForm = htmlHeader(cssFilename, loginCSS)
loginForm += '<br>\n'
loginForm += '<form method="POST" action="/login">\n'
loginForm += ' <div class="imgcontainer">\n'
loginForm += \
' <img loading="lazy" src="' + loginImage + \
'" alt="login image" class="loginimage">\n'
loginForm += loginText + TOSstr + '\n'
loginForm += ' </div>\n'
loginForm += '\n'
loginForm += ' <div class="container">\n'
loginForm += ' <label for="nickname"><b>' + \
translate['Nickname'] + '</b></label>\n'
loginForm += \
' <input type="text" ' + autocompleteStr + ' placeholder="' + \
translate['Enter Nickname'] + '" name="username" required autofocus>\n'
loginForm += '\n'
loginForm += ' <label for="password"><b>' + \
translate['Password'] + '</b></label>\n'
loginForm += \
' <input type="password" ' + autocompleteStr + \
' placeholder="' + translate['Enter Password'] + \
'" name="password" required>\n'
loginForm += loginButtonStr + registerButtonStr + '\n'
loginForm += ' </div>\n'
loginForm += '</form>\n'
loginForm += \
'<a href="https://gitlab.com/bashrc2/epicyon">' + \
'<img loading="lazy" class="license" title="' + \
translate['Get the source code'] + '" alt="' + \
translate['Get the source code'] + '" src="/icons/agpl.png" /></a>\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 += '<div class="container">' + TOSText + '</div>\n'
if adminNickname:
adminActor = httpPrefix + '://' + domainFull + \
'/users/' + adminNickname
TOSForm += \
'<div class="container"><center>\n' + \
'<p class="administeredby">Administered by <a href="' + \
adminActor + '">' + adminNickname + '</a></p>\n' + \
'</center></div>\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 += '<div class="container">' + aboutText + '</div>'
if onionDomain:
aboutForm += \
'<div class="container"><center>\n' + \
'<p class="administeredby">' + \
'http://' + onionDomain + '</p>\n</center></div>\n'
if adminNickname:
adminActor = '/users/' + adminNickname
aboutForm += \
'<div class="container"><center>\n' + \
'<p class="administeredby">Administered by <a href="' + \
adminActor + '">' + adminNickname + '</a></p>\n' + \
'</center></div>\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 += '<div><center>\n'
blockedHashtagForm += \
' <p class="screentitle">' + \
translate['Hashtag Blocked'] + '</p>\n'
blockedHashtagForm += \
' <p>See <a href="/terms">' + \
translate['Terms of Service'] + '</a></p>\n'
blockedHashtagForm += '</center></div>\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 += '<div><center>\n'
suspendedForm += ' <p class="screentitle">Account Suspended</p>\n'
suspendedForm += ' <p>See <a href="/terms">Terms of Service</a></p>\n'
suspendedForm += '</center></div>\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 += '<div class="follow">\n'
sharesStr += ' <div class="followAvatar">\n'
sharesStr += ' <center>\n'
if sharedItemImageUrl:
sharesStr += ' <img loading="lazy" src="' + \
sharedItemImageUrl + '"/>\n'
sharesStr += \
' <p class="followText">' + translate['Remove'] + \
' ' + sharedItemDisplayName + ' ?</p>\n'
postActor = getAltPath(actor, domainFull, callingDomain)
sharesStr += ' <form method="POST" action="' + postActor + '/rmshare">\n'
sharesStr += \
' <input type="hidden" name="actor" value="' + actor + '">\n'
sharesStr += ' <input type="hidden" name="shareName" value="' + \
shareName + '">\n'
sharesStr += \
' <button type="submit" class="button" name="submitYes">' + \
translate['Yes'] + '</button>\n'
sharesStr += \
' <a href="' + actor + '/inbox' + '"><button class="button">' + \
translate['No'] + '</button></a>\n'
sharesStr += ' </form>\n'
sharesStr += ' </center>\n'
sharesStr += ' </div>\n'
sharesStr += '</div>\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 += '<center>'
deletePostStr += \
' <p class="followText">' + \
translate['Delete this post?'] + '</p>'
postActor = getAltPath(actor, domainFull, callingDomain)
deletePostStr += \
' <form method="POST" action="' + postActor + '/rmpost">\n'
deletePostStr += \
' <input type="hidden" name="pageNumber" value="' + \
str(pageNumber) + '">\n'
deletePostStr += \
' <input type="hidden" name="messageId" value="' + \
messageId + '">\n'
deletePostStr += \
' <button type="submit" class="button" name="submitYes">' + \
translate['Yes'] + '</button>\n'
deletePostStr += \
' <a href="' + actor + '/inbox"><button class="button">' + \
translate['No'] + '</button></a>\n'
deletePostStr += ' </form>\n'
deletePostStr += '</center>\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 += '<div class="follow">\n'
followStr += ' <div class="followAvatar">\n'
followStr += ' <center>\n'
followStr += ' <a href="' + followActor + '">\n'
followStr += ' <img loading="lazy" src="' + followProfileUrl + '"/></a>\n'
followStr += \
' <p class="followText">' + translate['Follow'] + ' ' + \
getNicknameFromActor(followActor) + '@' + followDomain + ' ?</p>\n'
followStr += ' <form method="POST" action="' + \
originPathStr + '/followconfirm">\n'
followStr += ' <input type="hidden" name="actor" value="' + \
followActor + '">\n'
followStr += \
' <button type="submit" class="button" name="submitYes">' + \
translate['Yes'] + '</button>\n'
followStr += \
' <a href="' + originPathStr + '"><button class="button">' + \
translate['No'] + '</button></a>\n'
followStr += ' </form>\n'
followStr += '</center>\n'
followStr += '</div>\n'
followStr += '</div>\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 += '<div class="follow">\n'
followStr += ' <div class="followAvatar">\n'
followStr += ' <center>\n'
followStr += ' <a href="' + followActor + '">\n'
followStr += ' <img loading="lazy" src="' + followProfileUrl + '"/></a>\n'
followStr += \
' <p class="followText">' + translate['Stop following'] + \
' ' + getNicknameFromActor(followActor) + \
'@' + followDomain + ' ?</p>\n'
followStr += ' <form method="POST" action="' + \
originPathStr + '/unfollowconfirm">\n'
followStr += ' <input type="hidden" name="actor" value="' + \
followActor + '">\n'
followStr += \
' <button type="submit" class="button" name="submitYes">' + \
translate['Yes'] + '</button>\n'
followStr += \
' <a href="' + originPathStr + '"><button class="button">' + \
translate['No'] + '</button></a>\n'
followStr += ' </form>\n'
followStr += '</center>\n'
followStr += '</div>\n'
followStr += '</div>\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 += '<div class="block">\n'
blockStr += ' <div class="blockAvatar">\n'
blockStr += ' <center>\n'
blockStr += ' <a href="' + blockActor + '">\n'
blockStr += ' <img loading="lazy" src="' + blockProfileUrl + '"/></a>\n'
blockStr += \
' <p class="blockText">' + translate['Stop blocking'] + ' ' + \
getNicknameFromActor(blockActor) + '@' + blockDomain + ' ?</p>\n'
blockStr += ' <form method="POST" action="' + \
originPathStr + '/unblockconfirm">\n'
blockStr += ' <input type="hidden" name="actor" value="' + \
blockActor + '">\n'
blockStr += \
' <button type="submit" class="button" name="submitYes">' + \
translate['Yes'] + '</button>\n'
blockStr += \
' <a href="' + originPathStr + '"><button class="button">' + \
translate['No'] + '</button></a>\n'
blockStr += ' </form>\n'
blockStr += '</center>\n'
blockStr += '</div>\n'
blockStr += '</div>\n'
blockStr += htmlFooter()
return blockStr

398
webapp_calendar.py 100644
View File

@ -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 += \
'<center><h1>' + postTime + ' ' + str(year) + '/' + \
str(monthNumber) + \
'/' + str(dayNumber) + '</h1></center>'
deletePostStr += '<center>'
deletePostStr += ' <p class="followText">' + \
translate['Delete this event'] + '</p>'
postActor = getAltPath(actor, domainFull, callingDomain)
deletePostStr += \
' <form method="POST" action="' + postActor + '/rmpost">\n'
deletePostStr += ' <input type="hidden" name="year" value="' + \
str(year) + '">\n'
deletePostStr += ' <input type="hidden" name="month" value="' + \
str(monthNumber) + '">\n'
deletePostStr += ' <input type="hidden" name="day" value="' + \
str(dayNumber) + '">\n'
deletePostStr += \
' <input type="hidden" name="pageNumber" value="1">\n'
deletePostStr += \
' <input type="hidden" name="messageId" value="' + \
messageId + '">\n'
deletePostStr += \
' <button type="submit" class="button" name="submitYes">' + \
translate['Yes'] + '</button>\n'
deletePostStr += \
' <a href="' + actor + '/calendar?year=' + \
str(year) + '?month=' + \
str(monthNumber) + '"><button class="button">' + \
translate['No'] + '</button></a>\n'
deletePostStr += ' </form>\n'
deletePostStr += '</center>\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 += '<main><table class="calendar">\n'
calendarStr += '<caption class="calendar__banner--month">\n'
calendarStr += \
' <a href="' + calActor + '/calendar?year=' + str(year) + \
'?month=' + str(monthNumber) + '">\n'
calendarStr += \
' <h1>' + str(dayNumber) + ' ' + monthName + \
'</h1></a><br><span class="year">' + str(year) + '</span>\n'
calendarStr += '</caption>\n'
calendarStr += '<tbody>\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 = \
'<td class="calendar__day__icons"><a href="' + calActor + \
'/eventdelete?id=' + postId + '?year=' + str(year) + \
'?month=' + str(monthNumber) + '?day=' + str(dayNumber) + \
'?time=' + eventTime + \
'">\n<img class="calendardayicon" loading="lazy" alt="' + \
translate['Delete this event'] + ' |" title="' + \
translate['Delete this event'] + '" src="/' + \
iconsDir + '/delete.png" /></a></td>\n'
if eventTime and eventDescription and eventPlace:
calendarStr += \
'<tr><td class="calendar__day__time"><b>' + eventTime + \
'</b></td><td class="calendar__day__event">' + \
'<span class="place">' + \
eventPlace + '</span><br>' + eventDescription + \
'</td>' + deleteButtonStr + '</tr>\n'
elif eventTime and eventDescription and not eventPlace:
calendarStr += \
'<tr><td class="calendar__day__time"><b>' + eventTime + \
'</b></td><td class="calendar__day__event">' + \
eventDescription + '</td>' + deleteButtonStr + '</tr>\n'
elif not eventTime and eventDescription and not eventPlace:
calendarStr += \
'<tr><td class="calendar__day__time">' + \
'</td><td class="calendar__day__event">' + \
eventDescription + '</td>' + deleteButtonStr + '</tr>\n'
elif not eventTime and eventDescription and eventPlace:
calendarStr += \
'<tr><td class="calendar__day__time"></td>' + \
'<td class="calendar__day__event"><span class="place">' + \
eventPlace + '</span><br>' + eventDescription + \
'</td>' + deleteButtonStr + '</tr>\n'
elif eventTime and not eventDescription and eventPlace:
calendarStr += \
'<tr><td class="calendar__day__time"><b>' + eventTime + \
'</b></td><td class="calendar__day__event">' + \
'<span class="place">' + \
eventPlace + '</span></td>' + \
deleteButtonStr + '</tr>\n'
calendarStr += '</tbody>\n'
calendarStr += '</table></main>\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 += '<main><table class="calendar">\n'
calendarStr += '<caption class="calendar__banner--month">\n'
calendarStr += \
' <a href="' + calActor + '/calendar?year=' + str(prevYear) + \
'?month=' + str(prevMonthNumber) + '">'
calendarStr += \
' <img loading="lazy" alt="' + translate['Previous month'] + \
'" title="' + translate['Previous month'] + '" src="/' + iconsDir + \
'/prev.png" class="buttonprev"/></a>\n'
calendarStr += ' <a href="' + calActor + '/inbox" title="'
calendarStr += translate['Switch to timeline view'] + '">'
calendarStr += ' <h1>' + monthName + '</h1></a>\n'
calendarStr += \
' <a href="' + calActor + '/calendar?year=' + str(nextYear) + \
'?month=' + str(nextMonthNumber) + '">'
calendarStr += \
' <img loading="lazy" alt="' + translate['Next month'] + \
'" title="' + translate['Next month'] + '" src="/' + iconsDir + \
'/prev.png" class="buttonnext"/></a>\n'
calendarStr += '</caption>\n'
calendarStr += '<thead>\n'
calendarStr += '<tr>\n'
calendarStr += ' <th class="calendar__day__header">' + \
translate['Sun'] + '</th>\n'
calendarStr += ' <th class="calendar__day__header">' + \
translate['Mon'] + '</th>\n'
calendarStr += ' <th class="calendar__day__header">' + \
translate['Tue'] + '</th>\n'
calendarStr += ' <th class="calendar__day__header">' + \
translate['Wed'] + '</th>\n'
calendarStr += ' <th class="calendar__day__header">' + \
translate['Thu'] + '</th>\n'
calendarStr += ' <th class="calendar__day__header">' + \
translate['Fri'] + '</th>\n'
calendarStr += ' <th class="calendar__day__header">' + \
translate['Sat'] + '</th>\n'
calendarStr += '</tr>\n'
calendarStr += '</thead>\n'
calendarStr += '<tbody>\n'
dayOfMonth = 0
dow = weekDayOfMonthStart(monthNumber, year)
for weekOfMonth in range(1, 7):
if dayOfMonth == daysInMonth:
continue
calendarStr += ' <tr>\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 = '<a href="' + url + '">' + \
str(dayOfMonth) + '</a>'
# there are events for this day
if not isToday:
calendarStr += \
' <td class="calendar__day__cell" ' + \
'data-event="">' + \
dayLink + '</td>\n'
else:
calendarStr += \
' <td class="calendar__day__cell" ' + \
'data-today-event="">' + \
dayLink + '</td>\n'
else:
# No events today
if not isToday:
calendarStr += \
' <td class="calendar__day__cell">' + \
str(dayOfMonth) + '</td>\n'
else:
calendarStr += \
' <td class="calendar__day__cell" ' + \
'data-today="">' + str(dayOfMonth) + '</td>\n'
else:
calendarStr += ' <td class="calendar__day__cell"></td>\n'
calendarStr += ' </tr>\n'
calendarStr += '</tbody>\n'
calendarStr += '</table></main>\n'
calendarStr += htmlFooter()
return calendarStr

View File

@ -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 <center>\n' + \
' <img class="leftColImg" ' + \
'loading="lazy" src="/users/' + \
nickname + '/' + leftImageFile + '" />\n' + \
' </center>\n'
if showBackButton:
htmlStr += \
' <div>' + \
' <a href="' + timelinePath + '">' + \
'<button class="cancelbtn">' + \
translate['Go Back'] + '</button></a>\n'
if (editor or rssIconAtTop) and not showHeaderImage:
htmlStr += '<div class="columnIcons">'
if editImageClass == 'leftColEdit':
htmlStr += '\n <center>\n'
htmlStr += ' <div class="leftColIcons">\n'
if editor:
# show the edit icon
htmlStr += \
' <a href="' + \
'/users/' + nickname + '/editlinks">' + \
'<img class="' + editImageClass + \
'" loading="lazy" alt="' + \
translate['Edit Links'] + '" title="' + \
translate['Edit Links'] + '" src="/' + \
iconsDir + '/edit.png" /></a>\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 = \
' <a href="' + rssUrl + '">' + \
'<img class="' + editImageClass + \
'" loading="lazy" alt="' + rssTitle + \
'" title="' + rssTitle + \
'" src="/' + iconsDir + '/logorss.png" /></a>\n'
if rssIconAtTop:
htmlStr += rssIconStr
htmlStr += ' </div>\n'
if editImageClass == 'leftColEdit':
htmlStr += ' </center>\n'
if (editor or rssIconAtTop) and not showHeaderImage:
htmlStr += '</div><br>'
# if showHeaderImage:
# htmlStr += '<br>'
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 += \
' <p><a href="' + linkStr + '">' + \
lineStr + '</a></p>\n'
linksFileContainsEntries = True
else:
if lineStr.startswith('#') or lineStr.startswith('*'):
lineStr = lineStr[1:].strip()
htmlStr += separatorStr
htmlStr += \
' <h3 class="linksHeader">' + \
lineStr + '</h3>\n'
else:
htmlStr += \
' <p>' + lineStr + '</p>\n'
linksFileContainsEntries = True
if linksFileContainsEntries and not rssIconAtTop:
htmlStr += '<br><div class="columnIcons">' + rssIconStr + '</div>'
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 += \
'<a href="/users/' + nickname + '/' + defaultTimeline + '">' + \
'<img loading="lazy" class="timeline-banner" ' + \
'src="/users/' + nickname + '/' + bannerFile + '" /></a>\n'
htmlStr += '<center>' + \
headerButtonsFrontScreen(translate, nickname,
'links', authorized,
iconsAsButtons, iconsDir) + '</center>'
htmlStr += \
getLeftColumnContent(baseDir, nickname, domainFull,
httpPrefix, translate,
iconsDir, editor,
False, timelinePath,
rssIconAtTop, False, False)
htmlStr += '</div>\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 += \
'<a href="/users/' + nickname + '/' + defaultTimeline + '" title="' + \
translate['Switch to timeline view'] + '" alt="' + \
translate['Switch to timeline view'] + '">\n'
editLinksForm += '<img loading="lazy" class="timeline-banner" src="' + \
'/users/' + nickname + '/' + bannerFile + '" /></a>\n'
editLinksForm += \
'<form enctype="multipart/form-data" method="POST" ' + \
'accept-charset="UTF-8" action="' + path + '/linksdata">\n'
editLinksForm += \
' <div class="vertical-center">\n'
editLinksForm += \
' <p class="new-post-text">' + translate['Edit Links'] + '</p>'
editLinksForm += \
' <div class="container">\n'
# editLinksForm += \
# ' <a href="' + pathOriginal + '"><button class="cancelbtn">' + \
# translate['Go Back'] + '</button></a>\n'
editLinksForm += \
' <center>\n' + \
' <input type="submit" name="submitLinks" value="' + \
translate['Submit'] + '">\n' + \
' </center>\n'
editLinksForm += \
' </div>\n'
linksFilename = baseDir + '/accounts/links.txt'
linksStr = ''
if os.path.isfile(linksFilename):
with open(linksFilename, 'r') as fp:
linksStr = fp.read()
editLinksForm += \
'<div class="container">'
editLinksForm += \
' ' + \
translate['One link per line. Description followed by the link.'] + \
'<br>'
editLinksForm += \
' <textarea id="message" name="editedLinks" style="height:80vh">' + \
linksStr + '</textarea>'
editLinksForm += \
'</div>'
editLinksForm += htmlFooter()
return editLinksForm

View File

@ -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 = \
' <a href="' + \
'/users/' + nickname + '/newblog" ' + \
'title="' + translate['Publish a news article'] + '">' + \
'<button class="publishbtn">' + \
translate['Publish'] + '</button></a>\n'
else:
# if not logged in then replace the publish button with
# a login button
publishButtonStr = \
' <a href="/login"><button class="publishbtn">' + \
translate['Login'] + '</button></a>\n'
# show publish button at the top if needed
if publishButtonAtTop:
htmlStr += '<center>' + publishButtonStr + '</center>'
# 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 <center>\n' + \
' <img class="rightColImg" ' + \
'loading="lazy" src="/users/' + \
nickname + '/' + rightImageFile + '" />\n' + \
' </center>\n'
if (showPublishButton or editor or rssIconAtTop) and not showHeaderImage:
htmlStr += '<div class="columnIcons">'
if editImageClass == 'rightColEdit':
htmlStr += '\n <center>\n'
# whether to show a back icon
# This is probably going to be osolete soon
if showBackButton:
htmlStr += \
' <a href="' + timelinePath + '">' + \
'<button class="cancelbtn">' + \
translate['Go Back'] + '</button></a>\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 += \
' <a href="' + \
'/users/' + nickname + '/editnewswire">' + \
'<img class="' + editImageClass + \
'" loading="lazy" alt="' + \
translate['Edit newswire'] + '" title="' + \
translate['Edit newswire'] + '" src="/' + \
iconsDir + '/edit_notify.png" /></a>\n'
else:
# show the edit icon
htmlStr += \
' <a href="' + \
'/users/' + nickname + '/editnewswire">' + \
'<img class="' + editImageClass + \
'" loading="lazy" alt="' + \
translate['Edit newswire'] + '" title="' + \
translate['Edit newswire'] + '" src="/' + \
iconsDir + '/edit.png" /></a>\n'
# show the RSS icon
rssIconStr = \
' <a href="/newswire.xml">' + \
'<img class="' + editImageClass + \
'" loading="lazy" alt="' + \
translate['Newswire RSS Feed'] + '" title="' + \
translate['Newswire RSS Feed'] + '" src="/' + \
iconsDir + '/logorss.png" /></a>\n'
if rssIconAtTop:
htmlStr += rssIconStr
# show publish icon at top
if showPublishButton:
if showPublishAsIcon:
htmlStr += \
' <a href="' + \
'/users/' + nickname + '/newblog">' + \
'<img class="' + editImageClass + \
'" loading="lazy" alt="' + \
translate['Publish a news article'] + '" title="' + \
translate['Publish a news article'] + '" src="/' + \
iconsDir + '/publish.png" /></a>\n'
if editImageClass == 'rightColEdit':
htmlStr += ' </center>\n'
else:
if showHeaderImage:
htmlStr += ' <br>\n'
if (showPublishButton or editor or rssIconAtTop) and not showHeaderImage:
htmlStr += '</div><br>'
# 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 += '<br><div class="columnIcons">' + rssIconStr + '</div>'
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', '<br>')
htmlStr += '<p class="newswireItemVotedOn">' + \
'<a href="' + item[1] + '">' + \
'<span class="newswireItemVotedOn">' + title + \
'</span></a>' + totalVotesStr
if moderator:
htmlStr += \
' ' + dateShown + '<a href="/users/' + nickname + \
'/newswireunvote=' + dateStrLink + '" ' + \
'title="' + translate['Remove Vote'] + '">'
htmlStr += '<img loading="lazy" class="voteicon" src="/' + \
iconsDir + '/vote.png" /></a></p>\n'
else:
htmlStr += ' <span class="newswireDateVotedOn">'
htmlStr += dateShown + '</span></p>\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', '<br>')
if moderator and moderatedItem:
htmlStr += '<p class="newswireItemModerated">' + \
'<a href="' + item[1] + '">' + \
title + '</a>' + totalVotesStr
htmlStr += ' ' + dateShown
htmlStr += '<a href="/users/' + nickname + \
'/newswirevote=' + dateStrLink + '" ' + \
'title="' + translate['Vote'] + '">'
htmlStr += '<img class="voteicon" src="/' + \
iconsDir + '/vote.png" /></a>'
htmlStr += '</p>\n'
else:
htmlStr += '<p class="newswireItem">' + \
'<a href="' + item[1] + '">' + \
title + '</a>' + \
totalVotesStr
htmlStr += ' <span class="newswireDate">'
htmlStr += dateShown + '</span></p>\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 += \
'<a href="/users/' + nickname + '/newblog" title="' + \
translate['Go Back'] + '" alt="' + \
translate['Go Back'] + '">\n'
htmlStr += '<img loading="lazy" class="timeline-banner" src="' + \
'/users/' + nickname + '/' + bannerFile + '" /></a>\n'
htmlStr += \
'<form enctype="multipart/form-data" method="POST" ' + \
'accept-charset="UTF-8" action="/users/' + nickname + \
'/citationsdata">\n'
htmlStr += ' <center>\n'
htmlStr += translate['Choose newswire items ' +
'referenced in your article'] + '<br>'
if blogTitle is None:
blogTitle = ''
htmlStr += \
' <input type="hidden" name="blogTitle" value="' + \
blogTitle + '">\n'
if blogContent is None:
blogContent = ''
htmlStr += \
' <input type="hidden" name="blogContent" value="' + \
blogContent + '">\n'
# submit button
htmlStr += \
' <input type="submit" name="submitCitations" value="' + \
translate['Submit'] + '">\n'
htmlStr += ' </center>\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', '<br>')
link = item[1]
citationValue = \
dateStr + citationsSeparator + \
title + citationsSeparator + \
link
htmlStr += \
'<input type="checkbox" name="newswire' + str(ctr) + \
'" value="' + citationValue + '"' + selectedStr + '/>' + \
'<a href="' + link + '"><cite>' + title + '</cite></a> '
htmlStr += '<span class="newswireDate">' + \
dateShown + '</span><br>\n'
ctr += 1
htmlStr += '</form>\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 += \
'<a href="/users/' + nickname + '/' + defaultTimeline + '">' + \
'<img loading="lazy" class="timeline-banner" ' + \
'src="/users/' + nickname + '/' + bannerFile + '" /></a>\n'
htmlStr += '<center>' + \
headerButtonsFrontScreen(translate, nickname,
'newswire', authorized,
iconsAsButtons, iconsDir) + '</center>'
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 += \
'<a href="/users/' + nickname + '/' + defaultTimeline + '" title="' + \
translate['Switch to timeline view'] + '" alt="' + \
translate['Switch to timeline view'] + '">\n'
editNewswireForm += '<img loading="lazy" class="timeline-banner" src="' + \
'/users/' + nickname + '/' + bannerFile + '" /></a>\n'
editNewswireForm += \
'<form enctype="multipart/form-data" method="POST" ' + \
'accept-charset="UTF-8" action="' + path + '/newswiredata">\n'
editNewswireForm += \
' <div class="vertical-center">\n'
editNewswireForm += \
' <p class="new-post-text">' + translate['Edit newswire'] + '</p>'
editNewswireForm += \
' <div class="container">\n'
# editNewswireForm += \
# ' <a href="' + pathOriginal + '"><button class="cancelbtn">' + \
# translate['Go Back'] + '</button></a>\n'
editNewswireForm += \
' <center>\n' + \
' <input type="submit" name="submitNewswire" value="' + \
translate['Submit'] + '">\n' + \
' </center>\n'
editNewswireForm += \
' </div>\n'
newswireFilename = baseDir + '/accounts/newswire.txt'
newswireStr = ''
if os.path.isfile(newswireFilename):
with open(newswireFilename, 'r') as fp:
newswireStr = fp.read()
editNewswireForm += \
'<div class="container">'
editNewswireForm += \
' ' + \
translate['Add RSS feed links below.'] + \
'<br>'
editNewswireForm += \
' <textarea id="message" name="editedNewswire" ' + \
'style="height:80vh">' + newswireStr + '</textarea>'
filterStr = ''
filterFilename = \
baseDir + '/accounts/news@' + domain + '/filters.txt'
if os.path.isfile(filterFilename):
with open(filterFilename, 'r') as filterfile:
filterStr = filterfile.read()
editNewswireForm += \
' <br><b><label class="labels">' + \
translate['Filtered words'] + '</label></b>\n'
editNewswireForm += ' <br><label class="labels">' + \
translate['One per line'] + '</label>'
editNewswireForm += ' <textarea id="message" ' + \
'name="filteredWordsNewswire" style="height:50vh">' + \
filterStr + '</textarea>\n'
hashtagRulesStr = ''
hashtagRulesFilename = \
baseDir + '/accounts/hashtagrules.txt'
if os.path.isfile(hashtagRulesFilename):
with open(hashtagRulesFilename, 'r') as rulesfile:
hashtagRulesStr = rulesfile.read()
editNewswireForm += \
' <br><b><label class="labels">' + \
translate['News tagging rules'] + '</label></b>\n'
editNewswireForm += ' <br><label class="labels">' + \
translate['One per line'] + '.</label>\n'
editNewswireForm += \
' <a href="' + \
'https://gitlab.com/bashrc2/epicyon/-/raw/main/hashtagrules.txt' + \
'">' + translate['See instructions'] + '</a>\n'
editNewswireForm += ' <textarea id="message" ' + \
'name="hashtagRulesList" style="height:80vh">' + \
hashtagRulesStr + '</textarea>\n'
editNewswireForm += \
'</div>'
editNewswireForm += htmlFooter()
return editNewswireForm

View File

@ -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 = '<datalist id="followingHandles">\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 += \
'<option>@' + followingAddress + '</option>\n'
listStr += '</datalist>\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 = '<div class="newPostDropdown">\n'
dropDownContent += ' <input type="checkbox" ' + \
'id="my-newPostDropdown" value="" name="my-checkbox">\n'
dropDownContent += ' <label for="my-newPostDropdown"\n'
dropDownContent += ' data-toggle="newPostDropdown">\n'
dropDownContent += ' <img loading="lazy" alt="" title="" src="/' + \
iconsDir + '/' + scopeIcon + '"/><b>' + \
scopeDescription + '</b></label>\n'
dropDownContent += ' <ul>\n'
if showPublicOnDropdown:
dropDownContent += \
'<li><a href="' + pathBase + dropdownNewPostSuffix + \
'"><img loading="lazy" alt="" title="" src="/' + \
iconsDir + '/scope_public.png"/><b>' + \
translate['Public'] + '</b><br>' + \
translate['Visible to anyone'] + '</a></li>\n'
if defaultTimeline == 'tlnews':
dropDownContent += \
'<li><a href="' + pathBase + dropdownNewBlogSuffix + \
'"><img loading="lazy" alt="" title="" src="/' + \
iconsDir + '/scope_blog.png"/><b>' + \
translate['Article'] + '</b><br>' + \
translate['Create an article'] + '</a></li>\n'
else:
dropDownContent += \
'<li><a href="' + pathBase + dropdownNewBlogSuffix + \
'"><img loading="lazy" alt="" title="" src="/' + \
iconsDir + '/scope_blog.png"/><b>' + \
translate['Blog'] + '</b><br>' + \
translate['Publicly visible post'] + '</a></li>\n'
dropDownContent += \
'<li><a href="' + pathBase + dropdownUnlistedSuffix + \
'"><img loading="lazy" alt="" title="" src="/' + \
iconsDir + '/scope_unlisted.png"/><b>' + \
translate['Unlisted'] + '</b><br>' + \
translate['Not on public timeline'] + '</a></li>\n'
dropDownContent += \
'<li><a href="' + pathBase + dropdownFollowersSuffix + \
'"><img loading="lazy" alt="" title="" src="/' + \
iconsDir + '/scope_followers.png"/><b>' + \
translate['Followers'] + '</b><br>' + \
translate['Only to followers'] + '</a></li>\n'
dropDownContent += \
'<li><a href="' + pathBase + dropdownDMSuffix + \
'"><img loading="lazy" alt="" title="" src="/' + \
iconsDir + '/scope_dm.png"/><b>' + \
translate['DM'] + '</b><br>' + \
translate['Only to mentioned people'] + '</a></li>\n'
dropDownContent += \
'<li><a href="' + pathBase + dropdownReminderSuffix + \
'"><img loading="lazy" alt="" title="" src="/' + \
iconsDir + '/scope_reminder.png"/><b>' + \
translate['Reminder'] + '</b><br>' + \
translate['Scheduled note to yourself'] + '</a></li>\n'
dropDownContent += \
'<li><a href="' + pathBase + dropdownEventSuffix + \
'"><img loading="lazy" alt="" title="" src="/' + \
iconsDir + '/scope_event.png"/><b>' + \
translate['Event'] + '</b><br>' + \
translate['Create an event'] + '</a></li>\n'
dropDownContent += \
'<li><a href="' + pathBase + dropdownReportSuffix + \
'"><img loading="lazy" alt="" title="" src="/' + \
iconsDir + '/scope_report.png"/><b>' + \
translate['Report'] + '</b><br>' + \
translate['Send to moderators'] + '</a></li>\n'
if not replyStr:
dropDownContent += \
'<li><a href="' + pathBase + \
'/newshare"><img loading="lazy" alt="" title="" src="/' + \
iconsDir + '/scope_share.png"/><b>' + \
translate['Shares'] + '</b><br>' + \
translate['Describe a shared item'] + '</a></li>\n'
dropDownContent += \
'<li><a href="' + pathBase + \
'/newquestion"><img loading="lazy" alt="" title="" src="/' + \
iconsDir + '/scope_question.png"/><b>' + \
translate['Question'] + '</b><br>' + \
translate['Ask a question'] + '</a></li>\n'
dropDownContent += ' </ul>\n'
dropDownContent += '</div>\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 = '<p class="new-post-text">' + \
translate['Write your post text below.'] + '</p>\n'
else:
newPostText = \
'<p class="new-post-text">' + \
translate['Write your reply to'] + \
' <a href="' + inReplyTo + '">' + \
translate['this post'] + '</a></p>\n'
replyStr = '<input type="hidden" ' + \
'name="replyTo" value="' + inReplyTo + '">\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 = \
'<p class="new-post-text">' + \
translate['Write your report below.'] + '</p>\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 '</p>' not in customReportText:
customReportText = \
'<p class="login-subtext">' + \
customReportText + '</p>\n'
repStr = '<p class="login-subtext">'
customReportText = \
customReportText.replace('<p>', repStr)
newPostText += customReportText
idx = 'This message only goes to moderators, even if it ' + \
'mentions other fediverse addresses.'
newPostText += \
'<p class="new-post-subtext">' + translate[idx] + '</p>\n' + \
'<p class="new-post-subtext">' + translate['Also see'] + \
' <a href="/terms">' + \
translate['Terms of Service'] + '</a></p>\n'
else:
newPostText = \
'<p class="new-post-text">' + \
translate['Enter the details for your shared item below.'] + \
'</p>\n'
if path.endswith('/newquestion'):
newPostText = \
'<p class="new-post-text">' + \
translate['Enter the choices for your question below.'] + \
'</p>\n'
if os.path.isfile(baseDir + '/accounts/newpost.txt'):
with open(baseDir + '/accounts/newpost.txt', 'r') as file:
newPostText = \
'<p class="new-post-text">' + file.read() + '</p>\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 = ' <div class="container">'
if not path.endswith('/newevent'):
newPostImageSection += \
' <label class="labels">' + \
translate['Image description'] + '</label>\n'
else:
newPostImageSection += \
' <label class="labels">' + \
translate['Event banner image description'] + '</label>\n'
newPostImageSection += \
' <input type="text" name="imageDescription">\n'
if path.endswith('/newevent'):
newPostImageSection += \
' <label class="labels">' + \
translate['Banner image'] + '</label>\n'
newPostImageSection += \
' <input type="file" id="attachpic" name="attachpic"'
newPostImageSection += \
' accept=".png, .jpg, .jpeg, .gif, .webp, .avif">\n'
else:
newPostImageSection += \
' <input type="file" id="attachpic" name="attachpic"'
newPostImageSection += \
' accept=".png, .jpg, .jpeg, .gif, ' + \
'.webp, .avif, .mp4, .webm, .ogv, .mp3, .ogg">\n'
newPostImageSection += ' </div>\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 = '<div class="container">\n'
extraFields += ' <label class="labels">' + \
translate['Possible answers'] + ':</label><br>\n'
for questionCtr in range(8):
extraFields += \
' <input type="text" class="questionOption" placeholder="' + \
str(questionCtr + 1) + \
'" name="questionOption' + str(questionCtr) + '"><br>\n'
extraFields += \
' <label class="labels">' + \
translate['Duration of listing in days'] + \
':</label> <input type="number" name="duration" ' + \
'min="1" max="365" step="1" value="14"><br>\n'
extraFields += '</div>'
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 = '<div class="container">\n'
extraFields += \
' <label class="labels">' + \
translate['Type of shared item. eg. hat'] + ':</label>\n'
extraFields += \
' <input type="text" class="itemType" name="itemType">\n'
extraFields += \
' <br><label class="labels">' + \
translate['Category of shared item. eg. clothing'] + ':</label>\n'
extraFields += \
' <input type="text" class="category" name="category">\n'
extraFields += \
' <br><label class="labels">' + \
translate['Duration of listing in days'] + ':</label>\n'
extraFields += ' <input type="number" name="duration" ' + \
'min="1" max="365" step="1" value="14">\n'
extraFields += '</div>\n'
extraFields += '<div class="container">\n'
extraFields += \
'<label class="labels">' + \
translate['City or location of the shared item'] + ':</label>\n'
extraFields += '<input type="text" name="location">\n'
extraFields += '</div>\n'
citationsStr = ''
if endpoint == 'newblog':
citationsFilename = \
baseDir + '/accounts/' + \
nickname + '@' + domain + '/.citations.txt'
if os.path.isfile(citationsFilename):
citationsStr = '<div class="container">\n'
citationsStr += '<p><label class="labels">' + \
translate['Citations'] + ':</label></p>\n'
citationsStr += ' <ul>\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 += \
' <li><a href="' + link + '"><cite>' + \
title + '</cite></a></li>'
citationsStr += ' </ul>\n'
citationsStr += '</div>\n'
dateAndLocation = ''
if endpoint != 'newshare' and \
endpoint != 'newreport' and \
endpoint != 'newquestion':
dateAndLocation = '<div class="container">\n'
if endpoint == 'newevent':
# event status
dateAndLocation += '<label class="labels">' + \
translate['Status of the event'] + ':</label><br>\n'
dateAndLocation += '<input type="radio" id="tentative" ' + \
'name="eventStatus" value="tentative">\n'
dateAndLocation += '<label class="labels" for="tentative">' + \
translate['Tentative'] + '</label><br>\n'
dateAndLocation += '<input type="radio" id="confirmed" ' + \
'name="eventStatus" value="confirmed" checked>\n'
dateAndLocation += '<label class="labels" for="confirmed">' + \
translate['Confirmed'] + '</label><br>\n'
dateAndLocation += '<input type="radio" id="cancelled" ' + \
'name="eventStatus" value="cancelled">\n'
dateAndLocation += '<label class="labels" for="cancelled">' + \
translate['Cancelled'] + '</label><br>\n'
dateAndLocation += '</div>\n'
dateAndLocation += '<div class="container">\n'
# maximum attendees
dateAndLocation += '<label class="labels" ' + \
'for="maximumAttendeeCapacity">' + \
translate['Maximum attendees'] + ':</label>\n'
dateAndLocation += '<input type="number" ' + \
'id="maximumAttendeeCapacity" ' + \
'name="maximumAttendeeCapacity" min="1" max="999999" ' + \
'value="100">\n'
dateAndLocation += '</div>\n'
dateAndLocation += '<div class="container">\n'
# event joining options
dateAndLocation += '<label class="labels">' + \
translate['Joining'] + ':</label><br>\n'
dateAndLocation += '<input type="radio" id="free" ' + \
'name="joinMode" value="free" checked>\n'
dateAndLocation += '<label class="labels" for="free">' + \
translate['Anyone can join'] + '</label><br>\n'
dateAndLocation += '<input type="radio" id="restricted" ' + \
'name="joinMode" value="restricted">\n'
dateAndLocation += '<label class="labels" for="female">' + \
translate['Apply to join'] + '</label><br>\n'
dateAndLocation += '<input type="radio" id="invite" ' + \
'name="joinMode" value="invite">\n'
dateAndLocation += '<label class="labels" for="other">' + \
translate['Invitation only'] + '</label>\n'
dateAndLocation += '</div>\n'
dateAndLocation += '<div class="container">\n'
# Event posts don't allow replies - they're just an announcement.
# They also have a few more checkboxes
dateAndLocation += \
'<p><input type="checkbox" class="profilecheckbox" ' + \
'name="privateEvent"><label class="labels"> ' + \
translate['This is a private event.'] + '</label></p>\n'
dateAndLocation += \
'<p><input type="checkbox" class="profilecheckbox" ' + \
'name="anonymousParticipationEnabled">' + \
'<label class="labels"> ' + \
translate['Allow anonymous participation.'] + '</label></p>\n'
else:
dateAndLocation += \
'<p><input type="checkbox" class="profilecheckbox" ' + \
'name="commentsEnabled" checked><label class="labels"> ' + \
translate['Allow replies.'] + '</label></p>\n'
if not inReplyTo and endpoint != 'newevent':
dateAndLocation += \
'<p><input type="checkbox" class="profilecheckbox" ' + \
'name="schedulePost"><label class="labels"> ' + \
translate['This is a scheduled post.'] + '</label></p>\n'
if endpoint != 'newevent':
dateAndLocation += \
'<p><img loading="lazy" alt="" title="" ' + \
'class="emojicalendar" src="/' + \
iconsDir + '/calendar.png"/>\n'
# select a date and time for this post
dateAndLocation += '<label class="labels">' + \
translate['Date'] + ': </label>\n'
dateAndLocation += '<input type="date" name="eventDate">\n'
dateAndLocation += '<label class="labelsright">' + \
translate['Time'] + ':'
dateAndLocation += \
'<input type="time" name="eventTime"></label></p>\n'
else:
dateAndLocation += '</div>\n'
dateAndLocation += '<div class="container">\n'
dateAndLocation += \
'<p><img loading="lazy" alt="" title="" ' + \
'class="emojicalendar" src="/' + \
iconsDir + '/calendar.png"/>\n'
# select start time for the event
dateAndLocation += '<label class="labels">' + \
translate['Start Date'] + ': </label>\n'
dateAndLocation += '<input type="date" name="eventDate">\n'
dateAndLocation += '<label class="labelsright">' + \
translate['Time'] + ':'
dateAndLocation += \
'<input type="time" name="eventTime"></label></p>\n'
# select end time for the event
dateAndLocation += \
'<br><img loading="lazy" alt="" title="" ' + \
'class="emojicalendar" src="/' + \
iconsDir + '/calendar.png"/>\n'
dateAndLocation += '<label class="labels">' + \
translate['End Date'] + ': </label>\n'
dateAndLocation += '<input type="date" name="endDate">\n'
dateAndLocation += '<label class="labelsright">' + \
translate['Time'] + ':'
dateAndLocation += \
'<input type="time" name="endTime"></label>\n'
if endpoint == 'newevent':
dateAndLocation += '</div>\n'
dateAndLocation += '<div class="container">\n'
dateAndLocation += '<br><label class="labels">' + \
translate['Moderation policy or code of conduct'] + \
': </label>\n'
dateAndLocation += \
' <textarea id="message" ' + \
'name="repliesModerationOption" style="height:' + \
str(messageBoxHeight) + 'px"></textarea>\n'
dateAndLocation += '</div>\n'
dateAndLocation += '<div class="container">\n'
dateAndLocation += '<br><label class="labels">' + \
translate['Location'] + ': </label>\n'
dateAndLocation += '<input type="text" name="location">\n'
if endpoint == 'newevent':
dateAndLocation += '<br><label class="labels">' + \
translate['Ticket URL'] + ': </label>\n'
dateAndLocation += '<input type="text" name="ticketUrl">\n'
dateAndLocation += '<br><label class="labels">' + \
translate['Categories'] + ': </label>\n'
dateAndLocation += '<input type="text" name="category">\n'
dateAndLocation += '</div>\n'
newPostForm = htmlHeader(cssFilename, newPostCSS)
newPostForm += \
'<a href="/users/' + nickname + '/' + defaultTimeline + '" title="' + \
translate['Switch to timeline view'] + '" alt="' + \
translate['Switch to timeline view'] + '">\n'
newPostForm += '<img loading="lazy" class="timeline-banner" src="' + \
'/users/' + nickname + '/' + bannerFile + '" /></a>\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 += \
'<form enctype="multipart/form-data" method="POST" ' + \
'accept-charset="UTF-8" action="' + \
path + '?' + endpoint + '?page=' + str(pageNumber) + '">\n'
newPostForm += ' <div class="vertical-center">\n'
newPostForm += \
' <label for="nickname"><b>' + newPostText + '</b></label>\n'
newPostForm += ' <div class="containerNewPost">\n'
newPostForm += ' <table style="width:100%" border="0"><tr>\n'
newPostForm += '<td>' + dropDownContent + '</td>\n'
newPostForm += \
' <td><a href="' + pathBase + \
'/searchemoji"><img loading="lazy" class="emojisearch" ' + \
'src="/emoji/1F601.png" title="' + \
translate['Search for emoji'] + '" alt="' + \
translate['Search for emoji'] + '"/></a></td>\n'
newPostForm += ' </tr>\n'
newPostForm += '</table>\n'
newPostForm += ' </div>\n'
newPostForm += ' <div class="containerSubmitNewPost"><center>\n'
# newPostForm += \
# ' <a href="' + pathBase + \
# '/inbox"><button class="cancelbtn">' + \
# translate['Go Back'] + '</button></a>\n'
# for a new blog if newswire items exist then add a citations button
if newswire and path.endswith('/newblog'):
newPostForm += \
' <input type="submit" name="submitCitations" value="' + \
translate['Citations'] + '">\n'
newPostForm += \
' <input type="submit" name="submitPost" value="' + \
translate['Submit'] + '">\n'
newPostForm += ' </center></div>\n'
newPostForm += replyStr
if mediaInstance and not replyStr:
newPostForm += newPostImageSection
newPostForm += \
' <label class="labels">' + placeholderSubject + '</label><br>'
newPostForm += ' <input type="text" name="subject">'
newPostForm += ''
selectedStr = ' selected'
if inReplyTo or endpoint == 'newdm':
if inReplyTo:
newPostForm += \
' <label class="labels">' + placeholderMentions + \
'</label><br>\n'
else:
newPostForm += \
' <a href="/users/' + nickname + \
'/followingaccounts" title="' + \
translate['Show a list of addresses to send to'] + '">' \
'<label class="labels">' + \
translate['Send to'] + ':' + '</label> 📄</a><br>\n'
newPostForm += \
' <input type="text" name="mentions" ' + \
'list="followingHandles" value="' + mentionsStr + '" selected>\n'
newPostForm += \
htmlFollowingDataList(baseDir, nickname, domain, domainFull)
newPostForm += ''
selectedStr = ''
newPostForm += \
' <br><label class="labels">' + placeholderMessage + '</label>'
if mediaInstance:
messageBoxHeight = 200
if endpoint == 'newquestion':
messageBoxHeight = 100
elif endpoint == 'newblog':
messageBoxHeight = 800
newPostForm += \
' <textarea id="message" name="message" style="height:' + \
str(messageBoxHeight) + 'px"' + selectedStr + '></textarea>\n'
newPostForm += extraFields + citationsStr + dateAndLocation
if not mediaInstance or replyStr:
newPostForm += newPostImageSection
newPostForm += ' </div>\n'
newPostForm += '</form>\n'
if not reportUrl:
newPostForm = \
newPostForm.replace('<body>', '<body onload="focusOnMessage()">')
newPostForm += htmlFooter()
return newPostForm

224
webapp_media.py 100644
View File

@ -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 + "<center>\n<iframe loading=\"lazy\" " + \
"src=\"https://player.vimeo.com/video/" + \
url + "\" width=\"" + str(width) + \
"\" height=\"" + str(height) + \
"\" frameborder=\"0\" allow=\"autoplay; " + \
"fullscreen\" allowfullscreen></iframe>\n</center>\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 + "<center>\n<iframe loading=\"lazy\" src=\"" + \
videoSite + url + "\" width=\"" + str(width) + \
"\" height=\"" + str(height) + \
"\" frameborder=\"0\" allow=\"autoplay; fullscreen\" " + \
"allowfullscreen></iframe>\n</center>\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 + "<center>\n<iframe loading=\"lazy\" src=\"" + \
videoSite + url + "\" width=\"" + \
str(width) + "\" height=\"" + str(height) + \
"\" frameborder=\"0\" allow=\"autoplay; fullscreen\" " + \
"allowfullscreen></iframe>\n</center>\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 + "<center>\n<iframe loading=\"lazy\" src=\"" + \
videoSite + url + "\" width=\"" + \
str(width) + "\" height=\"" + str(height) + \
"\" frameborder=\"0\" allow=\"fullscreen\" " + \
"allowfullscreen></iframe>\n</center>\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 + "<center>\n<iframe loading=\"lazy\" " + \
"sandbox=\"allow-same-origin " + \
"allow-scripts\" src=\"https://" + \
site + url + "\" width=\"" + str(width) + \
"\" height=\"" + str(height) + \
"\" frameborder=\"0\" allow=\"autoplay; " + \
"fullscreen\" allowfullscreen></iframe>\n</center>\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 '<audio ' in content:
return content
extension = '.mp3'
if '.ogg' in content:
extension = '.ogg'
words = content.strip('\n').split(' ')
for w in words:
if extension not in w:
continue
w = w.replace('href="', '').replace('">', '')
if w.endswith('.'):
w = w[:-1]
if w.endswith('"'):
w = w[:-1]
if w.endswith(';'):
w = w[:-1]
if w.endswith(':'):
w = w[:-1]
if not w.endswith(extension):
continue
if not (w.startswith('http') or w.startswith('dat:') or
w.startswith('hyper:') or w.startswith('i2p:') or
w.startswith('gnunet:') or
'/' in w):
continue
url = w
content += '<center>\n<audio controls>\n'
content += \
'<source src="' + url + '" type="audio/' + \
extension.replace('.', '') + '">'
content += \
translate['Your browser does not support the audio element.']
content += '</audio>\n</center>\n'
return content
def addEmbeddedVideo(translate: {}, content: str,
width=400, height=300) -> str:
"""Adds embedded video for mp4/webm/ogv
"""
if not ('.mp4' in content or '.webm' in content or '.ogv' in content):
return content
if '<video ' in content:
return content
extension = '.mp4'
if '.webm' in content:
extension = '.webm'
elif '.ogv' in content:
extension = '.ogv'
words = content.strip('\n').split(' ')
for w in words:
if extension not in w:
continue
w = w.replace('href="', '').replace('">', '')
if w.endswith('.'):
w = w[:-1]
if w.endswith('"'):
w = w[:-1]
if w.endswith(';'):
w = w[:-1]
if w.endswith(':'):
w = w[:-1]
if not w.endswith(extension):
continue
if not (w.startswith('http') or w.startswith('dat:') or
w.startswith('hyper:') or w.startswith('i2p:') or
w.startswith('gnunet:') or
'/' in w):
continue
url = w
content += \
'<center>\n<video width="' + str(width) + '" height="' + \
str(height) + '" controls>\n'
content += \
'<source src="' + url + '" type="video/' + \
extension.replace('.', '') + '">\n'
content += \
translate['Your browser does not support the video element.']
content += '</video>\n</center>\n'
return content
def addEmbeddedElements(translate: {}, content: str) -> str:
"""Adds embedded elements for various media types
"""
content = addEmbeddedVideoFromSites(translate, content)
content = addEmbeddedAudio(translate, content)
return addEmbeddedVideo(translate, content)

View File

@ -0,0 +1,251 @@
__filename__ = "webapp_person_options.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 petnames import getPetName
from person import isPersonSnoozed
from posts import isModerator
from utils import getDomainFromActor
from utils import getNicknameFromActor
from utils import getCSS
from blocking import isBlocked
from follow import isFollowingActor
from followingCalendar import receivingCalendarEvents
from webapp_utils import htmlHeader
from webapp_utils import htmlFooter
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 = \
' <input type="hidden" name="postUrl" value="' + \
optionsLink + '">\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 = \
' <a href="' + donateUrl + \
'"><button class="button" name="submitDonate">' + \
translate['Donate'] + '</button></a>\n'
optionsStr = htmlHeader(cssFilename, profileStyle)
optionsStr += '<br><br>\n'
optionsStr += '<div class="options">\n'
optionsStr += ' <div class="optionsAvatar">\n'
optionsStr += ' <center>\n'
optionsStr += ' <a href="' + optionsActor + '">\n'
optionsStr += ' <img loading="lazy" src="' + optionsProfileUrl + \
'"/></a>\n'
handle = getNicknameFromActor(optionsActor) + '@' + optionsDomain
optionsStr += \
' <p class="optionsText">' + translate['Options for'] + \
' @' + handle + '</p>\n'
if emailAddress:
optionsStr += \
'<p class="imText">' + translate['Email'] + \
': <a href="mailto:' + \
emailAddress + '">' + emailAddress + '</a></p>\n'
if xmppAddress:
optionsStr += \
'<p class="imText">' + translate['XMPP'] + \
': <a href="xmpp:' + xmppAddress + '">' + \
xmppAddress + '</a></p>\n'
if matrixAddress:
optionsStr += \
'<p class="imText">' + translate['Matrix'] + ': ' + \
matrixAddress + '</p>\n'
if ssbAddress:
optionsStr += \
'<p class="imText">SSB: ' + ssbAddress + '</p>\n'
if blogAddress:
optionsStr += \
'<p class="imText">Blog: <a href="' + blogAddress + '">' + \
blogAddress + '</a></p>\n'
if toxAddress:
optionsStr += \
'<p class="imText">Tox: ' + toxAddress + '</p>\n'
if PGPfingerprint:
optionsStr += '<p class="pgp">PGP: ' + \
PGPfingerprint.replace('\n', '<br>') + '</p>\n'
if PGPpubKey:
optionsStr += '<p class="pgp">' + \
PGPpubKey.replace('\n', '<br>') + '</p>\n'
optionsStr += ' <form method="POST" action="' + \
originPathStr + '/personoptions">\n'
optionsStr += ' <input type="hidden" name="pageNumber" value="' + \
str(pageNumber) + '">\n'
optionsStr += ' <input type="hidden" name="actor" value="' + \
optionsActor + '">\n'
optionsStr += ' <input type="hidden" name="avatarUrl" value="' + \
optionsProfileUrl + '">\n'
if optionsNickname:
handle = optionsNickname + '@' + optionsDomainFull
petname = getPetName(baseDir, nickname, domain, handle)
optionsStr += \
' ' + translate['Petname'] + ': \n' + \
' <input type="text" name="optionpetname" value="' + \
petname + '">\n' \
' <button type="submit" class="buttonsmall" ' + \
'name="submitPetname">' + \
translate['Submit'] + '</button><br>\n'
# checkbox for receiving calendar events
if isFollowingActor(baseDir, nickname, domain, optionsActor):
checkboxStr = \
' <input type="checkbox" ' + \
'class="profilecheckbox" name="onCalendar" checked> ' + \
translate['Receive calendar events from this account'] + \
'\n <button type="submit" class="buttonsmall" ' + \
'name="submitOnCalendar">' + \
translate['Submit'] + '</button><br>\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 = \
' <input type="checkbox" ' + \
'class="profilecheckbox" name="postsToNews" checked> ' + \
translate['Allow news posts'] + \
'\n <button type="submit" class="buttonsmall" ' + \
'name="submitPostToNews">' + \
translate['Submit'] + '</button><br>\n'
if os.path.isfile(newswireBlockedFilename):
checkboxStr = checkboxStr.replace(' checked>', '>')
optionsStr += checkboxStr
optionsStr += optionsLinkStr
optionsStr += \
' <a href="/"><button type="button" class="buttonIcon" ' + \
'name="submitBack">' + translate['Go Back'] + '</button></a>'
optionsStr += \
' <button type="submit" class="button" name="submitView">' + \
translate['View'] + '</button>'
optionsStr += donateStr
optionsStr += \
' <button type="submit" class="button" name="submit' + \
followStr + '">' + translate[followStr] + '</button>'
optionsStr += \
' <button type="submit" class="button" name="submit' + \
blockStr + '">' + translate[blockStr] + '</button>'
optionsStr += \
' <button type="submit" class="button" name="submitDM">' + \
translate['DM'] + '</button>'
optionsStr += \
' <button type="submit" class="button" name="submit' + \
snoozeButtonStr + '">' + translate[snoozeButtonStr] + '</button>'
optionsStr += \
' <button type="submit" class="button" name="submitReport">' + \
translate['Report'] + '</button>'
personNotes = ''
personNotesFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + \
'/notes/' + handle + '.txt'
if os.path.isfile(personNotesFilename):
with open(personNotesFilename, 'r') as fp:
personNotes = fp.read()
optionsStr += \
' <br><br>' + translate['Notes'] + ': \n'
optionsStr += ' <button type="submit" class="buttonsmall" ' + \
'name="submitPersonNotes">' + \
translate['Submit'] + '</button><br>\n'
optionsStr += \
' <textarea id="message" ' + \
'name="optionnotes" style="height:400px">' + \
personNotes + '</textarea>\n'
optionsStr += ' </form>\n'
optionsStr += '</center>\n'
optionsStr += '</div>\n'
optionsStr += '</div>\n'
optionsStr += htmlFooter()
return optionsStr

1384
webapp_post.py 100644

File diff suppressed because it is too large Load Diff

1472
webapp_profile.py 100644

File diff suppressed because it is too large Load Diff

104
webapp_question.py 100644
View File

@ -0,0 +1,104 @@
__filename__ = "webapp_question.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.1.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
import os
from question import isQuestion
from utils import removeIdEnding
def insertQuestion(baseDir: str, translate: {},
nickname: str, domain: str, port: int,
content: str,
postJsonObject: {}, pageNumber: int) -> str:
""" Inserts question selection into a post
"""
if not isQuestion(postJsonObject):
return content
if len(postJsonObject['object']['oneOf']) == 0:
return content
messageId = removeIdEnding(postJsonObject['id'])
if '#' in messageId:
messageId = messageId.split('#', 1)[0]
pageNumberStr = ''
if pageNumber:
pageNumberStr = '?page=' + str(pageNumber)
votesFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + '/questions.txt'
showQuestionResults = False
if os.path.isfile(votesFilename):
if messageId in open(votesFilename).read():
showQuestionResults = True
if not showQuestionResults:
# show the question options
content += '<div class="question">'
content += \
'<form method="POST" action="/users/' + \
nickname + '/question' + pageNumberStr + '">\n'
content += \
'<input type="hidden" name="messageId" value="' + \
messageId + '">\n<br>\n'
for choice in postJsonObject['object']['oneOf']:
if not choice.get('type'):
continue
if not choice.get('name'):
continue
content += \
'<input type="radio" name="answer" value="' + \
choice['name'] + '"> ' + choice['name'] + '<br><br>\n'
content += \
'<input type="submit" value="' + \
translate['Vote'] + '" class="vote"><br><br>\n'
content += '</form>\n</div>\n'
else:
# show the responses to a question
content += '<div class="questionresult">\n'
# get the maximum number of votes
maxVotes = 1
for questionOption in postJsonObject['object']['oneOf']:
if not questionOption.get('name'):
continue
if not questionOption.get('replies'):
continue
votes = 0
try:
votes = int(questionOption['replies']['totalItems'])
except BaseException:
pass
if votes > maxVotes:
maxVotes = int(votes+1)
# show the votes as sliders
questionCtr = 1
for questionOption in postJsonObject['object']['oneOf']:
if not questionOption.get('name'):
continue
if not questionOption.get('replies'):
continue
votes = 0
try:
votes = int(questionOption['replies']['totalItems'])
except BaseException:
pass
votesPercent = str(int(votes * 100 / maxVotes))
content += \
'<p><input type="text" title="' + str(votes) + \
'" name="skillName' + str(questionCtr) + \
'" value="' + questionOption['name'] + \
' (' + str(votes) + ')" style="width:40%">\n'
content += \
'<input type="range" min="1" max="100" ' + \
'class="slider" title="' + \
str(votes) + '" name="skillValue' + str(questionCtr) + \
'" value="' + votesPercent + '"></p>\n'
questionCtr += 1
content += '</div>\n'
return content

967
webapp_search.py 100644
View File

@ -0,0 +1,967 @@
__filename__ = "webapp_search.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
import urllib.parse
from datetime import datetime
from utils import getCSS
from utils import loadJson
from utils import getDomainFromActor
from utils import getNicknameFromActor
from utils import getConfigParam
from utils import locatePost
from utils import isPublicPost
from utils import firstParagraphFromString
from utils import searchBoxPosts
from feeds import rss2TagHeader
from feeds import rss2TagFooter
from webapp_utils import getAltPath
from webapp_utils import getIconsDir
from webapp_utils import getImageFile
from webapp_utils import htmlHeader
from webapp_utils import htmlFooter
from webapp_utils import getSearchBannerFile
from webapp_utils import htmlPostSeparator
from webapp_post import individualPostAsHtml
from blocking import isBlockedHashtag
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 += '<center><h1>' + \
translate['Emoji Search'] + \
'</h1></center>'
# does the lookup file exist?
if not os.path.isfile(emojiLookupFilename):
emojiForm += '<center><h5>' + \
translate['No results'] + '</h5></center>'
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 += '<center>'
msgStr1 = translate['Copy the text then paste it into your post']
msgStr2 = ':<img loading="lazy" class="searchEmoji" src="/emoji/'
for emojiName, filename in results.items():
if os.path.isfile(baseDir + '/emoji/' + filename):
if not headingShown:
emojiForm += \
'<center><h5>' + msgStr1 + \
'</h5></center>'
headingShown = True
emojiForm += \
'<h3>:' + emojiName + msgStr2 + \
filename + '"/></h3>'
emojiForm += '</center>'
emojiForm += htmlFooter()
return emojiForm
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 += \
'<center><h1>' + translate['Shared Items Search'] + \
'</h1></center>'
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 += '<div class="container">\n'
sharedItemsForm += \
'<p class="share-title">' + \
sharedItem['displayName'] + '</p>\n'
if sharedItem.get('imageUrl'):
sharedItemsForm += \
'<a href="' + \
sharedItem['imageUrl'] + '">\n'
sharedItemsForm += \
'<img loading="lazy" src="' + \
sharedItem['imageUrl'] + \
'" alt="Item image"></a>\n'
sharedItemsForm += \
'<p>' + sharedItem['summary'] + '</p>\n'
sharedItemsForm += \
'<p><b>' + translate['Type'] + \
':</b> ' + sharedItem['itemType'] + ' '
sharedItemsForm += \
'<b>' + translate['Category'] + \
':</b> ' + sharedItem['category'] + ' '
sharedItemsForm += \
'<b>' + translate['Location'] + \
':</b> ' + sharedItem['location'] + '</p>\n'
contactActor = \
httpPrefix + '://' + domainFull + \
'/users/' + contactNickname
sharedItemsForm += \
'<p><a href="' + actor + \
'?replydm=sharedesc:' + \
sharedItem['displayName'] + \
'?mention=' + contactActor + \
'"><button class="button">' + \
translate['Contact'] + '</button></a>\n'
if actor.endswith('/users/' + contactNickname):
sharedItemsForm += \
' <a href="' + actor + '?rmshare=' + \
name + '"><button class="button">' + \
translate['Remove'] + '</button></a>\n'
sharedItemsForm += '</p></div>\n'
if not resultsExist and currPage > 1:
postActor = \
getAltPath(actor, domainFull,
callingDomain)
# previous page link, needs to be a POST
sharedItemsForm += \
'<form method="POST" action="' + \
postActor + \
'/searchhandle?page=' + \
str(pageNumber - 1) + '">\n'
sharedItemsForm += \
' <input type="hidden" ' + \
'name="actor" value="' + actor + '">\n'
sharedItemsForm += \
' <input type="hidden" ' + \
'name="searchtext" value="' + \
searchStrLower + '"><br>\n'
sharedItemsForm += \
' <center>\n' + \
' <a href="' + actor + \
'" type="submit" name="submitSearch">\n'
sharedItemsForm += \
' <img loading="lazy" ' + \
'class="pageicon" src="/' + iconsDir + \
'/pageup.png" title="' + \
translate['Page up'] + \
'" alt="' + translate['Page up'] + \
'"/></a>\n'
sharedItemsForm += ' </center>\n'
sharedItemsForm += '</form>\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 += \
'<form method="POST" action="' + \
postActor + \
'/searchhandle?page=' + \
str(pageNumber + 1) + '">\n'
sharedItemsForm += \
' <input type="hidden" ' + \
'name="actor" value="' + actor + '">\n'
sharedItemsForm += \
' <input type="hidden" ' + \
'name="searchtext" value="' + \
searchStrLower + '"><br>\n'
sharedItemsForm += \
' <center>\n' + \
' <a href="' + actor + \
'" type="submit" name="submitSearch">\n'
sharedItemsForm += \
' <img loading="lazy" ' + \
'class="pageicon" src="/' + iconsDir + \
'/pagedown.png" title="' + \
translate['Page down'] + \
'" alt="' + translate['Page down'] + \
'"/></a>\n'
sharedItemsForm += ' </center>\n'
sharedItemsForm += '</form>\n'
break
ctr = 0
if not resultsExist:
sharedItemsForm += \
'<center><h5>' + translate['No results'] + '</h5></center>\n'
sharedItemsForm += htmlFooter()
return sharedItemsForm
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 += '<div class="follow">\n'
emojiStr += ' <div class="followAvatar">\n'
emojiStr += ' <center>\n'
emojiStr += \
' <p class="followText">' + \
translate['Enter an emoji name to search for'] + '</p>\n'
emojiStr += ' <form method="POST" action="' + \
actor + '/searchhandleemoji">\n'
emojiStr += ' <input type="hidden" name="actor" value="' + \
actor + '">\n'
emojiStr += ' <input type="text" name="searchtext" autofocus><br>\n'
emojiStr += \
' <button type="submit" class="button" name="submitSearch">' + \
translate['Submit'] + '</button>\n'
emojiStr += ' </form>\n'
emojiStr += ' </center>\n'
emojiStr += ' </div>\n'
emojiStr += '</div>\n'
emojiStr += htmlFooter()
return emojiStr
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 += \
'<a href="' + usersPath + '/' + defaultTimeline + '" title="' + \
translate['Switch to timeline view'] + '" alt="' + \
translate['Switch to timeline view'] + '">\n'
followStr += '<img loading="lazy" class="timeline-banner" src="' + \
usersPath + '/' + searchBannerFile + '" /></a>\n'
# show the search box
followStr += '<div class="follow">\n'
followStr += ' <div class="followAvatar">\n'
followStr += ' <center>\n'
idx = 'Enter an address, shared item, !history, #hashtag, ' + \
'*skill or :emoji: to search for'
followStr += \
' <p class="followText">' + translate[idx] + '</p>\n'
followStr += ' <form method="POST" ' + \
'accept-charset="UTF-8" action="' + actor + '/searchhandle">\n'
followStr += \
' <input type="hidden" name="actor" value="' + actor + '">\n'
followStr += ' <input type="text" name="searchtext" autofocus><br>\n'
# followStr += ' <a href="/"><button type="button" class="button" ' + \
# 'name="submitBack">' + translate['Go Back'] + '</button></a>\n'
followStr += ' <button type="submit" class="button" ' + \
'name="submitSearch">' + translate['Submit'] + '</button>\n'
followStr += ' </form>\n'
followStr += ' <p class="hashtagswarm">' + \
htmlHashTagSwarm(baseDir, actor) + '</p>\n'
followStr += ' </center>\n'
followStr += ' </div>\n'
followStr += '</div>\n'
followStr += htmlFooter()
return followStr
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 += \
'<a href="' + actor + '/tags/' + tagName + \
'" class="hashtagswarm">' + tagName + '</a>\n'
ctr += 1
tagSwarmHtml = tagSwarmStr.strip() + '\n'
return tagSwarmHtml
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 += '<center>\n' + \
'<h1><a href="/users/' + nickname + '/search">#' + \
hashtag + '</a></h1>\n' + '</center>\n'
else:
hashtagSearchForm += '<center>\n' + \
'<h1>#' + hashtag + '</h1>\n' + '</center>\n'
# RSS link for hashtag feed
hashtagSearchForm += '<center><a href="/tags/rss2/' + hashtag + '">'
hashtagSearchForm += \
'<img style="width:3%;min-width:50px" ' + \
'loading="lazy" alt="RSS 2.0" ' + \
'title="RSS 2.0" src="/' + \
iconsDir + '/logorss.png" /></a></center>'
if startIndex > 0:
# previous page link
hashtagSearchForm += \
' <center>\n' + \
' <a href="/tags/' + hashtag + '?page=' + \
str(pageNumber - 1) + \
'"><img loading="lazy" class="pageicon" src="/' + \
iconsDir + '/pageup.png" title="' + \
translate['Page up'] + \
'" alt="' + translate['Page up'] + \
'"></a>\n </center>\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 += \
' <center>\n' + \
' <a href="/tags/' + hashtag + \
'?page=' + str(pageNumber + 1) + \
'"><img loading="lazy" class="pageicon" src="/' + iconsDir + \
'/pagedown.png" title="' + translate['Page down'] + \
'" alt="' + translate['Page down'] + '"></a>' + \
' </center>'
hashtagSearchForm += htmlFooter()
return hashtagSearchForm
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 += ' <item>'
hashtagFeed += \
' <author>' + \
postJsonObject['object']['attributedTo'] + \
'</author>'
if postJsonObject['object'].get('summary'):
hashtagFeed += \
' <title>' + \
postJsonObject['object']['summary'] + \
'</title>'
description = postJsonObject['object']['content']
description = firstParagraphFromString(description)
hashtagFeed += \
' <description>' + description + '</description>'
hashtagFeed += \
' <pubDate>' + rssDateStr + '</pubDate>'
if postJsonObject['object'].get('attachment'):
for attach in postJsonObject['object']['attachment']:
if not attach.get('url'):
continue
hashtagFeed += \
' <link>' + attach['url'] + '</link>'
hashtagFeed += ' </item>'
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 += \
'<center><h1>' + translate['Skills search'] + ': ' + \
skillsearch + '</h1></center>'
if len(results) == 0:
skillSearchForm += \
'<center><h5>' + translate['No results'] + \
'</h5></center>'
else:
skillSearchForm += '<center>'
ctr = 0
for skillMatch in results:
skillMatchFields = skillMatch.split(';')
if len(skillMatchFields) != 4:
continue
actor = skillMatchFields[1]
actorName = skillMatchFields[2]
avatarUrl = skillMatchFields[3]
skillSearchForm += \
'<div class="search-result""><a href="' + \
actor + '/skills">'
skillSearchForm += \
'<img loading="lazy" src="' + avatarUrl + \
'"/><span class="search-result-text">' + actorName + \
'</span></a></div>'
ctr += 1
if ctr >= postsPerPage:
break
skillSearchForm += '</center>'
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 += \
'<center><h1>' + translate['Your Posts'] + '</h1></center>'
if len(boxFilenames) == 0:
historySearchForm += \
'<center><h5>' + translate['No results'] + \
'</h5></center>'
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

1411
webapp_timeline.py 100644

File diff suppressed because it is too large Load Diff

812
webapp_utils.py 100644
View File

@ -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 ' <details><summary><b>' + \
translate['SHOW MORE'] + '</b></summary>' + \
'<div id="' + postID + '">' + content + \
'</div></details>\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 = '<!DOCTYPE html>\n'
htmlStr += '<html lang="' + lang + '">\n'
htmlStr += ' <head>\n'
htmlStr += ' <meta charset="utf-8">\n'
fontName, fontFormat = getFontFromCss(css)
if fontName:
htmlStr += ' <link rel="preload" as="font" type="' + \
fontFormat + '" href="' + fontName + '" crossorigin>\n'
htmlStr += ' <style>\n' + css + '</style>\n'
htmlStr += ' <link rel="manifest" href="/manifest.json">\n'
htmlStr += ' <meta name="theme-color" content="grey">\n'
htmlStr += ' <title>Epicyon</title>\n'
htmlStr += ' </head>\n'
htmlStr += ' <body>\n'
return htmlStr
def htmlFooter() -> str:
htmlStr = ' </body>\n'
htmlStr += '</html>\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('<p>', '').replace('</p>', '')
emojiTags = {}
print('TAG: displayName before tags: ' + displayName)
displayName = \
addHtmlTags(baseDir, httpPrefix,
nickname, domain, displayName, [], emojiTags)
displayName = displayName.replace('<p>', '').replace('</p>', '')
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 += '<div class="media">\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 += '<br>'
if boxName == 'tlmedia':
galleryStr += '<div class="gallery">\n'
if not isMuted:
galleryStr += ' <a href="' + attach['url'] + '">\n'
galleryStr += \
' <img loading="lazy" src="' + \
attach['url'] + '" alt="" title="">\n'
galleryStr += ' </a>\n'
if postJsonObject['object'].get('url'):
imagePostUrl = postJsonObject['object']['url']
else:
imagePostUrl = postJsonObject['object']['id']
if imageDescription and not isMuted:
galleryStr += \
' <a href="' + imagePostUrl + \
'" class="gallerytext"><div ' + \
'class="gallerytext">' + \
imageDescription + '</div></a>\n'
else:
galleryStr += \
'<label class="transparent">---</label><br>'
galleryStr += ' <div class="mediaicons">\n'
galleryStr += \
' ' + replyStr+announceStr + likeStr + \
bookmarkStr + deleteStr + muteStr + '\n'
galleryStr += ' </div>\n'
galleryStr += ' <div class="mediaavatar">\n'
galleryStr += ' ' + avatarLink + '\n'
galleryStr += ' </div>\n'
galleryStr += '</div>\n'
attachmentStr += '<a href="' + attach['url'] + '">'
attachmentStr += \
'<img loading="lazy" src="' + attach['url'] + \
'" alt="' + imageDescription + '" title="' + \
imageDescription + '" class="attachment"></a>\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 += '<br>'
if boxName == 'tlmedia':
galleryStr += '<div class="gallery">\n'
if not isMuted:
galleryStr += ' <a href="' + attach['url'] + '">\n'
galleryStr += \
' <video width="600" height="400" controls>\n'
galleryStr += \
' <source src="' + attach['url'] + \
'" alt="' + imageDescription + \
'" title="' + imageDescription + \
'" class="attachment" type="video/' + \
extension.replace('.', '') + '">'
idx = 'Your browser does not support the video tag.'
galleryStr += translate[idx]
galleryStr += ' </video>\n'
galleryStr += ' </a>\n'
if postJsonObject['object'].get('url'):
videoPostUrl = postJsonObject['object']['url']
else:
videoPostUrl = postJsonObject['object']['id']
if imageDescription and not isMuted:
galleryStr += \
' <a href="' + videoPostUrl + \
'" class="gallerytext"><div ' + \
'class="gallerytext">' + \
imageDescription + '</div></a>\n'
else:
galleryStr += \
'<label class="transparent">---</label><br>'
galleryStr += ' <div class="mediaicons">\n'
galleryStr += \
' ' + replyStr + announceStr + likeStr + \
bookmarkStr + deleteStr + muteStr + '\n'
galleryStr += ' </div>\n'
galleryStr += ' <div class="mediaavatar">\n'
galleryStr += ' ' + avatarLink + '\n'
galleryStr += ' </div>\n'
galleryStr += '</div>\n'
attachmentStr += \
'<center><video width="400" height="300" controls>'
attachmentStr += \
'<source src="' + attach['url'] + '" alt="' + \
imageDescription + '" title="' + imageDescription + \
'" class="attachment" type="video/' + \
extension.replace('.', '') + '">'
attachmentStr += \
translate['Your browser does not support the video tag.']
attachmentStr += '</video></center>'
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 += '<br>'
if boxName == 'tlmedia':
galleryStr += '<div class="gallery">\n'
if not isMuted:
galleryStr += ' <a href="' + attach['url'] + '">\n'
galleryStr += ' <audio controls>\n'
galleryStr += \
' <source src="' + attach['url'] + \
'" alt="' + imageDescription + \
'" title="' + imageDescription + \
'" class="attachment" type="audio/' + \
extension.replace('.', '') + '">'
idx = 'Your browser does not support the audio tag.'
galleryStr += translate[idx]
galleryStr += ' </audio>\n'
galleryStr += ' </a>\n'
if postJsonObject['object'].get('url'):
audioPostUrl = postJsonObject['object']['url']
else:
audioPostUrl = postJsonObject['object']['id']
if imageDescription and not isMuted:
galleryStr += \
' <a href="' + audioPostUrl + \
'" class="gallerytext"><div ' + \
'class="gallerytext">' + \
imageDescription + '</div></a>\n'
else:
galleryStr += \
'<label class="transparent">---</label><br>'
galleryStr += ' <div class="mediaicons">\n'
galleryStr += \
' ' + replyStr + announceStr + \
likeStr + bookmarkStr + \
deleteStr + muteStr+'\n'
galleryStr += ' </div>\n'
galleryStr += ' <div class="mediaavatar">\n'
galleryStr += ' ' + avatarLink + '\n'
galleryStr += ' </div>\n'
galleryStr += '</div>\n'
attachmentStr += '<center>\n<audio controls>\n'
attachmentStr += \
'<source src="' + attach['url'] + '" alt="' + \
imageDescription + '" title="' + imageDescription + \
'" class="attachment" type="audio/' + \
extension.replace('.', '') + '">'
attachmentStr += \
translate['Your browser does not support the audio tag.']
attachmentStr += '</audio>\n</center>\n'
attachmentCtr += 1
attachmentStr += '</div>'
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 = \
'<div class="postSeparatorImage"><center>' + \
'<img src="/' + iconsDir + '/' + filename + '"/>' + \
'</center></div>\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 += \
' <a href="/">' + \
'<button class="' + buttonFeatures + '">' + \
'<span>' + translate['Features'] + \
'</span></button></a>'
if not authorized:
headerStr += \
' <a href="/login">' + \
'<button class="buttonMobile">' + \
'<span>' + translate['Login'] + \
'</span></button></a>'
if iconsAsButtons:
headerStr += \
' <a href="/users/news/newswiremobile">' + \
'<button class="' + buttonNewswire + '">' + \
'<span>' + translate['Newswire'] + \
'</span></button></a>'
headerStr += \
' <a href="/users/news/linksmobile">' + \
'<button class="' + buttonLinks + '">' + \
'<span>' + translate['Links'] + \
'</span></button></a>'
else:
headerStr += \
' <a href="' + \
'/users/news/newswiremobile">' + \
'<img loading="lazy" src="/' + iconsDir + \
'/newswire.png" title="' + translate['Newswire'] + \
'" alt="| ' + translate['Newswire'] + '"/></a>\n'
headerStr += \
' <a href="' + \
'/users/news/linksmobile">' + \
'<img loading="lazy" src="/' + iconsDir + \
'/links.png" title="' + translate['Links'] + \
'" alt="| ' + translate['Links'] + '"/></a>\n'
else:
if not authorized:
headerStr += \
' <a href="/login">' + \
'<button class="buttonMobile">' + \
'<span>' + translate['Login'] + \
'</span></button></a>'
if headerStr:
headerStr = \
'\n <div class="frontPageMobileButtons">\n' + \
headerStr + \
' </div>\n'
return headerStr

File diff suppressed because it is too large Load Diff