mirror of https://gitlab.com/bashrc2/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 content import replaceEmojiFromTags
|
||||||
from webapp import getIconsDir
|
from webapp import getIconsDir
|
||||||
from webapp import getPostAttachmentsAsHtml
|
|
||||||
from webapp import htmlHeader
|
from webapp import htmlHeader
|
||||||
from webapp import htmlFooter
|
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 getNicknameFromActor
|
||||||
from utils import getDomainFromActor
|
from utils import getDomainFromActor
|
||||||
from utils import locatePost
|
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 htmlNewPost
|
||||||
from webapp import htmlFollowConfirm
|
from webapp import htmlFollowConfirm
|
||||||
from webapp import htmlCalendar
|
from webapp import htmlCalendar
|
||||||
from webapp import htmlSearch
|
|
||||||
from webapp import htmlNewswireMobile
|
from webapp import htmlNewswireMobile
|
||||||
from webapp import htmlLinksMobile
|
from webapp import htmlLinksMobile
|
||||||
from webapp import htmlSearchEmoji
|
|
||||||
from webapp import htmlSearchEmojiTextEntry
|
|
||||||
from webapp import htmlUnfollowConfirm
|
from webapp import htmlUnfollowConfirm
|
||||||
from webapp import htmlProfileAfterSearch
|
from webapp import htmlProfileAfterSearch
|
||||||
from webapp import htmlEditProfile
|
from webapp import htmlEditProfile
|
||||||
|
@ -155,13 +152,16 @@ from webapp import htmlEditLinks
|
||||||
from webapp import htmlEditNewswire
|
from webapp import htmlEditNewswire
|
||||||
from webapp import htmlEditNewsPost
|
from webapp import htmlEditNewsPost
|
||||||
from webapp import htmlTermsOfService
|
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 htmlModerationInfo
|
||||||
from webapp import htmlSearchSharedItems
|
|
||||||
from webapp import htmlHashtagBlocked
|
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 getSharesFeedForPerson
|
||||||
from shares import addShare
|
from shares import addShare
|
||||||
from shares import removeShare
|
from shares import removeShare
|
||||||
|
|
32
delete.py
32
delete.py
|
@ -6,6 +6,8 @@ __maintainer__ = "Bob Mottram"
|
||||||
__email__ = "bob@freedombone.net"
|
__email__ = "bob@freedombone.net"
|
||||||
__status__ = "Production"
|
__status__ = "Production"
|
||||||
|
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
from utils import removeIdEnding
|
from utils import removeIdEnding
|
||||||
from utils import getStatusNumber
|
from utils import getStatusNumber
|
||||||
from utils import urlPermitted
|
from utils import urlPermitted
|
||||||
|
@ -295,3 +297,33 @@ def outboxDelete(baseDir: str, httpPrefix: str,
|
||||||
postFilename, debug, recentPostsCache)
|
postFilename, debug, recentPostsCache)
|
||||||
if debug:
|
if debug:
|
||||||
print('DEBUG: post deleted via c2s - ' + postFilename)
|
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 posts import sendToFollowersThread
|
||||||
from webapp import individualPostAsHtml
|
from webapp import individualPostAsHtml
|
||||||
from webapp import getIconsDir
|
from webapp import getIconsDir
|
||||||
from webapp import removeOldHashtags
|
|
||||||
from question import questionUpdateVotes
|
from question import questionUpdateVotes
|
||||||
from media import replaceYouTube
|
from media import replaceYouTube
|
||||||
from git import isGitPatch
|
from git import isGitPatch
|
||||||
|
@ -66,6 +65,7 @@ from git import receiveGitPatch
|
||||||
from followingCalendar import receivingCalendarEvents
|
from followingCalendar import receivingCalendarEvents
|
||||||
from content import dangerousMarkup
|
from content import dangerousMarkup
|
||||||
from happening import saveEventPost
|
from happening import saveEventPost
|
||||||
|
from delete import removeOldHashtags
|
||||||
|
|
||||||
|
|
||||||
def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None:
|
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')
|
print('DEBUG: c2s POST block success')
|
||||||
|
|
||||||
return newBlockJson
|
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:
|
if e.errno == errno.ECONNRESET:
|
||||||
print('WARN: connection was reset during siteIsActive')
|
print('WARN: connection was reset during siteIsActive')
|
||||||
return False
|
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 session import getJson
|
||||||
from utils import getProtocolPrefixes
|
from utils import getProtocolPrefixes
|
||||||
from utils import loadJson
|
from utils import loadJson
|
||||||
|
from utils import getCachedPostFilename
|
||||||
from utils import getConfigParam
|
from utils import getConfigParam
|
||||||
from cache import getPersonFromCache
|
from cache import getPersonFromCache
|
||||||
from cache import storePersonInCache
|
from cache import storePersonInCache
|
||||||
|
from content import addHtmlTags
|
||||||
|
from content import replaceEmojiFromTags
|
||||||
|
|
||||||
|
|
||||||
def getAltPath(actor: str, domainFull: str, callingDomain: str) -> str:
|
def getAltPath(actor: str, domainFull: str, callingDomain: str) -> str:
|
||||||
|
@ -414,3 +417,326 @@ def getRightImageFile(baseDir: str,
|
||||||
return getImageFile(baseDir, 'right_col_image',
|
return getImageFile(baseDir, 'right_col_image',
|
||||||
baseDir + '/accounts/' + nickname + '@' + domain,
|
baseDir + '/accounts/' + nickname + '@' + domain,
|
||||||
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