epicyon/webapp_search.py

1002 lines
40 KiB
Python
Raw Normal View History

2020-11-09 19:41:01 +00:00
__filename__ = "webapp_search.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
2021-01-26 10:07:42 +00:00
__version__ = "1.2.0"
2020-11-09 19:41:01 +00:00
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
2021-06-15 15:08:12 +00:00
__module_group__ = "Web Interface"
2020-11-09 19:41:01 +00:00
import os
from shutil import copyfile
import urllib.parse
from datetime import datetime
from utils import getBaseContentFromPost
2021-06-25 18:02:05 +00:00
from utils import isAccountDir
2021-01-11 19:46:21 +00:00
from utils import getConfigParam
2020-12-16 11:19:16 +00:00
from utils import getFullDomain
2020-12-01 21:44:27 +00:00
from utils import isEditor
2020-11-09 19:41:01 +00:00
from utils import loadJson
from utils import getDomainFromActor
from utils import getNicknameFromActor
from utils import locatePost
from utils import isPublicPost
from utils import firstParagraphFromString
from utils import searchBoxPosts
2021-06-26 11:16:41 +00:00
from utils import getAltPath
2021-07-13 21:59:53 +00:00
from utils import acctDir
2021-07-25 13:09:39 +00:00
from utils import isfloat
from skills import noOfActorSkills
2021-05-14 20:02:55 +00:00
from skills import getSkillsFromList
from categories import getHashtagCategory
2020-11-09 19:41:01 +00:00
from feeds import rss2TagHeader
from feeds import rss2TagFooter
2021-02-07 13:33:30 +00:00
from webapp_utils import htmlKeyboardNavigation
from webapp_utils import htmlHeaderWithExternalStyle
2020-11-09 19:41:01 +00:00
from webapp_utils import htmlFooter
from webapp_utils import getSearchBannerFile
from webapp_utils import htmlPostSeparator
from webapp_post import individualPostAsHtml
2020-11-19 14:02:16 +00:00
from webapp_hashtagswarm import htmlHashTagSwarm
2020-11-09 19:41:01 +00:00
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'
emojiLookupFilename = baseDir + '/emoji/emoji.json'
# create header
2021-01-11 19:46:21 +00:00
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
emojiForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
emojiForm += '<center><h1>' + \
translate['Emoji Search'] + \
'</h1></center>'
2020-11-09 19:41:01 +00:00
# does the lookup file exist?
if not os.path.isfile(emojiLookupFilename):
emojiForm += '<center><h5>' + \
translate['No results'] + '</h5></center>'
2020-11-09 19:41:01 +00:00
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()
2020-11-09 19:41:01 +00:00
return emojiForm
def _matchSharedItem(searchStrLowerList: [],
sharedItem: {}) -> bool:
"""Returns true if the shared item matches search criteria
"""
for searchSubstr in searchStrLowerList:
searchSubstr = searchSubstr.strip()
if searchSubstr in sharedItem['location'].lower():
return True
elif searchSubstr in sharedItem['summary'].lower():
return True
elif searchSubstr in sharedItem['displayName'].lower():
return True
elif searchSubstr in sharedItem['category'].lower():
return True
return False
def _htmlSearchResultShare(sharedItem: {}, translate: {},
httpPrefix: str, domainFull: str,
contactNickname: str, name: str,
actor: str) -> str:
"""Returns the html for an individual shared item
"""
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<p>'
if sharedItem.get('itemQty'):
sharedItemsForm += \
'<b>' + translate['Quantity'] + \
':</b> ' + str(sharedItem['itemQty']) + ' '
sharedItemsForm += \
'<b>' + translate['Type'] + ':</b> ' + sharedItem['itemType'] + ' '
sharedItemsForm += \
'<b>' + translate['Category'] + ':</b> ' + sharedItem['category'] + ' '
sharedItemsForm += \
'<b>' + translate['Location'] + ':</b> ' + sharedItem['location']
if sharedItem.get('itemPrice') and \
sharedItem.get('itemCurrency'):
if isfloat(sharedItem['itemPrice']):
if float(sharedItem['itemPrice']) > 0:
sharedItemsForm += \
' <b>' + translate['Price'] + \
':</b> ' + sharedItem['itemPrice'] + \
' ' + sharedItem['itemCurrency']
sharedItemsForm += '</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'
return sharedItemsForm
2021-07-27 10:05:57 +00:00
def _htmlSearchResultSharePage(actor: str, domainFull: str,
2021-07-27 09:57:52 +00:00
callingDomain: str, pageNumber: int,
2021-07-27 10:05:57 +00:00
searchStrLower: str, translate: {},
previous: bool) -> str:
2021-07-27 09:57:52 +00:00
"""Returns the html for the previous button on shared items search results
"""
postActor = getAltPath(actor, domainFull, callingDomain)
# previous page link, needs to be a POST
2021-07-27 10:05:57 +00:00
if previous:
pageNumber -= 1
titleStr = translate['Page up']
imageUrl = 'pageup.png'
else:
pageNumber += 1
titleStr = translate['Page down']
imageUrl = 'pagedown.png'
2021-07-27 09:57:52 +00:00
sharedItemsForm = \
'<form method="POST" action="' + postActor + '/searchhandle?page=' + \
2021-07-27 10:05:57 +00:00
str(pageNumber) + '">\n'
2021-07-27 09:57:52 +00:00
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="/icons' + \
2021-07-27 10:05:57 +00:00
'/' + imageUrl + '" title="' + titleStr + \
'" alt="' + titleStr + '"/></a>\n'
2021-07-27 09:57:52 +00:00
sharedItemsForm += ' </center>\n'
sharedItemsForm += '</form>\n'
return sharedItemsForm
def _htmlSharesResult(sharesJson: {}, pageNumber: int, resultsPerPage: int,
searchStrLowerList: [], currPage: int, ctr: int,
callingDomain: str, httpPrefix: str, domainFull: str,
contactNickname: str, actor: str,
resultsExist: bool, searchStrLower: str,
translate: {}) -> (bool, int, int, str):
"""
"""
sharedItemsForm = ''
if currPage > pageNumber:
return resultsExist, currPage, ctr, sharedItemsForm
for name, sharedItem in sharesJson.items():
if _matchSharedItem(searchStrLowerList, sharedItem):
if currPage == pageNumber:
# show individual search result
sharedItemsForm += \
_htmlSearchResultShare(sharedItem, translate,
httpPrefix, domainFull,
contactNickname,
name, actor)
if not resultsExist and currPage > 1:
# show the previous page button
sharedItemsForm += \
_htmlSearchResultSharePage(actor, domainFull,
callingDomain,
pageNumber,
searchStrLower,
translate, True)
resultsExist = True
ctr += 1
if ctr >= resultsPerPage:
currPage += 1
if currPage > pageNumber:
# show the next page button
sharedItemsForm += \
_htmlSearchResultSharePage(actor, domainFull,
callingDomain,
pageNumber,
searchStrLower,
translate, False)
return resultsExist, currPage, ctr, sharedItemsForm
ctr = 0
return resultsExist, currPage, ctr, sharedItemsForm
2020-11-09 19:41:01 +00:00
def htmlSearchSharedItems(cssCache: {}, translate: {},
baseDir: str, searchStr: str,
pageNumber: int,
resultsPerPage: int,
httpPrefix: str,
domainFull: str, actor: str,
callingDomain: str,
sharedItemsFederatedDomains: []) -> str:
2020-11-09 19:41:01 +00:00
"""Search results for shared items
"""
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'
2021-01-11 19:46:21 +00:00
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
sharedItemsForm = \
2021-01-11 19:46:21 +00:00
htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
sharedItemsForm += \
2020-12-07 12:06:30 +00:00
'<center><h1>' + \
'<a href="' + actor + '/search">' + \
translate['Shared Items Search'] + \
'</a></h1></center>'
resultsExist = False
for subdir, dirs, files in os.walk(baseDir + '/accounts'):
for handle in dirs:
if not isAccountDir(handle):
continue
contactNickname = handle.split('@')[0]
sharesFilename = baseDir + '/accounts/' + handle + \
'/shares.json'
if not os.path.isfile(sharesFilename):
continue
2020-11-09 19:41:01 +00:00
sharesJson = loadJson(sharesFilename)
if not sharesJson:
continue
2020-11-09 19:41:01 +00:00
(resultsExist, currPage, ctr,
resultStr) = _htmlSharesResult(sharesJson, pageNumber,
resultsPerPage,
searchStrLowerList,
currPage, ctr,
callingDomain, httpPrefix,
domainFull,
contactNickname,
actor, resultsExist,
searchStrLower, translate)
sharedItemsForm += resultStr
if currPage > pageNumber:
break
2020-12-13 22:13:45 +00:00
break
# search federated shared items
catalogsDir = baseDir + '/cache/catalogs'
if currPage <= pageNumber and os.path.isdir(catalogsDir):
for subdir, dirs, files in os.walk(catalogsDir):
for f in files:
if '#' in f:
continue
if not f.endswith('.shares.json'):
continue
federatedDomain = f.split('.')[0]
if federatedDomain not in sharedItemsFederatedDomains:
continue
sharesFilename = catalogsDir + '/' + f
sharesJson = loadJson(sharesFilename)
if not sharesJson:
continue
(resultsExist, currPage, ctr,
resultStr) = _htmlSharesResult(sharesJson, pageNumber,
resultsPerPage,
searchStrLowerList,
currPage, ctr,
callingDomain, httpPrefix,
domainFull,
contactNickname,
actor, resultsExist,
searchStrLower, translate)
sharedItemsForm += resultStr
if currPage > pageNumber:
break
break
if not resultsExist:
sharedItemsForm += \
'<center><h5>' + translate['No results'] + '</h5></center>\n'
sharedItemsForm += htmlFooter()
2020-11-09 19:41:01 +00:00
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'
2021-01-11 19:46:21 +00:00
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
emojiStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
2020-11-09 19:41:01 +00:00
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'
2020-12-27 17:17:48 +00:00
emojiStr += ' <form role="search" method="POST" action="' + \
2020-11-09 19:41:01 +00:00
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,
2021-02-07 13:33:30 +00:00
defaultTimeline: str, theme: str,
2021-04-23 09:15:53 +00:00
textModeBanner: str, accessKeys: {}) -> str:
2020-11-09 19:41:01 +00:00
"""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'
2021-07-06 12:50:38 +00:00
instanceTitle = getConfigParam(baseDir, 'instanceTitle')
2021-01-11 19:46:21 +00:00
followStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
2020-11-09 19:41:01 +00:00
# show a banner above the search box
searchBannerFile, searchBannerFilename = \
2020-12-20 17:26:38 +00:00
getSearchBannerFile(baseDir, searchNickname, domain, theme)
2020-11-09 19:41:01 +00:00
2021-04-22 11:51:19 +00:00
textModeBannerStr = htmlKeyboardNavigation(textModeBanner, {}, {})
2021-02-07 13:33:30 +00:00
if textModeBannerStr is None:
textModeBannerStr = ''
2020-11-09 19:41:01 +00:00
if os.path.isfile(searchBannerFilename):
2021-04-23 09:15:53 +00:00
timelineKey = accessKeys['menuTimeline']
2020-11-09 19:41:01 +00:00
usersPath = '/users/' + searchNickname
followStr += \
2021-02-07 13:33:30 +00:00
'<header>\n' + textModeBannerStr + \
2020-11-09 19:41:01 +00:00
'<a href="' + usersPath + '/' + defaultTimeline + '" title="' + \
translate['Switch to timeline view'] + '" alt="' + \
2021-04-23 09:15:53 +00:00
translate['Switch to timeline view'] + '" ' + \
'accesskey="' + timelineKey + '">\n'
2020-11-09 19:41:01 +00:00
followStr += '<img loading="lazy" class="timeline-banner" src="' + \
2021-02-01 20:20:13 +00:00
usersPath + '/' + searchBannerFile + '" alt="" /></a>\n' + \
2020-12-27 16:57:15 +00:00
'</header>\n'
2020-11-09 19:41:01 +00:00
# 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'
2020-12-27 16:59:03 +00:00
followStr += ' <form role="search" method="POST" ' + \
2020-11-09 19:41:01 +00:00
'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'
2021-04-23 09:15:53 +00:00
submitKey = accessKeys['submitButton']
2020-11-09 19:41:01 +00:00
followStr += ' <button type="submit" class="button" ' + \
2021-04-23 09:15:53 +00:00
'name="submitSearch" accesskey="' + submitKey + '">' + \
translate['Submit'] + '</button>\n'
2020-11-09 19:41:01 +00:00
followStr += ' </form>\n'
followStr += ' <p class="hashtagswarm">' + \
2020-11-19 17:09:50 +00:00
htmlHashTagSwarm(baseDir, actor, translate) + '</p>\n'
2020-11-09 19:41:01 +00:00
followStr += ' </center>\n'
followStr += ' </div>\n'
followStr += '</div>\n'
followStr += htmlFooter()
return followStr
2020-12-07 12:25:32 +00:00
def htmlSkillsSearch(actor: str,
cssCache: {}, translate: {}, baseDir: str,
2020-11-19 14:02:16 +00:00
httpPrefix: str,
skillsearch: str, instanceOnly: bool,
postsPerPage: int) -> str:
"""Show a page containing search results for a skill
2020-11-09 19:41:01 +00:00
"""
2020-11-19 14:02:16 +00:00
if skillsearch.startswith('*'):
skillsearch = skillsearch[1:].strip()
2020-11-09 19:41:01 +00:00
2020-11-19 14:02:16 +00:00
skillsearch = skillsearch.lower().strip('\n').strip('\r')
results = []
# search instance accounts
for subdir, dirs, files in os.walk(baseDir + '/accounts/'):
2020-11-09 19:41:01 +00:00
for f in files:
2020-11-19 14:02:16 +00:00
if not f.endswith('.json'):
2020-11-09 19:41:01 +00:00
continue
2021-06-25 18:02:05 +00:00
if not isAccountDir(f):
2021-04-21 16:09:56 +00:00
continue
2020-11-19 14:02:16 +00:00
actorFilename = os.path.join(subdir, f)
actorJson = loadJson(actorFilename)
if actorJson:
if actorJson.get('id') and \
noOfActorSkills(actorJson) > 0 and \
2020-11-19 14:02:16 +00:00
actorJson.get('name') and \
actorJson.get('icon'):
actor = actorJson['id']
2021-05-14 17:41:05 +00:00
actorSkillsList = actorJson['hasOccupation']['skills']
2021-05-14 20:02:55 +00:00
skills = getSkillsFromList(actorSkillsList)
for skillName, skillLevel in skills.items():
2020-11-19 14:02:16 +00:00
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)
2020-12-13 22:13:45 +00:00
break
2020-11-19 14:02:16 +00:00
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
2021-06-25 18:02:05 +00:00
if not isAccountDir(f):
2021-04-21 16:09:56 +00:00
continue
2020-11-19 14:02:16 +00:00
actorFilename = os.path.join(subdir, f)
cachedActorJson = loadJson(actorFilename)
if cachedActorJson:
if cachedActorJson.get('actor'):
actorJson = cachedActorJson['actor']
if actorJson.get('id') and \
noOfActorSkills(actorJson) > 0 and \
2020-11-19 14:02:16 +00:00
actorJson.get('name') and \
actorJson.get('icon'):
actor = actorJson['id']
2021-05-14 17:41:05 +00:00
actorSkillsList = \
actorJson['hasOccupation']['skills']
2021-05-14 20:02:55 +00:00
skills = getSkillsFromList(actorSkillsList)
for skillName, skillLevel in skills.items():
2020-11-19 14:02:16 +00:00
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)
2020-12-13 22:13:45 +00:00
break
2020-11-19 14:02:16 +00:00
results.sort(reverse=True)
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
2021-01-11 19:46:21 +00:00
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
skillSearchForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
2020-11-19 14:02:16 +00:00
skillSearchForm += \
2020-12-07 12:28:13 +00:00
'<center><h1><a href = "' + actor + '/search">' + \
2020-12-07 12:25:32 +00:00
translate['Skills search'] + ': ' + \
skillsearch + \
'</a></h1></center>'
2020-11-19 14:02:16 +00:00
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:
2020-11-09 19:41:01 +00:00
continue
2020-11-19 14:02:16 +00:00
actor = skillMatchFields[1]
actorName = skillMatchFields[2]
avatarUrl = skillMatchFields[3]
skillSearchForm += \
'<div class="search-result""><a href="' + \
actor + '/skills">'
skillSearchForm += \
'<img loading="lazy" src="' + avatarUrl + \
2021-02-01 20:20:13 +00:00
'" alt="" /><span class="search-result-text">' + actorName + \
2020-11-19 14:02:16 +00:00
'</span></a></div>'
ctr += 1
if ctr >= postsPerPage:
break
skillSearchForm += '</center>'
skillSearchForm += htmlFooter()
return skillSearchForm
2020-11-09 19:41:01 +00:00
2020-11-19 14:02:16 +00:00
def htmlHistorySearch(cssCache: {}, translate: {}, baseDir: str,
httpPrefix: str,
nickname: str, domain: str,
historysearch: str,
postsPerPage: int, pageNumber: int,
projectVersion: str,
recentPostsCache: {},
maxRecentPosts: int,
session,
cachedWebfingers,
2020-11-19 14:02:16 +00:00
personCache: {},
port: int,
YTReplacementDomain: str,
2020-12-23 23:59:49 +00:00
showPublishedDateOnly: bool,
peertubeInstances: [],
2021-03-09 19:52:10 +00:00
allowLocalNetworkAccess: bool,
themeName: str, boxName: str,
systemLanguage: str) -> str:
2020-11-19 14:02:16 +00:00
"""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,
2021-05-03 22:31:06 +00:00
historysearch, postsPerPage, boxName)
2020-11-19 14:02:16 +00:00
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
2021-01-11 19:46:21 +00:00
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
2020-11-19 14:02:16 +00:00
historySearchForm = \
2021-01-11 19:46:21 +00:00
htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
2020-11-19 14:02:16 +00:00
# add the page title
2020-12-16 11:19:16 +00:00
domainFull = getFullDomain(domain, port)
2020-12-07 12:16:30 +00:00
actor = httpPrefix + '://' + domainFull + '/users/' + nickname
2021-05-03 22:31:06 +00:00
historySearchTitle = '🔍 ' + translate['Your Posts']
2021-05-03 22:33:12 +00:00
if boxName == 'bookmarks':
2021-05-03 22:31:06 +00:00
historySearchTitle = '🔍 ' + translate['Bookmarks']
2020-11-19 14:02:16 +00:00
historySearchForm += \
2020-12-07 12:16:30 +00:00
'<center><h1><a href="' + actor + '/search">' + \
2021-05-03 22:31:06 +00:00
historySearchTitle + '</a></h1></center>'
2020-11-19 14:02:16 +00:00
if len(boxFilenames) == 0:
historySearchForm += \
'<center><h5>' + translate['No results'] + \
'</h5></center>'
return historySearchForm
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
postStr = \
individualPostAsHtml(True, recentPostsCache,
maxRecentPosts,
2020-12-09 13:31:54 +00:00
translate, None,
baseDir, session, cachedWebfingers,
2020-11-19 14:02:16 +00:00
personCache,
nickname, domain, port,
postJsonObject,
None, True, allowDeletion,
httpPrefix, projectVersion,
'search',
YTReplacementDomain,
showPublishedDateOnly,
2020-12-23 23:59:49 +00:00
peertubeInstances,
allowLocalNetworkAccess,
themeName, systemLanguage,
2020-11-19 14:02:16 +00:00
showIndividualPostIcons,
showIndividualPostIcons,
False, False, False)
if postStr:
historySearchForm += separatorStr + postStr
index += 1
historySearchForm += htmlFooter()
return historySearchForm
2020-11-09 19:41:01 +00:00
def htmlHashtagSearch(cssCache: {},
nickname: str, domain: str, port: int,
recentPostsCache: {}, maxRecentPosts: int,
translate: {},
baseDir: str, hashtag: str, pageNumber: int,
postsPerPage: int,
session, cachedWebfingers: {}, personCache: {},
2020-11-09 19:41:01 +00:00
httpPrefix: str, projectVersion: str,
YTReplacementDomain: str,
2020-12-23 23:59:49 +00:00
showPublishedDateOnly: bool,
peertubeInstances: [],
2021-03-09 19:52:10 +00:00
allowLocalNetworkAccess: bool,
themeName: str, systemLanguage: str) -> str:
2020-11-09 19:41:01 +00:00
"""Show a page containing search results for a hashtag
2021-07-21 13:43:04 +00:00
or after selecting a hashtag from the swarm
2020-11-09 19:41:01 +00:00
"""
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
separatorStr = htmlPostSeparator(baseDir, None)
# check that the directory for the nickname exists
if nickname:
2021-07-13 21:59:53 +00:00
accountDir = acctDir(baseDir, nickname, domain)
if not os.path.isdir(accountDir):
2020-11-09 19:41:01 +00:00
nickname = None
# read the index
2021-07-13 14:40:49 +00:00
with open(hashtagIndexFile, 'r') as f:
2020-11-09 19:41:01 +00:00
lines = f.readlines()
# read the css
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
# 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
2021-01-11 19:46:21 +00:00
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
hashtagSearchForm = \
2021-01-11 19:46:21 +00:00
htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
2020-11-09 19:41:01 +00:00
if nickname:
hashtagSearchForm += '<center>\n' + \
'<h1><a href="/users/' + nickname + '/search">#' + \
2020-12-01 22:21:33 +00:00
hashtag + '</a></h1>\n'
2020-11-09 19:41:01 +00:00
else:
hashtagSearchForm += '<center>\n' + \
2020-12-01 22:21:33 +00:00
'<h1>#' + hashtag + '</h1>\n'
# RSS link for hashtag feed
hashtagSearchForm += '<a href="/tags/rss2/' + hashtag + '">'
hashtagSearchForm += \
'<img style="width:3%;min-width:50px" ' + \
'loading="lazy" alt="RSS 2.0" title="RSS 2.0" src="/' + \
2020-12-09 13:08:26 +00:00
'icons/logorss.png" /></a></center>\n'
2020-11-09 19:41:01 +00:00
2020-12-01 21:44:27 +00:00
# edit the category for this hashtag
if isEditor(baseDir, nickname):
category = getHashtagCategory(baseDir, hashtag)
hashtagSearchForm += '<div class="hashtagCategoryContainer">\n'
2020-12-02 11:55:40 +00:00
hashtagSearchForm += ' <form enctype="multipart/form-data" ' + \
'method="POST" accept-charset="UTF-8" action="' + \
2020-12-02 11:12:56 +00:00
'/users/' + nickname + '/tags/' + hashtag + \
'/sethashtagcategory">\n'
2020-12-01 21:44:27 +00:00
hashtagSearchForm += ' <center>\n'
2020-12-01 21:48:12 +00:00
hashtagSearchForm += translate['Category']
2020-12-01 21:44:27 +00:00
hashtagSearchForm += \
2020-12-01 22:56:26 +00:00
' <input type="text" style="width: 20ch" ' + \
2020-12-01 22:54:09 +00:00
'name="hashtagCategory" value="' + category + '">\n'
2020-12-01 21:44:27 +00:00
hashtagSearchForm += \
' <button type="submit" class="button" name="submitYes">' + \
translate['Submit'] + '</button>\n'
hashtagSearchForm += ' </center>\n'
hashtagSearchForm += ' </form>\n'
hashtagSearchForm += '</div>\n'
2020-11-09 19:41:01 +00:00
if startIndex > 0:
# previous page link
hashtagSearchForm += \
' <center>\n' + \
' <a href="/tags/' + hashtag + '?page=' + \
str(pageNumber - 1) + \
'"><img loading="lazy" class="pageicon" src="/' + \
2020-12-09 13:08:26 +00:00
'icons/pageup.png" title="' + \
2020-11-09 19:41:01 +00:00
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)
2020-11-30 10:44:37 +00:00
if not postJsonObject:
index += 1
continue
if not isPublicPost(postJsonObject):
index += 1
continue
showIndividualPostIcons = False
if nickname:
showIndividualPostIcons = True
allowDeletion = False
showRepeats = showIndividualPostIcons
showIcons = showIndividualPostIcons
manuallyApprovesFollowers = False
showPublicOnly = False
storeToCache = False
allowDownloads = True
avatarUrl = None
showAvatarOptions = True
postStr = \
individualPostAsHtml(allowDownloads, recentPostsCache,
maxRecentPosts,
2020-12-09 13:31:54 +00:00
translate, None,
baseDir, session, cachedWebfingers,
2020-11-30 10:44:37 +00:00
personCache,
nickname, domain, port,
postJsonObject,
avatarUrl, showAvatarOptions,
allowDeletion,
httpPrefix, projectVersion,
'search',
YTReplacementDomain,
showPublishedDateOnly,
2020-12-23 23:59:49 +00:00
peertubeInstances,
allowLocalNetworkAccess,
themeName, systemLanguage,
2020-11-30 10:44:37 +00:00
showRepeats, showIcons,
manuallyApprovesFollowers,
showPublicOnly,
storeToCache)
if postStr:
hashtagSearchForm += separatorStr + postStr
2020-11-09 19:41:01 +00:00
index += 1
if endIndex < noOfLines - 1:
# next page link
hashtagSearchForm += \
' <center>\n' + \
' <a href="/tags/' + hashtag + \
'?page=' + str(pageNumber + 1) + \
2020-12-09 13:08:26 +00:00
'"><img loading="lazy" class="pageicon" src="/icons' + \
2020-11-09 19:41:01 +00:00
'/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, cachedWebfingers: {}, personCache: {},
2020-11-09 19:41:01 +00:00
httpPrefix: str, projectVersion: str,
YTReplacementDomain: str, systemLanguage: str) -> str:
2020-11-09 19:41:01 +00:00
"""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:
2021-07-13 21:59:53 +00:00
accountDir = acctDir(baseDir, nickname, domain)
if not os.path.isdir(accountDir):
2020-11-09 19:41:01 +00:00
nickname = None
# read the index
lines = []
2021-07-13 14:40:49 +00:00
with open(hashtagIndexFile, 'r') as f:
2020-11-09 19:41:01 +00:00
lines = f.readlines()
if not lines:
return None
2020-12-16 11:19:16 +00:00
domainFull = getFullDomain(domain, port)
2020-11-09 19:41:01 +00:00
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 = \
getBaseContentFromPost(postJsonObject, systemLanguage)
2020-11-09 19:41:01 +00:00
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()