mirror of https://gitlab.com/bashrc2/epicyon
				
				
				
			
		
			
				
	
	
		
			1611 lines
		
	
	
		
			64 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			1611 lines
		
	
	
		
			64 KiB
		
	
	
	
		
			Python
		
	
	
__filename__ = "webapp_utils.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
 | 
						|
from collections import OrderedDict
 | 
						|
from session import get_json
 | 
						|
from utils import is_account_dir
 | 
						|
from utils import remove_html
 | 
						|
from utils import get_protocol_prefixes
 | 
						|
from utils import load_json
 | 
						|
from utils import get_cached_post_filename
 | 
						|
from utils import get_config_param
 | 
						|
from utils import acct_dir
 | 
						|
from utils import get_nickname_from_actor
 | 
						|
from utils import is_float
 | 
						|
from utils import get_audio_extensions
 | 
						|
from utils import get_video_extensions
 | 
						|
from utils import get_image_extensions
 | 
						|
from utils import local_actor_url
 | 
						|
from cache import store_person_in_cache
 | 
						|
from content import add_html_tags
 | 
						|
from content import replace_emoji_from_tags
 | 
						|
from person import get_person_avatar_url
 | 
						|
from posts import is_moderator
 | 
						|
from blocking import is_blocked
 | 
						|
 | 
						|
 | 
						|
def get_broken_link_substitute() -> str:
 | 
						|
    """Returns html used to show a default image if the link to
 | 
						|
    an image is broken
 | 
						|
    """
 | 
						|
    return " onerror=\"this.onerror=null; this.src='" + \
 | 
						|
        "/icons/avatar_default.png'\""
 | 
						|
 | 
						|
 | 
						|
def html_following_list(css_cache: {}, base_dir: str,
 | 
						|
                        following_filename: str) -> str:
 | 
						|
    """Returns a list of handles being followed
 | 
						|
    """
 | 
						|
    with open(following_filename, 'r') as following_file:
 | 
						|
        msg = following_file.read()
 | 
						|
        following_list = msg.split('\n')
 | 
						|
        following_list.sort()
 | 
						|
        if following_list:
 | 
						|
            css_filename = base_dir + '/epicyon-profile.css'
 | 
						|
            if os.path.isfile(base_dir + '/epicyon.css'):
 | 
						|
                css_filename = base_dir + '/epicyon.css'
 | 
						|
 | 
						|
            instance_title = \
 | 
						|
                get_config_param(base_dir, 'instanceTitle')
 | 
						|
            following_list_html = \
 | 
						|
                html_header_with_external_style(css_filename,
 | 
						|
                                                instance_title, None)
 | 
						|
            for following_address in following_list:
 | 
						|
                if following_address:
 | 
						|
                    following_list_html += \
 | 
						|
                        '<h3>@' + following_address + '</h3>'
 | 
						|
            following_list_html += html_footer()
 | 
						|
            msg = following_list_html
 | 
						|
        return msg
 | 
						|
    return ''
 | 
						|
 | 
						|
 | 
						|
def html_hashtag_blocked(css_cache: {}, base_dir: str, translate: {}) -> str:
 | 
						|
    """Show the screen for a blocked hashtag
 | 
						|
    """
 | 
						|
    blocked_hashtag_form = ''
 | 
						|
    css_filename = base_dir + '/epicyon-suspended.css'
 | 
						|
    if os.path.isfile(base_dir + '/suspended.css'):
 | 
						|
        css_filename = base_dir + '/suspended.css'
 | 
						|
 | 
						|
    instance_title = \
 | 
						|
        get_config_param(base_dir, 'instanceTitle')
 | 
						|
    blocked_hashtag_form = \
 | 
						|
        html_header_with_external_style(css_filename, instance_title, None)
 | 
						|
    blocked_hashtag_form += '<div><center>\n'
 | 
						|
    blocked_hashtag_form += \
 | 
						|
        '  <p class="screentitle">' + \
 | 
						|
        translate['Hashtag Blocked'] + '</p>\n'
 | 
						|
    blocked_hashtag_form += \
 | 
						|
        '  <p>See <a href="/terms">' + \
 | 
						|
        translate['Terms of Service'] + '</a></p>\n'
 | 
						|
    blocked_hashtag_form += '</center></div>\n'
 | 
						|
    blocked_hashtag_form += html_footer()
 | 
						|
    return blocked_hashtag_form
 | 
						|
 | 
						|
 | 
						|
def header_buttons_front_screen(translate: {},
 | 
						|
                                nickname: str, box_name: str,
 | 
						|
                                authorized: bool,
 | 
						|
                                icons_as_buttons: bool) -> str:
 | 
						|
    """Returns the header buttons for the front page of a news instance
 | 
						|
    """
 | 
						|
    header_str = ''
 | 
						|
    if nickname == 'news':
 | 
						|
        button_features = 'buttonMobile'
 | 
						|
        button_newswire = 'buttonMobile'
 | 
						|
        button_links = 'buttonMobile'
 | 
						|
        if box_name == 'features':
 | 
						|
            button_features = 'buttonselected'
 | 
						|
        elif box_name == 'newswire':
 | 
						|
            button_newswire = 'buttonselected'
 | 
						|
        elif box_name == 'links':
 | 
						|
            button_links = 'buttonselected'
 | 
						|
 | 
						|
        header_str += \
 | 
						|
            '        <a href="/">' + \
 | 
						|
            '<button class="' + button_features + '">' + \
 | 
						|
            '<span>' + translate['Features'] + \
 | 
						|
            '</span></button></a>'
 | 
						|
        if not authorized:
 | 
						|
            header_str += \
 | 
						|
                '        <a href="/login">' + \
 | 
						|
                '<button class="buttonMobile">' + \
 | 
						|
                '<span>' + translate['Login'] + \
 | 
						|
                '</span></button></a>'
 | 
						|
        if icons_as_buttons:
 | 
						|
            header_str += \
 | 
						|
                '        <a href="/users/news/newswiremobile">' + \
 | 
						|
                '<button class="' + button_newswire + '">' + \
 | 
						|
                '<span>' + translate['Newswire'] + \
 | 
						|
                '</span></button></a>'
 | 
						|
            header_str += \
 | 
						|
                '        <a href="/users/news/linksmobile">' + \
 | 
						|
                '<button class="' + button_links + '">' + \
 | 
						|
                '<span>' + translate['Links'] + \
 | 
						|
                '</span></button></a>'
 | 
						|
        else:
 | 
						|
            header_str += \
 | 
						|
                '        <a href="' + \
 | 
						|
                '/users/news/newswiremobile">' + \
 | 
						|
                '<img loading="lazy" src="/icons' + \
 | 
						|
                '/newswire.png" title="' + translate['Newswire'] + \
 | 
						|
                '" alt="| ' + translate['Newswire'] + '"/></a>\n'
 | 
						|
            header_str += \
 | 
						|
                '        <a href="' + \
 | 
						|
                '/users/news/linksmobile">' + \
 | 
						|
                '<img loading="lazy" src="/icons' + \
 | 
						|
                '/links.png" title="' + translate['Links'] + \
 | 
						|
                '" alt="| ' + translate['Links'] + '"/></a>\n'
 | 
						|
    else:
 | 
						|
        if not authorized:
 | 
						|
            header_str += \
 | 
						|
                '        <a href="/login">' + \
 | 
						|
                '<button class="buttonMobile">' + \
 | 
						|
                '<span>' + translate['Login'] + \
 | 
						|
                '</span></button></a>'
 | 
						|
 | 
						|
    if header_str:
 | 
						|
        header_str = \
 | 
						|
            '\n      <div class="frontPageMobileButtons">\n' + \
 | 
						|
            header_str + \
 | 
						|
            '      </div>\n'
 | 
						|
    return header_str
 | 
						|
 | 
						|
 | 
						|
def get_content_warning_button(post_id: str, translate: {},
 | 
						|
                               content: str) -> str:
 | 
						|
    """Returns the markup for a content warning button
 | 
						|
    """
 | 
						|
    return '       <details><summary class="cw">' + \
 | 
						|
        translate['SHOW MORE'] + '</summary>' + \
 | 
						|
        '<div id="' + post_id + '">' + content + \
 | 
						|
        '</div></details>\n'
 | 
						|
 | 
						|
 | 
						|
def _set_actor_property_url(actor_json: {},
 | 
						|
                            property_name: str, url: str) -> None:
 | 
						|
    """Sets a url for the given actor property
 | 
						|
    """
 | 
						|
    if not actor_json.get('attachment'):
 | 
						|
        actor_json['attachment'] = []
 | 
						|
 | 
						|
    property_name_lower = property_name.lower()
 | 
						|
 | 
						|
    # remove any existing value
 | 
						|
    property_found = None
 | 
						|
    for property_value in actor_json['attachment']:
 | 
						|
        if not property_value.get('name'):
 | 
						|
            continue
 | 
						|
        if not property_value.get('type'):
 | 
						|
            continue
 | 
						|
        if not property_value['name'].lower().startswith(property_name_lower):
 | 
						|
            continue
 | 
						|
        property_found = property_value
 | 
						|
        break
 | 
						|
    if property_found:
 | 
						|
        actor_json['attachment'].remove(property_found)
 | 
						|
 | 
						|
    prefixes = get_protocol_prefixes()
 | 
						|
    prefix_found = False
 | 
						|
    for prefix in prefixes:
 | 
						|
        if url.startswith(prefix):
 | 
						|
            prefix_found = True
 | 
						|
            break
 | 
						|
    if not prefix_found:
 | 
						|
        return
 | 
						|
    if '.' not in url:
 | 
						|
        return
 | 
						|
    if ' ' in url:
 | 
						|
        return
 | 
						|
    if ',' in url:
 | 
						|
        return
 | 
						|
 | 
						|
    for property_value in actor_json['attachment']:
 | 
						|
        if not property_value.get('name'):
 | 
						|
            continue
 | 
						|
        if not property_value.get('type'):
 | 
						|
            continue
 | 
						|
        if not property_value['name'].lower().startswith(property_name_lower):
 | 
						|
            continue
 | 
						|
        if property_value['type'] != 'PropertyValue':
 | 
						|
            continue
 | 
						|
        property_value['value'] = url
 | 
						|
        return
 | 
						|
 | 
						|
    new_address = {
 | 
						|
        "name": property_name,
 | 
						|
        "type": "PropertyValue",
 | 
						|
        "value": url
 | 
						|
    }
 | 
						|
    actor_json['attachment'].append(new_address)
 | 
						|
 | 
						|
 | 
						|
def set_blog_address(actor_json: {}, blog_address: str) -> None:
 | 
						|
    """Sets an blog address for the given actor
 | 
						|
    """
 | 
						|
    _set_actor_property_url(actor_json, 'Blog', remove_html(blog_address))
 | 
						|
 | 
						|
 | 
						|
def update_avatar_image_cache(signing_priv_key_pem: str,
 | 
						|
                              session, base_dir: str, http_prefix: str,
 | 
						|
                              actor: str, avatar_url: str,
 | 
						|
                              person_cache: {}, allow_downloads: bool,
 | 
						|
                              force: bool = False, debug: bool = False) -> str:
 | 
						|
    """Updates the cached avatar for the given actor
 | 
						|
    """
 | 
						|
    if not avatar_url:
 | 
						|
        return None
 | 
						|
    actor_str = actor.replace('/', '-')
 | 
						|
    avatar_image_path = base_dir + '/cache/avatars/' + actor_str
 | 
						|
 | 
						|
    # try different image types
 | 
						|
    image_formats = {
 | 
						|
        'png': 'png',
 | 
						|
        'jpg': 'jpeg',
 | 
						|
        'jpeg': 'jpeg',
 | 
						|
        'gif': 'gif',
 | 
						|
        'svg': 'svg+xml',
 | 
						|
        'webp': 'webp',
 | 
						|
        'avif': 'avif'
 | 
						|
    }
 | 
						|
    avatar_image_filename = None
 | 
						|
    for im_format, mime_type in image_formats.items():
 | 
						|
        if avatar_url.endswith('.' + im_format) or \
 | 
						|
           '.' + im_format + '?' in avatar_url:
 | 
						|
            session_headers = {
 | 
						|
                'Accept': 'image/' + mime_type
 | 
						|
            }
 | 
						|
            avatar_image_filename = avatar_image_path + '.' + im_format
 | 
						|
 | 
						|
    if not avatar_image_filename:
 | 
						|
        return None
 | 
						|
 | 
						|
    if (not os.path.isfile(avatar_image_filename) or force) and \
 | 
						|
       allow_downloads:
 | 
						|
        try:
 | 
						|
            if debug:
 | 
						|
                print('avatar image url: ' + avatar_url)
 | 
						|
            result = session.get(avatar_url,
 | 
						|
                                 headers=session_headers,
 | 
						|
                                 params=None)
 | 
						|
            if result.status_code < 200 or \
 | 
						|
               result.status_code > 202:
 | 
						|
                if debug:
 | 
						|
                    print('Avatar image download failed with status ' +
 | 
						|
                          str(result.status_code))
 | 
						|
                # remove partial download
 | 
						|
                if os.path.isfile(avatar_image_filename):
 | 
						|
                    try:
 | 
						|
                        os.remove(avatar_image_filename)
 | 
						|
                    except OSError:
 | 
						|
                        print('EX: ' +
 | 
						|
                              'update_avatar_image_cache unable to delete ' +
 | 
						|
                              avatar_image_filename)
 | 
						|
            else:
 | 
						|
                with open(avatar_image_filename, 'wb') as fp_av:
 | 
						|
                    fp_av.write(result.content)
 | 
						|
                    if debug:
 | 
						|
                        print('avatar image downloaded for ' + actor)
 | 
						|
                    return avatar_image_filename.replace(base_dir + '/cache',
 | 
						|
                                                         '')
 | 
						|
        except Exception as ex:
 | 
						|
            print('EX: Failed to download avatar image: ' +
 | 
						|
                  str(avatar_url) + ' ' + str(ex))
 | 
						|
        prof = 'https://www.w3.org/ns/activitystreams'
 | 
						|
        if '/channel/' not in actor or '/accounts/' not in actor:
 | 
						|
            session_headers = {
 | 
						|
                'Accept': 'application/activity+json; profile="' + prof + '"'
 | 
						|
            }
 | 
						|
        else:
 | 
						|
            session_headers = {
 | 
						|
                'Accept': 'application/ld+json; profile="' + prof + '"'
 | 
						|
            }
 | 
						|
        person_json = \
 | 
						|
            get_json(signing_priv_key_pem, session, actor,
 | 
						|
                     session_headers, None,
 | 
						|
                     debug, __version__, http_prefix, None)
 | 
						|
        if person_json:
 | 
						|
            if not person_json.get('id'):
 | 
						|
                return None
 | 
						|
            if not person_json.get('publicKey'):
 | 
						|
                return None
 | 
						|
            if not person_json['publicKey'].get('publicKeyPem'):
 | 
						|
                return None
 | 
						|
            if person_json['id'] != actor:
 | 
						|
                return None
 | 
						|
            if not person_cache.get(actor):
 | 
						|
                return None
 | 
						|
            if person_cache[actor]['actor']['publicKey']['publicKeyPem'] != \
 | 
						|
               person_json['publicKey']['publicKeyPem']:
 | 
						|
                print("ERROR: " +
 | 
						|
                      "public keys don't match when downloading actor for " +
 | 
						|
                      actor)
 | 
						|
                return None
 | 
						|
            store_person_in_cache(base_dir, actor, person_json, person_cache,
 | 
						|
                                  allow_downloads)
 | 
						|
            return get_person_avatar_url(base_dir, actor, person_cache,
 | 
						|
                                         allow_downloads)
 | 
						|
        return None
 | 
						|
    return avatar_image_filename.replace(base_dir + '/cache', '')
 | 
						|
 | 
						|
 | 
						|
def scheduled_posts_exist(base_dir: str, nickname: str, domain: str) -> bool:
 | 
						|
    """Returns true if there are posts scheduled to be delivered
 | 
						|
    """
 | 
						|
    schedule_index_filename = \
 | 
						|
        acct_dir(base_dir, nickname, domain) + '/schedule.index'
 | 
						|
    if not os.path.isfile(schedule_index_filename):
 | 
						|
        return False
 | 
						|
    if '#users#' in open(schedule_index_filename).read():
 | 
						|
        return True
 | 
						|
    return False
 | 
						|
 | 
						|
 | 
						|
def shares_timeline_json(actor: str, pageNumber: int, items_per_page: int,
 | 
						|
                         base_dir: str, domain: str, nickname: str,
 | 
						|
                         max_shares_per_account: int,
 | 
						|
                         shared_items_federated_domains: [],
 | 
						|
                         shares_file_type: str) -> ({}, bool):
 | 
						|
    """Get a page on the shared items timeline as json
 | 
						|
    max_shares_per_account helps to avoid one person dominating the timeline
 | 
						|
    by sharing a large number of things
 | 
						|
    """
 | 
						|
    all_shares_json = {}
 | 
						|
    for _, dirs, files in os.walk(base_dir + '/accounts'):
 | 
						|
        for handle in dirs:
 | 
						|
            if not is_account_dir(handle):
 | 
						|
                continue
 | 
						|
            account_dir = base_dir + '/accounts/' + handle
 | 
						|
            shares_filename = account_dir + '/' + shares_file_type + '.json'
 | 
						|
            if not os.path.isfile(shares_filename):
 | 
						|
                continue
 | 
						|
            shares_json = load_json(shares_filename)
 | 
						|
            if not shares_json:
 | 
						|
                continue
 | 
						|
            account_nickname = handle.split('@')[0]
 | 
						|
            # Don't include shared items from blocked accounts
 | 
						|
            if account_nickname != nickname:
 | 
						|
                if is_blocked(base_dir, nickname, domain,
 | 
						|
                              account_nickname, domain, None):
 | 
						|
                    continue
 | 
						|
            # actor who owns this share
 | 
						|
            owner = actor.split('/users/')[0] + '/users/' + account_nickname
 | 
						|
            ctr = 0
 | 
						|
            for item_id, item in shares_json.items():
 | 
						|
                # assign owner to the item
 | 
						|
                item['actor'] = owner
 | 
						|
                item['shareId'] = item_id
 | 
						|
                all_shares_json[str(item['published'])] = item
 | 
						|
                ctr += 1
 | 
						|
                if ctr >= max_shares_per_account:
 | 
						|
                    break
 | 
						|
        break
 | 
						|
    if shared_items_federated_domains:
 | 
						|
        if shares_file_type == 'shares':
 | 
						|
            catalogs_dir = base_dir + '/cache/catalogs'
 | 
						|
        else:
 | 
						|
            catalogs_dir = base_dir + '/cache/wantedItems'
 | 
						|
        if os.path.isdir(catalogs_dir):
 | 
						|
            for _, dirs, files in os.walk(catalogs_dir):
 | 
						|
                for fname in files:
 | 
						|
                    if '#' in fname:
 | 
						|
                        continue
 | 
						|
                    if not fname.endswith('.' + shares_file_type + '.json'):
 | 
						|
                        continue
 | 
						|
                    federated_domain = fname.split('.')[0]
 | 
						|
                    if federated_domain not in shared_items_federated_domains:
 | 
						|
                        continue
 | 
						|
                    shares_filename = catalogs_dir + '/' + fname
 | 
						|
                    shares_json = load_json(shares_filename)
 | 
						|
                    if not shares_json:
 | 
						|
                        continue
 | 
						|
                    ctr = 0
 | 
						|
                    for item_id, item in shares_json.items():
 | 
						|
                        # assign owner to the item
 | 
						|
                        if '--shareditems--' not in item_id:
 | 
						|
                            continue
 | 
						|
                        share_actor = item_id.split('--shareditems--')[0]
 | 
						|
                        share_actor = share_actor.replace('___', '://')
 | 
						|
                        share_actor = share_actor.replace('--', '/')
 | 
						|
                        share_nickname = get_nickname_from_actor(share_actor)
 | 
						|
                        if is_blocked(base_dir, nickname, domain,
 | 
						|
                                      share_nickname, federated_domain, None):
 | 
						|
                            continue
 | 
						|
                        item['actor'] = share_actor
 | 
						|
                        item['shareId'] = item_id
 | 
						|
                        all_shares_json[str(item['published'])] = item
 | 
						|
                        ctr += 1
 | 
						|
                        if ctr >= max_shares_per_account:
 | 
						|
                            break
 | 
						|
                break
 | 
						|
    # sort the shared items in descending order of publication date
 | 
						|
    shares_json = OrderedDict(sorted(all_shares_json.items(), reverse=True))
 | 
						|
    last_page = False
 | 
						|
    start_index = items_per_page * pageNumber
 | 
						|
    max_index = len(shares_json.items())
 | 
						|
    if max_index < items_per_page:
 | 
						|
        last_page = True
 | 
						|
    if start_index >= max_index - items_per_page:
 | 
						|
        last_page = True
 | 
						|
        start_index = max_index - items_per_page
 | 
						|
        if start_index < 0:
 | 
						|
            start_index = 0
 | 
						|
    ctr = 0
 | 
						|
    result_json = {}
 | 
						|
    for published, item in shares_json.items():
 | 
						|
        if ctr >= start_index + items_per_page:
 | 
						|
            break
 | 
						|
        if ctr < start_index:
 | 
						|
            ctr += 1
 | 
						|
            continue
 | 
						|
        result_json[published] = item
 | 
						|
        ctr += 1
 | 
						|
    return result_json, last_page
 | 
						|
 | 
						|
 | 
						|
def post_contains_public(post_json_object: {}) -> bool:
 | 
						|
    """Does the given post contain #Public
 | 
						|
    """
 | 
						|
    contains_public = False
 | 
						|
    if not post_json_object['object'].get('to'):
 | 
						|
        return contains_public
 | 
						|
 | 
						|
    for to_address in post_json_object['object']['to']:
 | 
						|
        if to_address.endswith('#Public'):
 | 
						|
            contains_public = True
 | 
						|
            break
 | 
						|
        if not contains_public:
 | 
						|
            if post_json_object['object'].get('cc'):
 | 
						|
                for to_address2 in post_json_object['object']['cc']:
 | 
						|
                    if to_address2.endswith('#Public'):
 | 
						|
                        contains_public = True
 | 
						|
                        break
 | 
						|
    return contains_public
 | 
						|
 | 
						|
 | 
						|
def _get_image_file(base_dir: str, name: str, directory: str,
 | 
						|
                    nickname: str, domain: str, theme: str) -> (str, str):
 | 
						|
    """
 | 
						|
    returns the filenames for an image with the given name
 | 
						|
    """
 | 
						|
    banner_extensions = get_image_extensions()
 | 
						|
    banner_file = ''
 | 
						|
    banner_filename = ''
 | 
						|
    for ext in banner_extensions:
 | 
						|
        banner_file_test = name + '.' + ext
 | 
						|
        banner_filename_test = directory + '/' + banner_file_test
 | 
						|
        if os.path.isfile(banner_filename_test):
 | 
						|
            banner_file = name + '_' + theme + '.' + ext
 | 
						|
            banner_filename = banner_filename_test
 | 
						|
            return banner_file, banner_filename
 | 
						|
    # if not found then use the default image
 | 
						|
    theme = 'default'
 | 
						|
    directory = base_dir + '/theme/' + theme
 | 
						|
    for ext in banner_extensions:
 | 
						|
        banner_file_test = name + '.' + ext
 | 
						|
        banner_filename_test = directory + '/' + banner_file_test
 | 
						|
        if os.path.isfile(banner_filename_test):
 | 
						|
            banner_file = name + '_' + theme + '.' + ext
 | 
						|
            banner_filename = banner_filename_test
 | 
						|
            break
 | 
						|
    return banner_file, banner_filename
 | 
						|
 | 
						|
 | 
						|
def get_banner_file(base_dir: str,
 | 
						|
                    nickname: str, domain: str, theme: str) -> (str, str):
 | 
						|
    """Gets the image for the timeline banner
 | 
						|
    """
 | 
						|
    account_dir = acct_dir(base_dir, nickname, domain)
 | 
						|
    return _get_image_file(base_dir, 'banner', account_dir,
 | 
						|
                           nickname, domain, theme)
 | 
						|
 | 
						|
 | 
						|
def get_search_banner_file(base_dir: str,
 | 
						|
                           nickname: str, domain: str,
 | 
						|
                           theme: str) -> (str, str):
 | 
						|
    """Gets the image for the search banner
 | 
						|
    """
 | 
						|
    account_dir = acct_dir(base_dir, nickname, domain)
 | 
						|
    return _get_image_file(base_dir, 'search_banner', account_dir,
 | 
						|
                           nickname, domain, theme)
 | 
						|
 | 
						|
 | 
						|
def get_left_image_file(base_dir: str,
 | 
						|
                        nickname: str, domain: str, theme: str) -> (str, str):
 | 
						|
    """Gets the image for the left column
 | 
						|
    """
 | 
						|
    account_dir = acct_dir(base_dir, nickname, domain)
 | 
						|
    return _get_image_file(base_dir, 'left_col_image', account_dir,
 | 
						|
                           nickname, domain, theme)
 | 
						|
 | 
						|
 | 
						|
def get_right_image_file(base_dir: str,
 | 
						|
                         nickname: str, domain: str, theme: str) -> (str, str):
 | 
						|
    """Gets the image for the right column
 | 
						|
    """
 | 
						|
    account_dir = acct_dir(base_dir, nickname, domain)
 | 
						|
    return _get_image_file(base_dir, 'right_col_image',
 | 
						|
                           account_dir, nickname, domain, theme)
 | 
						|
 | 
						|
 | 
						|
def html_header_with_external_style(css_filename: str, instance_title: str,
 | 
						|
                                    metadata: str, lang='en') -> str:
 | 
						|
    if metadata is None:
 | 
						|
        metadata = ''
 | 
						|
    css_file = '/' + css_filename.split('/')[-1]
 | 
						|
    html_str = \
 | 
						|
        '<!DOCTYPE html>\n' + \
 | 
						|
        '<html lang="' + lang + '">\n' + \
 | 
						|
        '  <head>\n' + \
 | 
						|
        '    <meta charset="utf-8">\n' + \
 | 
						|
        '    <link rel="stylesheet" media="all" ' + \
 | 
						|
        'href="' + css_file + '">\n' + \
 | 
						|
        '    <link rel="manifest" href="/manifest.json">\n' + \
 | 
						|
        '    <link href="/favicon.ico" rel="icon" type="image/x-icon">\n' + \
 | 
						|
        '    <meta content="/browserconfig.xml" ' + \
 | 
						|
        'name="msapplication-config">\n' + \
 | 
						|
        '    <meta content="yes" name="apple-mobile-web-app-capable">\n' + \
 | 
						|
        '    <link href="/apple-touch-icon.png" rel="apple-touch-icon" ' + \
 | 
						|
        'sizes="180x180">\n' + \
 | 
						|
        '    <meta name="theme-color" content="grey">\n' + \
 | 
						|
        metadata + \
 | 
						|
        '    <title>' + instance_title + '</title>\n' + \
 | 
						|
        '  </head>\n' + \
 | 
						|
        '  <body>\n'
 | 
						|
    return html_str
 | 
						|
 | 
						|
 | 
						|
def html_header_with_person_markup(css_filename: str, instance_title: str,
 | 
						|
                                   actor_json: {}, city: str,
 | 
						|
                                   content_license_url: str,
 | 
						|
                                   lang='en') -> str:
 | 
						|
    """html header which includes person markup
 | 
						|
    https://schema.org/Person
 | 
						|
    """
 | 
						|
    if not actor_json:
 | 
						|
        html_str = \
 | 
						|
            html_header_with_external_style(css_filename,
 | 
						|
                                            instance_title, None, lang)
 | 
						|
        return html_str
 | 
						|
 | 
						|
    city_markup = ''
 | 
						|
    if city:
 | 
						|
        city = city.lower().title()
 | 
						|
        add_comma = ''
 | 
						|
        country_markup = ''
 | 
						|
        if ',' in city:
 | 
						|
            country = city.split(',', 1)[1].strip().title()
 | 
						|
            city = city.split(',', 1)[0]
 | 
						|
            country_markup = \
 | 
						|
                '          "addressCountry": "' + country + '"\n'
 | 
						|
            add_comma = ','
 | 
						|
        city_markup = \
 | 
						|
            '        "address": {\n' + \
 | 
						|
            '          "@type": "PostalAddress",\n' + \
 | 
						|
            '          "addressLocality": "' + city + '"' + \
 | 
						|
            add_comma + '\n' + country_markup + '        },\n'
 | 
						|
 | 
						|
    skills_markup = ''
 | 
						|
    if actor_json.get('hasOccupation'):
 | 
						|
        if isinstance(actor_json['hasOccupation'], list):
 | 
						|
            skills_markup = '        "hasOccupation": [\n'
 | 
						|
            first_entry = True
 | 
						|
            for skill_dict in actor_json['hasOccupation']:
 | 
						|
                if skill_dict['@type'] == 'Role':
 | 
						|
                    if not first_entry:
 | 
						|
                        skills_markup += ',\n'
 | 
						|
                    skl = skill_dict['hasOccupation']
 | 
						|
                    role_name = skl['name']
 | 
						|
                    if not role_name:
 | 
						|
                        role_name = 'member'
 | 
						|
                    category = \
 | 
						|
                        skl['occupationalCategory']['codeValue']
 | 
						|
                    category_url = \
 | 
						|
                        'https://www.onetonline.org/link/summary/' + category
 | 
						|
                    skills_markup += \
 | 
						|
                        '        {\n' + \
 | 
						|
                        '          "@type": "Role",\n' + \
 | 
						|
                        '          "hasOccupation": {\n' + \
 | 
						|
                        '            "@type": "Occupation",\n' + \
 | 
						|
                        '            "name": "' + role_name + '",\n' + \
 | 
						|
                        '            "description": ' + \
 | 
						|
                        '"Fediverse instance role",\n' + \
 | 
						|
                        '            "occupationLocation": {\n' + \
 | 
						|
                        '              "@type": "City",\n' + \
 | 
						|
                        '              "name": "' + city + '"\n' + \
 | 
						|
                        '            },\n' + \
 | 
						|
                        '            "occupationalCategory": {\n' + \
 | 
						|
                        '              "@type": "CategoryCode",\n' + \
 | 
						|
                        '              "inCodeSet": {\n' + \
 | 
						|
                        '                "@type": "CategoryCodeSet",\n' + \
 | 
						|
                        '                "name": "O*Net-SOC",\n' + \
 | 
						|
                        '                "dateModified": "2019",\n' + \
 | 
						|
                        '                ' + \
 | 
						|
                        '"url": "https://www.onetonline.org/"\n' + \
 | 
						|
                        '              },\n' + \
 | 
						|
                        '              "codeValue": "' + category + '",\n' + \
 | 
						|
                        '              "url": "' + category_url + '"\n' + \
 | 
						|
                        '            }\n' + \
 | 
						|
                        '          }\n' + \
 | 
						|
                        '        }'
 | 
						|
                elif skill_dict['@type'] == 'Occupation':
 | 
						|
                    if not first_entry:
 | 
						|
                        skills_markup += ',\n'
 | 
						|
                    oc_name = skill_dict['name']
 | 
						|
                    if not oc_name:
 | 
						|
                        oc_name = 'member'
 | 
						|
                    skills_list = skill_dict['skills']
 | 
						|
                    skills_list_str = '['
 | 
						|
                    for skill_str in skills_list:
 | 
						|
                        if skills_list_str != '[':
 | 
						|
                            skills_list_str += ', '
 | 
						|
                        skills_list_str += '"' + skill_str + '"'
 | 
						|
                    skills_list_str += ']'
 | 
						|
                    skills_markup += \
 | 
						|
                        '        {\n' + \
 | 
						|
                        '          "@type": "Occupation",\n' + \
 | 
						|
                        '          "name": "' + oc_name + '",\n' + \
 | 
						|
                        '          "description": ' + \
 | 
						|
                        '"Fediverse instance occupation",\n' + \
 | 
						|
                        '          "occupationLocation": {\n' + \
 | 
						|
                        '            "@type": "City",\n' + \
 | 
						|
                        '            "name": "' + city + '"\n' + \
 | 
						|
                        '          },\n' + \
 | 
						|
                        '          "skills": ' + skills_list_str + '\n' + \
 | 
						|
                        '        }'
 | 
						|
                first_entry = False
 | 
						|
            skills_markup += '\n        ],\n'
 | 
						|
 | 
						|
    description = remove_html(actor_json['summary'])
 | 
						|
    name_str = remove_html(actor_json['name'])
 | 
						|
    domain_full = actor_json['id'].split('://')[1].split('/')[0]
 | 
						|
    handle = actor_json['preferredUsername'] + '@' + domain_full
 | 
						|
 | 
						|
    person_markup = \
 | 
						|
        '      "about": {\n' + \
 | 
						|
        '        "@type" : "Person",\n' + \
 | 
						|
        '        "name": "' + name_str + '",\n' + \
 | 
						|
        '        "image": "' + actor_json['icon']['url'] + '",\n' + \
 | 
						|
        '        "description": "' + description + '",\n' + \
 | 
						|
        city_markup + skills_markup + \
 | 
						|
        '        "url": "' + actor_json['id'] + '"\n' + \
 | 
						|
        '      },\n'
 | 
						|
 | 
						|
    profile_markup = \
 | 
						|
        '    <script id="initial-state" type="application/ld+json">\n' + \
 | 
						|
        '    {\n' + \
 | 
						|
        '      "@context":"https://schema.org",\n' + \
 | 
						|
        '      "@type": "ProfilePage",\n' + \
 | 
						|
        '      "mainEntityOfPage": {\n' + \
 | 
						|
        '        "@type": "WebPage",\n' + \
 | 
						|
        "        \"@id\": \"" + actor_json['id'] + "\"\n" + \
 | 
						|
        '      },\n' + person_markup + \
 | 
						|
        '      "accountablePerson": {\n' + \
 | 
						|
        '        "@type": "Person",\n' + \
 | 
						|
        '        "name": "' + name_str + '"\n' + \
 | 
						|
        '      },\n' + \
 | 
						|
        '      "copyrightHolder": {\n' + \
 | 
						|
        '        "@type": "Person",\n' + \
 | 
						|
        '        "name": "' + name_str + '"\n' + \
 | 
						|
        '      },\n' + \
 | 
						|
        '      "name": "' + name_str + '",\n' + \
 | 
						|
        '      "image": "' + actor_json['icon']['url'] + '",\n' + \
 | 
						|
        '      "description": "' + description + '",\n' + \
 | 
						|
        '      "license": "' + content_license_url + '"\n' + \
 | 
						|
        '    }\n' + \
 | 
						|
        '    </script>\n'
 | 
						|
 | 
						|
    description = remove_html(description)
 | 
						|
    og_metadata = \
 | 
						|
        "    <meta content=\"profile\" property=\"og:type\" />\n" + \
 | 
						|
        "    <meta content=\"" + description + \
 | 
						|
        "\" name='description'>\n" + \
 | 
						|
        "    <meta content=\"" + actor_json['url'] + \
 | 
						|
        "\" property=\"og:url\" />\n" + \
 | 
						|
        "    <meta content=\"" + domain_full + \
 | 
						|
        "\" property=\"og:site_name\" />\n" + \
 | 
						|
        "    <meta content=\"" + name_str + " (@" + handle + \
 | 
						|
        ")\" property=\"og:title\" />\n" + \
 | 
						|
        "    <meta content=\"" + description + \
 | 
						|
        "\" property=\"og:description\" />\n" + \
 | 
						|
        "    <meta content=\"" + actor_json['icon']['url'] + \
 | 
						|
        "\" property=\"og:image\" />\n" + \
 | 
						|
        "    <meta content=\"400\" property=\"og:image:width\" />\n" + \
 | 
						|
        "    <meta content=\"400\" property=\"og:image:height\" />\n" + \
 | 
						|
        "    <meta content=\"summary\" property=\"twitter:card\" />\n" + \
 | 
						|
        "    <meta content=\"" + handle + \
 | 
						|
        "\" property=\"profile:username\" />\n"
 | 
						|
    if actor_json.get('attachment'):
 | 
						|
        og_tags = (
 | 
						|
            'email', 'openpgp', 'blog', 'xmpp', 'matrix', 'briar',
 | 
						|
            'jami', 'cwtch', 'languages'
 | 
						|
        )
 | 
						|
        for attach_json in actor_json['attachment']:
 | 
						|
            if not attach_json.get('name'):
 | 
						|
                continue
 | 
						|
            if not attach_json.get('value'):
 | 
						|
                continue
 | 
						|
            name = attach_json['name'].lower()
 | 
						|
            value = attach_json['value']
 | 
						|
            for og_tag in og_tags:
 | 
						|
                if name != og_tag:
 | 
						|
                    continue
 | 
						|
                og_metadata += \
 | 
						|
                    "    <meta content=\"" + value + \
 | 
						|
                    "\" property=\"og:" + og_tag + "\" />\n"
 | 
						|
 | 
						|
    html_str = \
 | 
						|
        html_header_with_external_style(css_filename, instance_title,
 | 
						|
                                        og_metadata + profile_markup, lang)
 | 
						|
    return html_str
 | 
						|
 | 
						|
 | 
						|
def html_header_with_website_markup(css_filename: str, instance_title: str,
 | 
						|
                                    http_prefix: str, domain: str,
 | 
						|
                                    system_language: str) -> str:
 | 
						|
    """html header which includes website markup
 | 
						|
    https://schema.org/WebSite
 | 
						|
    """
 | 
						|
    license_url = 'https://www.gnu.org/licenses/agpl-3.0.rdf'
 | 
						|
 | 
						|
    # social networking category
 | 
						|
    genre_url = 'http://vocab.getty.edu/aat/300312270'
 | 
						|
 | 
						|
    website_markup = \
 | 
						|
        '    <script id="initial-state" type="application/ld+json">\n' + \
 | 
						|
        '    {\n' + \
 | 
						|
        '      "@context" : "http://schema.org",\n' + \
 | 
						|
        '      "@type" : "WebSite",\n' + \
 | 
						|
        '      "name": "' + instance_title + '",\n' + \
 | 
						|
        '      "url": "' + http_prefix + '://' + domain + '",\n' + \
 | 
						|
        '      "license": "' + license_url + '",\n' + \
 | 
						|
        '      "inLanguage": "' + system_language + '",\n' + \
 | 
						|
        '      "isAccessibleForFree": true,\n' + \
 | 
						|
        '      "genre": "' + genre_url + '",\n' + \
 | 
						|
        '      "accessMode": ["textual", "visual"],\n' + \
 | 
						|
        '      "accessModeSufficient": ["textual"],\n' + \
 | 
						|
        '      "accessibilityAPI" : ["ARIA"],\n' + \
 | 
						|
        '      "accessibilityControl" : [\n' + \
 | 
						|
        '        "fullKeyboardControl",\n' + \
 | 
						|
        '        "fullTouchControl",\n' + \
 | 
						|
        '        "fullMouseControl"\n' + \
 | 
						|
        '      ],\n' + \
 | 
						|
        '      "encodingFormat" : [\n' + \
 | 
						|
        '        "text/html", "image/png", "image/webp",\n' + \
 | 
						|
        '        "image/jpeg", "image/gif", "text/css"\n' + \
 | 
						|
        '      ]\n' + \
 | 
						|
        '    }\n' + \
 | 
						|
        '    </script>\n'
 | 
						|
 | 
						|
    og_metadata = \
 | 
						|
        '    <meta content="Epicyon hosted on ' + domain + \
 | 
						|
        '" property="og:site_name" />\n' + \
 | 
						|
        '    <meta content="' + http_prefix + '://' + domain + \
 | 
						|
        '/about" property="og:url" />\n' + \
 | 
						|
        '    <meta content="website" property="og:type" />\n' + \
 | 
						|
        '    <meta content="' + instance_title + \
 | 
						|
        '" property="og:title" />\n' + \
 | 
						|
        '    <meta content="' + http_prefix + '://' + domain + \
 | 
						|
        '/logo.png" property="og:image" />\n' + \
 | 
						|
        '    <meta content="' + system_language + \
 | 
						|
        '" property="og:locale" />\n' + \
 | 
						|
        '    <meta content="summary_large_image" property="twitter:card" />\n'
 | 
						|
 | 
						|
    html_str = \
 | 
						|
        html_header_with_external_style(css_filename, instance_title,
 | 
						|
                                        og_metadata + website_markup,
 | 
						|
                                        system_language)
 | 
						|
    return html_str
 | 
						|
 | 
						|
 | 
						|
def html_header_with_blog_markup(css_filename: str, instance_title: str,
 | 
						|
                                 http_prefix: str, domain: str, nickname: str,
 | 
						|
                                 system_language: str,
 | 
						|
                                 published: str, modified: str,
 | 
						|
                                 title: str, snippet: str,
 | 
						|
                                 translate: {}, url: str,
 | 
						|
                                 content_license_url: str) -> str:
 | 
						|
    """html header which includes blog post markup
 | 
						|
    https://schema.org/BlogPosting
 | 
						|
    """
 | 
						|
    author_url = local_actor_url(http_prefix, nickname, domain)
 | 
						|
    about_url = http_prefix + '://' + domain + '/about.html'
 | 
						|
 | 
						|
    # license for content on the site may be different from
 | 
						|
    # the software license
 | 
						|
 | 
						|
    blog_markup = \
 | 
						|
        '    <script id="initial-state" type="application/ld+json">\n' + \
 | 
						|
        '    {\n' + \
 | 
						|
        '      "@context" : "http://schema.org",\n' + \
 | 
						|
        '      "@type" : "BlogPosting",\n' + \
 | 
						|
        '      "headline": "' + title + '",\n' + \
 | 
						|
        '      "datePublished": "' + published + '",\n' + \
 | 
						|
        '      "dateModified": "' + modified + '",\n' + \
 | 
						|
        '      "author": {\n' + \
 | 
						|
        '        "@type": "Person",\n' + \
 | 
						|
        '        "name": "' + nickname + '",\n' + \
 | 
						|
        '        "sameAs": "' + author_url + '"\n' + \
 | 
						|
        '      },\n' + \
 | 
						|
        '      "publisher": {\n' + \
 | 
						|
        '        "@type": "WebSite",\n' + \
 | 
						|
        '        "name": "' + instance_title + '",\n' + \
 | 
						|
        '        "sameAs": "' + about_url + '"\n' + \
 | 
						|
        '      },\n' + \
 | 
						|
        '      "license": "' + content_license_url + '",\n' + \
 | 
						|
        '      "description": "' + snippet + '"\n' + \
 | 
						|
        '    }\n' + \
 | 
						|
        '    </script>\n'
 | 
						|
 | 
						|
    og_metadata = \
 | 
						|
        '    <meta property="og:locale" content="' + \
 | 
						|
        system_language + '" />\n' + \
 | 
						|
        '    <meta property="og:type" content="article" />\n' + \
 | 
						|
        '    <meta property="og:title" content="' + title + '" />\n' + \
 | 
						|
        '    <meta property="og:url" content="' + url + '" />\n' + \
 | 
						|
        '    <meta content="Epicyon hosted on ' + domain + \
 | 
						|
        '" property="og:site_name" />\n' + \
 | 
						|
        '    <meta property="article:published_time" content="' + \
 | 
						|
        published + '" />\n' + \
 | 
						|
        '    <meta property="article:modified_time" content="' + \
 | 
						|
        modified + '" />\n'
 | 
						|
 | 
						|
    html_str = \
 | 
						|
        html_header_with_external_style(css_filename, instance_title,
 | 
						|
                                        og_metadata + blog_markup,
 | 
						|
                                        system_language)
 | 
						|
    return html_str
 | 
						|
 | 
						|
 | 
						|
def html_footer() -> str:
 | 
						|
    html_str = '  </body>\n'
 | 
						|
    html_str += '</html>\n'
 | 
						|
    return html_str
 | 
						|
 | 
						|
 | 
						|
def load_individual_post_as_html_from_cache(base_dir: str,
 | 
						|
                                            nickname: str, domain: str,
 | 
						|
                                            post_json_object: {}) -> 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
 | 
						|
    """
 | 
						|
    cached_post_filename = \
 | 
						|
        get_cached_post_filename(base_dir, nickname, domain, post_json_object)
 | 
						|
 | 
						|
    post_html = ''
 | 
						|
    if not cached_post_filename:
 | 
						|
        return post_html
 | 
						|
 | 
						|
    if not os.path.isfile(cached_post_filename):
 | 
						|
        return post_html
 | 
						|
 | 
						|
    tries = 0
 | 
						|
    while tries < 3:
 | 
						|
        try:
 | 
						|
            with open(cached_post_filename, 'r') as file:
 | 
						|
                post_html = file.read()
 | 
						|
                break
 | 
						|
        except Exception as ex:
 | 
						|
            print('ERROR: load_individual_post_as_html_from_cache ' +
 | 
						|
                  str(tries) + ' ' + str(ex))
 | 
						|
            # no sleep
 | 
						|
            tries += 1
 | 
						|
    if post_html:
 | 
						|
        return post_html
 | 
						|
 | 
						|
 | 
						|
def add_emoji_to_display_name(session, base_dir: str, http_prefix: str,
 | 
						|
                              nickname: str, domain: str,
 | 
						|
                              display_name: str, in_profile_name: bool) -> str:
 | 
						|
    """Adds emoji icons to display names or CW on individual posts
 | 
						|
    """
 | 
						|
    if ':' not in display_name:
 | 
						|
        return display_name
 | 
						|
 | 
						|
    display_name = display_name.replace('<p>', '').replace('</p>', '')
 | 
						|
    emoji_tags = {}
 | 
						|
#    print('TAG: display_name before tags: ' + display_name)
 | 
						|
    display_name = \
 | 
						|
        add_html_tags(base_dir, http_prefix,
 | 
						|
                      nickname, domain, display_name, [], emoji_tags)
 | 
						|
    display_name = display_name.replace('<p>', '').replace('</p>', '')
 | 
						|
#    print('TAG: display_name after tags: ' + display_name)
 | 
						|
    # convert the emoji dictionary to a list
 | 
						|
    emoji_tags_list = []
 | 
						|
    for _, tag in emoji_tags.items():
 | 
						|
        emoji_tags_list.append(tag)
 | 
						|
#    print('TAG: emoji tags list: ' + str(emoji_tags_list))
 | 
						|
    if not in_profile_name:
 | 
						|
        display_name = \
 | 
						|
            replace_emoji_from_tags(session, base_dir,
 | 
						|
                                    display_name, emoji_tags_list,
 | 
						|
                                    'post header', False)
 | 
						|
    else:
 | 
						|
        display_name = \
 | 
						|
            replace_emoji_from_tags(session, base_dir,
 | 
						|
                                    display_name, emoji_tags_list, 'profile',
 | 
						|
                                    False)
 | 
						|
#    print('TAG: display_name after tags 2: ' + display_name)
 | 
						|
 | 
						|
    # remove any stray emoji
 | 
						|
    while ':' in display_name:
 | 
						|
        if '://' in display_name:
 | 
						|
            break
 | 
						|
        emoji_str = display_name.split(':')[1]
 | 
						|
        prev_display_name = display_name
 | 
						|
        display_name = display_name.replace(':' + emoji_str + ':', '').strip()
 | 
						|
        if prev_display_name == display_name:
 | 
						|
            break
 | 
						|
#        print('TAG: display_name after tags 3: ' + display_name)
 | 
						|
#    print('TAG: display_name after tag replacements: ' + display_name)
 | 
						|
 | 
						|
    return display_name
 | 
						|
 | 
						|
 | 
						|
def _is_image_mime_type(mime_type: str) -> bool:
 | 
						|
    """Is the given mime type an image?
 | 
						|
    """
 | 
						|
    if mime_type == 'image/svg+xml':
 | 
						|
        return True
 | 
						|
    if not mime_type.startswith('image/'):
 | 
						|
        return False
 | 
						|
    extensions = get_image_extensions()
 | 
						|
    ext = mime_type.split('/')[1]
 | 
						|
    if ext in extensions:
 | 
						|
        return True
 | 
						|
    return False
 | 
						|
 | 
						|
 | 
						|
def _is_video_mime_type(mime_type: str) -> bool:
 | 
						|
    """Is the given mime type a video?
 | 
						|
    """
 | 
						|
    if not mime_type.startswith('video/'):
 | 
						|
        return False
 | 
						|
    extensions = get_video_extensions()
 | 
						|
    ext = mime_type.split('/')[1]
 | 
						|
    if ext in extensions:
 | 
						|
        return True
 | 
						|
    return False
 | 
						|
 | 
						|
 | 
						|
def _is_audio_mime_type(mime_type: str) -> bool:
 | 
						|
    """Is the given mime type an audio file?
 | 
						|
    """
 | 
						|
    if mime_type == 'audio/mpeg':
 | 
						|
        return True
 | 
						|
    if not mime_type.startswith('audio/'):
 | 
						|
        return False
 | 
						|
    extensions = get_audio_extensions()
 | 
						|
    ext = mime_type.split('/')[1]
 | 
						|
    if ext in extensions:
 | 
						|
        return True
 | 
						|
    return False
 | 
						|
 | 
						|
 | 
						|
def _is_attached_image(attachment_filename: str) -> bool:
 | 
						|
    """Is the given attachment filename an image?
 | 
						|
    """
 | 
						|
    if '.' not in attachment_filename:
 | 
						|
        return False
 | 
						|
    image_ext = (
 | 
						|
        'png', 'jpg', 'jpeg', 'webp', 'avif', 'svg', 'gif'
 | 
						|
    )
 | 
						|
    ext = attachment_filename.split('.')[-1]
 | 
						|
    if ext in image_ext:
 | 
						|
        return True
 | 
						|
    return False
 | 
						|
 | 
						|
 | 
						|
def _is_attached_video(attachment_filename: str) -> bool:
 | 
						|
    """Is the given attachment filename a video?
 | 
						|
    """
 | 
						|
    if '.' not in attachment_filename:
 | 
						|
        return False
 | 
						|
    video_ext = (
 | 
						|
        'mp4', 'webm', 'ogv'
 | 
						|
    )
 | 
						|
    ext = attachment_filename.split('.')[-1]
 | 
						|
    if ext in video_ext:
 | 
						|
        return True
 | 
						|
    return False
 | 
						|
 | 
						|
 | 
						|
def get_post_attachments_as_html(post_json_object: {}, box_name: str,
 | 
						|
                                 translate: {},
 | 
						|
                                 is_muted: bool, avatar_link: str,
 | 
						|
                                 reply_str: str, announce_str: str,
 | 
						|
                                 like_str: str,
 | 
						|
                                 bookmark_str: str, delete_str: str,
 | 
						|
                                 mute_str: str) -> (str, str):
 | 
						|
    """Returns a string representing any attachments
 | 
						|
    """
 | 
						|
    attachment_str = ''
 | 
						|
    gallery_str = ''
 | 
						|
    if not post_json_object['object'].get('attachment'):
 | 
						|
        return attachment_str, gallery_str
 | 
						|
 | 
						|
    if not isinstance(post_json_object['object']['attachment'], list):
 | 
						|
        return attachment_str, gallery_str
 | 
						|
 | 
						|
    attachment_ctr = 0
 | 
						|
    attachment_str = ''
 | 
						|
    media_style_added = False
 | 
						|
    for attach in post_json_object['object']['attachment']:
 | 
						|
        if not (attach.get('mediaType') and attach.get('url')):
 | 
						|
            continue
 | 
						|
 | 
						|
        media_type = attach['mediaType']
 | 
						|
        image_description = ''
 | 
						|
        if attach.get('name'):
 | 
						|
            image_description = attach['name'].replace('"', "'")
 | 
						|
        if _is_image_mime_type(media_type):
 | 
						|
            image_url = attach['url']
 | 
						|
            if _is_attached_image(attach['url']) and 'svg' not in media_type:
 | 
						|
                if not attachment_str:
 | 
						|
                    attachment_str += '<div class="media">\n'
 | 
						|
                    media_style_added = True
 | 
						|
 | 
						|
                if attachment_ctr > 0:
 | 
						|
                    attachment_str += '<br>'
 | 
						|
                if box_name == 'tlmedia':
 | 
						|
                    gallery_str += '<div class="gallery">\n'
 | 
						|
                    if not is_muted:
 | 
						|
                        gallery_str += '  <a href="' + image_url + '">\n'
 | 
						|
                        gallery_str += \
 | 
						|
                            '    <img loading="lazy" src="' + \
 | 
						|
                            image_url + '" alt="" title="">\n'
 | 
						|
                        gallery_str += '  </a>\n'
 | 
						|
                    if post_json_object['object'].get('url'):
 | 
						|
                        image_post_url = post_json_object['object']['url']
 | 
						|
                    else:
 | 
						|
                        image_post_url = post_json_object['object']['id']
 | 
						|
                    if image_description and not is_muted:
 | 
						|
                        gallery_str += \
 | 
						|
                            '  <a href="' + image_post_url + \
 | 
						|
                            '" class="gallerytext"><div ' + \
 | 
						|
                            'class="gallerytext">' + \
 | 
						|
                            image_description + '</div></a>\n'
 | 
						|
                    else:
 | 
						|
                        gallery_str += \
 | 
						|
                            '<label class="transparent">---</label><br>'
 | 
						|
                    gallery_str += '  <div class="mediaicons">\n'
 | 
						|
                    gallery_str += \
 | 
						|
                        '    ' + reply_str + announce_str + like_str + \
 | 
						|
                        bookmark_str + delete_str + mute_str + '\n'
 | 
						|
                    gallery_str += '  </div>\n'
 | 
						|
                    gallery_str += '  <div class="mediaavatar">\n'
 | 
						|
                    gallery_str += '    ' + avatar_link + '\n'
 | 
						|
                    gallery_str += '  </div>\n'
 | 
						|
                    gallery_str += '</div>\n'
 | 
						|
 | 
						|
                attachment_str += '<a href="' + image_url + '">'
 | 
						|
                attachment_str += \
 | 
						|
                    '<img loading="lazy" src="' + image_url + \
 | 
						|
                    '" alt="' + image_description + '" title="' + \
 | 
						|
                    image_description + '" class="attachment"></a>\n'
 | 
						|
                attachment_ctr += 1
 | 
						|
        elif _is_video_mime_type(media_type):
 | 
						|
            if _is_attached_video(attach['url']):
 | 
						|
                extension = attach['url'].split('.')[-1]
 | 
						|
                if attachment_ctr > 0:
 | 
						|
                    attachment_str += '<br>'
 | 
						|
                if box_name == 'tlmedia':
 | 
						|
                    gallery_str += '<div class="gallery">\n'
 | 
						|
                    if not is_muted:
 | 
						|
                        gallery_str += '  <a href="' + attach['url'] + '">\n'
 | 
						|
                        gallery_str += \
 | 
						|
                            '    <figure id="videoContainer" ' + \
 | 
						|
                            'data-fullscreen="false">\n' + \
 | 
						|
                            '    <video id="video" controls ' + \
 | 
						|
                            'preload="metadata">\n'
 | 
						|
                        gallery_str += \
 | 
						|
                            '      <source src="' + attach['url'] + \
 | 
						|
                            '" alt="' + image_description + \
 | 
						|
                            '" title="' + image_description + \
 | 
						|
                            '" class="attachment" type="video/' + \
 | 
						|
                            extension + '">'
 | 
						|
                        idx = 'Your browser does not support the video tag.'
 | 
						|
                        gallery_str += translate[idx]
 | 
						|
                        gallery_str += '    </video>\n'
 | 
						|
                        gallery_str += '    </figure>\n'
 | 
						|
                        gallery_str += '  </a>\n'
 | 
						|
                    if post_json_object['object'].get('url'):
 | 
						|
                        video_post_url = post_json_object['object']['url']
 | 
						|
                    else:
 | 
						|
                        video_post_url = post_json_object['object']['id']
 | 
						|
                    if image_description and not is_muted:
 | 
						|
                        gallery_str += \
 | 
						|
                            '  <a href="' + video_post_url + \
 | 
						|
                            '" class="gallerytext"><div ' + \
 | 
						|
                            'class="gallerytext">' + \
 | 
						|
                            image_description + '</div></a>\n'
 | 
						|
                    else:
 | 
						|
                        gallery_str += \
 | 
						|
                            '<label class="transparent">---</label><br>'
 | 
						|
                    gallery_str += '  <div class="mediaicons">\n'
 | 
						|
                    gallery_str += \
 | 
						|
                        '    ' + reply_str + announce_str + like_str + \
 | 
						|
                        bookmark_str + delete_str + mute_str + '\n'
 | 
						|
                    gallery_str += '  </div>\n'
 | 
						|
                    gallery_str += '  <div class="mediaavatar">\n'
 | 
						|
                    gallery_str += '    ' + avatar_link + '\n'
 | 
						|
                    gallery_str += '  </div>\n'
 | 
						|
                    gallery_str += '</div>\n'
 | 
						|
 | 
						|
                attachment_str += \
 | 
						|
                    '<center><figure id="videoContainer" ' + \
 | 
						|
                    'data-fullscreen="false">\n' + \
 | 
						|
                    '    <video id="video" controls ' + \
 | 
						|
                    'preload="metadata">\n'
 | 
						|
                attachment_str += \
 | 
						|
                    '<source src="' + attach['url'] + '" alt="' + \
 | 
						|
                    image_description + '" title="' + image_description + \
 | 
						|
                    '" class="attachment" type="video/' + \
 | 
						|
                    extension + '">'
 | 
						|
                attachment_str += \
 | 
						|
                    translate['Your browser does not support the video tag.']
 | 
						|
                attachment_str += '</video></figure></center>'
 | 
						|
                attachment_ctr += 1
 | 
						|
        elif _is_audio_mime_type(media_type):
 | 
						|
            extension = '.mp3'
 | 
						|
            if attach['url'].endswith('.ogg'):
 | 
						|
                extension = '.ogg'
 | 
						|
            if attach['url'].endswith(extension):
 | 
						|
                if attachment_ctr > 0:
 | 
						|
                    attachment_str += '<br>'
 | 
						|
                if box_name == 'tlmedia':
 | 
						|
                    gallery_str += '<div class="gallery">\n'
 | 
						|
                    if not is_muted:
 | 
						|
                        gallery_str += '  <a href="' + attach['url'] + '">\n'
 | 
						|
                        gallery_str += '    <audio controls>\n'
 | 
						|
                        gallery_str += \
 | 
						|
                            '      <source src="' + attach['url'] + \
 | 
						|
                            '" alt="' + image_description + \
 | 
						|
                            '" title="' + image_description + \
 | 
						|
                            '" class="attachment" type="audio/' + \
 | 
						|
                            extension.replace('.', '') + '">'
 | 
						|
                        idx = 'Your browser does not support the audio tag.'
 | 
						|
                        gallery_str += translate[idx]
 | 
						|
                        gallery_str += '    </audio>\n'
 | 
						|
                        gallery_str += '  </a>\n'
 | 
						|
                    if post_json_object['object'].get('url'):
 | 
						|
                        audio_post_url = post_json_object['object']['url']
 | 
						|
                    else:
 | 
						|
                        audio_post_url = post_json_object['object']['id']
 | 
						|
                    if image_description and not is_muted:
 | 
						|
                        gallery_str += \
 | 
						|
                            '  <a href="' + audio_post_url + \
 | 
						|
                            '" class="gallerytext"><div ' + \
 | 
						|
                            'class="gallerytext">' + \
 | 
						|
                            image_description + '</div></a>\n'
 | 
						|
                    else:
 | 
						|
                        gallery_str += \
 | 
						|
                            '<label class="transparent">---</label><br>'
 | 
						|
                    gallery_str += '  <div class="mediaicons">\n'
 | 
						|
                    gallery_str += \
 | 
						|
                        '    ' + reply_str + announce_str + \
 | 
						|
                        like_str + bookmark_str + \
 | 
						|
                        delete_str + mute_str + '\n'
 | 
						|
                    gallery_str += '  </div>\n'
 | 
						|
                    gallery_str += '  <div class="mediaavatar">\n'
 | 
						|
                    gallery_str += '    ' + avatar_link + '\n'
 | 
						|
                    gallery_str += '  </div>\n'
 | 
						|
                    gallery_str += '</div>\n'
 | 
						|
 | 
						|
                attachment_str += '<center>\n<audio controls>\n'
 | 
						|
                attachment_str += \
 | 
						|
                    '<source src="' + attach['url'] + '" alt="' + \
 | 
						|
                    image_description + '" title="' + image_description + \
 | 
						|
                    '" class="attachment" type="audio/' + \
 | 
						|
                    extension.replace('.', '') + '">'
 | 
						|
                attachment_str += \
 | 
						|
                    translate['Your browser does not support the audio tag.']
 | 
						|
                attachment_str += '</audio>\n</center>\n'
 | 
						|
                attachment_ctr += 1
 | 
						|
    if media_style_added:
 | 
						|
        attachment_str += '</div>'
 | 
						|
    return attachment_str, gallery_str
 | 
						|
 | 
						|
 | 
						|
def html_post_separator(base_dir: str, column: str) -> str:
 | 
						|
    """Returns the html for a timeline post separator image
 | 
						|
    """
 | 
						|
    theme = get_config_param(base_dir, 'theme')
 | 
						|
    filename = 'separator.png'
 | 
						|
    separator_class = "postSeparatorImage"
 | 
						|
    if column:
 | 
						|
        separator_class = "postSeparatorImage" + column.title()
 | 
						|
        filename = 'separator_' + column + '.png'
 | 
						|
    separator_image_filename = \
 | 
						|
        base_dir + '/theme/' + theme + '/icons/' + filename
 | 
						|
    separator_str = ''
 | 
						|
    if os.path.isfile(separator_image_filename):
 | 
						|
        separator_str = \
 | 
						|
            '<div class="' + separator_class + '"><center>' + \
 | 
						|
            '<img src="/icons/' + filename + '" ' + \
 | 
						|
            'alt="" /></center></div>\n'
 | 
						|
    return separator_str
 | 
						|
 | 
						|
 | 
						|
def html_highlight_label(label: str, highlight: bool) -> str:
 | 
						|
    """If the given text should be highlighted then return
 | 
						|
    the appropriate markup.
 | 
						|
    This is so that in shell browsers, like lynx, it's possible
 | 
						|
    to see if the replies or DM button are highlighted.
 | 
						|
    """
 | 
						|
    if not highlight:
 | 
						|
        return label
 | 
						|
    return '*' + str(label) + '*'
 | 
						|
 | 
						|
 | 
						|
def get_avatar_image_url(session,
 | 
						|
                         base_dir: str, http_prefix: str,
 | 
						|
                         post_actor: str, person_cache: {},
 | 
						|
                         avatar_url: str, allow_downloads: bool,
 | 
						|
                         signing_priv_key_pem: str) -> str:
 | 
						|
    """Returns the avatar image url
 | 
						|
    """
 | 
						|
    # get the avatar image url for the post actor
 | 
						|
    if not avatar_url:
 | 
						|
        avatar_url = \
 | 
						|
            get_person_avatar_url(base_dir, post_actor, person_cache,
 | 
						|
                                  allow_downloads)
 | 
						|
        avatar_url = \
 | 
						|
            update_avatar_image_cache(signing_priv_key_pem,
 | 
						|
                                      session, base_dir, http_prefix,
 | 
						|
                                      post_actor, avatar_url, person_cache,
 | 
						|
                                      allow_downloads)
 | 
						|
    else:
 | 
						|
        update_avatar_image_cache(signing_priv_key_pem,
 | 
						|
                                  session, base_dir, http_prefix,
 | 
						|
                                  post_actor, avatar_url, person_cache,
 | 
						|
                                  allow_downloads)
 | 
						|
 | 
						|
    if not avatar_url:
 | 
						|
        avatar_url = post_actor + '/avatar.png'
 | 
						|
 | 
						|
    return avatar_url
 | 
						|
 | 
						|
 | 
						|
def html_hide_from_screen_reader(html_str: str) -> str:
 | 
						|
    """Returns html which is hidden from screen readers
 | 
						|
    """
 | 
						|
    return '<span aria-hidden="true">' + html_str + '</span>'
 | 
						|
 | 
						|
 | 
						|
def html_keyboard_navigation(banner: str, links: {}, access_keys: {},
 | 
						|
                             sub_heading: str = None,
 | 
						|
                             users_path: str = None, translate: {} = None,
 | 
						|
                             follow_approvals: bool = False) -> str:
 | 
						|
    """Given a set of links return the html for keyboard navigation
 | 
						|
    """
 | 
						|
    html_str = '<div class="transparent"><ul>\n'
 | 
						|
 | 
						|
    if banner:
 | 
						|
        html_str += '<pre aria-label="">\n' + banner + '\n<br><br></pre>\n'
 | 
						|
 | 
						|
    if sub_heading:
 | 
						|
        html_str += '<strong><label class="transparent">' + \
 | 
						|
            sub_heading + '</label></strong><br>\n'
 | 
						|
 | 
						|
    # show new follower approvals
 | 
						|
    if users_path and translate and follow_approvals:
 | 
						|
        html_str += '<strong><label class="transparent">' + \
 | 
						|
            '<a href="' + users_path + '/followers#timeline">' + \
 | 
						|
            translate['Approve follow requests'] + '</a>' + \
 | 
						|
            '</label></strong><br><br>\n'
 | 
						|
 | 
						|
    # show the list of links
 | 
						|
    for title, url in links.items():
 | 
						|
        access_key_str = ''
 | 
						|
        if access_keys.get(title):
 | 
						|
            access_key_str = 'accesskey="' + access_keys[title] + '"'
 | 
						|
 | 
						|
        html_str += '<li><label class="transparent">' + \
 | 
						|
            '<a href="' + str(url) + '" ' + access_key_str + '>' + \
 | 
						|
            str(title) + '</a></label></li>\n'
 | 
						|
    html_str += '</ul></div>\n'
 | 
						|
    return html_str
 | 
						|
 | 
						|
 | 
						|
def begin_edit_section(label: str) -> str:
 | 
						|
    """returns the html for begining a dropdown section on edit profile screen
 | 
						|
    """
 | 
						|
    return \
 | 
						|
        '    <details><summary class="cw">' + label + '</summary>\n' + \
 | 
						|
        '<div class="container">'
 | 
						|
 | 
						|
 | 
						|
def end_edit_section() -> str:
 | 
						|
    """returns the html for ending a dropdown section on edit profile screen
 | 
						|
    """
 | 
						|
    return '    </div></details>\n'
 | 
						|
 | 
						|
 | 
						|
def edit_text_field(label: str, name: str, value: str = "",
 | 
						|
                    placeholder: str = "", required: bool = False) -> str:
 | 
						|
    """Returns html for editing a text field
 | 
						|
    """
 | 
						|
    if value is None:
 | 
						|
        value = ''
 | 
						|
    placeholder_str = ''
 | 
						|
    if placeholder:
 | 
						|
        placeholder_str = ' placeholder="' + placeholder + '"'
 | 
						|
    required_str = ''
 | 
						|
    if required:
 | 
						|
        required_str = ' required'
 | 
						|
    text_field_str = ''
 | 
						|
    if label:
 | 
						|
        text_field_str = \
 | 
						|
            '<label class="labels">' + label + '</label><br>\n'
 | 
						|
    text_field_str += \
 | 
						|
        '      <input type="text" name="' + name + '" value="' + \
 | 
						|
        value + '"' + placeholder_str + required_str + '>\n'
 | 
						|
    return text_field_str
 | 
						|
 | 
						|
 | 
						|
def edit_number_field(label: str, name: str, value: int,
 | 
						|
                      min_value: int, max_value: int,
 | 
						|
                      placeholder: int) -> str:
 | 
						|
    """Returns html for editing an integer number field
 | 
						|
    """
 | 
						|
    if value is None:
 | 
						|
        value = ''
 | 
						|
    placeholder_str = ''
 | 
						|
    if placeholder:
 | 
						|
        placeholder_str = ' placeholder="' + str(placeholder) + '"'
 | 
						|
    return \
 | 
						|
        '<label class="labels">' + label + '</label><br>\n' + \
 | 
						|
        '      <input type="number" name="' + name + '" value="' + \
 | 
						|
        str(value) + '"' + placeholder_str + ' ' + \
 | 
						|
        'min="' + str(min_value) + '" max="' + str(max_value) + '" step="1">\n'
 | 
						|
 | 
						|
 | 
						|
def edit_currency_field(label: str, name: str, value: str,
 | 
						|
                        placeholder: str, required: bool) -> str:
 | 
						|
    """Returns html for editing a currency field
 | 
						|
    """
 | 
						|
    if value is None:
 | 
						|
        value = '0.00'
 | 
						|
    placeholder_str = ''
 | 
						|
    if placeholder:
 | 
						|
        if placeholder.isdigit():
 | 
						|
            placeholder_str = ' placeholder="' + str(placeholder) + '"'
 | 
						|
    required_str = ''
 | 
						|
    if required:
 | 
						|
        required_str = ' required'
 | 
						|
    return \
 | 
						|
        '<label class="labels">' + label + '</label><br>\n' + \
 | 
						|
        '      <input type="text" name="' + name + '" value="' + \
 | 
						|
        str(value) + '"' + placeholder_str + ' ' + \
 | 
						|
        ' pattern="^\\d{1,3}(,\\d{3})*(\\.\\d+)?" data-type="currency"' + \
 | 
						|
        required_str + '>\n'
 | 
						|
 | 
						|
 | 
						|
def edit_check_box(label: str, name: str, checked: bool) -> str:
 | 
						|
    """Returns html for editing a checkbox field
 | 
						|
    """
 | 
						|
    checked_str = ''
 | 
						|
    if checked:
 | 
						|
        checked_str = ' checked'
 | 
						|
 | 
						|
    return \
 | 
						|
        '      <input type="checkbox" class="profilecheckbox" ' + \
 | 
						|
        'name="' + name + '"' + checked_str + '> ' + label + '<br>\n'
 | 
						|
 | 
						|
 | 
						|
def edit_text_area(label: str, name: str, value: str,
 | 
						|
                   height: int, placeholder: str, spellcheck: bool) -> str:
 | 
						|
    """Returns html for editing a textarea field
 | 
						|
    """
 | 
						|
    if value is None:
 | 
						|
        value = ''
 | 
						|
    text = ''
 | 
						|
    if label:
 | 
						|
        text = '<label class="labels">' + label + '</label><br>\n'
 | 
						|
    text += \
 | 
						|
        '      <textarea id="message" placeholder=' + \
 | 
						|
        '"' + placeholder + '" '
 | 
						|
    text += 'name="' + name + '" '
 | 
						|
    text += 'style="height:' + str(height) + 'px" '
 | 
						|
    text += 'spellcheck="' + str(spellcheck).lower() + '">'
 | 
						|
    text += value + '</textarea>\n'
 | 
						|
    return text
 | 
						|
 | 
						|
 | 
						|
def html_search_result_share(base_dir: str, shared_item: {}, translate: {},
 | 
						|
                             http_prefix: str, domain_full: str,
 | 
						|
                             contact_nickname: str, item_id: str,
 | 
						|
                             actor: str, shares_file_type: str,
 | 
						|
                             category: str) -> str:
 | 
						|
    """Returns the html for an individual shared item
 | 
						|
    """
 | 
						|
    shared_items_form = '<div class="container">\n'
 | 
						|
    shared_items_form += \
 | 
						|
        '<p class="share-title">' + shared_item['displayName'] + '</p>\n'
 | 
						|
    if shared_item.get('imageUrl'):
 | 
						|
        shared_items_form += \
 | 
						|
            '<a href="' + shared_item['imageUrl'] + '">\n'
 | 
						|
        shared_items_form += \
 | 
						|
            '<img loading="lazy" src="' + shared_item['imageUrl'] + \
 | 
						|
            '" alt="Item image"></a>\n'
 | 
						|
    shared_items_form += '<p>' + shared_item['summary'] + '</p>\n<p>'
 | 
						|
    if shared_item.get('itemQty'):
 | 
						|
        if shared_item['itemQty'] > 1:
 | 
						|
            shared_items_form += \
 | 
						|
                '<b>' + translate['Quantity'] + \
 | 
						|
                ':</b> ' + str(shared_item['itemQty']) + '<br>'
 | 
						|
    shared_items_form += \
 | 
						|
        '<b>' + translate['Type'] + ':</b> ' + shared_item['itemType'] + '<br>'
 | 
						|
    shared_items_form += \
 | 
						|
        '<b>' + translate['Category'] + ':</b> ' + \
 | 
						|
        shared_item['category'] + '<br>'
 | 
						|
    if shared_item.get('location'):
 | 
						|
        shared_items_form += \
 | 
						|
            '<b>' + translate['Location'] + ':</b> ' + \
 | 
						|
            shared_item['location'] + '<br>'
 | 
						|
    contact_title_str = translate['Contact']
 | 
						|
    if shared_item.get('itemPrice') and \
 | 
						|
       shared_item.get('itemCurrency'):
 | 
						|
        if is_float(shared_item['itemPrice']):
 | 
						|
            if float(shared_item['itemPrice']) > 0:
 | 
						|
                shared_items_form += \
 | 
						|
                    ' <b>' + translate['Price'] + \
 | 
						|
                    ':</b> ' + shared_item['itemPrice'] + \
 | 
						|
                    ' ' + shared_item['itemCurrency']
 | 
						|
                contact_title_str = translate['Buy']
 | 
						|
    shared_items_form += '</p>\n'
 | 
						|
    contact_actor = \
 | 
						|
        local_actor_url(http_prefix, contact_nickname, domain_full)
 | 
						|
    button_style_str = 'button'
 | 
						|
    if category == 'accommodation':
 | 
						|
        contact_title_str = translate['Request to stay']
 | 
						|
        button_style_str = 'contactbutton'
 | 
						|
 | 
						|
    shared_items_form += \
 | 
						|
        '<p>' + \
 | 
						|
        '<a href="' + actor + '?replydm=sharedesc:' + \
 | 
						|
        shared_item['displayName'] + '?mention=' + contact_actor + \
 | 
						|
        '?category=' + category + \
 | 
						|
        '"><button class="' + button_style_str + '">' + contact_title_str + \
 | 
						|
        '</button></a>\n' + \
 | 
						|
        '<a href="' + contact_actor + '"><button class="button">' + \
 | 
						|
        translate['Profile'] + '</button></a>\n'
 | 
						|
 | 
						|
    # should the remove button be shown?
 | 
						|
    show_remove_button = False
 | 
						|
    nickname = get_nickname_from_actor(actor)
 | 
						|
    if actor.endswith('/users/' + contact_nickname):
 | 
						|
        show_remove_button = True
 | 
						|
    elif is_moderator(base_dir, nickname):
 | 
						|
        show_remove_button = True
 | 
						|
    else:
 | 
						|
        admin_nickname = get_config_param(base_dir, 'admin')
 | 
						|
        if admin_nickname:
 | 
						|
            if actor.endswith('/users/' + admin_nickname):
 | 
						|
                show_remove_button = True
 | 
						|
 | 
						|
    if show_remove_button:
 | 
						|
        if shares_file_type == 'shares':
 | 
						|
            shared_items_form += \
 | 
						|
                ' <a href="' + actor + '?rmshare=' + \
 | 
						|
                item_id + '"><button class="button">' + \
 | 
						|
                translate['Remove'] + '</button></a>\n'
 | 
						|
        else:
 | 
						|
            shared_items_form += \
 | 
						|
                ' <a href="' + actor + '?rmwanted=' + \
 | 
						|
                item_id + '"><button class="button">' + \
 | 
						|
                translate['Remove'] + '</button></a>\n'
 | 
						|
    shared_items_form += '</p></div>\n'
 | 
						|
    return shared_items_form
 | 
						|
 | 
						|
 | 
						|
def html_show_share(base_dir: str, domain: str, nickname: str,
 | 
						|
                    http_prefix: str, domain_full: str,
 | 
						|
                    item_id: str, translate: {},
 | 
						|
                    shared_items_federated_domains: [],
 | 
						|
                    default_timeline: str, theme: str,
 | 
						|
                    shares_file_type: str, category: str) -> str:
 | 
						|
    """Shows an individual shared item after selecting it from the left column
 | 
						|
    """
 | 
						|
    shares_json = None
 | 
						|
 | 
						|
    share_url = item_id.replace('___', '://').replace('--', '/')
 | 
						|
    contact_nickname = get_nickname_from_actor(share_url)
 | 
						|
    if not contact_nickname:
 | 
						|
        return None
 | 
						|
 | 
						|
    if '://' + domain_full + '/' in share_url:
 | 
						|
        # shared item on this instance
 | 
						|
        shares_filename = \
 | 
						|
            acct_dir(base_dir, contact_nickname, domain) + '/' + \
 | 
						|
            shares_file_type + '.json'
 | 
						|
        if not os.path.isfile(shares_filename):
 | 
						|
            return None
 | 
						|
        shares_json = load_json(shares_filename)
 | 
						|
    else:
 | 
						|
        # federated shared item
 | 
						|
        if shares_file_type == 'shares':
 | 
						|
            catalogs_dir = base_dir + '/cache/catalogs'
 | 
						|
        else:
 | 
						|
            catalogs_dir = base_dir + '/cache/wantedItems'
 | 
						|
        if not os.path.isdir(catalogs_dir):
 | 
						|
            return None
 | 
						|
        for _, _, files in os.walk(catalogs_dir):
 | 
						|
            for fname in files:
 | 
						|
                if '#' in fname:
 | 
						|
                    continue
 | 
						|
                if not fname.endswith('.' + shares_file_type + '.json'):
 | 
						|
                    continue
 | 
						|
                federated_domain = fname.split('.')[0]
 | 
						|
                if federated_domain not in shared_items_federated_domains:
 | 
						|
                    continue
 | 
						|
                shares_filename = catalogs_dir + '/' + fname
 | 
						|
                shares_json = load_json(shares_filename)
 | 
						|
                if not shares_json:
 | 
						|
                    continue
 | 
						|
                if shares_json.get(item_id):
 | 
						|
                    break
 | 
						|
            break
 | 
						|
 | 
						|
    if not shares_json:
 | 
						|
        return None
 | 
						|
    if not shares_json.get(item_id):
 | 
						|
        return None
 | 
						|
    shared_item = shares_json[item_id]
 | 
						|
    actor = local_actor_url(http_prefix, nickname, domain_full)
 | 
						|
 | 
						|
    # filename of the banner shown at the top
 | 
						|
    banner_file, _ = \
 | 
						|
        get_banner_file(base_dir, nickname, domain, theme)
 | 
						|
 | 
						|
    share_str = \
 | 
						|
        '<header>\n' + \
 | 
						|
        '<a href="/users/' + nickname + '/' + \
 | 
						|
        default_timeline + '" title="" alt="">\n'
 | 
						|
    share_str += '<img loading="lazy" class="timeline-banner" ' + \
 | 
						|
        'alt="" ' + \
 | 
						|
        'src="/users/' + nickname + '/' + banner_file + '" /></a>\n' + \
 | 
						|
        '</header><br>\n'
 | 
						|
    share_str += \
 | 
						|
        html_search_result_share(base_dir, shared_item, translate, http_prefix,
 | 
						|
                                 domain_full, contact_nickname, item_id,
 | 
						|
                                 actor, shares_file_type, category)
 | 
						|
 | 
						|
    css_filename = base_dir + '/epicyon-profile.css'
 | 
						|
    if os.path.isfile(base_dir + '/epicyon.css'):
 | 
						|
        css_filename = base_dir + '/epicyon.css'
 | 
						|
    instance_title = \
 | 
						|
        get_config_param(base_dir, 'instanceTitle')
 | 
						|
 | 
						|
    return html_header_with_external_style(css_filename,
 | 
						|
                                           instance_title, None) + \
 | 
						|
        share_str + html_footer()
 | 
						|
 | 
						|
 | 
						|
def set_custom_background(base_dir: str, background: str,
 | 
						|
                          new_background: str) -> str:
 | 
						|
    """Sets a custom background
 | 
						|
    Returns the extension, if found
 | 
						|
    """
 | 
						|
    ext = 'jpg'
 | 
						|
    if os.path.isfile(base_dir + '/img/' + background + '.' + ext):
 | 
						|
        if not new_background:
 | 
						|
            new_background = background
 | 
						|
        if not os.path.isfile(base_dir + '/accounts/' +
 | 
						|
                              new_background + '.' + ext):
 | 
						|
            copyfile(base_dir + '/img/' + background + '.' + ext,
 | 
						|
                     base_dir + '/accounts/' + new_background + '.' + ext)
 | 
						|
        return ext
 | 
						|
    return None
 |