forked from indymedia/epicyon
Splitting webapp into smaller modules
parent
d34d22dc76
commit
a3a022c917
4
blog.py
4
blog.py
|
@ -11,10 +11,10 @@ from datetime import datetime
|
|||
|
||||
from content import replaceEmojiFromTags
|
||||
from webapp import getIconsDir
|
||||
from webapp import getPostAttachmentsAsHtml
|
||||
from webapp import htmlHeader
|
||||
from webapp import htmlFooter
|
||||
from webapp import addEmbeddedElements
|
||||
from webapp_media import addEmbeddedElements
|
||||
from webapp_utils import getPostAttachmentsAsHtml
|
||||
from utils import getNicknameFromActor
|
||||
from utils import getDomainFromActor
|
||||
from utils import locatePost
|
||||
|
|
16
daemon.py
16
daemon.py
|
@ -143,11 +143,8 @@ from webapp import htmlGetLoginCredentials
|
|||
from webapp import htmlNewPost
|
||||
from webapp import htmlFollowConfirm
|
||||
from webapp import htmlCalendar
|
||||
from webapp import htmlSearch
|
||||
from webapp import htmlNewswireMobile
|
||||
from webapp import htmlLinksMobile
|
||||
from webapp import htmlSearchEmoji
|
||||
from webapp import htmlSearchEmojiTextEntry
|
||||
from webapp import htmlUnfollowConfirm
|
||||
from webapp import htmlProfileAfterSearch
|
||||
from webapp import htmlEditProfile
|
||||
|
@ -155,13 +152,16 @@ from webapp import htmlEditLinks
|
|||
from webapp import htmlEditNewswire
|
||||
from webapp import htmlEditNewsPost
|
||||
from webapp import htmlTermsOfService
|
||||
from webapp import htmlSkillsSearch
|
||||
from webapp import htmlHistorySearch
|
||||
from webapp import htmlHashtagSearch
|
||||
from webapp import rssHashtagSearch
|
||||
from webapp import htmlModerationInfo
|
||||
from webapp import htmlSearchSharedItems
|
||||
from webapp import htmlHashtagBlocked
|
||||
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
|
||||
|
|
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
|
||||
|
|
2
inbox.py
2
inbox.py
|
@ -58,7 +58,6 @@ from posts import sendSignedJson
|
|||
from posts import sendToFollowersThread
|
||||
from webapp import individualPostAsHtml
|
||||
from webapp import getIconsDir
|
||||
from webapp import removeOldHashtags
|
||||
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:
|
||||
|
|
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
|
||||
|
|
8
utils.py
8
utils.py
|
@ -1494,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(year, monthNumber, 1, 0, 0)
|
||||
return int(firstDayOfMonth.strftime("%w")) + 1
|
||||
|
|
|
@ -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,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
|
326
webapp_utils.py
326
webapp_utils.py
|
@ -11,9 +11,12 @@ 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:
|
||||
|
@ -414,3 +417,326 @@ def getRightImageFile(baseDir: 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
|
||||
|
|
Loading…
Reference in New Issue