mirror of https://gitlab.com/bashrc2/epicyon
Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon into main
commit
8193d8ab7c
27
blog.py
27
blog.py
|
@ -10,15 +10,16 @@ import os
|
|||
from datetime import datetime
|
||||
|
||||
from content import replaceEmojiFromTags
|
||||
from webinterface import getIconsDir
|
||||
from webinterface import getPostAttachmentsAsHtml
|
||||
from webinterface import htmlHeader
|
||||
from webinterface import htmlFooter
|
||||
from webinterface import addEmbeddedElements
|
||||
from webapp import getIconsDir
|
||||
from webapp import htmlHeader
|
||||
from webapp import htmlFooter
|
||||
from webapp_media import addEmbeddedElements
|
||||
from webapp_utils import getPostAttachmentsAsHtml
|
||||
from utils import getNicknameFromActor
|
||||
from utils import getDomainFromActor
|
||||
from utils import locatePost
|
||||
from utils import loadJson
|
||||
from utils import firstParagraphFromString
|
||||
from posts import createBlogsTimeline
|
||||
from newswire import rss2Header
|
||||
from newswire import rss2Footer
|
||||
|
@ -165,10 +166,12 @@ def htmlBlogPostContent(authorized: bool,
|
|||
if postJsonObject['object'].get('id'):
|
||||
messageLink = postJsonObject['object']['id'].replace('/statuses/', '/')
|
||||
titleStr = ''
|
||||
articleAdded = False
|
||||
if postJsonObject['object'].get('summary'):
|
||||
titleStr = postJsonObject['object']['summary']
|
||||
blogStr += '<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
101
daemon.py
|
@ -113,59 +113,60 @@ from blog import htmlBlogView
|
|||
from blog import htmlBlogPage
|
||||
from blog import htmlBlogPost
|
||||
from blog import htmlEditBlog
|
||||
from webinterface import htmlCitations
|
||||
from webinterface import htmlFollowingList
|
||||
from webinterface import getBlogAddress
|
||||
from webinterface import setBlogAddress
|
||||
from webinterface import htmlCalendarDeleteConfirm
|
||||
from webinterface import htmlDeletePost
|
||||
from webinterface import htmlAbout
|
||||
from webinterface import htmlRemoveSharedItem
|
||||
from webinterface import htmlInboxDMs
|
||||
from webinterface import htmlInboxReplies
|
||||
from webinterface import htmlInboxMedia
|
||||
from webinterface import htmlInboxBlogs
|
||||
from webinterface import htmlInboxNews
|
||||
from webinterface import htmlUnblockConfirm
|
||||
from webinterface import htmlPersonOptions
|
||||
from webinterface import htmlIndividualPost
|
||||
from webinterface import htmlProfile
|
||||
from webinterface import htmlInbox
|
||||
from webinterface import htmlBookmarks
|
||||
from webinterface import htmlEvents
|
||||
from webinterface import htmlShares
|
||||
from webinterface import htmlOutbox
|
||||
from webinterface import htmlModeration
|
||||
from webinterface import htmlPostReplies
|
||||
from webinterface import htmlLogin
|
||||
from webinterface import htmlSuspended
|
||||
from webinterface import htmlGetLoginCredentials
|
||||
from webinterface import htmlNewPost
|
||||
from webinterface import htmlFollowConfirm
|
||||
from webinterface import htmlCalendar
|
||||
from webinterface import htmlSearch
|
||||
from webinterface import htmlNewswireMobile
|
||||
from webinterface import htmlLinksMobile
|
||||
from webinterface import htmlSearchEmoji
|
||||
from webinterface import htmlSearchEmojiTextEntry
|
||||
from webinterface import htmlUnfollowConfirm
|
||||
from webinterface import htmlProfileAfterSearch
|
||||
from webinterface import htmlEditProfile
|
||||
from webinterface import htmlEditLinks
|
||||
from webinterface import htmlEditNewswire
|
||||
from webinterface import htmlEditNewsPost
|
||||
from webinterface import htmlTermsOfService
|
||||
from webinterface import htmlSkillsSearch
|
||||
from webinterface import htmlHistorySearch
|
||||
from webinterface import htmlHashtagSearch
|
||||
from webinterface import rssHashtagSearch
|
||||
from webinterface import htmlModerationInfo
|
||||
from webinterface import htmlSearchSharedItems
|
||||
from webinterface import htmlHashtagBlocked
|
||||
from webapp_utils import setBlogAddress
|
||||
from webapp_utils import getBlogAddress
|
||||
from webapp_calendar import htmlCalendarDeleteConfirm
|
||||
from webapp_calendar import htmlCalendar
|
||||
from webapp import htmlFollowingList
|
||||
from webapp import htmlDeletePost
|
||||
from webapp import htmlAbout
|
||||
from webapp import htmlRemoveSharedItem
|
||||
from webapp import htmlUnblockConfirm
|
||||
from webapp_person_options import htmlPersonOptions
|
||||
from webapp_timeline import htmlShares
|
||||
from webapp_timeline import htmlInbox
|
||||
from webapp_timeline import htmlBookmarks
|
||||
from webapp_timeline import htmlEvents
|
||||
from webapp_timeline import htmlInboxDMs
|
||||
from webapp_timeline import htmlInboxReplies
|
||||
from webapp_timeline import htmlInboxMedia
|
||||
from webapp_timeline import htmlInboxBlogs
|
||||
from webapp_timeline import htmlInboxNews
|
||||
from webapp_timeline import htmlOutbox
|
||||
from webapp_timeline import htmlModeration
|
||||
from webapp_create_post import htmlNewPost
|
||||
from webapp import htmlLogin
|
||||
from webapp import htmlSuspended
|
||||
from webapp import htmlGetLoginCredentials
|
||||
from webapp import htmlFollowConfirm
|
||||
from webapp import htmlUnfollowConfirm
|
||||
from webapp import htmlEditNewsPost
|
||||
from webapp import htmlTermsOfService
|
||||
from webapp import htmlModerationInfo
|
||||
from webapp import htmlHashtagBlocked
|
||||
from webapp_post import htmlPostReplies
|
||||
from webapp_post import htmlIndividualPost
|
||||
from webapp_profile import htmlEditProfile
|
||||
from webapp_profile import htmlProfileAfterSearch
|
||||
from webapp_profile import htmlProfile
|
||||
from webapp_column_left import htmlLinksMobile
|
||||
from webapp_column_left import htmlEditLinks
|
||||
from webapp_column_right import htmlNewswireMobile
|
||||
from webapp_column_right import htmlEditNewswire
|
||||
from webapp_column_right import htmlCitations
|
||||
from webapp_search import htmlSkillsSearch
|
||||
from webapp_search import htmlHistorySearch
|
||||
from webapp_search import htmlHashtagSearch
|
||||
from webapp_search import rssHashtagSearch
|
||||
from webapp_search import htmlSearchEmoji
|
||||
from webapp_search import htmlSearchSharedItems
|
||||
from webapp_search import htmlSearchEmojiTextEntry
|
||||
from webapp_search import htmlSearch
|
||||
from shares import getSharesFeedForPerson
|
||||
from shares import addShare
|
||||
from shares import removeShare
|
||||
from shares import expireShares
|
||||
from utils import firstParagraphFromString
|
||||
from utils import clearFromPostCaches
|
||||
from utils import containsInvalidChars
|
||||
from utils import isSystemAccount
|
||||
|
@ -3272,7 +3273,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
self.server.newswire[publishedDate][0] = \
|
||||
newsPostTitle
|
||||
self.server.newswire[publishedDate][4] = \
|
||||
newsPostContent
|
||||
firstParagraphFromString(newsPostContent)
|
||||
# save newswire
|
||||
newswireStateFilename = \
|
||||
baseDir + '/accounts/.newswirestate.json'
|
||||
|
|
32
delete.py
32
delete.py
|
@ -6,6 +6,8 @@ __maintainer__ = "Bob Mottram"
|
|||
__email__ = "bob@freedombone.net"
|
||||
__status__ = "Production"
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
from utils import removeIdEnding
|
||||
from utils import getStatusNumber
|
||||
from utils import urlPermitted
|
||||
|
@ -295,3 +297,33 @@ def outboxDelete(baseDir: str, httpPrefix: str,
|
|||
postFilename, debug, recentPostsCache)
|
||||
if debug:
|
||||
print('DEBUG: post deleted via c2s - ' + postFilename)
|
||||
|
||||
|
||||
def removeOldHashtags(baseDir: str, maxMonths: int) -> str:
|
||||
"""Remove old hashtags
|
||||
"""
|
||||
if maxMonths > 11:
|
||||
maxMonths = 11
|
||||
maxDaysSinceEpoch = \
|
||||
(datetime.utcnow() - datetime(1970, 1 + maxMonths, 1)).days
|
||||
removeHashtags = []
|
||||
|
||||
for subdir, dirs, files in os.walk(baseDir + '/tags'):
|
||||
for f in files:
|
||||
tagsFilename = os.path.join(baseDir + '/tags', f)
|
||||
if not os.path.isfile(tagsFilename):
|
||||
continue
|
||||
# get last modified datetime
|
||||
modTimesinceEpoc = os.path.getmtime(tagsFilename)
|
||||
lastModifiedDate = datetime.fromtimestamp(modTimesinceEpoc)
|
||||
fileDaysSinceEpoch = (lastModifiedDate - datetime(1970, 1, 1)).days
|
||||
|
||||
# check of the file is too old
|
||||
if fileDaysSinceEpoch < maxDaysSinceEpoch:
|
||||
removeHashtags.append(tagsFilename)
|
||||
|
||||
for removeFilename in removeHashtags:
|
||||
try:
|
||||
os.remove(removeFilename)
|
||||
except BaseException:
|
||||
pass
|
||||
|
|
|
@ -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
|
14
follow.py
14
follow.py
|
@ -1232,3 +1232,17 @@ def outboxUndoFollow(baseDir: str, messageJson: {}, debug: bool) -> None:
|
|||
if debug:
|
||||
print('WARN: ' + nicknameFollower + ' could not unfollow ' +
|
||||
nicknameFollowing + '@' + domainFollowingFull)
|
||||
|
||||
|
||||
def followerApprovalActive(baseDir: str, nickname: str, domain: str) -> bool:
|
||||
"""Returns true if the given account requires follower approval
|
||||
"""
|
||||
manuallyApprovesFollowers = False
|
||||
actorFilename = baseDir + '/accounts/' + nickname + '@' + domain + '.json'
|
||||
if os.path.isfile(actorFilename):
|
||||
actorJson = loadJson(actorFilename)
|
||||
if actorJson:
|
||||
if actorJson.get('manuallyApprovesFollowers'):
|
||||
manuallyApprovesFollowers = \
|
||||
actorJson['manuallyApprovesFollowers']
|
||||
return manuallyApprovesFollowers
|
||||
|
|
6
inbox.py
6
inbox.py
|
@ -56,9 +56,8 @@ from posts import isMuted
|
|||
from posts import isImageMedia
|
||||
from posts import sendSignedJson
|
||||
from posts import sendToFollowersThread
|
||||
from webinterface import individualPostAsHtml
|
||||
from webinterface import getIconsDir
|
||||
from webinterface import removeOldHashtags
|
||||
from webapp import individualPostAsHtml
|
||||
from webapp import getIconsDir
|
||||
from question import questionUpdateVotes
|
||||
from media import replaceYouTube
|
||||
from git import isGitPatch
|
||||
|
@ -66,6 +65,7 @@ from git import receiveGitPatch
|
|||
from followingCalendar import receivingCalendarEvents
|
||||
from content import dangerousMarkup
|
||||
from happening import saveEventPost
|
||||
from delete import removeOldHashtags
|
||||
|
||||
|
||||
def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
12
newswire.py
12
newswire.py
|
@ -12,6 +12,7 @@ from socket import error as SocketError
|
|||
import errno
|
||||
from datetime import datetime
|
||||
from collections import OrderedDict
|
||||
from utils import firstParagraphFromString
|
||||
from utils import isPublicPost
|
||||
from utils import locatePost
|
||||
from utils import loadJson
|
||||
|
@ -386,9 +387,12 @@ def getRSSfromDict(baseDir: str, newswire: {},
|
|||
continue
|
||||
rssStr += '<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'],
|
||||
|
|
24
posts.py
24
posts.py
|
@ -3977,3 +3977,27 @@ def sendUndoBlockViaServer(baseDir: str, session,
|
|||
print('DEBUG: c2s POST block success')
|
||||
|
||||
return newBlockJson
|
||||
|
||||
|
||||
def postIsMuted(baseDir: str, nickname: str, domain: str,
|
||||
postJsonObject: {}, messageId: str) -> bool:
|
||||
""" Returns true if the given post is muted
|
||||
"""
|
||||
isMuted = postJsonObject.get('muted')
|
||||
if isMuted is True or isMuted is False:
|
||||
return isMuted
|
||||
postDir = baseDir + '/accounts/' + nickname + '@' + domain
|
||||
muteFilename = \
|
||||
postDir + '/inbox/' + messageId.replace('/', '#') + '.json.muted'
|
||||
if os.path.isfile(muteFilename):
|
||||
return True
|
||||
muteFilename = \
|
||||
postDir + '/outbox/' + messageId.replace('/', '#') + '.json.muted'
|
||||
if os.path.isfile(muteFilename):
|
||||
return True
|
||||
muteFilename = \
|
||||
baseDir + '/accounts/cache/announce/' + nickname + \
|
||||
'/' + messageId.replace('/', '#') + '.json.muted'
|
||||
if os.path.isfile(muteFilename):
|
||||
return True
|
||||
return False
|
||||
|
|
19
question.py
19
question.py
|
@ -124,3 +124,22 @@ def questionUpdateVotes(baseDir: str, nickname: str, domain: str,
|
|||
# save the question with altered totals
|
||||
saveJson(questionJson, questionPostFilename)
|
||||
return questionJson
|
||||
|
||||
|
||||
def isQuestion(postObjectJson: {}) -> bool:
|
||||
""" is the given post a question?
|
||||
"""
|
||||
if postObjectJson['type'] != 'Create' and \
|
||||
postObjectJson['type'] != 'Update':
|
||||
return False
|
||||
if not isinstance(postObjectJson['object'], dict):
|
||||
return False
|
||||
if not postObjectJson['object'].get('type'):
|
||||
return False
|
||||
if postObjectJson['object']['type'] != 'Question':
|
||||
return False
|
||||
if not postObjectJson['object'].get('oneOf'):
|
||||
return False
|
||||
if not isinstance(postObjectJson['object']['oneOf'], list):
|
||||
return False
|
||||
return True
|
||||
|
|
15
tests.py
15
tests.py
|
@ -32,6 +32,7 @@ from follow import clearFollows
|
|||
from follow import clearFollowers
|
||||
from follow import sendFollowRequestViaServer
|
||||
from follow import sendUnfollowRequestViaServer
|
||||
from utils import firstParagraphFromString
|
||||
from utils import removeIdEnding
|
||||
from utils import siteIsActive
|
||||
from utils import updateRecentPostsCache
|
||||
|
@ -2336,8 +2337,22 @@ def testGetNewswireTags():
|
|||
assert '#ExcitingHashtag' in tags
|
||||
|
||||
|
||||
def testFirstParagraphFromString():
|
||||
print('testFirstParagraphFromString')
|
||||
testStr = \
|
||||
'<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()
|
||||
|
|
|
@ -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 لمدونتك"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -327,5 +327,6 @@
|
|||
"Create an article": "Cruthaigh alt",
|
||||
"Settings": "Socruithe",
|
||||
"Citations": "Citations",
|
||||
"Choose newswire items referenced in your article": "Roghnaigh míreanna sreanga nuachta dá dtagraítear i d’alt"
|
||||
"Choose newswire items referenced in your article": "Roghnaigh míreanna sreanga nuachta dá dtagraítear i d’alt",
|
||||
"RSS feed for your blog": "Fotha RSS do do bhlag"
|
||||
}
|
||||
|
|
|
@ -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 आपके ब्लॉग के लिए फ़ीड करता है"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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フィード"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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-канал для вашего блога"
|
||||
}
|
||||
|
|
|
@ -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供稿"
|
||||
}
|
||||
|
|
20
utils.py
20
utils.py
|
@ -38,6 +38,18 @@ def removeHtml(content: str) -> str:
|
|||
return result
|
||||
|
||||
|
||||
def firstParagraphFromString(content: str) -> str:
|
||||
"""Get the first paragraph from a blog post
|
||||
to be used as a summary in the newswire feed
|
||||
"""
|
||||
if '<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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -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
|
|
@ -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
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
9244
webinterface.py
9244
webinterface.py
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue