mirror of https://gitlab.com/bashrc2/epicyon
				
				
				
			Splitting webapp into smaller modules
							parent
							
								
									d34d22dc76
								
							
						
					
					
						commit
						a3a022c917
					
				
							
								
								
									
										4
									
								
								blog.py
								
								
								
								
							
							
						
						
									
										4
									
								
								blog.py
								
								
								
								
							|  | @ -11,10 +11,10 @@ from datetime import datetime | ||||||
| 
 | 
 | ||||||
| from content import replaceEmojiFromTags | from content import replaceEmojiFromTags | ||||||
| from webapp import getIconsDir | from webapp import getIconsDir | ||||||
| from webapp import getPostAttachmentsAsHtml |  | ||||||
| from webapp import htmlHeader | from webapp import htmlHeader | ||||||
| from webapp import htmlFooter | from webapp import htmlFooter | ||||||
| from webapp import addEmbeddedElements | from webapp_media import addEmbeddedElements | ||||||
|  | from webapp_utils import getPostAttachmentsAsHtml | ||||||
| from utils import getNicknameFromActor | from utils import getNicknameFromActor | ||||||
| from utils import getDomainFromActor | from utils import getDomainFromActor | ||||||
| from utils import locatePost | from utils import locatePost | ||||||
|  |  | ||||||
							
								
								
									
										16
									
								
								daemon.py
								
								
								
								
							
							
						
						
									
										16
									
								
								daemon.py
								
								
								
								
							|  | @ -143,11 +143,8 @@ from webapp import htmlGetLoginCredentials | ||||||
| from webapp import htmlNewPost | from webapp import htmlNewPost | ||||||
| from webapp import htmlFollowConfirm | from webapp import htmlFollowConfirm | ||||||
| from webapp import htmlCalendar | from webapp import htmlCalendar | ||||||
| from webapp import htmlSearch |  | ||||||
| from webapp import htmlNewswireMobile | from webapp import htmlNewswireMobile | ||||||
| from webapp import htmlLinksMobile | from webapp import htmlLinksMobile | ||||||
| from webapp import htmlSearchEmoji |  | ||||||
| from webapp import htmlSearchEmojiTextEntry |  | ||||||
| from webapp import htmlUnfollowConfirm | from webapp import htmlUnfollowConfirm | ||||||
| from webapp import htmlProfileAfterSearch | from webapp import htmlProfileAfterSearch | ||||||
| from webapp import htmlEditProfile | from webapp import htmlEditProfile | ||||||
|  | @ -155,13 +152,16 @@ from webapp import htmlEditLinks | ||||||
| from webapp import htmlEditNewswire | from webapp import htmlEditNewswire | ||||||
| from webapp import htmlEditNewsPost | from webapp import htmlEditNewsPost | ||||||
| from webapp import htmlTermsOfService | from webapp import htmlTermsOfService | ||||||
| from webapp import htmlSkillsSearch |  | ||||||
| from webapp import htmlHistorySearch |  | ||||||
| from webapp import htmlHashtagSearch |  | ||||||
| from webapp import rssHashtagSearch |  | ||||||
| from webapp import htmlModerationInfo | from webapp import htmlModerationInfo | ||||||
| from webapp import htmlSearchSharedItems |  | ||||||
| from webapp import htmlHashtagBlocked | from webapp import htmlHashtagBlocked | ||||||
|  | from webapp_search import htmlSkillsSearch | ||||||
|  | from webapp_search import htmlHistorySearch | ||||||
|  | from webapp_search import htmlHashtagSearch | ||||||
|  | from webapp_search import rssHashtagSearch | ||||||
|  | from webapp_search import htmlSearchEmoji | ||||||
|  | from webapp_search import htmlSearchSharedItems | ||||||
|  | from webapp_search import htmlSearchEmojiTextEntry | ||||||
|  | from webapp_search import htmlSearch | ||||||
| from shares import getSharesFeedForPerson | from shares import getSharesFeedForPerson | ||||||
| from shares import addShare | from shares import addShare | ||||||
| from shares import removeShare | from shares import removeShare | ||||||
|  |  | ||||||
							
								
								
									
										32
									
								
								delete.py
								
								
								
								
							
							
						
						
									
										32
									
								
								delete.py
								
								
								
								
							|  | @ -6,6 +6,8 @@ __maintainer__ = "Bob Mottram" | ||||||
| __email__ = "bob@freedombone.net" | __email__ = "bob@freedombone.net" | ||||||
| __status__ = "Production" | __status__ = "Production" | ||||||
| 
 | 
 | ||||||
|  | import os | ||||||
|  | from datetime import datetime | ||||||
| from utils import removeIdEnding | from utils import removeIdEnding | ||||||
| from utils import getStatusNumber | from utils import getStatusNumber | ||||||
| from utils import urlPermitted | from utils import urlPermitted | ||||||
|  | @ -295,3 +297,33 @@ def outboxDelete(baseDir: str, httpPrefix: str, | ||||||
|                postFilename, debug, recentPostsCache) |                postFilename, debug, recentPostsCache) | ||||||
|     if debug: |     if debug: | ||||||
|         print('DEBUG: post deleted via c2s - ' + postFilename) |         print('DEBUG: post deleted via c2s - ' + postFilename) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def removeOldHashtags(baseDir: str, maxMonths: int) -> str: | ||||||
|  |     """Remove old hashtags | ||||||
|  |     """ | ||||||
|  |     if maxMonths > 11: | ||||||
|  |         maxMonths = 11 | ||||||
|  |     maxDaysSinceEpoch = \ | ||||||
|  |         (datetime.utcnow() - datetime(1970, 1 + maxMonths, 1)).days | ||||||
|  |     removeHashtags = [] | ||||||
|  | 
 | ||||||
|  |     for subdir, dirs, files in os.walk(baseDir + '/tags'): | ||||||
|  |         for f in files: | ||||||
|  |             tagsFilename = os.path.join(baseDir + '/tags', f) | ||||||
|  |             if not os.path.isfile(tagsFilename): | ||||||
|  |                 continue | ||||||
|  |             # get last modified datetime | ||||||
|  |             modTimesinceEpoc = os.path.getmtime(tagsFilename) | ||||||
|  |             lastModifiedDate = datetime.fromtimestamp(modTimesinceEpoc) | ||||||
|  |             fileDaysSinceEpoch = (lastModifiedDate - datetime(1970, 1, 1)).days | ||||||
|  | 
 | ||||||
|  |             # check of the file is too old | ||||||
|  |             if fileDaysSinceEpoch < maxDaysSinceEpoch: | ||||||
|  |                 removeHashtags.append(tagsFilename) | ||||||
|  | 
 | ||||||
|  |     for removeFilename in removeHashtags: | ||||||
|  |         try: | ||||||
|  |             os.remove(removeFilename) | ||||||
|  |         except BaseException: | ||||||
|  |             pass | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								inbox.py
								
								
								
								
							
							
						
						
									
										2
									
								
								inbox.py
								
								
								
								
							|  | @ -58,7 +58,6 @@ from posts import sendSignedJson | ||||||
| from posts import sendToFollowersThread | from posts import sendToFollowersThread | ||||||
| from webapp import individualPostAsHtml | from webapp import individualPostAsHtml | ||||||
| from webapp import getIconsDir | from webapp import getIconsDir | ||||||
| from webapp import removeOldHashtags |  | ||||||
| from question import questionUpdateVotes | from question import questionUpdateVotes | ||||||
| from media import replaceYouTube | from media import replaceYouTube | ||||||
| from git import isGitPatch | from git import isGitPatch | ||||||
|  | @ -66,6 +65,7 @@ from git import receiveGitPatch | ||||||
| from followingCalendar import receivingCalendarEvents | from followingCalendar import receivingCalendarEvents | ||||||
| from content import dangerousMarkup | from content import dangerousMarkup | ||||||
| from happening import saveEventPost | from happening import saveEventPost | ||||||
|  | from delete import removeOldHashtags | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None: | def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None: | ||||||
|  |  | ||||||
							
								
								
									
										24
									
								
								posts.py
								
								
								
								
							
							
						
						
									
										24
									
								
								posts.py
								
								
								
								
							|  | @ -3977,3 +3977,27 @@ def sendUndoBlockViaServer(baseDir: str, session, | ||||||
|         print('DEBUG: c2s POST block success') |         print('DEBUG: c2s POST block success') | ||||||
| 
 | 
 | ||||||
|     return newBlockJson |     return newBlockJson | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def postIsMuted(baseDir: str, nickname: str, domain: str, | ||||||
|  |                 postJsonObject: {}, messageId: str) -> bool: | ||||||
|  |     """ Returns true if the given post is muted | ||||||
|  |     """ | ||||||
|  |     isMuted = postJsonObject.get('muted') | ||||||
|  |     if isMuted is True or isMuted is False: | ||||||
|  |         return isMuted | ||||||
|  |     postDir = baseDir + '/accounts/' + nickname + '@' + domain | ||||||
|  |     muteFilename = \ | ||||||
|  |         postDir + '/inbox/' + messageId.replace('/', '#') + '.json.muted' | ||||||
|  |     if os.path.isfile(muteFilename): | ||||||
|  |         return True | ||||||
|  |     muteFilename = \ | ||||||
|  |         postDir + '/outbox/' + messageId.replace('/', '#') + '.json.muted' | ||||||
|  |     if os.path.isfile(muteFilename): | ||||||
|  |         return True | ||||||
|  |     muteFilename = \ | ||||||
|  |         baseDir + '/accounts/cache/announce/' + nickname + \ | ||||||
|  |         '/' + messageId.replace('/', '#') + '.json.muted' | ||||||
|  |     if os.path.isfile(muteFilename): | ||||||
|  |         return True | ||||||
|  |     return False | ||||||
|  |  | ||||||
							
								
								
									
										8
									
								
								utils.py
								
								
								
								
							
							
						
						
									
										8
									
								
								utils.py
								
								
								
								
							|  | @ -1494,3 +1494,11 @@ def siteIsActive(url: str) -> bool: | ||||||
|         if e.errno == errno.ECONNRESET: |         if e.errno == errno.ECONNRESET: | ||||||
|             print('WARN: connection was reset during siteIsActive') |             print('WARN: connection was reset during siteIsActive') | ||||||
|     return False |     return False | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def weekDayOfMonthStart(monthNumber: int, year: int) -> int: | ||||||
|  |     """Gets the day number of the first day of the month | ||||||
|  |     1=sun, 7=sat | ||||||
|  |     """ | ||||||
|  |     firstDayOfMonth = datetime(year, monthNumber, 1, 0, 0) | ||||||
|  |     return int(firstDayOfMonth.strftime("%w")) + 1 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,224 @@ | ||||||
|  | __filename__ = "webapp_media.py" | ||||||
|  | __author__ = "Bob Mottram" | ||||||
|  | __license__ = "AGPL3+" | ||||||
|  | __version__ = "1.1.0" | ||||||
|  | __maintainer__ = "Bob Mottram" | ||||||
|  | __email__ = "bob@freedombone.net" | ||||||
|  | __status__ = "Production" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def addEmbeddedVideoFromSites(translate: {}, content: str, | ||||||
|  |                               width=400, height=300) -> str: | ||||||
|  |     """Adds embedded videos | ||||||
|  |     """ | ||||||
|  |     if '>vimeo.com/' in content: | ||||||
|  |         url = content.split('>vimeo.com/')[1] | ||||||
|  |         if '<' in url: | ||||||
|  |             url = url.split('<')[0] | ||||||
|  |             content = \ | ||||||
|  |                 content + "<center>\n<iframe loading=\"lazy\" " + \ | ||||||
|  |                 "src=\"https://player.vimeo.com/video/" + \ | ||||||
|  |                 url + "\" width=\"" + str(width) + \ | ||||||
|  |                 "\" height=\"" + str(height) + \ | ||||||
|  |                 "\" frameborder=\"0\" allow=\"autoplay; " + \ | ||||||
|  |                 "fullscreen\" allowfullscreen></iframe>\n</center>\n" | ||||||
|  |             return content | ||||||
|  | 
 | ||||||
|  |     videoSite = 'https://www.youtube.com' | ||||||
|  |     if '"' + videoSite in content: | ||||||
|  |         url = content.split('"' + videoSite)[1] | ||||||
|  |         if '"' in url: | ||||||
|  |             url = url.split('"')[0].replace('/watch?v=', '/embed/') | ||||||
|  |             if '&' in url: | ||||||
|  |                 url = url.split('&')[0] | ||||||
|  |             content = \ | ||||||
|  |                 content + "<center>\n<iframe loading=\"lazy\" src=\"" + \ | ||||||
|  |                 videoSite + url + "\" width=\"" + str(width) + \ | ||||||
|  |                 "\" height=\"" + str(height) + \ | ||||||
|  |                 "\" frameborder=\"0\" allow=\"autoplay; fullscreen\" " + \ | ||||||
|  |                 "allowfullscreen></iframe>\n</center>\n" | ||||||
|  |             return content | ||||||
|  | 
 | ||||||
|  |     invidiousSites = ('https://invidio.us', | ||||||
|  |                       'https://invidious.snopyta.org', | ||||||
|  |                       'http://c7hqkpkpemu6e7emz5b4vy' + | ||||||
|  |                       'z7idjgdvgaaa3dyimmeojqbgpea3xqjoid.onion', | ||||||
|  |                       'http://axqzx4s6s54s32yentfqojs3x5i7faxza6xo3ehd4' + | ||||||
|  |                       'bzzsg2ii4fv2iid.onion') | ||||||
|  |     for videoSite in invidiousSites: | ||||||
|  |         if '"' + videoSite in content: | ||||||
|  |             url = content.split('"' + videoSite)[1] | ||||||
|  |             if '"' in url: | ||||||
|  |                 url = url.split('"')[0].replace('/watch?v=', '/embed/') | ||||||
|  |                 if '&' in url: | ||||||
|  |                     url = url.split('&')[0] | ||||||
|  |                 content = \ | ||||||
|  |                     content + "<center>\n<iframe loading=\"lazy\" src=\"" + \ | ||||||
|  |                     videoSite + url + "\" width=\"" + \ | ||||||
|  |                     str(width) + "\" height=\"" + str(height) + \ | ||||||
|  |                     "\" frameborder=\"0\" allow=\"autoplay; fullscreen\" " + \ | ||||||
|  |                     "allowfullscreen></iframe>\n</center>\n" | ||||||
|  |                 return content | ||||||
|  | 
 | ||||||
|  |     videoSite = 'https://media.ccc.de' | ||||||
|  |     if '"' + videoSite in content: | ||||||
|  |         url = content.split('"' + videoSite)[1] | ||||||
|  |         if '"' in url: | ||||||
|  |             url = url.split('"')[0] | ||||||
|  |             if not url.endswith('/oembed'): | ||||||
|  |                 url = url + '/oembed' | ||||||
|  |             content = \ | ||||||
|  |                 content + "<center>\n<iframe loading=\"lazy\" src=\"" + \ | ||||||
|  |                 videoSite + url + "\" width=\"" + \ | ||||||
|  |                 str(width) + "\" height=\"" + str(height) + \ | ||||||
|  |                 "\" frameborder=\"0\" allow=\"fullscreen\" " + \ | ||||||
|  |                 "allowfullscreen></iframe>\n</center>\n" | ||||||
|  |             return content | ||||||
|  | 
 | ||||||
|  |     if '"https://' in content: | ||||||
|  |         # A selection of the current larger peertube sites, mostly | ||||||
|  |         # French and German language | ||||||
|  |         # These have been chosen based on reported numbers of users | ||||||
|  |         # and the content of each has not been reviewed, so mileage could vary | ||||||
|  |         peerTubeSites = ('peertube.mastodon.host', 'open.tube', 'share.tube', | ||||||
|  |                          'tube.tr4sk.me', 'videos.elbinario.net', | ||||||
|  |                          'hkvideo.live', | ||||||
|  |                          'peertube.snargol.com', 'tube.22decembre.eu', | ||||||
|  |                          'tube.fabrigli.fr', 'libretube.net', 'libre.video', | ||||||
|  |                          'peertube.linuxrocks.online', 'spacepub.space', | ||||||
|  |                          'video.ploud.jp', 'video.omniatv.com', | ||||||
|  |                          'peertube.servebeer.com', | ||||||
|  |                          'tube.tchncs.de', 'tubee.fr', 'video.alternanet.fr', | ||||||
|  |                          'devtube.dev-wiki.de', 'video.samedi.pm', | ||||||
|  |                          'video.irem.univ-paris-diderot.fr', | ||||||
|  |                          'peertube.openstreetmap.fr', 'video.antopie.org', | ||||||
|  |                          'scitech.video', 'tube.4aem.com', 'video.ploud.fr', | ||||||
|  |                          'peervideo.net', 'video.valme.io', | ||||||
|  |                          'videos.pair2jeux.tube', | ||||||
|  |                          'vault.mle.party', 'hostyour.tv', | ||||||
|  |                          'diode.zone', 'visionon.tv', | ||||||
|  |                          'artitube.artifaille.fr', 'peertube.fr', | ||||||
|  |                          'peertube.live', | ||||||
|  |                          'tube.ac-lyon.fr', 'www.yiny.org', 'betamax.video', | ||||||
|  |                          'tube.piweb.be', 'pe.ertu.be', 'peertube.social', | ||||||
|  |                          'videos.lescommuns.org', 'peertube.nogafa.org', | ||||||
|  |                          'skeptikon.fr', 'video.tedomum.net', | ||||||
|  |                          'tube.p2p.legal', | ||||||
|  |                          'sikke.fi', 'exode.me', 'peertube.video') | ||||||
|  |         for site in peerTubeSites: | ||||||
|  |             if '"https://' + site in content: | ||||||
|  |                 url = content.split('"https://' + site)[1] | ||||||
|  |                 if '"' in url: | ||||||
|  |                     url = url.split('"')[0].replace('/watch/', '/embed/') | ||||||
|  |                     content = \ | ||||||
|  |                         content + "<center>\n<iframe loading=\"lazy\" " + \ | ||||||
|  |                         "sandbox=\"allow-same-origin " + \ | ||||||
|  |                         "allow-scripts\" src=\"https://" + \ | ||||||
|  |                         site + url + "\" width=\"" + str(width) + \ | ||||||
|  |                         "\" height=\"" + str(height) + \ | ||||||
|  |                         "\" frameborder=\"0\" allow=\"autoplay; " + \ | ||||||
|  |                         "fullscreen\" allowfullscreen></iframe>\n</center>\n" | ||||||
|  |                     return content | ||||||
|  |     return content | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def addEmbeddedAudio(translate: {}, content: str) -> str: | ||||||
|  |     """Adds embedded audio for mp3/ogg | ||||||
|  |     """ | ||||||
|  |     if not ('.mp3' in content or '.ogg' in content): | ||||||
|  |         return content | ||||||
|  | 
 | ||||||
|  |     if '<audio ' in content: | ||||||
|  |         return content | ||||||
|  | 
 | ||||||
|  |     extension = '.mp3' | ||||||
|  |     if '.ogg' in content: | ||||||
|  |         extension = '.ogg' | ||||||
|  | 
 | ||||||
|  |     words = content.strip('\n').split(' ') | ||||||
|  |     for w in words: | ||||||
|  |         if extension not in w: | ||||||
|  |             continue | ||||||
|  |         w = w.replace('href="', '').replace('">', '') | ||||||
|  |         if w.endswith('.'): | ||||||
|  |             w = w[:-1] | ||||||
|  |         if w.endswith('"'): | ||||||
|  |             w = w[:-1] | ||||||
|  |         if w.endswith(';'): | ||||||
|  |             w = w[:-1] | ||||||
|  |         if w.endswith(':'): | ||||||
|  |             w = w[:-1] | ||||||
|  |         if not w.endswith(extension): | ||||||
|  |             continue | ||||||
|  | 
 | ||||||
|  |         if not (w.startswith('http') or w.startswith('dat:') or | ||||||
|  |                 w.startswith('hyper:') or w.startswith('i2p:') or | ||||||
|  |                 w.startswith('gnunet:') or | ||||||
|  |                 '/' in w): | ||||||
|  |             continue | ||||||
|  |         url = w | ||||||
|  |         content += '<center>\n<audio controls>\n' | ||||||
|  |         content += \ | ||||||
|  |             '<source src="' + url + '" type="audio/' + \ | ||||||
|  |             extension.replace('.', '') + '">' | ||||||
|  |         content += \ | ||||||
|  |             translate['Your browser does not support the audio element.'] | ||||||
|  |         content += '</audio>\n</center>\n' | ||||||
|  |     return content | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def addEmbeddedVideo(translate: {}, content: str, | ||||||
|  |                      width=400, height=300) -> str: | ||||||
|  |     """Adds embedded video for mp4/webm/ogv | ||||||
|  |     """ | ||||||
|  |     if not ('.mp4' in content or '.webm' in content or '.ogv' in content): | ||||||
|  |         return content | ||||||
|  | 
 | ||||||
|  |     if '<video ' in content: | ||||||
|  |         return content | ||||||
|  | 
 | ||||||
|  |     extension = '.mp4' | ||||||
|  |     if '.webm' in content: | ||||||
|  |         extension = '.webm' | ||||||
|  |     elif '.ogv' in content: | ||||||
|  |         extension = '.ogv' | ||||||
|  | 
 | ||||||
|  |     words = content.strip('\n').split(' ') | ||||||
|  |     for w in words: | ||||||
|  |         if extension not in w: | ||||||
|  |             continue | ||||||
|  |         w = w.replace('href="', '').replace('">', '') | ||||||
|  |         if w.endswith('.'): | ||||||
|  |             w = w[:-1] | ||||||
|  |         if w.endswith('"'): | ||||||
|  |             w = w[:-1] | ||||||
|  |         if w.endswith(';'): | ||||||
|  |             w = w[:-1] | ||||||
|  |         if w.endswith(':'): | ||||||
|  |             w = w[:-1] | ||||||
|  |         if not w.endswith(extension): | ||||||
|  |             continue | ||||||
|  |         if not (w.startswith('http') or w.startswith('dat:') or | ||||||
|  |                 w.startswith('hyper:') or w.startswith('i2p:') or | ||||||
|  |                 w.startswith('gnunet:') or | ||||||
|  |                 '/' in w): | ||||||
|  |             continue | ||||||
|  |         url = w | ||||||
|  |         content += \ | ||||||
|  |             '<center>\n<video width="' + str(width) + '" height="' + \ | ||||||
|  |             str(height) + '" controls>\n' | ||||||
|  |         content += \ | ||||||
|  |             '<source src="' + url + '" type="video/' + \ | ||||||
|  |             extension.replace('.', '') + '">\n' | ||||||
|  |         content += \ | ||||||
|  |             translate['Your browser does not support the video element.'] | ||||||
|  |         content += '</video>\n</center>\n' | ||||||
|  |     return content | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def addEmbeddedElements(translate: {}, content: str) -> str: | ||||||
|  |     """Adds embedded elements for various media types | ||||||
|  |     """ | ||||||
|  |     content = addEmbeddedVideoFromSites(translate, content) | ||||||
|  |     content = addEmbeddedAudio(translate, content) | ||||||
|  |     return addEmbeddedVideo(translate, content) | ||||||
|  | @ -0,0 +1,104 @@ | ||||||
|  | __filename__ = "webapp_question.py" | ||||||
|  | __author__ = "Bob Mottram" | ||||||
|  | __license__ = "AGPL3+" | ||||||
|  | __version__ = "1.1.0" | ||||||
|  | __maintainer__ = "Bob Mottram" | ||||||
|  | __email__ = "bob@freedombone.net" | ||||||
|  | __status__ = "Production" | ||||||
|  | 
 | ||||||
|  | import os | ||||||
|  | from question import isQuestion | ||||||
|  | from utils import removeIdEnding | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def insertQuestion(baseDir: str, translate: {}, | ||||||
|  |                    nickname: str, domain: str, port: int, | ||||||
|  |                    content: str, | ||||||
|  |                    postJsonObject: {}, pageNumber: int) -> str: | ||||||
|  |     """ Inserts question selection into a post | ||||||
|  |     """ | ||||||
|  |     if not isQuestion(postJsonObject): | ||||||
|  |         return content | ||||||
|  |     if len(postJsonObject['object']['oneOf']) == 0: | ||||||
|  |         return content | ||||||
|  |     messageId = removeIdEnding(postJsonObject['id']) | ||||||
|  |     if '#' in messageId: | ||||||
|  |         messageId = messageId.split('#', 1)[0] | ||||||
|  |     pageNumberStr = '' | ||||||
|  |     if pageNumber: | ||||||
|  |         pageNumberStr = '?page=' + str(pageNumber) | ||||||
|  | 
 | ||||||
|  |     votesFilename = \ | ||||||
|  |         baseDir + '/accounts/' + nickname + '@' + domain + '/questions.txt' | ||||||
|  | 
 | ||||||
|  |     showQuestionResults = False | ||||||
|  |     if os.path.isfile(votesFilename): | ||||||
|  |         if messageId in open(votesFilename).read(): | ||||||
|  |             showQuestionResults = True | ||||||
|  | 
 | ||||||
|  |     if not showQuestionResults: | ||||||
|  |         # show the question options | ||||||
|  |         content += '<div class="question">' | ||||||
|  |         content += \ | ||||||
|  |             '<form method="POST" action="/users/' + \ | ||||||
|  |             nickname + '/question' + pageNumberStr + '">\n' | ||||||
|  |         content += \ | ||||||
|  |             '<input type="hidden" name="messageId" value="' + \ | ||||||
|  |             messageId + '">\n<br>\n' | ||||||
|  |         for choice in postJsonObject['object']['oneOf']: | ||||||
|  |             if not choice.get('type'): | ||||||
|  |                 continue | ||||||
|  |             if not choice.get('name'): | ||||||
|  |                 continue | ||||||
|  |             content += \ | ||||||
|  |                 '<input type="radio" name="answer" value="' + \ | ||||||
|  |                 choice['name'] + '"> ' + choice['name'] + '<br><br>\n' | ||||||
|  |         content += \ | ||||||
|  |             '<input type="submit" value="' + \ | ||||||
|  |             translate['Vote'] + '" class="vote"><br><br>\n' | ||||||
|  |         content += '</form>\n</div>\n' | ||||||
|  |     else: | ||||||
|  |         # show the responses to a question | ||||||
|  |         content += '<div class="questionresult">\n' | ||||||
|  | 
 | ||||||
|  |         # get the maximum number of votes | ||||||
|  |         maxVotes = 1 | ||||||
|  |         for questionOption in postJsonObject['object']['oneOf']: | ||||||
|  |             if not questionOption.get('name'): | ||||||
|  |                 continue | ||||||
|  |             if not questionOption.get('replies'): | ||||||
|  |                 continue | ||||||
|  |             votes = 0 | ||||||
|  |             try: | ||||||
|  |                 votes = int(questionOption['replies']['totalItems']) | ||||||
|  |             except BaseException: | ||||||
|  |                 pass | ||||||
|  |             if votes > maxVotes: | ||||||
|  |                 maxVotes = int(votes+1) | ||||||
|  | 
 | ||||||
|  |         # show the votes as sliders | ||||||
|  |         questionCtr = 1 | ||||||
|  |         for questionOption in postJsonObject['object']['oneOf']: | ||||||
|  |             if not questionOption.get('name'): | ||||||
|  |                 continue | ||||||
|  |             if not questionOption.get('replies'): | ||||||
|  |                 continue | ||||||
|  |             votes = 0 | ||||||
|  |             try: | ||||||
|  |                 votes = int(questionOption['replies']['totalItems']) | ||||||
|  |             except BaseException: | ||||||
|  |                 pass | ||||||
|  |             votesPercent = str(int(votes * 100 / maxVotes)) | ||||||
|  |             content += \ | ||||||
|  |                 '<p><input type="text" title="' + str(votes) + \ | ||||||
|  |                 '" name="skillName' + str(questionCtr) + \ | ||||||
|  |                 '" value="' + questionOption['name'] + \ | ||||||
|  |                 ' (' + str(votes) + ')" style="width:40%">\n' | ||||||
|  |             content += \ | ||||||
|  |                 '<input type="range" min="1" max="100" ' + \ | ||||||
|  |                 'class="slider" title="' + \ | ||||||
|  |                 str(votes) + '" name="skillValue' + str(questionCtr) + \ | ||||||
|  |                 '" value="' + votesPercent + '"></p>\n' | ||||||
|  |             questionCtr += 1 | ||||||
|  |         content += '</div>\n' | ||||||
|  |     return content | ||||||
|  | @ -0,0 +1,967 @@ | ||||||
|  | __filename__ = "webapp_search.py" | ||||||
|  | __author__ = "Bob Mottram" | ||||||
|  | __license__ = "AGPL3+" | ||||||
|  | __version__ = "1.1.0" | ||||||
|  | __maintainer__ = "Bob Mottram" | ||||||
|  | __email__ = "bob@freedombone.net" | ||||||
|  | __status__ = "Production" | ||||||
|  | 
 | ||||||
|  | import os | ||||||
|  | from shutil import copyfile | ||||||
|  | import urllib.parse | ||||||
|  | from datetime import datetime | ||||||
|  | from utils import getCSS | ||||||
|  | from utils import loadJson | ||||||
|  | from utils import getDomainFromActor | ||||||
|  | from utils import getNicknameFromActor | ||||||
|  | from utils import getConfigParam | ||||||
|  | from utils import locatePost | ||||||
|  | from utils import isPublicPost | ||||||
|  | from utils import firstParagraphFromString | ||||||
|  | from utils import searchBoxPosts | ||||||
|  | from feeds import rss2TagHeader | ||||||
|  | from feeds import rss2TagFooter | ||||||
|  | from webapp_utils import getAltPath | ||||||
|  | from webapp_utils import getIconsDir | ||||||
|  | from webapp_utils import getImageFile | ||||||
|  | from webapp_utils import htmlHeader | ||||||
|  | from webapp_utils import htmlFooter | ||||||
|  | from webapp_utils import getSearchBannerFile | ||||||
|  | from webapp_utils import htmlPostSeparator | ||||||
|  | from webapp_post import individualPostAsHtml | ||||||
|  | from blocking import isBlockedHashtag | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def htmlSearchEmoji(cssCache: {}, translate: {}, | ||||||
|  |                     baseDir: str, httpPrefix: str, | ||||||
|  |                     searchStr: str) -> str: | ||||||
|  |     """Search results for emoji | ||||||
|  |     """ | ||||||
|  |     # emoji.json is generated so that it can be customized and the changes | ||||||
|  |     # will be retained even if default_emoji.json is subsequently updated | ||||||
|  |     if not os.path.isfile(baseDir + '/emoji/emoji.json'): | ||||||
|  |         copyfile(baseDir + '/emoji/default_emoji.json', | ||||||
|  |                  baseDir + '/emoji/emoji.json') | ||||||
|  | 
 | ||||||
|  |     searchStr = searchStr.lower().replace(':', '').strip('\n').strip('\r') | ||||||
|  |     cssFilename = baseDir + '/epicyon-profile.css' | ||||||
|  |     if os.path.isfile(baseDir + '/epicyon.css'): | ||||||
|  |         cssFilename = baseDir + '/epicyon.css' | ||||||
|  | 
 | ||||||
|  |     emojiCSS = getCSS(baseDir, cssFilename, cssCache) | ||||||
|  |     if emojiCSS: | ||||||
|  |         if httpPrefix != 'https': | ||||||
|  |             emojiCSS = emojiCSS.replace('https://', | ||||||
|  |                                         httpPrefix + '://') | ||||||
|  |         emojiLookupFilename = baseDir + '/emoji/emoji.json' | ||||||
|  | 
 | ||||||
|  |         # create header | ||||||
|  |         emojiForm = htmlHeader(cssFilename, emojiCSS) | ||||||
|  |         emojiForm += '<center><h1>' + \ | ||||||
|  |             translate['Emoji Search'] + \ | ||||||
|  |             '</h1></center>' | ||||||
|  | 
 | ||||||
|  |         # does the lookup file exist? | ||||||
|  |         if not os.path.isfile(emojiLookupFilename): | ||||||
|  |             emojiForm += '<center><h5>' + \ | ||||||
|  |                 translate['No results'] + '</h5></center>' | ||||||
|  |             emojiForm += htmlFooter() | ||||||
|  |             return emojiForm | ||||||
|  | 
 | ||||||
|  |         emojiJson = loadJson(emojiLookupFilename) | ||||||
|  |         if emojiJson: | ||||||
|  |             results = {} | ||||||
|  |             for emojiName, filename in emojiJson.items(): | ||||||
|  |                 if searchStr in emojiName: | ||||||
|  |                     results[emojiName] = filename + '.png' | ||||||
|  |             for emojiName, filename in emojiJson.items(): | ||||||
|  |                 if emojiName in searchStr: | ||||||
|  |                     results[emojiName] = filename + '.png' | ||||||
|  |             headingShown = False | ||||||
|  |             emojiForm += '<center>' | ||||||
|  |             msgStr1 = translate['Copy the text then paste it into your post'] | ||||||
|  |             msgStr2 = ':<img loading="lazy" class="searchEmoji" src="/emoji/' | ||||||
|  |             for emojiName, filename in results.items(): | ||||||
|  |                 if os.path.isfile(baseDir + '/emoji/' + filename): | ||||||
|  |                     if not headingShown: | ||||||
|  |                         emojiForm += \ | ||||||
|  |                             '<center><h5>' + msgStr1 + \ | ||||||
|  |                             '</h5></center>' | ||||||
|  |                         headingShown = True | ||||||
|  |                     emojiForm += \ | ||||||
|  |                         '<h3>:' + emojiName + msgStr2 + \ | ||||||
|  |                         filename + '"/></h3>' | ||||||
|  |             emojiForm += '</center>' | ||||||
|  | 
 | ||||||
|  |         emojiForm += htmlFooter() | ||||||
|  |     return emojiForm | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def htmlSearchSharedItems(cssCache: {}, translate: {}, | ||||||
|  |                           baseDir: str, searchStr: str, | ||||||
|  |                           pageNumber: int, | ||||||
|  |                           resultsPerPage: int, | ||||||
|  |                           httpPrefix: str, | ||||||
|  |                           domainFull: str, actor: str, | ||||||
|  |                           callingDomain: str) -> str: | ||||||
|  |     """Search results for shared items | ||||||
|  |     """ | ||||||
|  |     iconsDir = getIconsDir(baseDir) | ||||||
|  |     currPage = 1 | ||||||
|  |     ctr = 0 | ||||||
|  |     sharedItemsForm = '' | ||||||
|  |     searchStrLower = urllib.parse.unquote(searchStr) | ||||||
|  |     searchStrLower = searchStrLower.lower().strip('\n').strip('\r') | ||||||
|  |     searchStrLowerList = searchStrLower.split('+') | ||||||
|  |     cssFilename = baseDir + '/epicyon-profile.css' | ||||||
|  |     if os.path.isfile(baseDir + '/epicyon.css'): | ||||||
|  |         cssFilename = baseDir + '/epicyon.css' | ||||||
|  | 
 | ||||||
|  |     sharedItemsCSS = getCSS(baseDir, cssFilename, cssCache) | ||||||
|  |     if sharedItemsCSS: | ||||||
|  |         if httpPrefix != 'https': | ||||||
|  |             sharedItemsCSS = \ | ||||||
|  |                 sharedItemsCSS.replace('https://', | ||||||
|  |                                        httpPrefix + '://') | ||||||
|  |         sharedItemsForm = htmlHeader(cssFilename, sharedItemsCSS) | ||||||
|  |         sharedItemsForm += \ | ||||||
|  |             '<center><h1>' + translate['Shared Items Search'] + \ | ||||||
|  |             '</h1></center>' | ||||||
|  |         resultsExist = False | ||||||
|  |         for subdir, dirs, files in os.walk(baseDir + '/accounts'): | ||||||
|  |             for handle in dirs: | ||||||
|  |                 if '@' not in handle: | ||||||
|  |                     continue | ||||||
|  |                 contactNickname = handle.split('@')[0] | ||||||
|  |                 sharesFilename = baseDir + '/accounts/' + handle + \ | ||||||
|  |                     '/shares.json' | ||||||
|  |                 if not os.path.isfile(sharesFilename): | ||||||
|  |                     continue | ||||||
|  | 
 | ||||||
|  |                 sharesJson = loadJson(sharesFilename) | ||||||
|  |                 if not sharesJson: | ||||||
|  |                     continue | ||||||
|  | 
 | ||||||
|  |                 for name, sharedItem in sharesJson.items(): | ||||||
|  |                     matched = True | ||||||
|  |                     for searchSubstr in searchStrLowerList: | ||||||
|  |                         subStrMatched = False | ||||||
|  |                         searchSubstr = searchSubstr.strip() | ||||||
|  |                         if searchSubstr in sharedItem['location'].lower(): | ||||||
|  |                             subStrMatched = True | ||||||
|  |                         elif searchSubstr in sharedItem['summary'].lower(): | ||||||
|  |                             subStrMatched = True | ||||||
|  |                         elif searchSubstr in sharedItem['displayName'].lower(): | ||||||
|  |                             subStrMatched = True | ||||||
|  |                         elif searchSubstr in sharedItem['category'].lower(): | ||||||
|  |                             subStrMatched = True | ||||||
|  |                         if not subStrMatched: | ||||||
|  |                             matched = False | ||||||
|  |                             break | ||||||
|  |                     if matched: | ||||||
|  |                         if currPage == pageNumber: | ||||||
|  |                             sharedItemsForm += '<div class="container">\n' | ||||||
|  |                             sharedItemsForm += \ | ||||||
|  |                                 '<p class="share-title">' + \ | ||||||
|  |                                 sharedItem['displayName'] + '</p>\n' | ||||||
|  |                             if sharedItem.get('imageUrl'): | ||||||
|  |                                 sharedItemsForm += \ | ||||||
|  |                                     '<a href="' + \ | ||||||
|  |                                     sharedItem['imageUrl'] + '">\n' | ||||||
|  |                                 sharedItemsForm += \ | ||||||
|  |                                     '<img loading="lazy" src="' + \ | ||||||
|  |                                     sharedItem['imageUrl'] + \ | ||||||
|  |                                     '" alt="Item image"></a>\n' | ||||||
|  |                             sharedItemsForm += \ | ||||||
|  |                                 '<p>' + sharedItem['summary'] + '</p>\n' | ||||||
|  |                             sharedItemsForm += \ | ||||||
|  |                                 '<p><b>' + translate['Type'] + \ | ||||||
|  |                                 ':</b> ' + sharedItem['itemType'] + ' ' | ||||||
|  |                             sharedItemsForm += \ | ||||||
|  |                                 '<b>' + translate['Category'] + \ | ||||||
|  |                                 ':</b> ' + sharedItem['category'] + ' ' | ||||||
|  |                             sharedItemsForm += \ | ||||||
|  |                                 '<b>' + translate['Location'] + \ | ||||||
|  |                                 ':</b> ' + sharedItem['location'] + '</p>\n' | ||||||
|  |                             contactActor = \ | ||||||
|  |                                 httpPrefix + '://' + domainFull + \ | ||||||
|  |                                 '/users/' + contactNickname | ||||||
|  |                             sharedItemsForm += \ | ||||||
|  |                                 '<p><a href="' + actor + \ | ||||||
|  |                                 '?replydm=sharedesc:' + \ | ||||||
|  |                                 sharedItem['displayName'] + \ | ||||||
|  |                                 '?mention=' + contactActor + \ | ||||||
|  |                                 '"><button class="button">' + \ | ||||||
|  |                                 translate['Contact'] + '</button></a>\n' | ||||||
|  |                             if actor.endswith('/users/' + contactNickname): | ||||||
|  |                                 sharedItemsForm += \ | ||||||
|  |                                     ' <a href="' + actor + '?rmshare=' + \ | ||||||
|  |                                     name + '"><button class="button">' + \ | ||||||
|  |                                     translate['Remove'] + '</button></a>\n' | ||||||
|  |                             sharedItemsForm += '</p></div>\n' | ||||||
|  |                             if not resultsExist and currPage > 1: | ||||||
|  |                                 postActor = \ | ||||||
|  |                                     getAltPath(actor, domainFull, | ||||||
|  |                                                callingDomain) | ||||||
|  |                                 # previous page link, needs to be a POST | ||||||
|  |                                 sharedItemsForm += \ | ||||||
|  |                                     '<form method="POST" action="' + \ | ||||||
|  |                                     postActor + \ | ||||||
|  |                                     '/searchhandle?page=' + \ | ||||||
|  |                                     str(pageNumber - 1) + '">\n' | ||||||
|  |                                 sharedItemsForm += \ | ||||||
|  |                                     '  <input type="hidden" ' + \ | ||||||
|  |                                     'name="actor" value="' + actor + '">\n' | ||||||
|  |                                 sharedItemsForm += \ | ||||||
|  |                                     '  <input type="hidden" ' + \ | ||||||
|  |                                     'name="searchtext" value="' + \ | ||||||
|  |                                     searchStrLower + '"><br>\n' | ||||||
|  |                                 sharedItemsForm += \ | ||||||
|  |                                     '  <center>\n' + \ | ||||||
|  |                                     '    <a href="' + actor + \ | ||||||
|  |                                     '" type="submit" name="submitSearch">\n' | ||||||
|  |                                 sharedItemsForm += \ | ||||||
|  |                                     '    <img loading="lazy" ' + \ | ||||||
|  |                                     'class="pageicon" src="/' + iconsDir + \ | ||||||
|  |                                     '/pageup.png" title="' + \ | ||||||
|  |                                     translate['Page up'] + \ | ||||||
|  |                                     '" alt="' + translate['Page up'] + \ | ||||||
|  |                                     '"/></a>\n' | ||||||
|  |                                 sharedItemsForm += '  </center>\n' | ||||||
|  |                                 sharedItemsForm += '</form>\n' | ||||||
|  |                                 resultsExist = True | ||||||
|  |                         ctr += 1 | ||||||
|  |                         if ctr >= resultsPerPage: | ||||||
|  |                             currPage += 1 | ||||||
|  |                             if currPage > pageNumber: | ||||||
|  |                                 postActor = \ | ||||||
|  |                                     getAltPath(actor, domainFull, | ||||||
|  |                                                callingDomain) | ||||||
|  |                                 # next page link, needs to be a POST | ||||||
|  |                                 sharedItemsForm += \ | ||||||
|  |                                     '<form method="POST" action="' + \ | ||||||
|  |                                     postActor + \ | ||||||
|  |                                     '/searchhandle?page=' + \ | ||||||
|  |                                     str(pageNumber + 1) + '">\n' | ||||||
|  |                                 sharedItemsForm += \ | ||||||
|  |                                     '  <input type="hidden" ' + \ | ||||||
|  |                                     'name="actor" value="' + actor + '">\n' | ||||||
|  |                                 sharedItemsForm += \ | ||||||
|  |                                     '  <input type="hidden" ' + \ | ||||||
|  |                                     'name="searchtext" value="' + \ | ||||||
|  |                                     searchStrLower + '"><br>\n' | ||||||
|  |                                 sharedItemsForm += \ | ||||||
|  |                                     '  <center>\n' + \ | ||||||
|  |                                     '    <a href="' + actor + \ | ||||||
|  |                                     '" type="submit" name="submitSearch">\n' | ||||||
|  |                                 sharedItemsForm += \ | ||||||
|  |                                     '    <img loading="lazy" ' + \ | ||||||
|  |                                     'class="pageicon" src="/' + iconsDir + \ | ||||||
|  |                                     '/pagedown.png" title="' + \ | ||||||
|  |                                     translate['Page down'] + \ | ||||||
|  |                                     '" alt="' + translate['Page down'] + \ | ||||||
|  |                                     '"/></a>\n' | ||||||
|  |                                 sharedItemsForm += '  </center>\n' | ||||||
|  |                                 sharedItemsForm += '</form>\n' | ||||||
|  |                                 break | ||||||
|  |                             ctr = 0 | ||||||
|  |         if not resultsExist: | ||||||
|  |             sharedItemsForm += \ | ||||||
|  |                 '<center><h5>' + translate['No results'] + '</h5></center>\n' | ||||||
|  |         sharedItemsForm += htmlFooter() | ||||||
|  |     return sharedItemsForm | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def htmlSearchEmojiTextEntry(cssCache: {}, translate: {}, | ||||||
|  |                              baseDir: str, path: str) -> str: | ||||||
|  |     """Search for an emoji by name | ||||||
|  |     """ | ||||||
|  |     # emoji.json is generated so that it can be customized and the changes | ||||||
|  |     # will be retained even if default_emoji.json is subsequently updated | ||||||
|  |     if not os.path.isfile(baseDir + '/emoji/emoji.json'): | ||||||
|  |         copyfile(baseDir + '/emoji/default_emoji.json', | ||||||
|  |                  baseDir + '/emoji/emoji.json') | ||||||
|  | 
 | ||||||
|  |     actor = path.replace('/search', '') | ||||||
|  |     domain, port = getDomainFromActor(actor) | ||||||
|  | 
 | ||||||
|  |     if os.path.isfile(baseDir + '/img/search-background.png'): | ||||||
|  |         if not os.path.isfile(baseDir + '/accounts/search-background.png'): | ||||||
|  |             copyfile(baseDir + '/img/search-background.png', | ||||||
|  |                      baseDir + '/accounts/search-background.png') | ||||||
|  | 
 | ||||||
|  |     cssFilename = baseDir + '/epicyon-follow.css' | ||||||
|  |     if os.path.isfile(baseDir + '/follow.css'): | ||||||
|  |         cssFilename = baseDir + '/follow.css' | ||||||
|  | 
 | ||||||
|  |     profileStyle = getCSS(baseDir, cssFilename, cssCache) | ||||||
|  | 
 | ||||||
|  |     emojiStr = htmlHeader(cssFilename, profileStyle) | ||||||
|  |     emojiStr += '<div class="follow">\n' | ||||||
|  |     emojiStr += '  <div class="followAvatar">\n' | ||||||
|  |     emojiStr += '  <center>\n' | ||||||
|  |     emojiStr += \ | ||||||
|  |         '  <p class="followText">' + \ | ||||||
|  |         translate['Enter an emoji name to search for'] + '</p>\n' | ||||||
|  |     emojiStr += '  <form method="POST" action="' + \ | ||||||
|  |         actor + '/searchhandleemoji">\n' | ||||||
|  |     emojiStr += '    <input type="hidden" name="actor" value="' + \ | ||||||
|  |         actor + '">\n' | ||||||
|  |     emojiStr += '    <input type="text" name="searchtext" autofocus><br>\n' | ||||||
|  |     emojiStr += \ | ||||||
|  |         '    <button type="submit" class="button" name="submitSearch">' + \ | ||||||
|  |         translate['Submit'] + '</button>\n' | ||||||
|  |     emojiStr += '  </form>\n' | ||||||
|  |     emojiStr += '  </center>\n' | ||||||
|  |     emojiStr += '  </div>\n' | ||||||
|  |     emojiStr += '</div>\n' | ||||||
|  |     emojiStr += htmlFooter() | ||||||
|  |     return emojiStr | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def htmlSearch(cssCache: {}, translate: {}, | ||||||
|  |                baseDir: str, path: str, domain: str, | ||||||
|  |                defaultTimeline: str) -> str: | ||||||
|  |     """Search called from the timeline icon | ||||||
|  |     """ | ||||||
|  |     actor = path.replace('/search', '') | ||||||
|  |     searchNickname = getNicknameFromActor(actor) | ||||||
|  | 
 | ||||||
|  |     if os.path.isfile(baseDir + '/img/search-background.png'): | ||||||
|  |         if not os.path.isfile(baseDir + '/accounts/search-background.png'): | ||||||
|  |             copyfile(baseDir + '/img/search-background.png', | ||||||
|  |                      baseDir + '/accounts/search-background.png') | ||||||
|  | 
 | ||||||
|  |     cssFilename = baseDir + '/epicyon-search.css' | ||||||
|  |     if os.path.isfile(baseDir + '/search.css'): | ||||||
|  |         cssFilename = baseDir + '/search.css' | ||||||
|  | 
 | ||||||
|  |     profileStyle = getCSS(baseDir, cssFilename, cssCache) | ||||||
|  | 
 | ||||||
|  |     if not os.path.isfile(baseDir + '/accounts/' + | ||||||
|  |                           'follow-background.jpg'): | ||||||
|  |         profileStyle = \ | ||||||
|  |             profileStyle.replace('background-image: ' + | ||||||
|  |                                  'url("follow-background.jpg");', | ||||||
|  |                                  'background-image: none;') | ||||||
|  | 
 | ||||||
|  |     followStr = htmlHeader(cssFilename, profileStyle) | ||||||
|  | 
 | ||||||
|  |     # show a banner above the search box | ||||||
|  |     searchBannerFile, searchBannerFilename = \ | ||||||
|  |         getSearchBannerFile(baseDir, searchNickname, domain) | ||||||
|  |     if not os.path.isfile(searchBannerFilename): | ||||||
|  |         # get the default search banner for the theme | ||||||
|  |         theme = getConfigParam(baseDir, 'theme').lower() | ||||||
|  |         if theme == 'default': | ||||||
|  |             theme = '' | ||||||
|  |         else: | ||||||
|  |             theme = '_' + theme | ||||||
|  |         themeSearchImageFile, themeSearchBannerFilename = \ | ||||||
|  |             getImageFile(baseDir, 'search_banner', baseDir + '/img', | ||||||
|  |                          searchNickname, domain) | ||||||
|  |         if os.path.isfile(themeSearchBannerFilename): | ||||||
|  |             searchBannerFilename = \ | ||||||
|  |                 baseDir + '/accounts/' + \ | ||||||
|  |                 searchNickname + '@' + domain + '/' + themeSearchImageFile | ||||||
|  |             copyfile(themeSearchBannerFilename, | ||||||
|  |                      searchBannerFilename) | ||||||
|  |             searchBannerFile = themeSearchImageFile | ||||||
|  | 
 | ||||||
|  |     if os.path.isfile(searchBannerFilename): | ||||||
|  |         usersPath = '/users/' + searchNickname | ||||||
|  |         followStr += \ | ||||||
|  |             '<a href="' + usersPath + '/' + defaultTimeline + '" title="' + \ | ||||||
|  |             translate['Switch to timeline view'] + '" alt="' + \ | ||||||
|  |             translate['Switch to timeline view'] + '">\n' | ||||||
|  |         followStr += '<img loading="lazy" class="timeline-banner" src="' + \ | ||||||
|  |             usersPath + '/' + searchBannerFile + '" /></a>\n' | ||||||
|  | 
 | ||||||
|  |     # show the search box | ||||||
|  |     followStr += '<div class="follow">\n' | ||||||
|  |     followStr += '  <div class="followAvatar">\n' | ||||||
|  |     followStr += '  <center>\n' | ||||||
|  |     idx = 'Enter an address, shared item, !history, #hashtag, ' + \ | ||||||
|  |         '*skill or :emoji: to search for' | ||||||
|  |     followStr += \ | ||||||
|  |         '  <p class="followText">' + translate[idx] + '</p>\n' | ||||||
|  |     followStr += '  <form method="POST" ' + \ | ||||||
|  |         'accept-charset="UTF-8" action="' + actor + '/searchhandle">\n' | ||||||
|  |     followStr += \ | ||||||
|  |         '    <input type="hidden" name="actor" value="' + actor + '">\n' | ||||||
|  |     followStr += '    <input type="text" name="searchtext" autofocus><br>\n' | ||||||
|  |     # followStr += '    <a href="/"><button type="button" class="button" ' + \ | ||||||
|  |     #    'name="submitBack">' + translate['Go Back'] + '</button></a>\n' | ||||||
|  |     followStr += '    <button type="submit" class="button" ' + \ | ||||||
|  |         'name="submitSearch">' + translate['Submit'] + '</button>\n' | ||||||
|  |     followStr += '  </form>\n' | ||||||
|  |     followStr += '  <p class="hashtagswarm">' + \ | ||||||
|  |         htmlHashTagSwarm(baseDir, actor) + '</p>\n' | ||||||
|  |     followStr += '  </center>\n' | ||||||
|  |     followStr += '  </div>\n' | ||||||
|  |     followStr += '</div>\n' | ||||||
|  |     followStr += htmlFooter() | ||||||
|  |     return followStr | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def htmlHashTagSwarm(baseDir: str, actor: str) -> str: | ||||||
|  |     """Returns a tag swarm of today's hashtags | ||||||
|  |     """ | ||||||
|  |     currTime = datetime.utcnow() | ||||||
|  |     daysSinceEpoch = (currTime - datetime(1970, 1, 1)).days | ||||||
|  |     daysSinceEpochStr = str(daysSinceEpoch) + ' ' | ||||||
|  |     tagSwarm = [] | ||||||
|  | 
 | ||||||
|  |     for subdir, dirs, files in os.walk(baseDir + '/tags'): | ||||||
|  |         for f in files: | ||||||
|  |             tagsFilename = os.path.join(baseDir + '/tags', f) | ||||||
|  |             if not os.path.isfile(tagsFilename): | ||||||
|  |                 continue | ||||||
|  |             # get last modified datetime | ||||||
|  |             modTimesinceEpoc = os.path.getmtime(tagsFilename) | ||||||
|  |             lastModifiedDate = datetime.fromtimestamp(modTimesinceEpoc) | ||||||
|  |             fileDaysSinceEpoch = (lastModifiedDate - datetime(1970, 1, 1)).days | ||||||
|  |             # check if the file was last modified today | ||||||
|  |             if fileDaysSinceEpoch != daysSinceEpoch: | ||||||
|  |                 continue | ||||||
|  | 
 | ||||||
|  |             hashTagName = f.split('.')[0] | ||||||
|  |             if isBlockedHashtag(baseDir, hashTagName): | ||||||
|  |                 continue | ||||||
|  |             if daysSinceEpochStr not in open(tagsFilename).read(): | ||||||
|  |                 continue | ||||||
|  |             with open(tagsFilename, 'r') as tagsFile: | ||||||
|  |                 line = tagsFile.readline() | ||||||
|  |                 lineCtr = 1 | ||||||
|  |                 tagCtr = 0 | ||||||
|  |                 maxLineCtr = 1 | ||||||
|  |                 while line: | ||||||
|  |                     if '  ' not in line: | ||||||
|  |                         line = tagsFile.readline() | ||||||
|  |                         lineCtr += 1 | ||||||
|  |                         # don't read too many lines | ||||||
|  |                         if lineCtr >= maxLineCtr: | ||||||
|  |                             break | ||||||
|  |                         continue | ||||||
|  |                     postDaysSinceEpochStr = line.split('  ')[0] | ||||||
|  |                     if not postDaysSinceEpochStr.isdigit(): | ||||||
|  |                         line = tagsFile.readline() | ||||||
|  |                         lineCtr += 1 | ||||||
|  |                         # don't read too many lines | ||||||
|  |                         if lineCtr >= maxLineCtr: | ||||||
|  |                             break | ||||||
|  |                         continue | ||||||
|  |                     postDaysSinceEpoch = int(postDaysSinceEpochStr) | ||||||
|  |                     if postDaysSinceEpoch < daysSinceEpoch: | ||||||
|  |                         break | ||||||
|  |                     if postDaysSinceEpoch == daysSinceEpoch: | ||||||
|  |                         if tagCtr == 0: | ||||||
|  |                             tagSwarm.append(hashTagName) | ||||||
|  |                         tagCtr += 1 | ||||||
|  | 
 | ||||||
|  |                     line = tagsFile.readline() | ||||||
|  |                     lineCtr += 1 | ||||||
|  |                     # don't read too many lines | ||||||
|  |                     if lineCtr >= maxLineCtr: | ||||||
|  |                         break | ||||||
|  | 
 | ||||||
|  |     if not tagSwarm: | ||||||
|  |         return '' | ||||||
|  |     tagSwarm.sort() | ||||||
|  |     tagSwarmStr = '' | ||||||
|  |     ctr = 0 | ||||||
|  |     for tagName in tagSwarm: | ||||||
|  |         tagSwarmStr += \ | ||||||
|  |             '<a href="' + actor + '/tags/' + tagName + \ | ||||||
|  |             '" class="hashtagswarm">' + tagName + '</a>\n' | ||||||
|  |         ctr += 1 | ||||||
|  |     tagSwarmHtml = tagSwarmStr.strip() + '\n' | ||||||
|  |     return tagSwarmHtml | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def htmlHashtagSearch(cssCache: {}, | ||||||
|  |                       nickname: str, domain: str, port: int, | ||||||
|  |                       recentPostsCache: {}, maxRecentPosts: int, | ||||||
|  |                       translate: {}, | ||||||
|  |                       baseDir: str, hashtag: str, pageNumber: int, | ||||||
|  |                       postsPerPage: int, | ||||||
|  |                       session, wfRequest: {}, personCache: {}, | ||||||
|  |                       httpPrefix: str, projectVersion: str, | ||||||
|  |                       YTReplacementDomain: str, | ||||||
|  |                       showPublishedDateOnly: bool) -> str: | ||||||
|  |     """Show a page containing search results for a hashtag | ||||||
|  |     """ | ||||||
|  |     if hashtag.startswith('#'): | ||||||
|  |         hashtag = hashtag[1:] | ||||||
|  |     hashtag = urllib.parse.unquote(hashtag) | ||||||
|  |     hashtagIndexFile = baseDir + '/tags/' + hashtag + '.txt' | ||||||
|  |     if not os.path.isfile(hashtagIndexFile): | ||||||
|  |         if hashtag != hashtag.lower(): | ||||||
|  |             hashtag = hashtag.lower() | ||||||
|  |             hashtagIndexFile = baseDir + '/tags/' + hashtag + '.txt' | ||||||
|  |     if not os.path.isfile(hashtagIndexFile): | ||||||
|  |         print('WARN: hashtag file not found ' + hashtagIndexFile) | ||||||
|  |         return None | ||||||
|  | 
 | ||||||
|  |     iconsDir = getIconsDir(baseDir) | ||||||
|  |     separatorStr = htmlPostSeparator(baseDir, None) | ||||||
|  | 
 | ||||||
|  |     # check that the directory for the nickname exists | ||||||
|  |     if nickname: | ||||||
|  |         if not os.path.isdir(baseDir + '/accounts/' + | ||||||
|  |                              nickname + '@' + domain): | ||||||
|  |             nickname = None | ||||||
|  | 
 | ||||||
|  |     # read the index | ||||||
|  |     with open(hashtagIndexFile, "r") as f: | ||||||
|  |         lines = f.readlines() | ||||||
|  | 
 | ||||||
|  |     # read the css | ||||||
|  |     cssFilename = baseDir + '/epicyon-profile.css' | ||||||
|  |     if os.path.isfile(baseDir + '/epicyon.css'): | ||||||
|  |         cssFilename = baseDir + '/epicyon.css' | ||||||
|  | 
 | ||||||
|  |     hashtagSearchCSS = getCSS(baseDir, cssFilename, cssCache) | ||||||
|  |     if hashtagSearchCSS: | ||||||
|  |         if httpPrefix != 'https': | ||||||
|  |             hashtagSearchCSS = \ | ||||||
|  |                 hashtagSearchCSS.replace('https://', | ||||||
|  |                                          httpPrefix + '://') | ||||||
|  | 
 | ||||||
|  |     # ensure that the page number is in bounds | ||||||
|  |     if not pageNumber: | ||||||
|  |         pageNumber = 1 | ||||||
|  |     elif pageNumber < 1: | ||||||
|  |         pageNumber = 1 | ||||||
|  | 
 | ||||||
|  |     # get the start end end within the index file | ||||||
|  |     startIndex = int((pageNumber - 1) * postsPerPage) | ||||||
|  |     endIndex = startIndex + postsPerPage | ||||||
|  |     noOfLines = len(lines) | ||||||
|  |     if endIndex >= noOfLines and noOfLines > 0: | ||||||
|  |         endIndex = noOfLines - 1 | ||||||
|  | 
 | ||||||
|  |     # add the page title | ||||||
|  |     hashtagSearchForm = htmlHeader(cssFilename, hashtagSearchCSS) | ||||||
|  |     if nickname: | ||||||
|  |         hashtagSearchForm += '<center>\n' + \ | ||||||
|  |             '<h1><a href="/users/' + nickname + '/search">#' + \ | ||||||
|  |             hashtag + '</a></h1>\n' + '</center>\n' | ||||||
|  |     else: | ||||||
|  |         hashtagSearchForm += '<center>\n' + \ | ||||||
|  |             '<h1>#' + hashtag + '</h1>\n' + '</center>\n' | ||||||
|  | 
 | ||||||
|  |     # RSS link for hashtag feed | ||||||
|  |     hashtagSearchForm += '<center><a href="/tags/rss2/' + hashtag + '">' | ||||||
|  |     hashtagSearchForm += \ | ||||||
|  |         '<img style="width:3%;min-width:50px" ' + \ | ||||||
|  |         'loading="lazy" alt="RSS 2.0" ' + \ | ||||||
|  |         'title="RSS 2.0" src="/' + \ | ||||||
|  |         iconsDir + '/logorss.png" /></a></center>' | ||||||
|  | 
 | ||||||
|  |     if startIndex > 0: | ||||||
|  |         # previous page link | ||||||
|  |         hashtagSearchForm += \ | ||||||
|  |             '  <center>\n' + \ | ||||||
|  |             '    <a href="/tags/' + hashtag + '?page=' + \ | ||||||
|  |             str(pageNumber - 1) + \ | ||||||
|  |             '"><img loading="lazy" class="pageicon" src="/' + \ | ||||||
|  |             iconsDir + '/pageup.png" title="' + \ | ||||||
|  |             translate['Page up'] + \ | ||||||
|  |             '" alt="' + translate['Page up'] + \ | ||||||
|  |             '"></a>\n  </center>\n' | ||||||
|  |     index = startIndex | ||||||
|  |     while index <= endIndex: | ||||||
|  |         postId = lines[index].strip('\n').strip('\r') | ||||||
|  |         if '  ' not in postId: | ||||||
|  |             nickname = getNicknameFromActor(postId) | ||||||
|  |             if not nickname: | ||||||
|  |                 index += 1 | ||||||
|  |                 continue | ||||||
|  |         else: | ||||||
|  |             postFields = postId.split('  ') | ||||||
|  |             if len(postFields) != 3: | ||||||
|  |                 index += 1 | ||||||
|  |                 continue | ||||||
|  |             nickname = postFields[1] | ||||||
|  |             postId = postFields[2] | ||||||
|  |         postFilename = locatePost(baseDir, nickname, domain, postId) | ||||||
|  |         if not postFilename: | ||||||
|  |             index += 1 | ||||||
|  |             continue | ||||||
|  |         postJsonObject = loadJson(postFilename) | ||||||
|  |         if postJsonObject: | ||||||
|  |             if not isPublicPost(postJsonObject): | ||||||
|  |                 index += 1 | ||||||
|  |                 continue | ||||||
|  |             showIndividualPostIcons = False | ||||||
|  |             if nickname: | ||||||
|  |                 showIndividualPostIcons = True | ||||||
|  |             allowDeletion = False | ||||||
|  |             hashtagSearchForm += separatorStr + \ | ||||||
|  |                 individualPostAsHtml(True, recentPostsCache, | ||||||
|  |                                      maxRecentPosts, | ||||||
|  |                                      iconsDir, translate, None, | ||||||
|  |                                      baseDir, session, wfRequest, | ||||||
|  |                                      personCache, | ||||||
|  |                                      nickname, domain, port, | ||||||
|  |                                      postJsonObject, | ||||||
|  |                                      None, True, allowDeletion, | ||||||
|  |                                      httpPrefix, projectVersion, | ||||||
|  |                                      'search', | ||||||
|  |                                      YTReplacementDomain, | ||||||
|  |                                      showPublishedDateOnly, | ||||||
|  |                                      showIndividualPostIcons, | ||||||
|  |                                      showIndividualPostIcons, | ||||||
|  |                                      False, False, False) | ||||||
|  |         index += 1 | ||||||
|  | 
 | ||||||
|  |     if endIndex < noOfLines - 1: | ||||||
|  |         # next page link | ||||||
|  |         hashtagSearchForm += \ | ||||||
|  |             '  <center>\n' + \ | ||||||
|  |             '    <a href="/tags/' + hashtag + \ | ||||||
|  |             '?page=' + str(pageNumber + 1) + \ | ||||||
|  |             '"><img loading="lazy" class="pageicon" src="/' + iconsDir + \ | ||||||
|  |             '/pagedown.png" title="' + translate['Page down'] + \ | ||||||
|  |             '" alt="' + translate['Page down'] + '"></a>' + \ | ||||||
|  |             '  </center>' | ||||||
|  |     hashtagSearchForm += htmlFooter() | ||||||
|  |     return hashtagSearchForm | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def rssHashtagSearch(nickname: str, domain: str, port: int, | ||||||
|  |                      recentPostsCache: {}, maxRecentPosts: int, | ||||||
|  |                      translate: {}, | ||||||
|  |                      baseDir: str, hashtag: str, | ||||||
|  |                      postsPerPage: int, | ||||||
|  |                      session, wfRequest: {}, personCache: {}, | ||||||
|  |                      httpPrefix: str, projectVersion: str, | ||||||
|  |                      YTReplacementDomain: str) -> str: | ||||||
|  |     """Show an rss feed for a hashtag | ||||||
|  |     """ | ||||||
|  |     if hashtag.startswith('#'): | ||||||
|  |         hashtag = hashtag[1:] | ||||||
|  |     hashtag = urllib.parse.unquote(hashtag) | ||||||
|  |     hashtagIndexFile = baseDir + '/tags/' + hashtag + '.txt' | ||||||
|  |     if not os.path.isfile(hashtagIndexFile): | ||||||
|  |         if hashtag != hashtag.lower(): | ||||||
|  |             hashtag = hashtag.lower() | ||||||
|  |             hashtagIndexFile = baseDir + '/tags/' + hashtag + '.txt' | ||||||
|  |     if not os.path.isfile(hashtagIndexFile): | ||||||
|  |         print('WARN: hashtag file not found ' + hashtagIndexFile) | ||||||
|  |         return None | ||||||
|  | 
 | ||||||
|  |     # check that the directory for the nickname exists | ||||||
|  |     if nickname: | ||||||
|  |         if not os.path.isdir(baseDir + '/accounts/' + | ||||||
|  |                              nickname + '@' + domain): | ||||||
|  |             nickname = None | ||||||
|  | 
 | ||||||
|  |     # read the index | ||||||
|  |     lines = [] | ||||||
|  |     with open(hashtagIndexFile, "r") as f: | ||||||
|  |         lines = f.readlines() | ||||||
|  |     if not lines: | ||||||
|  |         return None | ||||||
|  | 
 | ||||||
|  |     domainFull = domain | ||||||
|  |     if port: | ||||||
|  |         if port != 80 and port != 443: | ||||||
|  |             domainFull = domain + ':' + str(port) | ||||||
|  | 
 | ||||||
|  |     maxFeedLength = 10 | ||||||
|  |     hashtagFeed = \ | ||||||
|  |         rss2TagHeader(hashtag, httpPrefix, domainFull) | ||||||
|  |     for index in range(len(lines)): | ||||||
|  |         postId = lines[index].strip('\n').strip('\r') | ||||||
|  |         if '  ' not in postId: | ||||||
|  |             nickname = getNicknameFromActor(postId) | ||||||
|  |             if not nickname: | ||||||
|  |                 index += 1 | ||||||
|  |                 if index >= maxFeedLength: | ||||||
|  |                     break | ||||||
|  |                 continue | ||||||
|  |         else: | ||||||
|  |             postFields = postId.split('  ') | ||||||
|  |             if len(postFields) != 3: | ||||||
|  |                 index += 1 | ||||||
|  |                 if index >= maxFeedLength: | ||||||
|  |                     break | ||||||
|  |                 continue | ||||||
|  |             nickname = postFields[1] | ||||||
|  |             postId = postFields[2] | ||||||
|  |         postFilename = locatePost(baseDir, nickname, domain, postId) | ||||||
|  |         if not postFilename: | ||||||
|  |             index += 1 | ||||||
|  |             if index >= maxFeedLength: | ||||||
|  |                 break | ||||||
|  |             continue | ||||||
|  |         postJsonObject = loadJson(postFilename) | ||||||
|  |         if postJsonObject: | ||||||
|  |             if not isPublicPost(postJsonObject): | ||||||
|  |                 index += 1 | ||||||
|  |                 if index >= maxFeedLength: | ||||||
|  |                     break | ||||||
|  |                 continue | ||||||
|  |             # add to feed | ||||||
|  |             if postJsonObject['object'].get('content') and \ | ||||||
|  |                postJsonObject['object'].get('attributedTo') and \ | ||||||
|  |                postJsonObject['object'].get('published'): | ||||||
|  |                 published = postJsonObject['object']['published'] | ||||||
|  |                 pubDate = datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ") | ||||||
|  |                 rssDateStr = pubDate.strftime("%a, %d %b %Y %H:%M:%S UT") | ||||||
|  |                 hashtagFeed += '     <item>' | ||||||
|  |                 hashtagFeed += \ | ||||||
|  |                     '         <author>' + \ | ||||||
|  |                     postJsonObject['object']['attributedTo'] + \ | ||||||
|  |                     '</author>' | ||||||
|  |                 if postJsonObject['object'].get('summary'): | ||||||
|  |                     hashtagFeed += \ | ||||||
|  |                         '         <title>' + \ | ||||||
|  |                         postJsonObject['object']['summary'] + \ | ||||||
|  |                         '</title>' | ||||||
|  |                 description = postJsonObject['object']['content'] | ||||||
|  |                 description = firstParagraphFromString(description) | ||||||
|  |                 hashtagFeed += \ | ||||||
|  |                     '         <description>' + description + '</description>' | ||||||
|  |                 hashtagFeed += \ | ||||||
|  |                     '         <pubDate>' + rssDateStr + '</pubDate>' | ||||||
|  |                 if postJsonObject['object'].get('attachment'): | ||||||
|  |                     for attach in postJsonObject['object']['attachment']: | ||||||
|  |                         if not attach.get('url'): | ||||||
|  |                             continue | ||||||
|  |                         hashtagFeed += \ | ||||||
|  |                             '         <link>' + attach['url'] + '</link>' | ||||||
|  |                 hashtagFeed += '     </item>' | ||||||
|  |         index += 1 | ||||||
|  |         if index >= maxFeedLength: | ||||||
|  |             break | ||||||
|  | 
 | ||||||
|  |     return hashtagFeed + rss2TagFooter() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def htmlSkillsSearch(cssCache: {}, translate: {}, baseDir: str, | ||||||
|  |                      httpPrefix: str, | ||||||
|  |                      skillsearch: str, instanceOnly: bool, | ||||||
|  |                      postsPerPage: int) -> str: | ||||||
|  |     """Show a page containing search results for a skill | ||||||
|  |     """ | ||||||
|  |     if skillsearch.startswith('*'): | ||||||
|  |         skillsearch = skillsearch[1:].strip() | ||||||
|  | 
 | ||||||
|  |     skillsearch = skillsearch.lower().strip('\n').strip('\r') | ||||||
|  | 
 | ||||||
|  |     results = [] | ||||||
|  |     # search instance accounts | ||||||
|  |     for subdir, dirs, files in os.walk(baseDir + '/accounts/'): | ||||||
|  |         for f in files: | ||||||
|  |             if not f.endswith('.json'): | ||||||
|  |                 continue | ||||||
|  |             if '@' not in f: | ||||||
|  |                 continue | ||||||
|  |             if f.startswith('inbox@'): | ||||||
|  |                 continue | ||||||
|  |             actorFilename = os.path.join(subdir, f) | ||||||
|  |             actorJson = loadJson(actorFilename) | ||||||
|  |             if actorJson: | ||||||
|  |                 if actorJson.get('id') and \ | ||||||
|  |                    actorJson.get('skills') and \ | ||||||
|  |                    actorJson.get('name') and \ | ||||||
|  |                    actorJson.get('icon'): | ||||||
|  |                     actor = actorJson['id'] | ||||||
|  |                     for skillName, skillLevel in actorJson['skills'].items(): | ||||||
|  |                         skillName = skillName.lower() | ||||||
|  |                         if not (skillName in skillsearch or | ||||||
|  |                                 skillsearch in skillName): | ||||||
|  |                             continue | ||||||
|  |                         skillLevelStr = str(skillLevel) | ||||||
|  |                         if skillLevel < 100: | ||||||
|  |                             skillLevelStr = '0' + skillLevelStr | ||||||
|  |                         if skillLevel < 10: | ||||||
|  |                             skillLevelStr = '0' + skillLevelStr | ||||||
|  |                         indexStr = \ | ||||||
|  |                             skillLevelStr + ';' + actor + ';' + \ | ||||||
|  |                             actorJson['name'] + \ | ||||||
|  |                             ';' + actorJson['icon']['url'] | ||||||
|  |                         if indexStr not in results: | ||||||
|  |                             results.append(indexStr) | ||||||
|  |     if not instanceOnly: | ||||||
|  |         # search actor cache | ||||||
|  |         for subdir, dirs, files in os.walk(baseDir + '/cache/actors/'): | ||||||
|  |             for f in files: | ||||||
|  |                 if not f.endswith('.json'): | ||||||
|  |                     continue | ||||||
|  |                 if '@' not in f: | ||||||
|  |                     continue | ||||||
|  |                 if f.startswith('inbox@'): | ||||||
|  |                     continue | ||||||
|  |                 actorFilename = os.path.join(subdir, f) | ||||||
|  |                 cachedActorJson = loadJson(actorFilename) | ||||||
|  |                 if cachedActorJson: | ||||||
|  |                     if cachedActorJson.get('actor'): | ||||||
|  |                         actorJson = cachedActorJson['actor'] | ||||||
|  |                         if actorJson.get('id') and \ | ||||||
|  |                            actorJson.get('skills') and \ | ||||||
|  |                            actorJson.get('name') and \ | ||||||
|  |                            actorJson.get('icon'): | ||||||
|  |                             actor = actorJson['id'] | ||||||
|  |                             for skillName, skillLevel in \ | ||||||
|  |                                     actorJson['skills'].items(): | ||||||
|  |                                 skillName = skillName.lower() | ||||||
|  |                                 if not (skillName in skillsearch or | ||||||
|  |                                         skillsearch in skillName): | ||||||
|  |                                     continue | ||||||
|  |                                 skillLevelStr = str(skillLevel) | ||||||
|  |                                 if skillLevel < 100: | ||||||
|  |                                     skillLevelStr = '0' + skillLevelStr | ||||||
|  |                                 if skillLevel < 10: | ||||||
|  |                                     skillLevelStr = '0' + skillLevelStr | ||||||
|  |                                 indexStr = \ | ||||||
|  |                                     skillLevelStr + ';' + actor + ';' + \ | ||||||
|  |                                     actorJson['name'] + \ | ||||||
|  |                                     ';' + actorJson['icon']['url'] | ||||||
|  |                                 if indexStr not in results: | ||||||
|  |                                     results.append(indexStr) | ||||||
|  | 
 | ||||||
|  |     results.sort(reverse=True) | ||||||
|  | 
 | ||||||
|  |     cssFilename = baseDir + '/epicyon-profile.css' | ||||||
|  |     if os.path.isfile(baseDir + '/epicyon.css'): | ||||||
|  |         cssFilename = baseDir + '/epicyon.css' | ||||||
|  | 
 | ||||||
|  |     skillSearchCSS = getCSS(baseDir, cssFilename, cssCache) | ||||||
|  |     if skillSearchCSS: | ||||||
|  |         if httpPrefix != 'https': | ||||||
|  |             skillSearchCSS = \ | ||||||
|  |                 skillSearchCSS.replace('https://', | ||||||
|  |                                        httpPrefix + '://') | ||||||
|  |     skillSearchForm = htmlHeader(cssFilename, skillSearchCSS) | ||||||
|  |     skillSearchForm += \ | ||||||
|  |         '<center><h1>' + translate['Skills search'] + ': ' + \ | ||||||
|  |         skillsearch + '</h1></center>' | ||||||
|  | 
 | ||||||
|  |     if len(results) == 0: | ||||||
|  |         skillSearchForm += \ | ||||||
|  |             '<center><h5>' + translate['No results'] + \ | ||||||
|  |             '</h5></center>' | ||||||
|  |     else: | ||||||
|  |         skillSearchForm += '<center>' | ||||||
|  |         ctr = 0 | ||||||
|  |         for skillMatch in results: | ||||||
|  |             skillMatchFields = skillMatch.split(';') | ||||||
|  |             if len(skillMatchFields) != 4: | ||||||
|  |                 continue | ||||||
|  |             actor = skillMatchFields[1] | ||||||
|  |             actorName = skillMatchFields[2] | ||||||
|  |             avatarUrl = skillMatchFields[3] | ||||||
|  |             skillSearchForm += \ | ||||||
|  |                 '<div class="search-result""><a href="' + \ | ||||||
|  |                 actor + '/skills">' | ||||||
|  |             skillSearchForm += \ | ||||||
|  |                 '<img loading="lazy" src="' + avatarUrl + \ | ||||||
|  |                 '"/><span class="search-result-text">' + actorName + \ | ||||||
|  |                 '</span></a></div>' | ||||||
|  |             ctr += 1 | ||||||
|  |             if ctr >= postsPerPage: | ||||||
|  |                 break | ||||||
|  |         skillSearchForm += '</center>' | ||||||
|  |     skillSearchForm += htmlFooter() | ||||||
|  |     return skillSearchForm | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def htmlHistorySearch(cssCache: {}, translate: {}, baseDir: str, | ||||||
|  |                       httpPrefix: str, | ||||||
|  |                       nickname: str, domain: str, | ||||||
|  |                       historysearch: str, | ||||||
|  |                       postsPerPage: int, pageNumber: int, | ||||||
|  |                       projectVersion: str, | ||||||
|  |                       recentPostsCache: {}, | ||||||
|  |                       maxRecentPosts: int, | ||||||
|  |                       session, | ||||||
|  |                       wfRequest, | ||||||
|  |                       personCache: {}, | ||||||
|  |                       port: int, | ||||||
|  |                       YTReplacementDomain: str, | ||||||
|  |                       showPublishedDateOnly: bool) -> str: | ||||||
|  |     """Show a page containing search results for your post history | ||||||
|  |     """ | ||||||
|  |     if historysearch.startswith('!'): | ||||||
|  |         historysearch = historysearch[1:].strip() | ||||||
|  | 
 | ||||||
|  |     historysearch = historysearch.lower().strip('\n').strip('\r') | ||||||
|  | 
 | ||||||
|  |     boxFilenames = \ | ||||||
|  |         searchBoxPosts(baseDir, nickname, domain, | ||||||
|  |                        historysearch, postsPerPage) | ||||||
|  | 
 | ||||||
|  |     cssFilename = baseDir + '/epicyon-profile.css' | ||||||
|  |     if os.path.isfile(baseDir + '/epicyon.css'): | ||||||
|  |         cssFilename = baseDir + '/epicyon.css' | ||||||
|  | 
 | ||||||
|  |     historySearchCSS = getCSS(baseDir, cssFilename, cssCache) | ||||||
|  |     if historySearchCSS: | ||||||
|  |         if httpPrefix != 'https': | ||||||
|  |             historySearchCSS = \ | ||||||
|  |                 historySearchCSS.replace('https://', | ||||||
|  |                                          httpPrefix + '://') | ||||||
|  |     historySearchForm = htmlHeader(cssFilename, historySearchCSS) | ||||||
|  | 
 | ||||||
|  |     # add the page title | ||||||
|  |     historySearchForm += \ | ||||||
|  |         '<center><h1>' + translate['Your Posts'] + '</h1></center>' | ||||||
|  | 
 | ||||||
|  |     if len(boxFilenames) == 0: | ||||||
|  |         historySearchForm += \ | ||||||
|  |             '<center><h5>' + translate['No results'] + \ | ||||||
|  |             '</h5></center>' | ||||||
|  |         return historySearchForm | ||||||
|  | 
 | ||||||
|  |     iconsDir = getIconsDir(baseDir) | ||||||
|  |     separatorStr = htmlPostSeparator(baseDir, None) | ||||||
|  | 
 | ||||||
|  |     # ensure that the page number is in bounds | ||||||
|  |     if not pageNumber: | ||||||
|  |         pageNumber = 1 | ||||||
|  |     elif pageNumber < 1: | ||||||
|  |         pageNumber = 1 | ||||||
|  | 
 | ||||||
|  |     # get the start end end within the index file | ||||||
|  |     startIndex = int((pageNumber - 1) * postsPerPage) | ||||||
|  |     endIndex = startIndex + postsPerPage | ||||||
|  |     noOfBoxFilenames = len(boxFilenames) | ||||||
|  |     if endIndex >= noOfBoxFilenames and noOfBoxFilenames > 0: | ||||||
|  |         endIndex = noOfBoxFilenames - 1 | ||||||
|  | 
 | ||||||
|  |     index = startIndex | ||||||
|  |     while index <= endIndex: | ||||||
|  |         postFilename = boxFilenames[index] | ||||||
|  |         if not postFilename: | ||||||
|  |             index += 1 | ||||||
|  |             continue | ||||||
|  |         postJsonObject = loadJson(postFilename) | ||||||
|  |         if not postJsonObject: | ||||||
|  |             index += 1 | ||||||
|  |             continue | ||||||
|  |         showIndividualPostIcons = True | ||||||
|  |         allowDeletion = False | ||||||
|  |         historySearchForm += separatorStr + \ | ||||||
|  |             individualPostAsHtml(True, recentPostsCache, | ||||||
|  |                                  maxRecentPosts, | ||||||
|  |                                  iconsDir, translate, None, | ||||||
|  |                                  baseDir, session, wfRequest, | ||||||
|  |                                  personCache, | ||||||
|  |                                  nickname, domain, port, | ||||||
|  |                                  postJsonObject, | ||||||
|  |                                  None, True, allowDeletion, | ||||||
|  |                                  httpPrefix, projectVersion, | ||||||
|  |                                  'search', | ||||||
|  |                                  YTReplacementDomain, | ||||||
|  |                                  showPublishedDateOnly, | ||||||
|  |                                  showIndividualPostIcons, | ||||||
|  |                                  showIndividualPostIcons, | ||||||
|  |                                  False, False, False) | ||||||
|  |         index += 1 | ||||||
|  | 
 | ||||||
|  |     historySearchForm += htmlFooter() | ||||||
|  |     return historySearchForm | ||||||
							
								
								
									
										326
									
								
								webapp_utils.py
								
								
								
								
							
							
						
						
									
										326
									
								
								webapp_utils.py
								
								
								
								
							|  | @ -11,9 +11,12 @@ from collections import OrderedDict | ||||||
| from session import getJson | from session import getJson | ||||||
| from utils import getProtocolPrefixes | from utils import getProtocolPrefixes | ||||||
| from utils import loadJson | from utils import loadJson | ||||||
|  | from utils import getCachedPostFilename | ||||||
| from utils import getConfigParam | from utils import getConfigParam | ||||||
| from cache import getPersonFromCache | from cache import getPersonFromCache | ||||||
| from cache import storePersonInCache | from cache import storePersonInCache | ||||||
|  | from content import addHtmlTags | ||||||
|  | from content import replaceEmojiFromTags | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def getAltPath(actor: str, domainFull: str, callingDomain: str) -> str: | def getAltPath(actor: str, domainFull: str, callingDomain: str) -> str: | ||||||
|  | @ -414,3 +417,326 @@ def getRightImageFile(baseDir: str, | ||||||
|     return getImageFile(baseDir, 'right_col_image', |     return getImageFile(baseDir, 'right_col_image', | ||||||
|                         baseDir + '/accounts/' + nickname + '@' + domain, |                         baseDir + '/accounts/' + nickname + '@' + domain, | ||||||
|                         nickname, domain) |                         nickname, domain) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def htmlHeader(cssFilename: str, css: str, lang='en') -> str: | ||||||
|  |     htmlStr = '<!DOCTYPE html>\n' | ||||||
|  |     htmlStr += '<html lang="' + lang + '">\n' | ||||||
|  |     htmlStr += '  <head>\n' | ||||||
|  |     htmlStr += '    <meta charset="utf-8">\n' | ||||||
|  |     fontName, fontFormat = getFontFromCss(css) | ||||||
|  |     if fontName: | ||||||
|  |         htmlStr += '    <link rel="preload" as="font" type="' + \ | ||||||
|  |             fontFormat + '" href="' + fontName + '" crossorigin>\n' | ||||||
|  |     htmlStr += '    <style>\n' + css + '</style>\n' | ||||||
|  |     htmlStr += '    <link rel="manifest" href="/manifest.json">\n' | ||||||
|  |     htmlStr += '    <meta name="theme-color" content="grey">\n' | ||||||
|  |     htmlStr += '    <title>Epicyon</title>\n' | ||||||
|  |     htmlStr += '  </head>\n' | ||||||
|  |     htmlStr += '  <body>\n' | ||||||
|  |     return htmlStr | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def htmlFooter() -> str: | ||||||
|  |     htmlStr = '  </body>\n' | ||||||
|  |     htmlStr += '</html>\n' | ||||||
|  |     return htmlStr | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def getFontFromCss(css: str) -> (str, str): | ||||||
|  |     """Returns the font name and format | ||||||
|  |     """ | ||||||
|  |     if ' url(' not in css: | ||||||
|  |         return None, None | ||||||
|  |     fontName = css.split(" url(")[1].split(")")[0].replace("'", '') | ||||||
|  |     fontFormat = css.split(" format('")[1].split("')")[0] | ||||||
|  |     return fontName, fontFormat | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def loadIndividualPostAsHtmlFromCache(baseDir: str, | ||||||
|  |                                       nickname: str, domain: str, | ||||||
|  |                                       postJsonObject: {}) -> str: | ||||||
|  |     """If a cached html version of the given post exists then load it and | ||||||
|  |     return the html text | ||||||
|  |     This is much quicker than generating the html from the json object | ||||||
|  |     """ | ||||||
|  |     cachedPostFilename = \ | ||||||
|  |         getCachedPostFilename(baseDir, nickname, domain, postJsonObject) | ||||||
|  | 
 | ||||||
|  |     postHtml = '' | ||||||
|  |     if not cachedPostFilename: | ||||||
|  |         return postHtml | ||||||
|  | 
 | ||||||
|  |     if not os.path.isfile(cachedPostFilename): | ||||||
|  |         return postHtml | ||||||
|  | 
 | ||||||
|  |     tries = 0 | ||||||
|  |     while tries < 3: | ||||||
|  |         try: | ||||||
|  |             with open(cachedPostFilename, 'r') as file: | ||||||
|  |                 postHtml = file.read() | ||||||
|  |                 break | ||||||
|  |         except Exception as e: | ||||||
|  |             print(e) | ||||||
|  |             # no sleep | ||||||
|  |             tries += 1 | ||||||
|  |     if postHtml: | ||||||
|  |         return postHtml | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def addEmojiToDisplayName(baseDir: str, httpPrefix: str, | ||||||
|  |                           nickname: str, domain: str, | ||||||
|  |                           displayName: str, inProfileName: bool) -> str: | ||||||
|  |     """Adds emoji icons to display names on individual posts | ||||||
|  |     """ | ||||||
|  |     if ':' not in displayName: | ||||||
|  |         return displayName | ||||||
|  | 
 | ||||||
|  |     displayName = displayName.replace('<p>', '').replace('</p>', '') | ||||||
|  |     emojiTags = {} | ||||||
|  |     print('TAG: displayName before tags: ' + displayName) | ||||||
|  |     displayName = \ | ||||||
|  |         addHtmlTags(baseDir, httpPrefix, | ||||||
|  |                     nickname, domain, displayName, [], emojiTags) | ||||||
|  |     displayName = displayName.replace('<p>', '').replace('</p>', '') | ||||||
|  |     print('TAG: displayName after tags: ' + displayName) | ||||||
|  |     # convert the emoji dictionary to a list | ||||||
|  |     emojiTagsList = [] | ||||||
|  |     for tagName, tag in emojiTags.items(): | ||||||
|  |         emojiTagsList.append(tag) | ||||||
|  |     print('TAG: emoji tags list: ' + str(emojiTagsList)) | ||||||
|  |     if not inProfileName: | ||||||
|  |         displayName = \ | ||||||
|  |             replaceEmojiFromTags(displayName, emojiTagsList, 'post header') | ||||||
|  |     else: | ||||||
|  |         displayName = \ | ||||||
|  |             replaceEmojiFromTags(displayName, emojiTagsList, 'profile') | ||||||
|  |     print('TAG: displayName after tags 2: ' + displayName) | ||||||
|  | 
 | ||||||
|  |     # remove any stray emoji | ||||||
|  |     while ':' in displayName: | ||||||
|  |         if '://' in displayName: | ||||||
|  |             break | ||||||
|  |         emojiStr = displayName.split(':')[1] | ||||||
|  |         prevDisplayName = displayName | ||||||
|  |         displayName = displayName.replace(':' + emojiStr + ':', '').strip() | ||||||
|  |         if prevDisplayName == displayName: | ||||||
|  |             break | ||||||
|  |         print('TAG: displayName after tags 3: ' + displayName) | ||||||
|  |     print('TAG: displayName after tag replacements: ' + displayName) | ||||||
|  | 
 | ||||||
|  |     return displayName | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def getPostAttachmentsAsHtml(postJsonObject: {}, boxName: str, translate: {}, | ||||||
|  |                              isMuted: bool, avatarLink: str, | ||||||
|  |                              replyStr: str, announceStr: str, likeStr: str, | ||||||
|  |                              bookmarkStr: str, deleteStr: str, | ||||||
|  |                              muteStr: str) -> (str, str): | ||||||
|  |     """Returns a string representing any attachments | ||||||
|  |     """ | ||||||
|  |     attachmentStr = '' | ||||||
|  |     galleryStr = '' | ||||||
|  |     if not postJsonObject['object'].get('attachment'): | ||||||
|  |         return attachmentStr, galleryStr | ||||||
|  | 
 | ||||||
|  |     if not isinstance(postJsonObject['object']['attachment'], list): | ||||||
|  |         return attachmentStr, galleryStr | ||||||
|  | 
 | ||||||
|  |     attachmentCtr = 0 | ||||||
|  |     attachmentStr += '<div class="media">\n' | ||||||
|  |     for attach in postJsonObject['object']['attachment']: | ||||||
|  |         if not (attach.get('mediaType') and attach.get('url')): | ||||||
|  |             continue | ||||||
|  | 
 | ||||||
|  |         mediaType = attach['mediaType'] | ||||||
|  |         imageDescription = '' | ||||||
|  |         if attach.get('name'): | ||||||
|  |             imageDescription = attach['name'].replace('"', "'") | ||||||
|  |         if mediaType == 'image/png' or \ | ||||||
|  |            mediaType == 'image/jpeg' or \ | ||||||
|  |            mediaType == 'image/webp' or \ | ||||||
|  |            mediaType == 'image/avif' or \ | ||||||
|  |            mediaType == 'image/gif': | ||||||
|  |             if attach['url'].endswith('.png') or \ | ||||||
|  |                attach['url'].endswith('.jpg') or \ | ||||||
|  |                attach['url'].endswith('.jpeg') or \ | ||||||
|  |                attach['url'].endswith('.webp') or \ | ||||||
|  |                attach['url'].endswith('.avif') or \ | ||||||
|  |                attach['url'].endswith('.gif'): | ||||||
|  |                 if attachmentCtr > 0: | ||||||
|  |                     attachmentStr += '<br>' | ||||||
|  |                 if boxName == 'tlmedia': | ||||||
|  |                     galleryStr += '<div class="gallery">\n' | ||||||
|  |                     if not isMuted: | ||||||
|  |                         galleryStr += '  <a href="' + attach['url'] + '">\n' | ||||||
|  |                         galleryStr += \ | ||||||
|  |                             '    <img loading="lazy" src="' + \ | ||||||
|  |                             attach['url'] + '" alt="" title="">\n' | ||||||
|  |                         galleryStr += '  </a>\n' | ||||||
|  |                     if postJsonObject['object'].get('url'): | ||||||
|  |                         imagePostUrl = postJsonObject['object']['url'] | ||||||
|  |                     else: | ||||||
|  |                         imagePostUrl = postJsonObject['object']['id'] | ||||||
|  |                     if imageDescription and not isMuted: | ||||||
|  |                         galleryStr += \ | ||||||
|  |                             '  <a href="' + imagePostUrl + \ | ||||||
|  |                             '" class="gallerytext"><div ' + \ | ||||||
|  |                             'class="gallerytext">' + \ | ||||||
|  |                             imageDescription + '</div></a>\n' | ||||||
|  |                     else: | ||||||
|  |                         galleryStr += \ | ||||||
|  |                             '<label class="transparent">---</label><br>' | ||||||
|  |                     galleryStr += '  <div class="mediaicons">\n' | ||||||
|  |                     galleryStr += \ | ||||||
|  |                         '    ' + replyStr+announceStr + likeStr + \ | ||||||
|  |                         bookmarkStr + deleteStr + muteStr + '\n' | ||||||
|  |                     galleryStr += '  </div>\n' | ||||||
|  |                     galleryStr += '  <div class="mediaavatar">\n' | ||||||
|  |                     galleryStr += '    ' + avatarLink + '\n' | ||||||
|  |                     galleryStr += '  </div>\n' | ||||||
|  |                     galleryStr += '</div>\n' | ||||||
|  | 
 | ||||||
|  |                 attachmentStr += '<a href="' + attach['url'] + '">' | ||||||
|  |                 attachmentStr += \ | ||||||
|  |                     '<img loading="lazy" src="' + attach['url'] + \ | ||||||
|  |                     '" alt="' + imageDescription + '" title="' + \ | ||||||
|  |                     imageDescription + '" class="attachment"></a>\n' | ||||||
|  |                 attachmentCtr += 1 | ||||||
|  |         elif (mediaType == 'video/mp4' or | ||||||
|  |               mediaType == 'video/webm' or | ||||||
|  |               mediaType == 'video/ogv'): | ||||||
|  |             extension = '.mp4' | ||||||
|  |             if attach['url'].endswith('.webm'): | ||||||
|  |                 extension = '.webm' | ||||||
|  |             elif attach['url'].endswith('.ogv'): | ||||||
|  |                 extension = '.ogv' | ||||||
|  |             if attach['url'].endswith(extension): | ||||||
|  |                 if attachmentCtr > 0: | ||||||
|  |                     attachmentStr += '<br>' | ||||||
|  |                 if boxName == 'tlmedia': | ||||||
|  |                     galleryStr += '<div class="gallery">\n' | ||||||
|  |                     if not isMuted: | ||||||
|  |                         galleryStr += '  <a href="' + attach['url'] + '">\n' | ||||||
|  |                         galleryStr += \ | ||||||
|  |                             '    <video width="600" height="400" controls>\n' | ||||||
|  |                         galleryStr += \ | ||||||
|  |                             '      <source src="' + attach['url'] + \ | ||||||
|  |                             '" alt="' + imageDescription + \ | ||||||
|  |                             '" title="' + imageDescription + \ | ||||||
|  |                             '" class="attachment" type="video/' + \ | ||||||
|  |                             extension.replace('.', '') + '">' | ||||||
|  |                         idx = 'Your browser does not support the video tag.' | ||||||
|  |                         galleryStr += translate[idx] | ||||||
|  |                         galleryStr += '    </video>\n' | ||||||
|  |                         galleryStr += '  </a>\n' | ||||||
|  |                     if postJsonObject['object'].get('url'): | ||||||
|  |                         videoPostUrl = postJsonObject['object']['url'] | ||||||
|  |                     else: | ||||||
|  |                         videoPostUrl = postJsonObject['object']['id'] | ||||||
|  |                     if imageDescription and not isMuted: | ||||||
|  |                         galleryStr += \ | ||||||
|  |                             '  <a href="' + videoPostUrl + \ | ||||||
|  |                             '" class="gallerytext"><div ' + \ | ||||||
|  |                             'class="gallerytext">' + \ | ||||||
|  |                             imageDescription + '</div></a>\n' | ||||||
|  |                     else: | ||||||
|  |                         galleryStr += \ | ||||||
|  |                             '<label class="transparent">---</label><br>' | ||||||
|  |                     galleryStr += '  <div class="mediaicons">\n' | ||||||
|  |                     galleryStr += \ | ||||||
|  |                         '    ' + replyStr + announceStr + likeStr + \ | ||||||
|  |                         bookmarkStr + deleteStr + muteStr + '\n' | ||||||
|  |                     galleryStr += '  </div>\n' | ||||||
|  |                     galleryStr += '  <div class="mediaavatar">\n' | ||||||
|  |                     galleryStr += '    ' + avatarLink + '\n' | ||||||
|  |                     galleryStr += '  </div>\n' | ||||||
|  |                     galleryStr += '</div>\n' | ||||||
|  | 
 | ||||||
|  |                 attachmentStr += \ | ||||||
|  |                     '<center><video width="400" height="300" controls>' | ||||||
|  |                 attachmentStr += \ | ||||||
|  |                     '<source src="' + attach['url'] + '" alt="' + \ | ||||||
|  |                     imageDescription + '" title="' + imageDescription + \ | ||||||
|  |                     '" class="attachment" type="video/' + \ | ||||||
|  |                     extension.replace('.', '') + '">' | ||||||
|  |                 attachmentStr += \ | ||||||
|  |                     translate['Your browser does not support the video tag.'] | ||||||
|  |                 attachmentStr += '</video></center>' | ||||||
|  |                 attachmentCtr += 1 | ||||||
|  |         elif (mediaType == 'audio/mpeg' or | ||||||
|  |               mediaType == 'audio/ogg'): | ||||||
|  |             extension = '.mp3' | ||||||
|  |             if attach['url'].endswith('.ogg'): | ||||||
|  |                 extension = '.ogg' | ||||||
|  |             if attach['url'].endswith(extension): | ||||||
|  |                 if attachmentCtr > 0: | ||||||
|  |                     attachmentStr += '<br>' | ||||||
|  |                 if boxName == 'tlmedia': | ||||||
|  |                     galleryStr += '<div class="gallery">\n' | ||||||
|  |                     if not isMuted: | ||||||
|  |                         galleryStr += '  <a href="' + attach['url'] + '">\n' | ||||||
|  |                         galleryStr += '    <audio controls>\n' | ||||||
|  |                         galleryStr += \ | ||||||
|  |                             '      <source src="' + attach['url'] + \ | ||||||
|  |                             '" alt="' + imageDescription + \ | ||||||
|  |                             '" title="' + imageDescription + \ | ||||||
|  |                             '" class="attachment" type="audio/' + \ | ||||||
|  |                             extension.replace('.', '') + '">' | ||||||
|  |                         idx = 'Your browser does not support the audio tag.' | ||||||
|  |                         galleryStr += translate[idx] | ||||||
|  |                         galleryStr += '    </audio>\n' | ||||||
|  |                         galleryStr += '  </a>\n' | ||||||
|  |                     if postJsonObject['object'].get('url'): | ||||||
|  |                         audioPostUrl = postJsonObject['object']['url'] | ||||||
|  |                     else: | ||||||
|  |                         audioPostUrl = postJsonObject['object']['id'] | ||||||
|  |                     if imageDescription and not isMuted: | ||||||
|  |                         galleryStr += \ | ||||||
|  |                             '  <a href="' + audioPostUrl + \ | ||||||
|  |                             '" class="gallerytext"><div ' + \ | ||||||
|  |                             'class="gallerytext">' + \ | ||||||
|  |                             imageDescription + '</div></a>\n' | ||||||
|  |                     else: | ||||||
|  |                         galleryStr += \ | ||||||
|  |                             '<label class="transparent">---</label><br>' | ||||||
|  |                     galleryStr += '  <div class="mediaicons">\n' | ||||||
|  |                     galleryStr += \ | ||||||
|  |                         '    ' + replyStr + announceStr + \ | ||||||
|  |                         likeStr + bookmarkStr + \ | ||||||
|  |                         deleteStr + muteStr+'\n' | ||||||
|  |                     galleryStr += '  </div>\n' | ||||||
|  |                     galleryStr += '  <div class="mediaavatar">\n' | ||||||
|  |                     galleryStr += '    ' + avatarLink + '\n' | ||||||
|  |                     galleryStr += '  </div>\n' | ||||||
|  |                     galleryStr += '</div>\n' | ||||||
|  | 
 | ||||||
|  |                 attachmentStr += '<center>\n<audio controls>\n' | ||||||
|  |                 attachmentStr += \ | ||||||
|  |                     '<source src="' + attach['url'] + '" alt="' + \ | ||||||
|  |                     imageDescription + '" title="' + imageDescription + \ | ||||||
|  |                     '" class="attachment" type="audio/' + \ | ||||||
|  |                     extension.replace('.', '') + '">' | ||||||
|  |                 attachmentStr += \ | ||||||
|  |                     translate['Your browser does not support the audio tag.'] | ||||||
|  |                 attachmentStr += '</audio>\n</center>\n' | ||||||
|  |                 attachmentCtr += 1 | ||||||
|  |     attachmentStr += '</div>' | ||||||
|  |     return attachmentStr, galleryStr | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def htmlPostSeparator(baseDir: str, column: str) -> str: | ||||||
|  |     """Returns the html for a timeline post separator image | ||||||
|  |     """ | ||||||
|  |     iconsDir = getIconsDir(baseDir) | ||||||
|  |     filename = 'separator.png' | ||||||
|  |     if column: | ||||||
|  |         filename = 'separator_' + column + '.png' | ||||||
|  |     separatorImageFilename = baseDir + '/img/' + iconsDir + '/' + filename | ||||||
|  |     separatorStr = '' | ||||||
|  |     if os.path.isfile(separatorImageFilename): | ||||||
|  |         separatorStr = \ | ||||||
|  |             '<div class="postSeparatorImage"><center>' + \ | ||||||
|  |             '<img src="/' + iconsDir + '/' + filename + '"/>' + \ | ||||||
|  |             '</center></div>\n' | ||||||
|  |     return separatorStr | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue