__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 is_account_dir
from utils import get_config_param
from utils import get_full_domain
from utils import is_editor
from utils import load_json
from utils import get_domain_from_actor
from utils import get_nickname_from_actor
from utils import locate_post
from utils import is_public_post
from utils import first_paragraph_from_string
from utils import search_box_posts
from utils import get_alt_path
from utils import acct_dir
from utils import local_actor_url
from skills import no_of_actor_skills
from skills import get_skills_from_list
from categories import get_hashtag_category
from feeds import rss2tag_header
from feeds import rss2tag_footer
from webapp_utils import set_custom_background
from webapp_utils import html_keyboard_navigation
from webapp_utils import html_header_with_external_style
from webapp_utils import html_footer
from webapp_utils import get_search_banner_file
from webapp_utils import html_post_separator
from webapp_utils import html_search_result_share
from webapp_post import individual_post_as_html
from webapp_hashtagswarm import html_hash_tag_swarm
def html_search_emoji(css_cache: {}, 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 = \
get_config_param(base_dir, 'instanceTitle')
emojiForm = \
html_header_with_external_style(cssFilename, instanceTitle, None)
emojiForm += '
' + \
translate['Emoji Search'] + \
'
'
# does the lookup file exist?
if not os.path.isfile(emojiLookupFilename):
emojiForm += '
' + \
translate['No results'] + '
'
emojiForm += html_footer()
return emojiForm
emojiJson = load_json(emojiLookupFilename)
if emojiJson:
if os.path.isfile(customEmojiLookupFilename):
customEmojiJson = load_json(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 += html_footer()
return emojiForm
def _match_shared_item(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 _html_search_result_share_page(actor: str, domain_full: str,
calling_domain: str, pageNumber: int,
searchStrLower: str, translate: {},
previous: bool) -> str:
"""Returns the html for the previous button on shared items search results
"""
postActor = get_alt_path(actor, domain_full, calling_domain)
# 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 _html_shares_result(base_dir: str,
sharesJson: {}, pageNumber: int, resultsPerPage: int,
searchStrLowerList: [], currPage: int, ctr: int,
calling_domain: 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 _match_shared_item(searchStrLowerList, sharedItem):
if currPage == pageNumber:
# show individual search result
sharedItemsForm += \
html_search_result_share(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 += \
_html_search_result_share_page(actor, domain_full,
calling_domain,
pageNumber,
searchStrLower,
translate, True)
resultsExist = True
ctr += 1
if ctr >= resultsPerPage:
currPage += 1
if currPage > pageNumber:
# show the next page button
sharedItemsForm += \
_html_search_result_share_page(actor, domain_full,
calling_domain,
pageNumber,
searchStrLower,
translate, False)
return resultsExist, currPage, ctr, sharedItemsForm
ctr = 0
return resultsExist, currPage, ctr, sharedItemsForm
def html_search_shared_items(css_cache: {}, translate: {},
base_dir: str, searchStr: str,
pageNumber: int,
resultsPerPage: int,
http_prefix: str,
domain_full: str, actor: str,
calling_domain: 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 = \
get_config_param(base_dir, 'instanceTitle')
sharedItemsForm = \
html_header_with_external_style(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 is_account_dir(handle):
continue
contactNickname = handle.split('@')[0]
sharesFilename = base_dir + '/accounts/' + handle + \
'/' + sharesFileType + '.json'
if not os.path.isfile(sharesFilename):
continue
sharesJson = load_json(sharesFilename)
if not sharesJson:
continue
(resultsExist, currPage, ctr,
resultStr) = _html_shares_result(base_dir, sharesJson, pageNumber,
resultsPerPage,
searchStrLowerList,
currPage, ctr,
calling_domain, 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 = load_json(sharesFilename)
if not sharesJson:
continue
(resultsExist, currPage, ctr,
resultStr) = _html_shares_result(base_dir, sharesJson,
pageNumber,
resultsPerPage,
searchStrLowerList,
currPage, ctr,
calling_domain, 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 += html_footer()
return sharedItemsForm
def html_search_emoji_text_entry(css_cache: {}, 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 = get_domain_from_actor(actor)
set_custom_background(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 = \
get_config_param(base_dir, 'instanceTitle')
emojiStr = \
html_header_with_external_style(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 += html_footer()
return emojiStr
def html_search(css_cache: {}, 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 = get_nickname_from_actor(actor)
set_custom_background(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 = get_config_param(base_dir, 'instanceTitle')
followStr = \
html_header_with_external_style(cssFilename, instanceTitle, None)
# show a banner above the search box
searchBannerFile, searchBannerFilename = \
get_search_banner_file(base_dir, searchNickname, domain, theme)
text_mode_bannerStr = html_keyboard_navigation(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: html_search unable to read cached hashtag swarm ' +
cachedHashtagSwarmFilename)
if not swarmStr:
swarmStr = html_hash_tag_swarm(base_dir, actor, translate)
if swarmStr:
try:
with open(cachedHashtagSwarmFilename, 'w+') as fp:
fp.write(swarmStr)
except OSError:
print('EX: html_search unable to save cached hashtag swarm ' +
cachedHashtagSwarmFilename)
followStr += '
' + swarmStr + '
\n'
followStr += '
\n'
followStr += '
\n'
followStr += '
\n'
followStr += html_footer()
return followStr
def html_skills_search(actor: str,
css_cache: {}, 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 is_account_dir(f):
continue
actorFilename = os.path.join(subdir, f)
actor_json = load_json(actorFilename)
if actor_json:
if actor_json.get('id') and \
no_of_actor_skills(actor_json) > 0 and \
actor_json.get('name') and \
actor_json.get('icon'):
actor = actor_json['id']
actorSkillsList = actor_json['hasOccupation']['skills']
skills = get_skills_from_list(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 is_account_dir(f):
continue
actorFilename = os.path.join(subdir, f)
cachedActorJson = load_json(actorFilename)
if cachedActorJson:
if cachedActorJson.get('actor'):
actor_json = cachedActorJson['actor']
if actor_json.get('id') and \
no_of_actor_skills(actor_json) > 0 and \
actor_json.get('name') and \
actor_json.get('icon'):
actor = actor_json['id']
actorSkillsList = \
actor_json['hasOccupation']['skills']
skills = get_skills_from_list(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 = \
get_config_param(base_dir, 'instanceTitle')
skillSearchForm = \
html_header_with_external_style(cssFilename, instanceTitle, None)
skillSearchForm += \
'