__filename__ = "webapp_search.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.2.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@libreserver.org"
__status__ = "Production"
__module_group__ = "Web Interface"
import os
from shutil import copyfile
import urllib.parse
from datetime import datetime
from utils import get_base_content_from_post
from utils import isAccountDir
from utils import getConfigParam
from utils import get_full_domain
from utils import isEditor
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
from utils import getAltPath
from utils import acct_dir
from utils import local_actor_url
from skills import noOfActorSkills
from skills import getSkillsFromList
from categories import getHashtagCategory
from feeds import rss2TagHeader
from feeds import rss2TagFooter
from webapp_utils import setCustomBackground
from webapp_utils import htmlKeyboardNavigation
from webapp_utils import htmlHeaderWithExternalStyle
from webapp_utils import htmlFooter
from webapp_utils import getSearchBannerFile
from webapp_utils import htmlPostSeparator
from webapp_utils import htmlSearchResultShare
from webapp_post import individualPostAsHtml
from webapp_hashtagswarm import htmlHashTagSwarm
def htmlSearchEmoji(cssCache: {}, translate: {},
base_dir: str, http_prefix: 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(base_dir + '/emoji/emoji.json'):
copyfile(base_dir + '/emoji/default_emoji.json',
base_dir + '/emoji/emoji.json')
searchStr = searchStr.lower().replace(':', '').strip('\n').strip('\r')
cssFilename = base_dir + '/epicyon-profile.css'
if os.path.isfile(base_dir + '/epicyon.css'):
cssFilename = base_dir + '/epicyon.css'
emojiLookupFilename = base_dir + '/emoji/emoji.json'
customEmojiLookupFilename = base_dir + '/emojicustom/emoji.json'
# create header
instanceTitle = \
getConfigParam(base_dir, 'instanceTitle')
emojiForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle, None)
emojiForm += '
' + \
translate['Emoji Search'] + \
'
'
# does the lookup file exist?
if not os.path.isfile(emojiLookupFilename):
emojiForm += '
' + \
translate['No results'] + '
'
emojiForm += htmlFooter()
return emojiForm
emojiJson = loadJson(emojiLookupFilename)
if emojiJson:
if os.path.isfile(customEmojiLookupFilename):
customEmojiJson = loadJson(customEmojiLookupFilename)
if customEmojiJson:
emojiJson = dict(emojiJson, **customEmojiJson)
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'
if not results:
emojiForm += '
' + \
translate['No results'] + '
'
headingShown = False
emojiForm += '
'
msgStr1 = translate['Copy the text then paste it into your post']
msgStr2 = ':'
emojiForm += '
'
emojiForm += htmlFooter()
return emojiForm
def _matchSharedItem(searchStrLowerList: [],
sharedItem: {}) -> bool:
"""Returns true if the shared item matches search criteria
"""
for searchSubstr in searchStrLowerList:
searchSubstr = searchSubstr.strip()
if sharedItem.get('location'):
if searchSubstr in sharedItem['location'].lower():
return True
if 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 _htmlSearchResultSharePage(actor: str, domain_full: str,
callingDomain: str, pageNumber: int,
searchStrLower: str, translate: {},
previous: bool) -> str:
"""Returns the html for the previous button on shared items search results
"""
postActor = getAltPath(actor, domain_full, callingDomain)
# previous page link, needs to be a POST
if previous:
pageNumber -= 1
titleStr = translate['Page up']
imageUrl = 'pageup.png'
else:
pageNumber += 1
titleStr = translate['Page down']
imageUrl = 'pagedown.png'
sharedItemsForm = \
'\n'
return sharedItemsForm
def _htmlSharesResult(base_dir: str,
sharesJson: {}, pageNumber: int, resultsPerPage: int,
searchStrLowerList: [], currPage: int, ctr: int,
callingDomain: str, http_prefix: str, domain_full: str,
contactNickname: str, actor: str,
resultsExist: bool, searchStrLower: str, translate: {},
sharesFileType: str) -> (bool, int, int, str):
"""Result for shared items search
"""
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(base_dir, sharedItem, translate,
http_prefix, domain_full,
contactNickname,
name, actor, sharesFileType,
sharedItem['category'])
if not resultsExist and currPage > 1:
# show the previous page button
sharedItemsForm += \
_htmlSearchResultSharePage(actor, domain_full,
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, domain_full,
callingDomain,
pageNumber,
searchStrLower,
translate, False)
return resultsExist, currPage, ctr, sharedItemsForm
ctr = 0
return resultsExist, currPage, ctr, sharedItemsForm
def htmlSearchSharedItems(cssCache: {}, translate: {},
base_dir: str, searchStr: str,
pageNumber: int,
resultsPerPage: int,
http_prefix: str,
domain_full: str, actor: str,
callingDomain: str,
shared_items_federated_domains: [],
sharesFileType: str) -> str:
"""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 = base_dir + '/epicyon-profile.css'
if os.path.isfile(base_dir + '/epicyon.css'):
cssFilename = base_dir + '/epicyon.css'
instanceTitle = \
getConfigParam(base_dir, 'instanceTitle')
sharedItemsForm = \
htmlHeaderWithExternalStyle(cssFilename, instanceTitle, None)
if sharesFileType == 'shares':
titleStr = translate['Shared Items Search']
else:
titleStr = translate['Wanted Items Search']
sharedItemsForm += \
'
'
resultsExist = False
for subdir, dirs, files in os.walk(base_dir + '/accounts'):
for handle in dirs:
if not isAccountDir(handle):
continue
contactNickname = handle.split('@')[0]
sharesFilename = base_dir + '/accounts/' + handle + \
'/' + sharesFileType + '.json'
if not os.path.isfile(sharesFilename):
continue
sharesJson = loadJson(sharesFilename)
if not sharesJson:
continue
(resultsExist, currPage, ctr,
resultStr) = _htmlSharesResult(base_dir, sharesJson, pageNumber,
resultsPerPage,
searchStrLowerList,
currPage, ctr,
callingDomain, http_prefix,
domain_full,
contactNickname,
actor, resultsExist,
searchStrLower, translate,
sharesFileType)
sharedItemsForm += resultStr
if currPage > pageNumber:
break
break
# search federated shared items
if sharesFileType == 'shares':
catalogsDir = base_dir + '/cache/catalogs'
else:
catalogsDir = base_dir + '/cache/wantedItems'
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('.' + sharesFileType + '.json'):
continue
federatedDomain = f.split('.')[0]
if federatedDomain not in shared_items_federated_domains:
continue
sharesFilename = catalogsDir + '/' + f
sharesJson = loadJson(sharesFilename)
if not sharesJson:
continue
(resultsExist, currPage, ctr,
resultStr) = _htmlSharesResult(base_dir, sharesJson,
pageNumber,
resultsPerPage,
searchStrLowerList,
currPage, ctr,
callingDomain, http_prefix,
domain_full,
contactNickname,
actor, resultsExist,
searchStrLower, translate,
sharesFileType)
sharedItemsForm += resultStr
if currPage > pageNumber:
break
break
if not resultsExist:
sharedItemsForm += \
'
' + translate['No results'] + '
\n'
sharedItemsForm += htmlFooter()
return sharedItemsForm
def htmlSearchEmojiTextEntry(cssCache: {}, translate: {},
base_dir: 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(base_dir + '/emoji/emoji.json'):
copyfile(base_dir + '/emoji/default_emoji.json',
base_dir + '/emoji/emoji.json')
actor = path.replace('/search', '')
domain, port = getDomainFromActor(actor)
setCustomBackground(base_dir, 'search-background', 'follow-background')
cssFilename = base_dir + '/epicyon-follow.css'
if os.path.isfile(base_dir + '/follow.css'):
cssFilename = base_dir + '/follow.css'
instanceTitle = \
getConfigParam(base_dir, 'instanceTitle')
emojiStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle, None)
emojiStr += '
\n'
emojiStr += '
\n'
emojiStr += '
\n'
emojiStr += \
'
' + \
translate['Enter an emoji name to search for'] + '
\n'
emojiStr += ' \n'
emojiStr += '
\n'
emojiStr += '
\n'
emojiStr += '
\n'
emojiStr += htmlFooter()
return emojiStr
def htmlSearch(cssCache: {}, translate: {},
base_dir: str, path: str, domain: str,
defaultTimeline: str, theme: str,
text_mode_banner: str, accessKeys: {}) -> str:
"""Search called from the timeline icon
"""
actor = path.replace('/search', '')
searchNickname = getNicknameFromActor(actor)
setCustomBackground(base_dir, 'search-background', 'follow-background')
cssFilename = base_dir + '/epicyon-search.css'
if os.path.isfile(base_dir + '/search.css'):
cssFilename = base_dir + '/search.css'
instanceTitle = getConfigParam(base_dir, 'instanceTitle')
followStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle, None)
# show a banner above the search box
searchBannerFile, searchBannerFilename = \
getSearchBannerFile(base_dir, searchNickname, domain, theme)
text_mode_bannerStr = htmlKeyboardNavigation(text_mode_banner, {}, {})
if text_mode_bannerStr is None:
text_mode_bannerStr = ''
if os.path.isfile(searchBannerFilename):
timelineKey = accessKeys['menuTimeline']
usersPath = '/users/' + searchNickname
followStr += \
'\n' + text_mode_bannerStr + \
'\n'
followStr += '\n' + \
'\n'
# show the search box
followStr += '
\n'
followStr += '
\n'
followStr += '
\n'
followStr += \
'
' + translate['Search screen text'] + '
\n'
followStr += ' \n'
cachedHashtagSwarmFilename = \
acct_dir(base_dir, searchNickname, domain) + '/.hashtagSwarm'
swarmStr = ''
if os.path.isfile(cachedHashtagSwarmFilename):
try:
with open(cachedHashtagSwarmFilename, 'r') as fp:
swarmStr = fp.read()
except OSError:
print('EX: htmlSearch unable to read cached hashtag swarm ' +
cachedHashtagSwarmFilename)
if not swarmStr:
swarmStr = htmlHashTagSwarm(base_dir, actor, translate)
if swarmStr:
try:
with open(cachedHashtagSwarmFilename, 'w+') as fp:
fp.write(swarmStr)
except OSError:
print('EX: htmlSearch unable to save cached hashtag swarm ' +
cachedHashtagSwarmFilename)
followStr += '
' + swarmStr + '
\n'
followStr += '
\n'
followStr += '
\n'
followStr += '
\n'
followStr += htmlFooter()
return followStr
def htmlSkillsSearch(actor: str,
cssCache: {}, translate: {}, base_dir: str,
http_prefix: 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(base_dir + '/accounts/'):
for f in files:
if not f.endswith('.json'):
continue
if not isAccountDir(f):
continue
actorFilename = os.path.join(subdir, f)
actor_json = loadJson(actorFilename)
if actor_json:
if actor_json.get('id') and \
noOfActorSkills(actor_json) > 0 and \
actor_json.get('name') and \
actor_json.get('icon'):
actor = actor_json['id']
actorSkillsList = actor_json['hasOccupation']['skills']
skills = getSkillsFromList(actorSkillsList)
for skillName, skillLevel in 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 + ';' + \
actor_json['name'] + \
';' + actor_json['icon']['url']
if indexStr not in results:
results.append(indexStr)
break
if not instanceOnly:
# search actor cache
for subdir, dirs, files in os.walk(base_dir + '/cache/actors/'):
for f in files:
if not f.endswith('.json'):
continue
if not isAccountDir(f):
continue
actorFilename = os.path.join(subdir, f)
cachedActorJson = loadJson(actorFilename)
if cachedActorJson:
if cachedActorJson.get('actor'):
actor_json = cachedActorJson['actor']
if actor_json.get('id') and \
noOfActorSkills(actor_json) > 0 and \
actor_json.get('name') and \
actor_json.get('icon'):
actor = actor_json['id']
actorSkillsList = \
actor_json['hasOccupation']['skills']
skills = getSkillsFromList(actorSkillsList)
for skillName, skillLevel in 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 + ';' + \
actor_json['name'] + \
';' + actor_json['icon']['url']
if indexStr not in results:
results.append(indexStr)
break
results.sort(reverse=True)
cssFilename = base_dir + '/epicyon-profile.css'
if os.path.isfile(base_dir + '/epicyon.css'):
cssFilename = base_dir + '/epicyon.css'
instanceTitle = \
getConfigParam(base_dir, 'instanceTitle')
skillSearchForm = \
htmlHeaderWithExternalStyle(cssFilename, instanceTitle, None)
skillSearchForm += \
'