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