__filename__ = "daemon.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.3.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@libreserver.org"
__status__ = "Production"
__module_group__ = "Core"

from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer, HTTPServer
import copy
import sys
import json
import time
import urllib.parse
import datetime
from socket import error as SocketError
import errno
from functools import partial
# for saving images
from hashlib import sha256
from hashlib import md5
from shutil import copyfile
from session import site_is_verified
from session import create_session
from session import get_session_for_domain
from session import get_session_for_domains
from session import set_session_for_sender
from webfinger import webfinger_meta
from webfinger import webfinger_node_info
from webfinger import webfinger_lookup
from webfinger import webfinger_update
from mastoapiv1 import masto_api_v1_response
from metadata import meta_data_node_info
from metadata import metadata_custom_emoji
from enigma import get_enigma_pub_key
from enigma import set_enigma_pub_key
from pgp import actor_to_vcard
from pgp import actor_to_vcard_xml
from pgp import get_email_address
from pgp import set_email_address
from pgp import get_pgp_pub_key
from pgp import get_pgp_fingerprint
from pgp import set_pgp_pub_key
from pgp import set_pgp_fingerprint
from xmpp import get_xmpp_address
from xmpp import set_xmpp_address
from ssb import get_ssb_address
from ssb import set_ssb_address
from tox import get_tox_address
from tox import set_tox_address
from briar import get_briar_address
from briar import set_briar_address
from cwtch import get_cwtch_address
from cwtch import set_cwtch_address
from matrix import get_matrix_address
from matrix import set_matrix_address
from donate import get_donation_url
from donate import set_donation_url
from donate import get_website
from donate import set_website
from donate import get_gemini_link
from donate import set_gemini_link
from person import clear_person_qrcodes
from person import add_alternate_domains
from person import add_actor_update_timestamp
from person import set_person_notes
from person import get_default_person_context
from person import get_actor_update_json
from person import save_person_qrcode
from person import randomize_actor_images
from person import person_upgrade_actor
from person import activate_account
from person import deactivate_account
from person import register_account
from person import person_lookup
from person import person_box_json
from person import create_shared_inbox
from person import create_news_inbox
from person import suspend_account
from person import reenable_account
from person import remove_account
from person import can_remove_post
from person import person_snooze
from person import person_unsnooze
from posts import get_post_expiry_keep_dms
from posts import set_post_expiry_keep_dms
from posts import get_post_expiry_days
from posts import set_post_expiry_days
from posts import get_original_post_from_announce_url
from posts import save_post_to_box
from posts import get_instance_actor_key
from posts import remove_post_interactions
from posts import outbox_message_create_wrap
from posts import get_pinned_post_as_json
from posts import pin_post
from posts import json_pin_post
from posts import undo_pinned_post
from posts import is_moderator
from posts import create_question_post
from posts import create_public_post
from posts import create_blog_post
from posts import create_report_post
from posts import create_unlisted_post
from posts import create_followers_only_post
from posts import create_direct_message_post
from posts import populate_replies_json
from posts import add_to_field
from posts import expire_cache
from inbox import clear_queue_items
from inbox import inbox_permitted_message
from inbox import inbox_message_has_params
from inbox import run_inbox_queue
from inbox import run_inbox_queue_watchdog
from inbox import save_post_to_inbox_queue
from inbox import populate_replies
from follow import follower_approval_active
from follow import is_following_actor
from follow import get_following_feed
from follow import send_follow_request
from follow import unfollow_account
from follow import create_initial_last_seen
from skills import get_skills_from_list
from skills import no_of_actor_skills
from skills import actor_has_skill
from skills import actor_skill_value
from skills import set_actor_skill_level
from auth import record_login_failure
from auth import authorize
from auth import create_password
from auth import create_basic_auth_header
from auth import authorize_basic
from auth import store_basic_credentials
from threads import begin_thread
from threads import thread_with_trace
from threads import remove_dormant_threads
from media import process_meta_data
from media import convert_image_to_low_bandwidth
from media import replace_you_tube
from media import replace_twitter
from media import attach_media
from media import path_is_video
from media import path_is_audio
from blocking import get_cw_list_variable
from blocking import load_cw_lists
from blocking import update_blocked_cache
from blocking import mute_post
from blocking import unmute_post
from blocking import set_broch_mode
from blocking import broch_mode_is_active
from blocking import add_block
from blocking import remove_block
from blocking import add_global_block
from blocking import remove_global_block
from blocking import is_blocked_hashtag
from blocking import is_blocked_domain
from blocking import get_domain_blocklist
from blocking import allowed_announce_add
from blocking import allowed_announce_remove
from roles import set_roles_from_list
from roles import get_actor_roles_list
from blog import path_contains_blog_link
from blog import html_blog_page_rss2
from blog import html_blog_page_rss3
from blog import html_blog_view
from blog import html_blog_page
from blog import html_blog_post
from blog import html_edit_blog
from blog import get_blog_address
from webapp_podcast import html_podcast_episode
from webapp_theme_designer import html_theme_designer
from webapp_minimalbutton import set_minimal
from webapp_minimalbutton import is_minimal
from webapp_utils import get_avatar_image_url
from webapp_utils import html_hashtag_blocked
from webapp_utils import html_following_list
from webapp_utils import csv_following_list
from webapp_utils import set_blog_address
from webapp_utils import html_show_share
from webapp_utils import get_pwa_theme_colors
from webapp_utils import text_mode_browser
from webapp_calendar import html_calendar_delete_confirm
from webapp_calendar import html_calendar
from webapp_about import html_about
from webapp_specification import html_specification
from webapp_manual import html_manual
from webapp_accesskeys import html_access_keys
from webapp_accesskeys import load_access_keys_for_accounts
from webapp_confirm import html_confirm_delete
from webapp_confirm import html_confirm_remove_shared_item
from webapp_confirm import html_confirm_block
from webapp_confirm import html_confirm_unblock
from webapp_person_options import person_minimize_images
from webapp_person_options import person_undo_minimize_images
from webapp_person_options import html_person_options
from webapp_timeline import html_shares
from webapp_timeline import html_wanted
from webapp_timeline import html_inbox
from webapp_timeline import html_bookmarks
from webapp_timeline import html_inbox_dms
from webapp_timeline import html_inbox_replies
from webapp_timeline import html_inbox_media
from webapp_timeline import html_inbox_blogs
from webapp_timeline import html_inbox_news
from webapp_timeline import html_inbox_features
from webapp_timeline import html_outbox
from webapp_media import load_peertube_instances
from webapp_moderation import html_account_info
from webapp_moderation import html_moderation
from webapp_moderation import html_moderation_info
from webapp_create_post import html_new_post
from webapp_login import html_login
from webapp_login import html_get_login_credentials
from webapp_suspended import html_suspended
from webapp_tos import html_terms_of_service
from webapp_confirm import html_confirm_follow
from webapp_confirm import html_confirm_unfollow
from webapp_post import html_emoji_reaction_picker
from webapp_post import html_post_replies
from webapp_post import html_individual_post
from webapp_post import individual_post_as_html
from webapp_profile import html_edit_profile
from webapp_profile import html_profile_after_search
from webapp_profile import html_profile
from webapp_column_left import html_links_mobile
from webapp_column_left import html_edit_links
from webapp_column_right import html_newswire_mobile
from webapp_column_right import html_edit_newswire
from webapp_column_right import html_citations
from webapp_column_right import html_edit_news_post
from webapp_search import html_skills_search
from webapp_search import html_history_search
from webapp_search import html_hashtag_search
from webapp_search import rss_hashtag_search
from webapp_search import html_search_emoji
from webapp_search import html_search_shared_items
from webapp_search import html_search_emoji_text_entry
from webapp_search import html_search
from webapp_hashtagswarm import get_hashtag_categories_feed
from webapp_hashtagswarm import html_search_hashtag_category
from webapp_welcome import welcome_screen_is_complete
from webapp_welcome import html_welcome_screen
from webapp_welcome import is_welcome_screen_complete
from webapp_welcome_profile import html_welcome_profile
from webapp_welcome_final import html_welcome_final
from shares import merge_shared_item_tokens
from shares import run_federated_shares_daemon
from shares import run_federated_shares_watchdog
from shares import update_shared_item_federation_token
from shares import create_shared_item_federation_token
from shares import authorize_shared_items
from shares import generate_shared_item_federation_tokens
from shares import get_shares_feed_for_person
from shares import add_share
from shares import remove_shared_item
from shares import expire_shares
from shares import shares_catalog_endpoint
from shares import shares_catalog_account_endpoint
from shares import shares_catalog_csv_endpoint
from categories import set_hashtag_category
from categories import update_hashtag_categories
from languages import get_actor_languages
from languages import set_actor_languages
from languages import get_understood_languages
from like import update_likes_collection
from reaction import update_reaction_collection
from utils import load_min_images_for_accounts
from utils import set_minimize_all_images
from utils import get_json_content_from_accept
from utils import remove_eol
from utils import text_in_file
from utils import is_onion_request
from utils import is_i2p_request
from utils import get_account_timezone
from utils import set_account_timezone
from utils import load_account_timezones
from utils import local_network_host
from utils import undo_reaction_collection_entry
from utils import get_new_post_endpoints
from utils import has_actor
from utils import set_reply_interval_hours
from utils import can_reply_to
from utils import is_dm
from utils import replace_users_with_at
from utils import local_actor_url
from utils import is_float
from utils import valid_password
from utils import get_base_content_from_post
from utils import acct_dir
from utils import get_image_extension_from_mime_type
from utils import get_image_mime_type
from utils import has_object_dict
from utils import user_agent_domain
from utils import is_local_network_address
from utils import permitted_dir
from utils import is_account_dir
from utils import get_occupation_skills
from utils import get_occupation_name
from utils import set_occupation_name
from utils import load_translations_from_file
from utils import load_bold_reading
from utils import get_local_network_addresses
from utils import decoded_host
from utils import is_public_post
from utils import get_locked_account
from utils import has_users_path
from utils import get_full_domain
from utils import remove_html
from utils import is_editor
from utils import is_artist
from utils import get_image_extensions
from utils import media_file_mime_type
from utils import get_css
from utils import first_paragraph_from_string
from utils import clear_from_post_caches
from utils import contains_invalid_chars
from utils import is_system_account
from utils import set_config_param
from utils import get_config_param
from utils import remove_id_ending
from utils import undo_likes_collection_entry
from utils import delete_post
from utils import is_blog_post
from utils import remove_avatar_from_cache
from utils import locate_post
from utils import get_cached_post_filename
from utils import remove_post_from_cache
from utils import get_nickname_from_actor
from utils import get_domain_from_actor
from utils import get_status_number
from utils import url_permitted
from utils import load_json
from utils import save_json
from utils import is_suspended
from utils import dangerous_markup
from utils import refresh_newswire
from utils import is_image_file
from utils import has_group_type
from manualapprove import manual_deny_follow_request_thread
from manualapprove import manual_approve_follow_request_thread
from announce import create_announce
from content import load_dogwhistles
from content import valid_url_lengths
from content import contains_invalid_local_links
from content import get_price_from_string
from content import replace_emoji_from_tags
from content import add_html_tags
from content import extract_media_in_form_post
from content import save_media_in_form_post
from content import extract_text_fields_in_post
from cache import check_for_changed_actor
from cache import store_person_in_cache
from cache import get_person_from_cache
from cache import get_person_pub_key
from httpsig import verify_post_headers
from theme import reset_theme_designer_settings
from theme import set_theme_from_designer
from theme import scan_themes_for_scripts
from theme import import_theme
from theme import export_theme
from theme import is_news_theme_name
from theme import get_text_mode_banner
from theme import set_news_avatar
from theme import set_theme
from theme import get_theme
from theme import enable_grayscale
from theme import disable_grayscale
from schedule import run_post_schedule
from schedule import run_post_schedule_watchdog
from schedule import remove_scheduled_posts
from outbox import post_message_to_outbox
from happening import remove_calendar_event
from happening import dav_propfind_response
from happening import dav_put_response
from happening import dav_report_response
from happening import dav_delete_response
from bookmarks import bookmark_post
from bookmarks import undo_bookmark_post
from petnames import set_pet_name
from followingCalendar import add_person_to_calendar
from followingCalendar import remove_person_from_calendar
from notifyOnPost import add_notify_on_post
from notifyOnPost import remove_notify_on_post
from devices import e2e_edevices_collection
from devices import e2e_evalid_device
from devices import e2e_eadd_device
from newswire import get_rs_sfrom_dict
from newswire import rss2header
from newswire import rss2footer
from newswire import load_hashtag_categories
from newsdaemon import run_newswire_watchdog
from newsdaemon import run_newswire_daemon
from filters import is_filtered
from filters import add_global_filter
from filters import remove_global_filter
from context import has_valid_context
from context import get_individual_post_context
from speaker import get_ssml_box
from city import get_spoofed_city
from fitnessFunctions import fitness_performance
from fitnessFunctions import fitness_thread
from fitnessFunctions import sorted_watch_points
from fitnessFunctions import html_watch_points_graph
from siteactive import referer_is_active
from webapp_likers import html_likers_of_post
from crawlers import update_known_crawlers
from crawlers import blocked_user_agent
from crawlers import load_known_web_bots
from qrcode import save_domain_qrcode
from importFollowing import run_import_following_watchdog
from maps import map_format_from_tagmaps_path
import os


# maximum number of posts to list in outbox feed
MAX_POSTS_IN_FEED = 12

# maximum number of posts in a hashtag feed
MAX_POSTS_IN_HASHTAG_FEED = 6

# reduced posts for media feed because it can take a while
MAX_POSTS_IN_MEDIA_FEED = 6

# Blogs can be longer, so don't show many per page
MAX_POSTS_IN_BLOGS_FEED = 4

MAX_POSTS_IN_NEWS_FEED = 10

# Maximum number of entries in returned rss.xml
MAX_POSTS_IN_RSS_FEED = 10

# number of follows/followers per page
FOLLOWS_PER_PAGE = 6

# number of item shares per page
SHARES_PER_PAGE = 12


class PubServer(BaseHTTPRequestHandler):
    protocol_version = 'HTTP/1.1'

    def _convert_domains(self, calling_domain, referer_domain,
                         msg_str: str) -> str:
        """Convert domains to onion or i2p, depending upon who is asking
        """
        curr_http_prefix = self.server.http_prefix + '://'
        if is_onion_request(calling_domain, referer_domain,
                            self.server.domain,
                            self.server.onion_domain):
            msg_str = msg_str.replace(curr_http_prefix +
                                      self.server.domain,
                                      'http://' +
                                      self.server.onion_domain)
        elif is_i2p_request(calling_domain, referer_domain,
                            self.server.domain,
                            self.server.i2p_domain):
            msg_str = msg_str.replace(curr_http_prefix +
                                      self.server.domain,
                                      'http://' +
                                      self.server.i2p_domain)
        return msg_str

    def _detect_mitm(self) -> bool:
        """Detect if a request contains a MiTM
        """
        mitm_domains = ['cloudflare']
        # look for domains within these headers
        check_headers = (
            'Server', 'Report-To', 'Report-to', 'report-to',
            'Expect-CT', 'Expect-Ct', 'expect-ct'
        )
        for interloper in mitm_domains:
            for header_name in check_headers:
                if self.headers.get(header_name):
                    if interloper in self.headers[header_name]:
                        print('MITM: ' + header_name + ' = ' +
                              self.headers[header_name])
                        return True
        # The presence of these headers on their own indicates a MiTM
        mitm_headers = (
            'CF-Connecting-IP', 'CF-RAY', 'CF-IPCountry', 'CF-Visitor',
            'CDN-Loop', 'CF-Worker', 'CF-Cache-Status'
        )
        for header_name in mitm_headers:
            if self.headers.get(header_name):
                print('MITM: ' + header_name + ' = ' +
                      self.headers[header_name])
                return True
            if self.headers.get(header_name.lower()):
                print('MITM: ' + header_name + ' = ' +
                      self.headers[header_name.lower()])
                return True
        return False

    def _get_instance_url(self, calling_domain: str) -> str:
        """Returns the URL for this instance
        """
        if calling_domain.endswith('.onion') and \
           self.server.onion_domain:
            instance_url = 'http://' + self.server.onion_domain
        elif (calling_domain.endswith('.i2p') and
              self.server.i2p_domain):
            instance_url = 'http://' + self.server.i2p_domain
        else:
            instance_url = \
                self.server.http_prefix + '://' + self.server.domain_full
        return instance_url

    def _getheader_signature_input(self):
        """There are different versions of http signatures with
        different header styles
        """
        if self.headers.get('Signature-Input'):
            # https://tools.ietf.org/html/
            # draft-ietf-httpbis-message-signatures-01
            return self.headers['Signature-Input']
        if self.headers.get('signature-input'):
            return self.headers['signature-input']
        if self.headers.get('signature'):
            # Ye olde Masto http sig
            return self.headers['signature']
        return None

    def handle_error(self, request, client_address):
        print('ERROR: http server error: ' + str(request) + ', ' +
              str(client_address))

    def _send_reply_to_question(self, nickname: str, message_id: str,
                                answer: str,
                                curr_session, proxy_type: str) -> None:
        """Sends a reply to a question
        """
        votes_filename = \
            acct_dir(self.server.base_dir, nickname, self.server.domain) + \
            '/questions.txt'

        if os.path.isfile(votes_filename):
            # have we already voted on this?
            if text_in_file(message_id, votes_filename):
                print('Already voted on message ' + message_id)
                return

        print('Voting on message ' + message_id)
        print('Vote for: ' + answer)
        comments_enabled = True
        attach_image_filename = None
        media_type = None
        image_description = None
        in_reply_to = message_id
        in_reply_to_atom_uri = message_id
        subject = None
        schedule_post = False
        event_date = None
        event_time = None
        event_end_time = None
        location = None
        conversation_id = None
        city = get_spoofed_city(self.server.city,
                                self.server.base_dir,
                                nickname, self.server.domain)
        languages_understood = \
            get_understood_languages(self.server.base_dir,
                                     self.server.http_prefix,
                                     nickname,
                                     self.server.domain_full,
                                     self.server.person_cache)

        message_json = \
            create_public_post(self.server.base_dir,
                               nickname,
                               self.server.domain, self.server.port,
                               self.server.http_prefix,
                               answer, False, False,
                               comments_enabled,
                               attach_image_filename, media_type,
                               image_description, city,
                               in_reply_to,
                               in_reply_to_atom_uri,
                               subject,
                               schedule_post,
                               event_date,
                               event_time, event_end_time,
                               location, False,
                               self.server.system_language,
                               conversation_id,
                               self.server.low_bandwidth,
                               self.server.content_license_url,
                               languages_understood,
                               self.server.translate)
        if message_json:
            # name field contains the answer
            message_json['object']['name'] = answer
            if self._post_to_outbox(message_json,
                                    self.server.project_version, nickname,
                                    curr_session, proxy_type):
                post_filename = \
                    locate_post(self.server.base_dir, nickname,
                                self.server.domain, message_id)
                if post_filename:
                    post_json_object = load_json(post_filename)
                    if post_json_object:
                        populate_replies(self.server.base_dir,
                                         self.server.http_prefix,
                                         self.server.domain_full,
                                         post_json_object,
                                         self.server.max_replies,
                                         self.server.debug)
                        # record the vote
                        try:
                            with open(votes_filename, 'a+',
                                      encoding='utf-8') as votes_file:
                                votes_file.write(message_id + '\n')
                        except OSError:
                            print('EX: unable to write vote ' +
                                  votes_filename)

                        # ensure that the cached post is removed if it exists,
                        # so that it then will be recreated
                        cached_post_filename = \
                            get_cached_post_filename(self.server.base_dir,
                                                     nickname,
                                                     self.server.domain,
                                                     post_json_object)
                        if cached_post_filename:
                            if os.path.isfile(cached_post_filename):
                                try:
                                    os.remove(cached_post_filename)
                                except OSError:
                                    print('EX: _send_reply_to_question ' +
                                          'unable to delete ' +
                                          cached_post_filename)
                        # remove from memory cache
                        remove_post_from_cache(post_json_object,
                                               self.server.recent_posts_cache)
            else:
                print('ERROR: unable to post vote to outbox')
        else:
            print('ERROR: unable to create vote')

    def _request_csv(self) -> bool:
        """Should a csv response be given?
        """
        if not self.headers.get('Accept'):
            return False
        accept_str = self.headers['Accept']
        if 'text/csv' in accept_str:
            return True
        return False

    def _request_ssml(self) -> bool:
        """Should a ssml response be given?
        """
        if not self.headers.get('Accept'):
            return False
        accept_str = self.headers['Accept']
        if 'application/ssml' in accept_str:
            if 'text/html' not in accept_str:
                return True
        return False

    def _request_http(self) -> bool:
        """Should a http response be given?
        """
        if not self.headers.get('Accept'):
            return False
        accept_str = self.headers['Accept']
        if self.server.debug:
            print('ACCEPT: ' + accept_str)
        if 'application/ssml' in accept_str:
            if 'text/html' not in accept_str:
                return False
        if 'image/' in accept_str:
            if 'text/html' not in accept_str:
                return False
        if 'video/' in accept_str:
            if 'text/html' not in accept_str:
                return False
        if 'audio/' in accept_str:
            if 'text/html' not in accept_str:
                return False
        if accept_str.startswith('*') or 'text/html' in accept_str:
            if self.headers.get('User-Agent'):
                if text_mode_browser(self.headers['User-Agent']):
                    return True
            if 'text/html' not in accept_str:
                return False
        if 'json' in accept_str:
            return False
        return True

    def _request_icalendar(self) -> bool:
        """Should an icalendar response be given?
        """
        if not self.headers.get('Accept'):
            return False
        accept_str = self.headers['Accept']
        if 'text/calendar' in accept_str:
            return True
        return False

    def _signed_get_key_id(self) -> str:
        """Returns the actor from the signed GET key_id
        """
        signature = None
        if self.headers.get('signature'):
            signature = self.headers['signature']
        elif self.headers.get('Signature'):
            signature = self.headers['Signature']

        # check that the headers are signed
        if not signature:
            if self.server.debug:
                print('AUTH: secure mode actor, ' +
                      'GET has no signature in headers')
            return None

        # get the key_id, which is typically the instance actor
        key_id = None
        signature_params = signature.split(',')
        for signature_item in signature_params:
            if signature_item.startswith('keyId='):
                if '"' in signature_item:
                    key_id = signature_item.split('"')[1]
                    # remove #/main-key or #main-key
                    if '#' in key_id:
                        key_id = key_id.split('#')[0]
                    return key_id
        return None

    def _establish_session(self,
                           calling_function: str,
                           curr_session,
                           proxy_type: str):
        """Recreates session if needed
        """
        if curr_session:
            return curr_session
        print('DEBUG: creating new session during ' + calling_function)
        curr_session = create_session(proxy_type)
        if curr_session:
            set_session_for_sender(self.server, proxy_type, curr_session)
            return curr_session
        print('ERROR: GET failed to create session during ' +
              calling_function)
        return None

    def _secure_mode(self, curr_session, proxy_type: str,
                     force: bool = False) -> bool:
        """http authentication of GET requests for json
        """
        if not self.server.secure_mode and not force:
            return True

        key_id = self._signed_get_key_id()
        if not key_id:
            if self.server.debug:
                print('AUTH: secure mode, ' +
                      'failed to obtain key_id from signature')
            return False

        # is the key_id (actor) valid?
        if not url_permitted(key_id, self.server.federation_list):
            if self.server.debug:
                print('AUTH: Secure mode GET request not permitted: ' + key_id)
            return False

        if self.server.onion_domain:
            if '.onion/' in key_id:
                curr_session = self.server.session_onion
                proxy_type = 'tor'
        if self.server.i2p_domain:
            if '.i2p/' in key_id:
                curr_session = self.server.session_i2p
                proxy_type = 'i2p'

        curr_session = \
            self._establish_session("secure mode",
                                    curr_session, proxy_type)
        if not curr_session:
            return False

        # obtain the public key. key_id is the actor
        pub_key = \
            get_person_pub_key(self.server.base_dir,
                               curr_session, key_id,
                               self.server.person_cache, self.server.debug,
                               self.server.project_version,
                               self.server.http_prefix,
                               self.server.domain,
                               self.server.onion_domain,
                               self.server.i2p_domain,
                               self.server.signing_priv_key_pem)
        if not pub_key:
            if self.server.debug:
                print('AUTH: secure mode failed to ' +
                      'obtain public key for ' + key_id)
            return False

        # verify the GET request without any digest
        if verify_post_headers(self.server.http_prefix,
                               self.server.domain_full,
                               pub_key, self.headers,
                               self.path, True, None, '', self.server.debug):
            return True

        if self.server.debug:
            print('AUTH: secure mode authorization failed for ' + key_id)
        return False

    def _get_account_pub_key(self, path: str, person_cache: {},
                             base_dir: str, http_prefix: str,
                             domain: str, onion_domain: str,
                             i2p_domain: str,
                             calling_domain: str) -> str:
        """Returns the public key for an account
        """
        if '/users/' not in path:
            return None
        nickname = path.split('/users/')[1]
        if '#main-key' in nickname:
            nickname = nickname.split('#main-key')[0]
        elif '/main-key' in nickname:
            nickname = nickname.split('/main-key')[0]
        elif '#/publicKey' in nickname:
            nickname = nickname.split('#/publicKey')[0]
        else:
            return None
        if calling_domain.endswith('.onion'):
            actor = 'http://' + onion_domain + '/users/' + nickname
        elif calling_domain.endswith('.i2p'):
            actor = 'http://' + i2p_domain + '/users/' + nickname
        else:
            actor = http_prefix + '://' + domain + '/users/' + nickname
        actor_json = get_person_from_cache(base_dir, actor, person_cache)
        if not actor_json:
            actor_filename = acct_dir(base_dir, nickname, domain) + '.json'
            if not os.path.isfile(actor_filename):
                return None
            actor_json = load_json(actor_filename, 1, 1)
            if not actor_json:
                return None
            store_person_in_cache(base_dir, actor, actor_json,
                                  person_cache, False)
        if not actor_json.get('publicKey'):
            return None
        return actor_json['publicKey']

    def _login_headers(self, file_format: str, length: int,
                       calling_domain: str) -> None:
        self.send_response(200)
        self.send_header('Content-type', file_format)
        self.send_header('Content-Length', str(length))
        self.send_header('Host', calling_domain)
        self.send_header('WWW-Authenticate',
                         'title="Login to Epicyon", Basic realm="epicyon"')
        self.end_headers()

    def _logout_headers(self, file_format: str, length: int,
                        calling_domain: str) -> None:
        self.send_response(200)
        self.send_header('Content-type', file_format)
        self.send_header('Content-Length', str(length))
        self.send_header('Set-Cookie', 'epicyon=; SameSite=Strict')
        self.send_header('Host', calling_domain)
        self.send_header('WWW-Authenticate',
                         'title="Login to Epicyon", Basic realm="epicyon"')
        self.end_headers()

    def _quoted_redirect(self, redirect: str) -> str:
        """hashtag screen urls sometimes contain non-ascii characters which
        need to be url encoded
        """
        if '/tags/' not in redirect:
            return redirect
        last_str = redirect.split('/')[-1]
        return redirect.replace('/' + last_str, '/' +
                                urllib.parse.quote_plus(last_str))

    def _logout_redirect(self, redirect: str, cookie: str,
                         calling_domain: str) -> None:
        if '://' not in redirect:
            if calling_domain.endswith('.onion') and self.server.onion_domain:
                redirect = 'http://' + self.server.onion_domain + redirect
            elif calling_domain.endswith('.i2p') and self.server.i2p_domain:
                redirect = 'http://' + self.server.i2p_domain + redirect
            else:
                redirect = \
                    self.server.http_prefix + '://' + \
                    self.server.domain_full + redirect
            print('WARN: redirect was not an absolute url, changed to ' +
                  redirect)

        self.send_response(303)
        self.send_header('Set-Cookie', 'epicyon=; SameSite=Strict')
        self.send_header('Location', self._quoted_redirect(redirect))
        self.send_header('Host', calling_domain)
        self.send_header('X-AP-Instance-ID', self.server.instance_id)
        self.send_header('Content-Length', '0')
        self.end_headers()

    def _set_headers_base(self, file_format: str, length: int, cookie: str,
                          calling_domain: str, permissive: bool) -> None:
        self.send_response(200)
        self.send_header('Content-type', file_format)
        if 'image/' in file_format or \
           'audio/' in file_format or \
           'video/' in file_format:
            cache_control = 'public, max-age=84600, immutable'
            self.send_header('Cache-Control', cache_control)
        else:
            self.send_header('Cache-Control', 'public')
        self.send_header('Origin', self.server.domain_full)
        if length > -1:
            self.send_header('Content-Length', str(length))
        if calling_domain:
            self.send_header('Host', calling_domain)
        if permissive:
            self.send_header('Access-Control-Allow-Origin', '*')
            return
        self.send_header('X-AP-Instance-ID', self.server.instance_id)
        self.send_header('X-Clacks-Overhead', self.server.clacks)
        self.send_header('User-Agent',
                         'Epicyon/' + __version__ +
                         '; +' + self.server.http_prefix + '://' +
                         self.server.domain_full + '/')
        if cookie:
            cookie_str = cookie
            if 'HttpOnly;' not in cookie_str:
                if self.server.http_prefix == 'https':
                    cookie_str += '; Secure'
                cookie_str += '; HttpOnly; SameSite=Strict'
            self.send_header('Cookie', cookie_str)

    def _set_headers(self, file_format: str, length: int, cookie: str,
                     calling_domain: str, permissive: bool) -> None:
        self._set_headers_base(file_format, length, cookie, calling_domain,
                               permissive)
        self.end_headers()

    def _set_headers_head(self, file_format: str, length: int, etag: str,
                          calling_domain: str, permissive: bool,
                          last_modified_time_str: str) -> None:
        self._set_headers_base(file_format, length, None, calling_domain,
                               permissive)
        if etag:
            self.send_header('ETag', '"' + etag + '"')
        if last_modified_time_str:
            self.send_header('last-modified',
                             last_modified_time_str)
        self.end_headers()

    def _set_headers_etag(self, media_filename: str, file_format: str,
                          data, cookie: str, calling_domain: str,
                          permissive: bool, last_modified: str) -> None:
        datalen = len(data)
        self._set_headers_base(file_format, datalen, cookie, calling_domain,
                               permissive)
        etag = None
        if os.path.isfile(media_filename + '.etag'):
            try:
                with open(media_filename + '.etag', 'r',
                          encoding='utf-8') as efile:
                    etag = efile.read()
            except OSError:
                print('EX: _set_headers_etag ' +
                      'unable to read ' + media_filename + '.etag')
        if not etag:
            etag = md5(data).hexdigest()  # nosec
            try:
                with open(media_filename + '.etag', 'w+',
                          encoding='utf-8') as efile:
                    efile.write(etag)
            except OSError:
                print('EX: _set_headers_etag ' +
                      'unable to write ' + media_filename + '.etag')
        # if etag:
        #     self.send_header('ETag', '"' + etag + '"')
        if last_modified:
            self.send_header('last-modified', last_modified)
        self.end_headers()

    def _etag_exists(self, media_filename: str) -> bool:
        """Does an etag header exist for the given file?
        """
        etag_header = 'If-None-Match'
        if not self.headers.get(etag_header):
            etag_header = 'if-none-match'
            if not self.headers.get(etag_header):
                etag_header = 'If-none-match'

        if self.headers.get(etag_header):
            old_etag = self.headers[etag_header].replace('"', '')
            if os.path.isfile(media_filename + '.etag'):
                # load the etag from file
                curr_etag = ''
                try:
                    with open(media_filename + '.etag', 'r',
                              encoding='utf-8') as efile:
                        curr_etag = efile.read()
                except OSError:
                    print('EX: _etag_exists unable to read ' +
                          str(media_filename))
                if curr_etag and old_etag == curr_etag:
                    # The file has not changed
                    return True
        return False

    def _redirect_headers(self, redirect: str, cookie: str,
                          calling_domain: str) -> None:
        if '://' not in redirect:
            if calling_domain.endswith('.onion') and self.server.onion_domain:
                redirect = 'http://' + self.server.onion_domain + redirect
            elif calling_domain.endswith('.i2p') and self.server.i2p_domain:
                redirect = 'http://' + self.server.i2p_domain + redirect
            else:
                redirect = \
                    self.server.http_prefix + '://' + \
                    self.server.domain_full + redirect
            print('WARN: redirect was not an absolute url, changed to ' +
                  redirect)

        self.send_response(303)

        if cookie:
            cookie_str = cookie.replace('SET:', '').strip()
            if 'HttpOnly;' not in cookie_str:
                if self.server.http_prefix == 'https':
                    cookie_str += '; Secure'
                cookie_str += '; HttpOnly; SameSite=Strict'
            if not cookie.startswith('SET:'):
                self.send_header('Cookie', cookie_str)
            else:
                self.send_header('Set-Cookie', cookie_str)
        self.send_header('Location', self._quoted_redirect(redirect))
        self.send_header('Host', calling_domain)
        self.send_header('X-AP-Instance-ID', self.server.instance_id)
        self.send_header('Content-Length', '0')
        self.end_headers()

    def _http_return_code(self, http_code: int, http_description: str,
                          long_description: str, etag: str) -> None:
        msg = \
            '<html><head><title>' + str(http_code) + '</title></head>' \
            '<body bgcolor="linen" text="black">' \
            '<div style="font-size: 400px; ' \
            'text-align: center;">' + str(http_code) + '</div>' \
            '<div style="font-size: 128px; ' \
            'text-align: center; font-variant: ' \
            'small-caps;"><p role="alert">' + http_description + '</p></div>' \
            '<div style="text-align: center;">' + long_description + '</div>' \
            '</body></html>'
        msg = msg.encode('utf-8')
        self.send_response(http_code)
        self.send_header('Content-Type', 'text/html; charset=utf-8')
        msg_len_str = str(len(msg))
        self.send_header('Content-Length', msg_len_str)
        if etag:
            self.send_header('ETag', etag)
        self.end_headers()
        if not self._write(msg):
            print('Error when showing ' + str(http_code))

    def _200(self) -> None:
        if self.server.translate:
            ok_str = self.server.translate['This is nothing ' +
                                           'less than an utter triumph']
            self._http_return_code(200, self.server.translate['Ok'],
                                   ok_str, None)
        else:
            self._http_return_code(200, 'Ok',
                                   'This is nothing less ' +
                                   'than an utter triumph', None)

    def _401(self, post_msg: str) -> None:
        if self.server.translate:
            ok_str = self.server.translate[post_msg]
            self._http_return_code(401, self.server.translate['Unauthorized'],
                                   ok_str, None)
        else:
            self._http_return_code(401, 'Unauthorized',
                                   post_msg, None)

    def _201(self, etag: str) -> None:
        if self.server.translate:
            done_str = self.server.translate['It is done']
            self._http_return_code(201,
                                   self.server.translate['Created'], done_str,
                                   etag)
        else:
            self._http_return_code(201, 'Created', 'It is done', etag)

    def _207(self) -> None:
        if self.server.translate:
            multi_str = self.server.translate['Lots of things']
            self._http_return_code(207,
                                   self.server.translate['Multi Status'],
                                   multi_str, None)
        else:
            self._http_return_code(207, 'Multi Status',
                                   'Lots of things', None)

    def _403(self) -> None:
        if self.server.translate:
            self._http_return_code(403, self.server.translate['Forbidden'],
                                   self.server.translate["You're not allowed"],
                                   None)
        else:
            self._http_return_code(403, 'Forbidden',
                                   "You're not allowed", None)

    def _404(self) -> None:
        if self.server.translate:
            self._http_return_code(404, self.server.translate['Not Found'],
                                   self.server.translate['These are not the ' +
                                                         'droids you are ' +
                                                         'looking for'],
                                   None)
        else:
            self._http_return_code(404, 'Not Found',
                                   'These are not the ' +
                                   'droids you are ' +
                                   'looking for', None)

    def _304(self) -> None:
        if self.server.translate:
            self._http_return_code(304, self.server.translate['Not changed'],
                                   self.server.translate['The contents of ' +
                                                         'your local cache ' +
                                                         'are up to date'],
                                   None)
        else:
            self._http_return_code(304, 'Not changed',
                                   'The contents of ' +
                                   'your local cache ' +
                                   'are up to date',
                                   None)

    def _400(self) -> None:
        if self.server.translate:
            self._http_return_code(400, self.server.translate['Bad Request'],
                                   self.server.translate['Better luck ' +
                                                         'next time'],
                                   None)
        else:
            self._http_return_code(400, 'Bad Request',
                                   'Better luck next time', None)

    def _503(self) -> None:
        if self.server.translate:
            busy_str = \
                self.server.translate['The server is busy. ' +
                                      'Please try again later']
            self._http_return_code(503, self.server.translate['Unavailable'],
                                   busy_str, None)
        else:
            self._http_return_code(503, 'Unavailable',
                                   'The server is busy. Please try again ' +
                                   'later', None)

    def _write(self, msg) -> bool:
        tries = 0
        while tries < 5:
            try:
                self.wfile.write(msg)
                return True
            except BrokenPipeError as ex:
                if self.server.debug:
                    print('EX: _write error ' + str(tries) + ' ' + str(ex))
                break
            except BaseException as ex:
                print('EX: _write error ' + str(tries) + ' ' + str(ex))
                time.sleep(0.5)
            tries += 1
        return False

    def _has_accept(self, calling_domain: str) -> bool:
        """Do the http headers have an Accept field?
        """
        if not self.headers.get('Accept'):
            if self.headers.get('accept'):
                print('Upper case Accept')
                self.headers['Accept'] = self.headers['accept']

        if self.headers.get('Accept') or calling_domain.endswith('.b32.i2p'):
            if not self.headers.get('Accept'):
                self.headers['Accept'] = \
                    'text/html,application/xhtml+xml,' \
                    'application/xml;q=0.9,image/webp,*/*;q=0.8'
            return True
        return False

    def _masto_api_v1(self, path: str, calling_domain: str,
                      ua_str: str,
                      authorized: bool,
                      http_prefix: str,
                      base_dir: str, nickname: str, domain: str,
                      domain_full: str,
                      onion_domain: str, i2p_domain: str,
                      translate: {},
                      registration: bool,
                      system_language: str,
                      project_version: str,
                      custom_emoji: [],
                      show_node_info_accounts: bool,
                      referer_domain: str,
                      debug: bool,
                      calling_site_timeout: int,
                      known_crawlers: {}) -> bool:
        """This is a vestigil mastodon API for the purpose
        of returning an empty result to sites like
        https://mastopeek.app-dist.eu
        """
        if not path.startswith('/api/v1/'):
            return False

        if not referer_domain:
            if not (debug and self.server.unit_test):
                print('mastodon api request has no referer domain ' +
                      str(ua_str))
                self._400()
                return True
        if referer_domain == self.server.domain_full:
            print('mastodon api request from self')
            self._400()
            return True
        if self.server.masto_api_is_active:
            print('mastodon api is busy during request from ' +
                  referer_domain)
            self._503()
            return True
        self.server.masto_api_is_active = True
        # is this a real website making the call ?
        if not debug and not self.server.unit_test and referer_domain:
            # Does calling_domain look like a domain?
            if ' ' in referer_domain or \
               ';' in referer_domain or \
               '.' not in referer_domain:
                print('mastodon api ' +
                      'referer does not look like a domain ' +
                      referer_domain)
                self._400()
                self.server.masto_api_is_active = False
                return True
            if not self.server.allow_local_network_access:
                if local_network_host(referer_domain):
                    print('mastodon api referer domain is from the ' +
                          'local network ' + referer_domain)
                    self._400()
                    self.server.masto_api_is_active = False
                    return True
            if not referer_is_active(http_prefix,
                                     referer_domain, ua_str,
                                     calling_site_timeout):
                print('mastodon api referer url is not active ' +
                      referer_domain)
                self._400()
                self.server.masto_api_is_active = False
                return True

        print('mastodon api v1: ' + path)
        print('mastodon api v1: authorized ' + str(authorized))
        print('mastodon api v1: nickname ' + str(nickname))
        print('mastodon api v1: referer ' + str(referer_domain))
        crawl_time = \
            update_known_crawlers(ua_str, base_dir,
                                  self.server.known_crawlers,
                                  self.server.last_known_crawler)
        if crawl_time is not None:
            self.server.last_known_crawler = crawl_time

        broch_mode = broch_mode_is_active(base_dir)
        send_json, send_json_str = \
            masto_api_v1_response(path,
                                  calling_domain,
                                  ua_str,
                                  authorized,
                                  http_prefix,
                                  base_dir,
                                  nickname, domain,
                                  domain_full,
                                  onion_domain,
                                  i2p_domain,
                                  translate,
                                  registration,
                                  system_language,
                                  project_version,
                                  custom_emoji,
                                  show_node_info_accounts,
                                  broch_mode)

        if send_json is not None:
            msg_str = json.dumps(send_json)
            msg_str = self._convert_domains(calling_domain, referer_domain,
                                            msg_str)
            msg = msg_str.encode('utf-8')
            msglen = len(msg)
            if self._has_accept(calling_domain):
                protocol_str = \
                    get_json_content_from_accept(self.headers.get('Accept'))
                self._set_headers(protocol_str, msglen,
                                  None, calling_domain, True)
            else:
                self._set_headers('application/ld+json', msglen,
                                  None, calling_domain, True)
            self._write(msg)
            if send_json_str:
                print(send_json_str)
            self.server.masto_api_is_active = False
            return True

        # no api endpoints were matched
        self._404()
        self.server.masto_api_is_active = False
        return True

    def _masto_api(self, path: str, calling_domain: str,
                   ua_str: str,
                   authorized: bool, http_prefix: str,
                   base_dir: str, nickname: str, domain: str,
                   domain_full: str,
                   onion_domain: str, i2p_domain: str,
                   translate: {},
                   registration: bool,
                   system_language: str,
                   project_version: str,
                   custom_emoji: [],
                   show_node_info_accounts: bool,
                   referer_domain: str, debug: bool,
                   known_crawlers: {}) -> bool:
        return self._masto_api_v1(path, calling_domain, ua_str, authorized,
                                  http_prefix, base_dir, nickname, domain,
                                  domain_full, onion_domain, i2p_domain,
                                  translate, registration, system_language,
                                  project_version, custom_emoji,
                                  show_node_info_accounts,
                                  referer_domain, debug, 5,
                                  known_crawlers)

    def _show_vcard(self, base_dir: str, path: str, calling_domain: str,
                    referer_domain: str, domain: str) -> bool:
        """Returns a vcard for the given account
        """
        if not self._has_accept(calling_domain):
            return False
        if path.endswith('.vcf'):
            path = path.split('.vcf')[0]
            accept_str = 'text/vcard'
        else:
            accept_str = self.headers['Accept']
        if 'text/vcard' not in accept_str and \
           'application/vcard+xml' not in accept_str:
            return False
        if path.startswith('/@'):
            path = path.replace('/@', '/users/', 1)
        if not path.startswith('/users/'):
            self._400()
            return True
        nickname = path.split('/users/')[1]
        if '/' in nickname:
            nickname = nickname.split('/')[0]
        if '?' in nickname:
            nickname = nickname.split('?')[0]
        if self.server.vcard_is_active:
            print('vcard is busy during request from ' + str(referer_domain))
            self._503()
            return True
        self.server.vcard_is_active = True
        actor_json = None
        actor_filename = \
            acct_dir(base_dir, nickname, domain) + '.json'
        if os.path.isfile(actor_filename):
            actor_json = load_json(actor_filename)
        if not actor_json:
            print('WARN: vcard actor not found ' + actor_filename)
            self._404()
            self.server.vcard_is_active = False
            return True
        if 'application/vcard+xml' in accept_str:
            vcard_str = actor_to_vcard_xml(actor_json, domain)
            header_type = 'application/vcard+xml; charset=utf-8'
        else:
            vcard_str = actor_to_vcard(actor_json, domain)
            header_type = 'text/vcard; charset=utf-8'
        if vcard_str:
            msg = vcard_str.encode('utf-8')
            msglen = len(msg)
            self._set_headers(header_type, msglen,
                              None, calling_domain, True)
            self._write(msg)
            print('vcard sent to ' + str(referer_domain))
            self.server.vcard_is_active = False
            return True
        print('WARN: vcard string not returned')
        self._404()
        self.server.vcard_is_active = False
        return True

    def _nodeinfo(self, ua_str: str, calling_domain: str,
                  referer_domain: str,
                  http_prefix: str, calling_site_timeout: int,
                  debug: bool) -> bool:
        if self.path.startswith('/nodeinfo/1.0'):
            self._400()
            return True
        if not self.path.startswith('/nodeinfo/2.0'):
            return False
        if not referer_domain:
            if not debug and not self.server.unit_test:
                print('nodeinfo request has no referer domain ' + str(ua_str))
                self._400()
                return True
        if referer_domain == self.server.domain_full:
            print('nodeinfo request from self')
            self._400()
            return True
        if self.server.nodeinfo_is_active:
            if not referer_domain:
                print('nodeinfo is busy during request without referer domain')
            else:
                print('nodeinfo is busy during request from ' + referer_domain)
            self._503()
            return True
        self.server.nodeinfo_is_active = True
        # is this a real website making the call ?
        if not debug and not self.server.unit_test and referer_domain:
            # Does calling_domain look like a domain?
            if ' ' in referer_domain or \
               ';' in referer_domain or \
               '.' not in referer_domain:
                print('nodeinfo referer domain does not look like a domain ' +
                      referer_domain)
                self._400()
                self.server.nodeinfo_is_active = False
                return True
            if not self.server.allow_local_network_access:
                if local_network_host(referer_domain):
                    print('nodeinfo referer domain is from the ' +
                          'local network ' + referer_domain)
                    self._400()
                    self.server.nodeinfo_is_active = False
                    return True

            if not referer_is_active(http_prefix,
                                     referer_domain, ua_str,
                                     calling_site_timeout):
                print('nodeinfo referer url is not active ' +
                      referer_domain)
                self._400()
                self.server.nodeinfo_is_active = False
                return True
        if self.server.debug:
            print('DEBUG: nodeinfo ' + self.path)
        crawl_time = \
            update_known_crawlers(ua_str,
                                  self.server.base_dir,
                                  self.server.known_crawlers,
                                  self.server.last_known_crawler)
        if crawl_time is not None:
            self.server.last_known_crawler = crawl_time

        # If we are in broch mode then don't show potentially
        # sensitive metadata.
        # For example, if this or allied instances are being attacked
        # then numbers of accounts may be changing as people
        # migrate, and that information may be useful to an adversary
        broch_mode = broch_mode_is_active(self.server.base_dir)

        node_info_version = self.server.project_version
        if not self.server.show_node_info_version or broch_mode:
            node_info_version = '0.0.0'

        show_node_info_accounts = self.server.show_node_info_accounts
        if broch_mode:
            show_node_info_accounts = False

        instance_url = self._get_instance_url(calling_domain)
        about_url = instance_url + '/about'
        terms_of_service_url = instance_url + '/terms'
        info = meta_data_node_info(self.server.base_dir,
                                   about_url, terms_of_service_url,
                                   self.server.registration,
                                   node_info_version,
                                   show_node_info_accounts)
        if info:
            msg_str = json.dumps(info)
            msg_str = self._convert_domains(calling_domain, referer_domain,
                                            msg_str)
            msg = msg_str.encode('utf-8')
            msglen = len(msg)
            if self._has_accept(calling_domain):
                protocol_str = \
                    get_json_content_from_accept(self.headers.get('Accept'))
                self._set_headers(protocol_str, msglen,
                                  None, calling_domain, True)
            else:
                self._set_headers('application/ld+json', msglen,
                                  None, calling_domain, True)
            self._write(msg)
            if referer_domain:
                print('nodeinfo sent to ' + referer_domain)
            else:
                print('nodeinfo sent to unknown referer')
            self.server.nodeinfo_is_active = False
            return True
        self._404()
        self.server.nodeinfo_is_active = False
        return True

    def _security_txt(self, ua_str: str, calling_domain: str,
                      referer_domain: str,
                      http_prefix: str, calling_site_timeout: int,
                      debug: bool) -> bool:
        """See https://www.rfc-editor.org/rfc/rfc9116
        """
        if not self.path.startswith('/security.txt'):
            return False
        if referer_domain == self.server.domain_full:
            print('security.txt request from self')
            self._400()
            return True
        if self.server.security_txt_is_active:
            if not referer_domain:
                print('security.txt is busy ' +
                      'during request without referer domain')
            else:
                print('security.txt is busy during request from ' +
                      referer_domain)
            self._503()
            return True
        self.server.security_txt_is_active = True
        # is this a real website making the call ?
        if not debug and not self.server.unit_test and referer_domain:
            # Does calling_domain look like a domain?
            if ' ' in referer_domain or \
               ';' in referer_domain or \
               '.' not in referer_domain:
                print('security.txt ' +
                      'referer domain does not look like a domain ' +
                      referer_domain)
                self._400()
                self.server.security_txt_is_active = False
                return True
            if not self.server.allow_local_network_access:
                if local_network_host(referer_domain):
                    print('security.txt referer domain is from the ' +
                          'local network ' + referer_domain)
                    self._400()
                    self.server.security_txt_is_active = False
                    return True

            if not referer_is_active(http_prefix,
                                     referer_domain, ua_str,
                                     calling_site_timeout):
                print('security.txt referer url is not active ' +
                      referer_domain)
                self._400()
                self.server.security_txt_is_active = False
                return True
        if self.server.debug:
            print('DEBUG: security.txt ' + self.path)

        # If we are in broch mode then don't reply
        if not broch_mode_is_active(self.server.base_dir):
            security_txt = \
                'Contact: https://gitlab.com/bashrc2/epicyon/-/issues'

            msg = security_txt.encode('utf-8')
            msglen = len(msg)
            self._set_headers('text/plain; charset=utf-8',
                              msglen, None, calling_domain, True)
            self._write(msg)
            if referer_domain:
                print('security.txt sent to ' + referer_domain)
            else:
                print('security.txt sent to unknown referer')
        self.server.security_txt_is_active = False
        return True

    def _webfinger(self, calling_domain: str, referer_domain: str) -> bool:
        if not self.path.startswith('/.well-known'):
            return False
        if self.server.debug:
            print('DEBUG: WEBFINGER well-known')

        if self.server.debug:
            print('DEBUG: WEBFINGER host-meta')
        if self.path.startswith('/.well-known/host-meta'):
            if calling_domain.endswith('.onion') and \
               self.server.onion_domain:
                wf_result = \
                    webfinger_meta('http', self.server.onion_domain)
            elif (calling_domain.endswith('.i2p') and
                  self.server.i2p_domain):
                wf_result = \
                    webfinger_meta('http', self.server.i2p_domain)
            else:
                wf_result = \
                    webfinger_meta(self.server.http_prefix,
                                   self.server.domain_full)
            if wf_result:
                msg = wf_result.encode('utf-8')
                msglen = len(msg)
                self._set_headers('application/xrd+xml', msglen,
                                  None, calling_domain, True)
                self._write(msg)
                return True
            self._404()
            return True
        if self.path.startswith('/api/statusnet') or \
           self.path.startswith('/api/gnusocial') or \
           self.path.startswith('/siteinfo') or \
           self.path.startswith('/poco') or \
           self.path.startswith('/friendi'):
            self._404()
            return True
        if self.path.startswith('/.well-known/nodeinfo') or \
           self.path.startswith('/.well-known/x-nodeinfo'):
            if calling_domain.endswith('.onion') and \
               self.server.onion_domain:
                wf_result = \
                    webfinger_node_info('http', self.server.onion_domain)
            elif (calling_domain.endswith('.i2p') and
                  self.server.i2p_domain):
                wf_result = \
                    webfinger_node_info('http', self.server.i2p_domain)
            else:
                wf_result = \
                    webfinger_node_info(self.server.http_prefix,
                                        self.server.domain_full)
            if wf_result:
                msg_str = json.dumps(wf_result)
                msg_str = self._convert_domains(calling_domain,
                                                referer_domain,
                                                msg_str)
                msg = msg_str.encode('utf-8')
                msglen = len(msg)
                if self._has_accept(calling_domain):
                    accept_str = self.headers.get('Accept')
                    protocol_str = \
                        get_json_content_from_accept(accept_str)
                    self._set_headers(protocol_str, msglen,
                                      None, calling_domain, True)
                else:
                    self._set_headers('application/ld+json', msglen,
                                      None, calling_domain, True)
                self._write(msg)
                return True
            self._404()
            return True

        if self.server.debug:
            print('DEBUG: WEBFINGER lookup ' + self.path + ' ' +
                  str(self.server.base_dir))
        wf_result = \
            webfinger_lookup(self.path, self.server.base_dir,
                             self.server.domain,
                             self.server.onion_domain,
                             self.server.i2p_domain,
                             self.server.port, self.server.debug)
        if wf_result:
            msg_str = json.dumps(wf_result)
            msg_str = self._convert_domains(calling_domain,
                                            referer_domain,
                                            msg_str)
            msg = msg_str.encode('utf-8')
            msglen = len(msg)
            self._set_headers('application/jrd+json', msglen,
                              None, calling_domain, True)
            self._write(msg)
        else:
            if self.server.debug:
                print('DEBUG: WEBFINGER lookup 404 ' + self.path)
            self._404()
        return True

    def _post_to_outbox(self, message_json: {}, version: str,
                        post_to_nickname: str,
                        curr_session, proxy_type: str) -> bool:
        """post is received by the outbox
        Client to server message post
        https://www.w3.org/TR/activitypub/#client-to-server-outbox-delivery
        """
        if not curr_session:
            return False

        city = self.server.city

        if post_to_nickname:
            print('Posting to nickname ' + post_to_nickname)
            self.post_to_nickname = post_to_nickname
            city = get_spoofed_city(self.server.city,
                                    self.server.base_dir,
                                    post_to_nickname, self.server.domain)

        shared_items_federated_domains = \
            self.server.shared_items_federated_domains
        shared_item_federation_tokens = \
            self.server.shared_item_federation_tokens
        return post_message_to_outbox(curr_session,
                                      self.server.translate,
                                      message_json, self.post_to_nickname,
                                      self.server, self.server.base_dir,
                                      self.server.http_prefix,
                                      self.server.domain,
                                      self.server.domain_full,
                                      self.server.onion_domain,
                                      self.server.i2p_domain,
                                      self.server.port,
                                      self.server.recent_posts_cache,
                                      self.server.followers_threads,
                                      self.server.federation_list,
                                      self.server.send_threads,
                                      self.server.postLog,
                                      self.server.cached_webfingers,
                                      self.server.person_cache,
                                      self.server.allow_deletion,
                                      proxy_type, version,
                                      self.server.debug,
                                      self.server.yt_replace_domain,
                                      self.server.twitter_replacement_domain,
                                      self.server.show_published_date_only,
                                      self.server.allow_local_network_access,
                                      city, self.server.system_language,
                                      shared_items_federated_domains,
                                      shared_item_federation_tokens,
                                      self.server.low_bandwidth,
                                      self.server.signing_priv_key_pem,
                                      self.server.peertube_instances,
                                      self.server.theme_name,
                                      self.server.max_like_count,
                                      self.server.max_recent_posts,
                                      self.server.cw_lists,
                                      self.server.lists_enabled,
                                      self.server.content_license_url,
                                      self.server.dogwhistles,
                                      self.server.min_images_for_accounts)

    def _get_outbox_thread_index(self, nickname: str,
                                 max_outbox_threads_per_account: int) -> int:
        """Returns the outbox thread index for the given account
        This is a ring buffer used to store the thread objects which
        are sending out posts
        """
        account_outbox_thread_name = nickname
        if not account_outbox_thread_name:
            account_outbox_thread_name = '*'

        # create the buffer for the given account
        if not self.server.outboxThread.get(account_outbox_thread_name):
            self.server.outboxThread[account_outbox_thread_name] = \
                [None] * max_outbox_threads_per_account
            self.server.outbox_thread_index[account_outbox_thread_name] = 0
            return 0

        # increment the ring buffer index
        index = self.server.outbox_thread_index[account_outbox_thread_name] + 1
        if index >= max_outbox_threads_per_account:
            index = 0

        self.server.outbox_thread_index[account_outbox_thread_name] = index

        # remove any existing thread from the current index in the buffer
        acct = account_outbox_thread_name
        if self.server.outboxThread.get(acct):
            if len(self.server.outboxThread[acct]) > index:
                try:
                    if self.server.outboxThread[acct][index].is_alive():
                        self.server.outboxThread[acct][index].kill()
                except BaseException:
                    pass
        return index

    def _post_to_outbox_thread(self, message_json: {},
                               curr_session, proxy_type: str) -> bool:
        """Creates a thread to send a post
        """
        account_outbox_thread_name = self.post_to_nickname
        if not account_outbox_thread_name:
            account_outbox_thread_name = '*'

        index = self._get_outbox_thread_index(account_outbox_thread_name, 8)

        print('Creating outbox thread ' +
              account_outbox_thread_name + '/' +
              str(self.server.outbox_thread_index[account_outbox_thread_name]))
        print('THREAD: _post_to_outbox')
        self.server.outboxThread[account_outbox_thread_name][index] = \
            thread_with_trace(target=self._post_to_outbox,
                              args=(message_json.copy(),
                                    self.server.project_version, None,
                                    curr_session, proxy_type),
                              daemon=True)
        print('Starting outbox thread')
        outbox_thread = \
            self.server.outboxThread[account_outbox_thread_name][index]
        begin_thread(outbox_thread, '_post_to_outbox_thread')
        return True

    def _update_inbox_queue(self, nickname: str, message_json: {},
                            message_bytes: str, debug: bool) -> int:
        """Update the inbox queue
        """
        if debug:
            print('INBOX: checking inbox queue restart')
        if self.server.restart_inbox_queue_in_progress:
            self._503()
            print('INBOX: ' +
                  'message arrived but currently restarting inbox queue')
            self.server.postreq_busy = False
            return 2

        # check that the incoming message has a fully recognized
        # linked data context
        if debug:
            print('INBOX: checking valid context')
        if not has_valid_context(message_json):
            print('INBOX: ' +
                  'message arriving at inbox queue has no valid context')
            self._400()
            self.server.postreq_busy = False
            return 3

        # check for blocked domains so that they can be rejected early
        if debug:
            print('INBOX: checking for actor')
        message_domain = None
        if not has_actor(message_json, self.server.debug):
            print('INBOX: message arriving at inbox queue has no actor')
            self._400()
            self.server.postreq_busy = False
            return 3

        # actor should be a string
        if debug:
            print('INBOX: checking that actor is string')
        if not isinstance(message_json['actor'], str):
            print('INBOX: ' +
                  'actor should be a string ' + str(message_json['actor']))
            self._400()
            self.server.postreq_busy = False
            return 3

        # check that some additional fields are strings
        if debug:
            print('INBOX: checking fields 1')
        string_fields = ('id', 'type', 'published')
        for check_field in string_fields:
            if not message_json.get(check_field):
                continue
            if not isinstance(message_json[check_field], str):
                print('INBOX: ' +
                      'id, type and published fields should be strings ' +
                      check_field + ' ' + str(message_json[check_field]))
                self._400()
                self.server.postreq_busy = False
                return 3

        # check that to/cc fields are lists
        if debug:
            print('INBOX: checking to and cc fields')
        list_fields = ('to', 'cc')
        for check_field in list_fields:
            if not message_json.get(check_field):
                continue
            if not isinstance(message_json[check_field], list):
                print('INBOX: To and Cc fields should be strings ' +
                      check_field + ' ' + str(message_json[check_field]))
                self._400()
                self.server.postreq_busy = False
                return 3

        if has_object_dict(message_json):
            if debug:
                print('INBOX: checking object fields')
            string_fields = (
                'id', 'actor', 'type', 'content', 'published',
                'summary', 'url', 'attributedTo'
            )
            for check_field in string_fields:
                if not message_json['object'].get(check_field):
                    continue
                if not isinstance(message_json['object'][check_field], str):
                    print('INBOX: ' +
                          check_field + ' should be a string ' +
                          str(message_json['object'][check_field]))
                    self._400()
                    self.server.postreq_busy = False
                    return 3
            # check that some fields are lists
            if debug:
                print('INBOX: checking object to and cc fields')
            list_fields = ('to', 'cc', 'attachment')
            for check_field in list_fields:
                if not message_json['object'].get(check_field):
                    continue
                if not isinstance(message_json['object'][check_field], list):
                    print('INBOX: ' +
                          check_field + ' should be a list ' +
                          str(message_json['object'][check_field]))
                    self._400()
                    self.server.postreq_busy = False
                    return 3
            # check that the content does not contain impossibly long urls
            if message_json['object'].get('content'):
                content_str = message_json['object']['content']
                if not valid_url_lengths(content_str, 2048):
                    print('INBOX: content contains urls which are too long ' +
                          message_json['actor'])
                    self._400()
                    self.server.postreq_busy = False
                    return 3
            # check that the summary does not contain links
            if message_json['object'].get('summary'):
                if len(message_json['object']['summary']) > 1024:
                    print('INBOX: summary is too long ' +
                          message_json['actor'] + ' ' +
                          message_json['object']['summary'])
                    self._400()
                    self.server.postreq_busy = False
                    return 3
                if '://' in message_json['object']['summary']:
                    print('INBOX: summary should not contain links ' +
                          message_json['actor'] + ' ' +
                          message_json['object']['summary'])
                    self._400()
                    self.server.postreq_busy = False
                    return 3

        # actor should look like a url
        if debug:
            print('INBOX: checking that actor looks like a url')
        if '://' not in message_json['actor'] or \
           '.' not in message_json['actor']:
            print('INBOX: POST actor does not look like a url ' +
                  message_json['actor'])
            self._400()
            self.server.postreq_busy = False
            return 3

        # sent by an actor on a local network address?
        if debug:
            print('INBOX: checking for local network access')
        if not self.server.allow_local_network_access:
            local_network_pattern_list = get_local_network_addresses()
            for local_network_pattern in local_network_pattern_list:
                if local_network_pattern in message_json['actor']:
                    print('INBOX: POST actor contains local network address ' +
                          message_json['actor'])
                    self._400()
                    self.server.postreq_busy = False
                    return 3

        message_domain, _ = \
            get_domain_from_actor(message_json['actor'])

        self.server.blocked_cache_last_updated = \
            update_blocked_cache(self.server.base_dir,
                                 self.server.blocked_cache,
                                 self.server.blocked_cache_last_updated,
                                 self.server.blocked_cache_update_secs)

        if debug:
            print('INBOX: checking for blocked domain ' + message_domain)
        if is_blocked_domain(self.server.base_dir, message_domain,
                             self.server.blocked_cache):
            print('INBOX: POST from blocked domain ' + message_domain)
            self._400()
            self.server.postreq_busy = False
            return 3

        # if the inbox queue is full then return a busy code
        if debug:
            print('INBOX: checking for full queue')
        if len(self.server.inbox_queue) >= self.server.max_queue_length:
            if message_domain:
                print('INBOX: Queue: ' +
                      'Inbox queue is full. Incoming post from ' +
                      message_json['actor'])
            else:
                print('INBOX: Queue: Inbox queue is full')
            self._503()
            clear_queue_items(self.server.base_dir, self.server.inbox_queue)
            if not self.server.restart_inbox_queue_in_progress:
                self.server.restart_inbox_queue = True
            self.server.postreq_busy = False
            return 2

        # Convert the headers needed for signature verification to dict
        headers_dict = {}
        headers_dict['host'] = self.headers['host']
        headers_dict['signature'] = self.headers['signature']
        if self.headers.get('Date'):
            headers_dict['Date'] = self.headers['Date']
        elif self.headers.get('date'):
            headers_dict['Date'] = self.headers['date']
        if self.headers.get('digest'):
            headers_dict['digest'] = self.headers['digest']
        if self.headers.get('Collection-Synchronization'):
            headers_dict['Collection-Synchronization'] = \
                self.headers['Collection-Synchronization']
        if self.headers.get('Content-type'):
            headers_dict['Content-type'] = self.headers['Content-type']
        if self.headers.get('Content-Length'):
            headers_dict['Content-Length'] = self.headers['Content-Length']
        elif self.headers.get('content-length'):
            headers_dict['content-length'] = self.headers['content-length']

        original_message_json = message_json.copy()

        # whether to add a 'to' field to the message
        add_to_field_types = (
            'Follow', 'Like', 'EmojiReact', 'Add', 'Remove', 'Ignore'
        )
        for add_to_type in add_to_field_types:
            message_json, _ = \
                add_to_field(add_to_type, message_json, self.server.debug)

        begin_save_time = time.time()
        # save the json for later queue processing
        message_bytes_decoded = message_bytes.decode('utf-8')

        if debug:
            print('INBOX: checking for invalid links')
        if contains_invalid_local_links(message_bytes_decoded):
            print('INBOX: post contains invalid local links ' +
                  str(original_message_json))
            return 5

        self.server.blocked_cache_last_updated = \
            update_blocked_cache(self.server.base_dir,
                                 self.server.blocked_cache,
                                 self.server.blocked_cache_last_updated,
                                 self.server.blocked_cache_update_secs)

        mitm = self._detect_mitm()

        if debug:
            print('INBOX: saving post to queue')
        queue_filename = \
            save_post_to_inbox_queue(self.server.base_dir,
                                     self.server.http_prefix,
                                     nickname,
                                     self.server.domain_full,
                                     message_json, original_message_json,
                                     message_bytes_decoded,
                                     headers_dict,
                                     self.path,
                                     self.server.debug,
                                     self.server.blocked_cache,
                                     self.server.system_language,
                                     mitm)
        if queue_filename:
            # add json to the queue
            if queue_filename not in self.server.inbox_queue:
                self.server.inbox_queue.append(queue_filename)
            if self.server.debug:
                time_diff = int((time.time() - begin_save_time) * 1000)
                if time_diff > 200:
                    print('SLOW: slow save of inbox queue item ' +
                          queue_filename + ' took ' + str(time_diff) + ' mS')
            self.send_response(201)
            self.end_headers()
            self.server.postreq_busy = False
            return 0
        self._503()
        self.server.postreq_busy = False
        return 1

    def _is_authorized(self) -> bool:
        self.authorized_nickname = None

        not_auth_paths = (
            '/icons/', '/avatars/', '/favicons/',
            '/system/accounts/avatars/',
            '/system/accounts/headers/',
            '/system/media_attachments/files/',
            '/accounts/avatars/', '/accounts/headers/',
            '/favicon.ico', '/newswire.xml',
            '/newswire_favicon.ico', '/categories.xml'
        )
        for not_auth_str in not_auth_paths:
            if self.path.startswith(not_auth_str):
                return False

        # token based authenticated used by the web interface
        if self.headers.get('Cookie'):
            if self.headers['Cookie'].startswith('epicyon='):
                token_str = self.headers['Cookie'].split('=', 1)[1].strip()
                if ';' in token_str:
                    token_str = token_str.split(';')[0].strip()
                if self.server.tokens_lookup.get(token_str):
                    nickname = self.server.tokens_lookup[token_str]
                    if not is_system_account(nickname):
                        self.authorized_nickname = nickname
                        # default to the inbox of the person
                        if self.path == '/':
                            self.path = '/users/' + nickname + '/inbox'
                        # check that the path contains the same nickname
                        # as the cookie otherwise it would be possible
                        # to be authorized to use an account you don't own
                        if '/' + nickname + '/' in self.path:
                            return True
                        if '/' + nickname + '?' in self.path:
                            return True
                        if self.path.endswith('/' + nickname):
                            return True
                        if self.server.debug:
                            print('AUTH: nickname ' + nickname +
                                  ' was not found in path ' + self.path)
                    return False
                print('AUTH: epicyon cookie ' +
                      'authorization failed, header=' +
                      self.headers['Cookie'].replace('epicyon=', '') +
                      ' token_str=' + token_str)
                return False
            print('AUTH: Header cookie was not authorized')
            return False
        # basic auth for c2s
        if self.headers.get('Authorization'):
            if authorize(self.server.base_dir, self.path,
                         self.headers['Authorization'],
                         self.server.debug):
                return True
            print('AUTH: C2S Basic auth did not authorize ' +
                  self.headers['Authorization'])
        return False

    def _clear_login_details(self, nickname: str, calling_domain: str) -> None:
        """Clears login details for the given account
        """
        # remove any token
        if self.server.tokens.get(nickname):
            del self.server.tokens_lookup[self.server.tokens[nickname]]
            del self.server.tokens[nickname]
        self._redirect_headers(self.server.http_prefix + '://' +
                               self.server.domain_full + '/login',
                               'epicyon=; SameSite=Strict',
                               calling_domain)

    def _post_login_screen(self, calling_domain: str, cookie: str,
                           base_dir: str, http_prefix: str,
                           domain: str, domain_full: str, port: int,
                           onion_domain: str, i2p_domain: str,
                           ua_str: str, debug: bool) -> None:
        """POST to login screen, containing credentials
        """
        # ensure that there is a minimum delay between failed login
        # attempts, to mitigate brute force
        if int(time.time()) - self.server.last_login_failure < 5:
            self._503()
            self.server.postreq_busy = False
            return

        # get the contents of POST containing login credentials
        length = int(self.headers['Content-length'])
        if length > 512:
            print('Login failed - credentials too long')
            self._401('Credentials are too long')
            self.server.postreq_busy = False
            return

        try:
            login_params = self.rfile.read(length).decode('utf-8')
        except SocketError as ex:
            if ex.errno == errno.ECONNRESET:
                print('EX: POST login read ' +
                      'connection reset by peer')
            else:
                print('EX: POST login read socket error')
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        except ValueError as ex:
            print('EX: POST login read failed, ' + str(ex))
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return

        login_nickname, login_password, register = \
            html_get_login_credentials(login_params,
                                       self.server.last_login_time,
                                       domain)
        if login_nickname and login_password:
            if is_system_account(login_nickname):
                print('Invalid username login: ' + login_nickname +
                      ' (system account)')
                self._clear_login_details(login_nickname, calling_domain)
                self.server.postreq_busy = False
                return
            self.server.last_login_time = int(time.time())
            if register:
                if not valid_password(login_password):
                    self.server.postreq_busy = False
                    if calling_domain.endswith('.onion') and onion_domain:
                        self._redirect_headers('http://' + onion_domain +
                                               '/login', cookie,
                                               calling_domain)
                    elif (calling_domain.endswith('.i2p') and i2p_domain):
                        self._redirect_headers('http://' + i2p_domain +
                                               '/login', cookie,
                                               calling_domain)
                    else:
                        self._redirect_headers(http_prefix + '://' +
                                               domain_full + '/login',
                                               cookie, calling_domain)
                    return

                if not register_account(base_dir, http_prefix, domain, port,
                                        login_nickname, login_password,
                                        self.server.manual_follower_approval):
                    self.server.postreq_busy = False
                    if calling_domain.endswith('.onion') and onion_domain:
                        self._redirect_headers('http://' + onion_domain +
                                               '/login', cookie,
                                               calling_domain)
                    elif (calling_domain.endswith('.i2p') and i2p_domain):
                        self._redirect_headers('http://' + i2p_domain +
                                               '/login', cookie,
                                               calling_domain)
                    else:
                        self._redirect_headers(http_prefix + '://' +
                                               domain_full + '/login',
                                               cookie, calling_domain)
                    return
            auth_header = \
                create_basic_auth_header(login_nickname, login_password)
            if self.headers.get('X-Forward-For'):
                ip_address = self.headers['X-Forward-For']
            elif self.headers.get('X-Forwarded-For'):
                ip_address = self.headers['X-Forwarded-For']
            else:
                ip_address = self.client_address[0]
            if not domain.endswith('.onion'):
                if not is_local_network_address(ip_address):
                    print('Login attempt from IP: ' + str(ip_address))
            if not authorize_basic(base_dir, '/users/' +
                                   login_nickname + '/outbox',
                                   auth_header, False):
                print('Login failed: ' + login_nickname)
                self._clear_login_details(login_nickname, calling_domain)
                fail_time = int(time.time())
                self.server.last_login_failure = fail_time
                if not domain.endswith('.onion'):
                    if not is_local_network_address(ip_address):
                        record_login_failure(base_dir, ip_address,
                                             self.server.login_failure_count,
                                             fail_time,
                                             self.server.log_login_failures)
                self.server.postreq_busy = False
                return
            else:
                if self.server.login_failure_count.get(ip_address):
                    del self.server.login_failure_count[ip_address]
                if is_suspended(base_dir, login_nickname):
                    msg = \
                        html_suspended(base_dir).encode('utf-8')
                    msglen = len(msg)
                    self._login_headers('text/html',
                                        msglen, calling_domain)
                    self._write(msg)
                    self.server.postreq_busy = False
                    return
                # login success - redirect with authorization
                print('====== Login success: ' + login_nickname +
                      ' ' + ua_str)
                # re-activate account if needed
                activate_account(base_dir, login_nickname, domain)
                # This produces a deterministic token based
                # on nick+password+salt
                salt_filename = \
                    acct_dir(base_dir, login_nickname, domain) + '/.salt'
                salt = create_password(32)
                if os.path.isfile(salt_filename):
                    try:
                        with open(salt_filename, 'r',
                                  encoding='utf-8') as fp_salt:
                            salt = fp_salt.read()
                    except OSError as ex:
                        print('EX: Unable to read salt for ' +
                              login_nickname + ' ' + str(ex))
                else:
                    try:
                        with open(salt_filename, 'w+',
                                  encoding='utf-8') as fp_salt:
                            fp_salt.write(salt)
                    except OSError as ex:
                        print('EX: Unable to save salt for ' +
                              login_nickname + ' ' + str(ex))

                token_text = login_nickname + login_password + salt
                token = sha256(token_text.encode('utf-8')).hexdigest()
                self.server.tokens[login_nickname] = token
                login_handle = login_nickname + '@' + domain
                token_filename = \
                    base_dir + '/accounts/' + \
                    login_handle + '/.token'
                try:
                    with open(token_filename, 'w+',
                              encoding='utf-8') as fp_tok:
                        fp_tok.write(token)
                except OSError as ex:
                    print('EX: Unable to save token for ' +
                          login_nickname + ' ' + str(ex))

                person_upgrade_actor(base_dir, None,
                                     base_dir + '/accounts/' +
                                     login_handle + '.json')

                index = self.server.tokens[login_nickname]
                self.server.tokens_lookup[index] = login_nickname
                cookie_str = 'SET:epicyon=' + \
                    self.server.tokens[login_nickname] + '; SameSite=Strict'
                if calling_domain.endswith('.onion') and onion_domain:
                    self._redirect_headers('http://' +
                                           onion_domain +
                                           '/users/' +
                                           login_nickname + '/' +
                                           self.server.default_timeline,
                                           cookie_str, calling_domain)
                elif (calling_domain.endswith('.i2p') and i2p_domain):
                    self._redirect_headers('http://' +
                                           i2p_domain +
                                           '/users/' +
                                           login_nickname + '/' +
                                           self.server.default_timeline,
                                           cookie_str, calling_domain)
                else:
                    self._redirect_headers(http_prefix + '://' +
                                           domain_full + '/users/' +
                                           login_nickname + '/' +
                                           self.server.default_timeline,
                                           cookie_str, calling_domain)
                self.server.postreq_busy = False
                return
        else:
            print('WARN: No login credentials presented to /login')
            if debug:
                # be careful to avoid logging the password
                login_str = login_params
                if '=' in login_params:
                    login_params_list = login_params.split('=')
                    login_str = ''
                    skip_param = False
                    for login_prm in login_params_list:
                        if not skip_param:
                            login_str += login_prm + '='
                        else:
                            len_str = login_prm.split('&')[0]
                            if len(len_str) > 0:
                                login_str += login_prm + '*'
                            len_str = ''
                            if '&' in login_prm:
                                login_str += \
                                    '&' + login_prm.split('&')[1] + '='
                        skip_param = False
                        if 'password' in login_prm:
                            skip_param = True
                    login_str = login_str[:len(login_str) - 1]
                print(login_str)
            self._401('No login credentials were posted')
            self.server.postreq_busy = False
        self._200()
        self.server.postreq_busy = False

    def _moderator_actions(self, path: str, calling_domain: str, cookie: str,
                           base_dir: str, http_prefix: str,
                           domain: str, port: int, debug: bool) -> None:
        """Actions on the moderator screen
        """
        users_path = path.replace('/moderationaction', '')
        nickname = users_path.replace('/users/', '')
        actor_str = self._get_instance_url(calling_domain) + users_path
        if not is_moderator(self.server.base_dir, nickname):
            self._redirect_headers(actor_str + '/moderation',
                                   cookie, calling_domain)
            self.server.postreq_busy = False
            return

        length = int(self.headers['Content-length'])

        try:
            moderation_params = self.rfile.read(length).decode('utf-8')
        except SocketError as ex:
            if ex.errno == errno.ECONNRESET:
                print('EX: POST moderation_params connection was reset')
            else:
                print('EX: POST moderation_params ' +
                      'rfile.read socket error')
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        except ValueError as ex:
            print('EX: POST moderation_params rfile.read failed, ' +
                  str(ex))
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return

        if '&' in moderation_params:
            moderation_text = None
            moderation_button = None
            # get the moderation text first
            act_str = 'moderationAction='
            for moderation_str in moderation_params.split('&'):
                if moderation_str.startswith(act_str):
                    if act_str in moderation_str:
                        moderation_text = \
                            moderation_str.split(act_str)[1].strip()
                        mod_text = moderation_text.replace('+', ' ')
                        moderation_text = \
                            urllib.parse.unquote_plus(mod_text.strip())
            # which button was pressed?
            for moderation_str in moderation_params.split('&'):
                if moderation_str.startswith('submitInfo='):
                    if not moderation_text and \
                       'submitInfo=' in moderation_str:
                        moderation_text = \
                            moderation_str.split('submitInfo=')[1].strip()
                        mod_text = moderation_text.replace('+', ' ')
                        moderation_text = \
                            urllib.parse.unquote_plus(mod_text.strip())
                    search_handle = moderation_text
                    if search_handle:
                        if '/@' in search_handle:
                            search_nickname = \
                                get_nickname_from_actor(search_handle)
                            if search_nickname:
                                search_domain, _ = \
                                    get_domain_from_actor(search_handle)
                                search_handle = \
                                    search_nickname + '@' + search_domain
                            else:
                                search_handle = ''
                        if '@' not in search_handle:
                            if search_handle.startswith('http') or \
                               search_handle.startswith('ipfs') or \
                               search_handle.startswith('ipns'):
                                search_nickname = \
                                    get_nickname_from_actor(search_handle)
                                if search_nickname:
                                    search_domain, _ = \
                                        get_domain_from_actor(search_handle)
                                    search_handle = \
                                        search_nickname + '@' + search_domain
                                else:
                                    search_handle = ''
                        if '@' not in search_handle:
                            # is this a local nickname on this instance?
                            local_handle = \
                                search_handle + '@' + self.server.domain
                            if os.path.isdir(self.server.base_dir +
                                             '/accounts/' + local_handle):
                                search_handle = local_handle
                            else:
                                search_handle = ''
                    if search_handle is None:
                        search_handle = ''
                    if '@' in search_handle:
                        msg = \
                            html_account_info(self.server.translate,
                                              base_dir, http_prefix,
                                              nickname,
                                              self.server.domain,
                                              self.server.port,
                                              search_handle,
                                              self.server.debug,
                                              self.server.system_language,
                                              self.server.signing_priv_key_pem)
                    else:
                        msg = \
                            html_moderation_info(self.server.translate,
                                                 base_dir, nickname,
                                                 self.server.domain,
                                                 self.server.theme_name,
                                                 self.server.access_keys)
                    if msg:
                        msg = msg.encode('utf-8')
                        msglen = len(msg)
                        self._login_headers('text/html',
                                            msglen, calling_domain)
                        self._write(msg)
                    self.server.postreq_busy = False
                    return
                if moderation_str.startswith('submitBlock'):
                    moderation_button = 'block'
                elif moderation_str.startswith('submitUnblock'):
                    moderation_button = 'unblock'
                elif moderation_str.startswith('submitFilter'):
                    moderation_button = 'filter'
                elif moderation_str.startswith('submitUnfilter'):
                    moderation_button = 'unfilter'
                elif moderation_str.startswith('submitSuspend'):
                    moderation_button = 'suspend'
                elif moderation_str.startswith('submitUnsuspend'):
                    moderation_button = 'unsuspend'
                elif moderation_str.startswith('submitRemove'):
                    moderation_button = 'remove'
            if moderation_button and moderation_text:
                if debug:
                    print('moderation_button: ' + moderation_button)
                    print('moderation_text: ' + moderation_text)
                nickname = moderation_text
                if nickname.startswith('http') or \
                   nickname.startswith('ipfs') or \
                   nickname.startswith('ipns') or \
                   nickname.startswith('hyper'):
                    nickname = get_nickname_from_actor(nickname)
                if '@' in nickname:
                    nickname = nickname.split('@')[0]
                if moderation_button == 'suspend':
                    suspend_account(base_dir, nickname, domain)
                if moderation_button == 'unsuspend':
                    reenable_account(base_dir, nickname)
                if moderation_button == 'filter':
                    add_global_filter(base_dir, moderation_text)
                if moderation_button == 'unfilter':
                    remove_global_filter(base_dir, moderation_text)
                if moderation_button == 'block':
                    full_block_domain = None
                    if moderation_text.startswith('http') or \
                       moderation_text.startswith('ipfs') or \
                       moderation_text.startswith('ipns') or \
                       moderation_text.startswith('hyper'):
                        # https://domain
                        block_domain, block_port = \
                            get_domain_from_actor(moderation_text)
                        full_block_domain = \
                            get_full_domain(block_domain, block_port)
                    if '@' in moderation_text:
                        # nick@domain or *@domain
                        full_block_domain = moderation_text.split('@')[1]
                    else:
                        # assume the text is a domain name
                        if not full_block_domain and '.' in moderation_text:
                            nickname = '*'
                            full_block_domain = moderation_text.strip()
                    if full_block_domain or nickname.startswith('#'):
                        add_global_block(base_dir, nickname, full_block_domain)
                if moderation_button == 'unblock':
                    full_block_domain = None
                    if moderation_text.startswith('http') or \
                       moderation_text.startswith('ipfs') or \
                       moderation_text.startswith('ipns') or \
                       moderation_text.startswith('hyper'):
                        # https://domain
                        block_domain, block_port = \
                            get_domain_from_actor(moderation_text)
                        full_block_domain = \
                            get_full_domain(block_domain, block_port)
                    if '@' in moderation_text:
                        # nick@domain or *@domain
                        full_block_domain = moderation_text.split('@')[1]
                    else:
                        # assume the text is a domain name
                        if not full_block_domain and '.' in moderation_text:
                            nickname = '*'
                            full_block_domain = moderation_text.strip()
                    if full_block_domain or nickname.startswith('#'):
                        remove_global_block(base_dir, nickname,
                                            full_block_domain)
                if moderation_button == 'remove':
                    if '/statuses/' not in moderation_text:
                        remove_account(base_dir, nickname, domain, port)
                    else:
                        # remove a post or thread
                        post_filename = \
                            locate_post(base_dir, nickname, domain,
                                        moderation_text)
                        if post_filename:
                            if can_remove_post(base_dir, domain, port,
                                               moderation_text):
                                delete_post(base_dir,
                                            http_prefix,
                                            nickname, domain,
                                            post_filename,
                                            debug,
                                            self.server.recent_posts_cache,
                                            True)
                        if nickname != 'news':
                            # if this is a local blog post then also remove it
                            # from the news actor
                            post_filename = \
                                locate_post(base_dir, 'news', domain,
                                            moderation_text)
                            if post_filename:
                                if can_remove_post(base_dir, domain, port,
                                                   moderation_text):
                                    delete_post(base_dir,
                                                http_prefix,
                                                'news', domain,
                                                post_filename,
                                                debug,
                                                self.server.recent_posts_cache,
                                                True)

        self._redirect_headers(actor_str + '/moderation',
                               cookie, calling_domain)
        self.server.postreq_busy = False
        return

    def _key_shortcuts(self, calling_domain: str, cookie: str,
                       base_dir: str, http_prefix: str, nickname: str,
                       domain: str, domain_full: str,
                       onion_domain: str, i2p_domain: str,
                       access_keys: {}, default_timeline: str) -> None:
        """Receive POST from webapp_accesskeys
        """
        users_path = '/users/' + nickname
        origin_path_str = \
            http_prefix + '://' + domain_full + users_path + '/' + \
            default_timeline
        length = int(self.headers['Content-length'])

        try:
            access_keys_params = self.rfile.read(length).decode('utf-8')
        except SocketError as ex:
            if ex.errno == errno.ECONNRESET:
                print('EX: POST access_keys_params ' +
                      'connection reset by peer')
            else:
                print('EX: POST access_keys_params socket error')
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        except ValueError as ex:
            print('EX: POST access_keys_params rfile.read failed, ' +
                  str(ex))
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        access_keys_params = \
            urllib.parse.unquote_plus(access_keys_params)

        # key shortcuts screen, back button
        # See html_access_keys
        if 'submitAccessKeysCancel=' in access_keys_params or \
           'submitAccessKeys=' not in access_keys_params:
            if calling_domain.endswith('.onion') and onion_domain:
                origin_path_str = \
                    'http://' + onion_domain + users_path + '/' + \
                    default_timeline
            elif calling_domain.endswith('.i2p') and i2p_domain:
                origin_path_str = \
                    'http://' + i2p_domain + users_path + \
                    '/' + default_timeline
            self._redirect_headers(origin_path_str, cookie, calling_domain)
            self.server.postreq_busy = False
            return

        save_keys = False
        access_keys_template = self.server.access_keys
        for variable_name, _ in access_keys_template.items():
            if not access_keys.get(variable_name):
                access_keys[variable_name] = \
                    access_keys_template[variable_name]

            variable_name2 = variable_name.replace(' ', '_')
            if variable_name2 + '=' in access_keys_params:
                new_key = access_keys_params.split(variable_name2 + '=')[1]
                if '&' in new_key:
                    new_key = new_key.split('&')[0]
                if new_key:
                    if len(new_key) > 1:
                        new_key = new_key[0]
                    if new_key != access_keys[variable_name]:
                        access_keys[variable_name] = new_key
                        save_keys = True

        if save_keys:
            access_keys_filename = \
                acct_dir(base_dir, nickname, domain) + '/access_keys.json'
            save_json(access_keys, access_keys_filename)
            if not self.server.key_shortcuts.get(nickname):
                self.server.key_shortcuts[nickname] = access_keys.copy()

        # redirect back from key shortcuts screen
        if calling_domain.endswith('.onion') and onion_domain:
            origin_path_str = \
                'http://' + onion_domain + users_path + '/' + default_timeline
        elif calling_domain.endswith('.i2p') and i2p_domain:
            origin_path_str = \
                'http://' + i2p_domain + users_path + '/' + default_timeline
        self._redirect_headers(origin_path_str, cookie, calling_domain)
        self.server.postreq_busy = False
        return

    def _theme_designer_edit(self, calling_domain: str, cookie: str,
                             base_dir: str, http_prefix: str, nickname: str,
                             domain: str, domain_full: str,
                             onion_domain: str, i2p_domain: str,
                             default_timeline: str, theme_name: str,
                             allow_local_network_access: bool,
                             system_language: str,
                             dyslexic_font: bool) -> None:
        """Receive POST from webapp_theme_designer
        """
        users_path = '/users/' + nickname
        origin_path_str = \
            http_prefix + '://' + domain_full + users_path + '/' + \
            default_timeline
        length = int(self.headers['Content-length'])

        try:
            theme_params = self.rfile.read(length).decode('utf-8')
        except SocketError as ex:
            if ex.errno == errno.ECONNRESET:
                print('EX: POST theme_params ' +
                      'connection reset by peer')
            else:
                print('EX: POST theme_params socket error')
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        except ValueError as ex:
            print('EX: POST theme_params rfile.read failed, ' + str(ex))
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        theme_params = \
            urllib.parse.unquote_plus(theme_params)

        # theme designer screen, reset button
        # See html_theme_designer
        if 'submitThemeDesignerReset=' in theme_params or \
           'submitThemeDesigner=' not in theme_params:
            if 'submitThemeDesignerReset=' in theme_params:
                reset_theme_designer_settings(base_dir)
                self.server.css_cache = {}
                set_theme(base_dir, theme_name, domain,
                          allow_local_network_access, system_language,
                          dyslexic_font, True)

            if calling_domain.endswith('.onion') and onion_domain:
                origin_path_str = \
                    'http://' + onion_domain + users_path + '/' + \
                    default_timeline
            elif calling_domain.endswith('.i2p') and i2p_domain:
                origin_path_str = \
                    'http://' + i2p_domain + users_path + \
                    '/' + default_timeline
            self._redirect_headers(origin_path_str, cookie, calling_domain)
            self.server.postreq_busy = False
            return

        fields = {}
        fields_list = theme_params.split('&')
        for field_str in fields_list:
            if '=' not in field_str:
                continue
            field_value = field_str.split('=')[1].strip()
            if not field_value:
                continue
            if field_value == 'on':
                field_value = 'True'
            fields_index = field_str.split('=')[0]
            fields[fields_index] = field_value

        # Check for boolean values which are False.
        # These don't come through via theme_params,
        # so need to be checked separately
        theme_filename = base_dir + '/theme/' + theme_name + '/theme.json'
        theme_json = load_json(theme_filename)
        if theme_json:
            for variable_name, value in theme_json.items():
                variable_name = 'themeSetting_' + variable_name
                if value.lower() == 'false' or value.lower() == 'true':
                    if variable_name not in fields:
                        fields[variable_name] = 'False'

        # get the parameters from the theme designer screen
        theme_designer_params = {}
        for variable_name, key in fields.items():
            if variable_name.startswith('themeSetting_'):
                variable_name = variable_name.replace('themeSetting_', '')
                theme_designer_params[variable_name] = key

        self.server.css_cache = {}
        set_theme_from_designer(base_dir, theme_name, domain,
                                theme_designer_params,
                                allow_local_network_access,
                                system_language, dyslexic_font)

        # set boolean values
        if 'rss-icon-at-top' in theme_designer_params:
            if theme_designer_params['rss-icon-at-top'].lower() == 'true':
                self.server.rss_icon_at_top = True
            else:
                self.server.rss_icon_at_top = False
        if 'publish-button-at-top' in theme_designer_params:
            publish_button_at_top_str = \
                theme_designer_params['publish-button-at-top'].lower()
            if publish_button_at_top_str == 'true':
                self.server.publish_button_at_top = True
            else:
                self.server.publish_button_at_top = False
        if 'newswire-publish-icon' in theme_designer_params:
            newswire_publish_icon_str = \
                theme_designer_params['newswire-publish-icon'].lower()
            if newswire_publish_icon_str == 'true':
                self.server.show_publish_as_icon = True
            else:
                self.server.show_publish_as_icon = False
        if 'icons-as-buttons' in theme_designer_params:
            if theme_designer_params['icons-as-buttons'].lower() == 'true':
                self.server.icons_as_buttons = True
            else:
                self.server.icons_as_buttons = False
        if 'full-width-timeline-buttons' in theme_designer_params:
            theme_value = theme_designer_params['full-width-timeline-buttons']
            if theme_value.lower() == 'true':
                self.server.full_width_tl_button_header = True
            else:
                self.server.full_width_tl_button_header = False

        # redirect back from theme designer screen
        if calling_domain.endswith('.onion') and onion_domain:
            origin_path_str = \
                'http://' + onion_domain + users_path + '/' + default_timeline
        elif calling_domain.endswith('.i2p') and i2p_domain:
            origin_path_str = \
                'http://' + i2p_domain + users_path + '/' + default_timeline
        self._redirect_headers(origin_path_str, cookie, calling_domain)
        self.server.postreq_busy = False
        return

    def _person_options(self, path: str,
                        calling_domain: str, cookie: str,
                        base_dir: str, http_prefix: str,
                        domain: str, domain_full: str, port: int,
                        onion_domain: str, i2p_domain: str,
                        debug: bool, curr_session) -> None:
        """Receive POST from person options screen
        """
        page_number = 1
        users_path = path.split('/personoptions')[0]
        origin_path_str = http_prefix + '://' + domain_full + users_path

        chooser_nickname = get_nickname_from_actor(origin_path_str)
        if not chooser_nickname:
            if calling_domain.endswith('.onion') and onion_domain:
                origin_path_str = 'http://' + onion_domain + users_path
            elif (calling_domain.endswith('.i2p') and i2p_domain):
                origin_path_str = 'http://' + i2p_domain + users_path
            print('WARN: unable to find nickname in ' + origin_path_str)
            self._redirect_headers(origin_path_str, cookie, calling_domain)
            self.server.postreq_busy = False
            return

        length = int(self.headers['Content-length'])

        try:
            options_confirm_params = self.rfile.read(length).decode('utf-8')
        except SocketError as ex:
            if ex.errno == errno.ECONNRESET:
                print('EX: POST options_confirm_params ' +
                      'connection reset by peer')
            else:
                print('EX: POST options_confirm_params socket error')
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        except ValueError as ex:
            print('EX: ' +
                  'POST options_confirm_params rfile.read failed, ' + str(ex))
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        options_confirm_params = \
            urllib.parse.unquote_plus(options_confirm_params)

        # page number to return to
        if 'pageNumber=' in options_confirm_params:
            page_number_str = options_confirm_params.split('pageNumber=')[1]
            if '&' in page_number_str:
                page_number_str = page_number_str.split('&')[0]
            if len(page_number_str) < 5:
                if page_number_str.isdigit():
                    page_number = int(page_number_str)

        # actor for the person
        options_actor = options_confirm_params.split('actor=')[1]
        if '&' in options_actor:
            options_actor = options_actor.split('&')[0]

        # url of the avatar
        options_avatar_url = options_confirm_params.split('avatarUrl=')[1]
        if '&' in options_avatar_url:
            options_avatar_url = options_avatar_url.split('&')[0]

        # link to a post, which can then be included in reports
        post_url = None
        if 'postUrl' in options_confirm_params:
            post_url = options_confirm_params.split('postUrl=')[1]
            if '&' in post_url:
                post_url = post_url.split('&')[0]

        # petname for this person
        petname = None
        if 'optionpetname' in options_confirm_params:
            petname = options_confirm_params.split('optionpetname=')[1]
            if '&' in petname:
                petname = petname.split('&')[0]
            # Limit the length of the petname
            if len(petname) > 20 or \
               ' ' in petname or '/' in petname or \
               '?' in petname or '#' in petname:
                petname = None

        # notes about this person
        person_notes = None
        if 'optionnotes' in options_confirm_params:
            person_notes = options_confirm_params.split('optionnotes=')[1]
            if '&' in person_notes:
                person_notes = person_notes.split('&')[0]
            person_notes = urllib.parse.unquote_plus(person_notes.strip())
            # Limit the length of the notes
            if len(person_notes) > 64000:
                person_notes = None

        # get the nickname
        options_nickname = get_nickname_from_actor(options_actor)
        if not options_nickname:
            if calling_domain.endswith('.onion') and onion_domain:
                origin_path_str = 'http://' + onion_domain + users_path
            elif (calling_domain.endswith('.i2p') and i2p_domain):
                origin_path_str = 'http://' + i2p_domain + users_path
            print('WARN: unable to find nickname in ' + options_actor)
            self._redirect_headers(origin_path_str, cookie, calling_domain)
            self.server.postreq_busy = False
            return

        options_domain, options_port = get_domain_from_actor(options_actor)
        options_domain_full = get_full_domain(options_domain, options_port)
        if chooser_nickname == options_nickname and \
           options_domain == domain and \
           options_port == port:
            if debug:
                print('You cannot perform an option action on yourself')

        # person options screen, view button
        # See html_person_options
        if '&submitView=' in options_confirm_params:
            if debug:
                print('Viewing ' + options_actor)
            self._redirect_headers(options_actor,
                                   cookie, calling_domain)
            self.server.postreq_busy = False
            return

        # person options screen, petname submit button
        # See html_person_options
        if '&submitPetname=' in options_confirm_params and petname:
            if debug:
                print('Change petname to ' + petname)
            handle = options_nickname + '@' + options_domain_full
            set_pet_name(base_dir,
                         chooser_nickname,
                         domain,
                         handle, petname)
            users_path_str = \
                users_path + '/' + self.server.default_timeline + \
                '?page=' + str(page_number)
            self._redirect_headers(users_path_str, cookie,
                                   calling_domain)
            self.server.postreq_busy = False
            return

        # person options screen, person notes submit button
        # See html_person_options
        if '&submitPersonNotes=' in options_confirm_params:
            if debug:
                print('Change person notes')
            handle = options_nickname + '@' + options_domain_full
            if not person_notes:
                person_notes = ''
            set_person_notes(base_dir,
                             chooser_nickname,
                             domain,
                             handle, person_notes)
            users_path_str = \
                users_path + '/' + self.server.default_timeline + \
                '?page=' + str(page_number)
            self._redirect_headers(users_path_str, cookie,
                                   calling_domain)
            self.server.postreq_busy = False
            return

        # person options screen, on calendar checkbox
        # See html_person_options
        if '&submitOnCalendar=' in options_confirm_params:
            on_calendar = None
            if 'onCalendar=' in options_confirm_params:
                on_calendar = options_confirm_params.split('onCalendar=')[1]
                if '&' in on_calendar:
                    on_calendar = on_calendar.split('&')[0]
            if on_calendar == 'on':
                add_person_to_calendar(base_dir,
                                       chooser_nickname,
                                       domain,
                                       options_nickname,
                                       options_domain_full)
            else:
                remove_person_from_calendar(base_dir,
                                            chooser_nickname,
                                            domain,
                                            options_nickname,
                                            options_domain_full)
            users_path_str = \
                users_path + '/' + self.server.default_timeline + \
                '?page=' + str(page_number)
            self._redirect_headers(users_path_str, cookie,
                                   calling_domain)
            self.server.postreq_busy = False
            return

        # person options screen, minimize images checkbox
        # See html_person_options
        if '&submitMinimizeImages=' in options_confirm_params:
            minimize_images = None
            if 'minimizeImages=' in options_confirm_params:
                minimize_images = \
                    options_confirm_params.split('minimizeImages=')[1]
                if '&' in minimize_images:
                    minimize_images = minimize_images.split('&')[0]
            if minimize_images == 'on':
                person_minimize_images(base_dir,
                                       chooser_nickname,
                                       domain,
                                       options_nickname,
                                       options_domain_full)
            else:
                person_undo_minimize_images(base_dir,
                                            chooser_nickname,
                                            domain,
                                            options_nickname,
                                            options_domain_full)
            users_path_str = \
                users_path + '/' + self.server.default_timeline + \
                '?page=' + str(page_number)
            self._redirect_headers(users_path_str, cookie,
                                   calling_domain)
            self.server.postreq_busy = False
            return

        # person options screen, allow announces checkbox
        # See html_person_options
        if '&submitAllowAnnounce=' in options_confirm_params:
            allow_announce = None
            if 'allowAnnounce=' in options_confirm_params:
                allow_announce = \
                    options_confirm_params.split('allowAnnounce=')[1]
                if '&' in allow_announce:
                    allow_announce = allow_announce.split('&')[0]
            if allow_announce == 'on':
                allowed_announce_add(base_dir,
                                     chooser_nickname,
                                     domain,
                                     options_nickname,
                                     options_domain_full)
            else:
                allowed_announce_remove(base_dir,
                                        chooser_nickname,
                                        domain,
                                        options_nickname,
                                        options_domain_full)
            users_path_str = \
                users_path + '/' + self.server.default_timeline + \
                '?page=' + str(page_number)
            self._redirect_headers(users_path_str, cookie,
                                   calling_domain)
            self.server.postreq_busy = False
            return

        # person options screen, on notify checkbox
        # See html_person_options
        if '&submitNotifyOnPost=' in options_confirm_params:
            notify = None
            if 'notifyOnPost=' in options_confirm_params:
                notify = options_confirm_params.split('notifyOnPost=')[1]
                if '&' in notify:
                    notify = notify.split('&')[0]
            if notify == 'on':
                add_notify_on_post(base_dir,
                                   chooser_nickname,
                                   domain,
                                   options_nickname,
                                   options_domain_full)
            else:
                remove_notify_on_post(base_dir,
                                      chooser_nickname,
                                      domain,
                                      options_nickname,
                                      options_domain_full)
            users_path_str = \
                users_path + '/' + self.server.default_timeline + \
                '?page=' + str(page_number)
            self._redirect_headers(users_path_str, cookie,
                                   calling_domain)
            self.server.postreq_busy = False
            return

        # person options screen, permission to post to newswire
        # See html_person_options
        if '&submitPostToNews=' in options_confirm_params:
            admin_nickname = get_config_param(self.server.base_dir, 'admin')
            if (chooser_nickname != options_nickname and
                (chooser_nickname == admin_nickname or
                 (is_moderator(self.server.base_dir, chooser_nickname) and
                  not is_moderator(self.server.base_dir, options_nickname)))):
                posts_to_news = None
                if 'postsToNews=' in options_confirm_params:
                    posts_to_news = \
                        options_confirm_params.split('postsToNews=')[1]
                    if '&' in posts_to_news:
                        posts_to_news = posts_to_news.split('&')[0]
                account_dir = acct_dir(self.server.base_dir,
                                       options_nickname, options_domain)
                newswire_blocked_filename = account_dir + '/.nonewswire'
                if posts_to_news == 'on':
                    if os.path.isfile(newswire_blocked_filename):
                        try:
                            os.remove(newswire_blocked_filename)
                        except OSError:
                            print('EX: _person_options unable to delete ' +
                                  newswire_blocked_filename)
                        refresh_newswire(self.server.base_dir)
                else:
                    if os.path.isdir(account_dir):
                        nw_filename = newswire_blocked_filename
                        nw_written = False
                        try:
                            with open(nw_filename, 'w+',
                                      encoding='utf-8') as nofile:
                                nofile.write('\n')
                                nw_written = True
                        except OSError as ex:
                            print('EX: unable to write ' + nw_filename +
                                  ' ' + str(ex))
                        if nw_written:
                            refresh_newswire(self.server.base_dir)
            users_path_str = \
                users_path + '/' + self.server.default_timeline + \
                '?page=' + str(page_number)
            self._redirect_headers(users_path_str, cookie,
                                   calling_domain)
            self.server.postreq_busy = False
            return

        # person options screen, permission to post to featured articles
        # See html_person_options
        if '&submitPostToFeatures=' in options_confirm_params:
            admin_nickname = get_config_param(self.server.base_dir, 'admin')
            if (chooser_nickname != options_nickname and
                (chooser_nickname == admin_nickname or
                 (is_moderator(self.server.base_dir, chooser_nickname) and
                  not is_moderator(self.server.base_dir, options_nickname)))):
                posts_to_features = None
                if 'postsToFeatures=' in options_confirm_params:
                    posts_to_features = \
                        options_confirm_params.split('postsToFeatures=')[1]
                    if '&' in posts_to_features:
                        posts_to_features = posts_to_features.split('&')[0]
                account_dir = acct_dir(self.server.base_dir,
                                       options_nickname, options_domain)
                features_blocked_filename = account_dir + '/.nofeatures'
                if posts_to_features == 'on':
                    if os.path.isfile(features_blocked_filename):
                        try:
                            os.remove(features_blocked_filename)
                        except OSError:
                            print('EX: _person_options unable to delete ' +
                                  features_blocked_filename)
                        refresh_newswire(self.server.base_dir)
                else:
                    if os.path.isdir(account_dir):
                        feat_filename = features_blocked_filename
                        feat_written = False
                        try:
                            with open(feat_filename, 'w+',
                                      encoding='utf-8') as nofile:
                                nofile.write('\n')
                                feat_written = True
                        except OSError as ex:
                            print('EX: unable to write ' + feat_filename +
                                  ' ' + str(ex))
                        if feat_written:
                            refresh_newswire(self.server.base_dir)
            users_path_str = \
                users_path + '/' + self.server.default_timeline + \
                '?page=' + str(page_number)
            self._redirect_headers(users_path_str, cookie,
                                   calling_domain)
            self.server.postreq_busy = False
            return

        # person options screen, permission to post to newswire
        # See html_person_options
        if '&submitModNewsPosts=' in options_confirm_params:
            admin_nickname = get_config_param(self.server.base_dir, 'admin')
            if (chooser_nickname != options_nickname and
                (chooser_nickname == admin_nickname or
                 (is_moderator(self.server.base_dir, chooser_nickname) and
                  not is_moderator(self.server.base_dir, options_nickname)))):
                mod_posts_to_news = None
                if 'modNewsPosts=' in options_confirm_params:
                    mod_posts_to_news = \
                        options_confirm_params.split('modNewsPosts=')[1]
                    if '&' in mod_posts_to_news:
                        mod_posts_to_news = mod_posts_to_news.split('&')[0]
                account_dir = acct_dir(self.server.base_dir,
                                       options_nickname, options_domain)
                newswire_mod_filename = account_dir + '/.newswiremoderated'
                if mod_posts_to_news != 'on':
                    if os.path.isfile(newswire_mod_filename):
                        try:
                            os.remove(newswire_mod_filename)
                        except OSError:
                            print('EX: _person_options unable to delete ' +
                                  newswire_mod_filename)
                else:
                    if os.path.isdir(account_dir):
                        nw_filename = newswire_mod_filename
                        try:
                            with open(nw_filename, 'w+',
                                      encoding='utf-8') as modfile:
                                modfile.write('\n')
                        except OSError:
                            print('EX: unable to write ' + nw_filename)
            users_path_str = \
                users_path + '/' + self.server.default_timeline + \
                '?page=' + str(page_number)
            self._redirect_headers(users_path_str, cookie,
                                   calling_domain)
            self.server.postreq_busy = False
            return

        # person options screen, block button
        # See html_person_options
        if '&submitBlock=' in options_confirm_params:
            if debug:
                print('Blocking ' + options_actor)
            msg = \
                html_confirm_block(self.server.translate,
                                   base_dir,
                                   users_path,
                                   options_actor,
                                   options_avatar_url).encode('utf-8')
            msglen = len(msg)
            self._set_headers('text/html', msglen,
                              cookie, calling_domain, False)
            self._write(msg)
            self.server.postreq_busy = False
            return

        # person options screen, unblock button
        # See html_person_options
        if '&submitUnblock=' in options_confirm_params:
            if debug:
                print('Unblocking ' + options_actor)
            msg = \
                html_confirm_unblock(self.server.translate,
                                     base_dir,
                                     users_path,
                                     options_actor,
                                     options_avatar_url).encode('utf-8')
            msglen = len(msg)
            self._set_headers('text/html', msglen,
                              cookie, calling_domain, False)
            self._write(msg)
            self.server.postreq_busy = False
            return

        # person options screen, follow button
        # See html_person_options followStr
        if '&submitFollow=' in options_confirm_params or \
           '&submitJoin=' in options_confirm_params:
            if debug:
                print('Following ' + options_actor)
            msg = \
                html_confirm_follow(self.server.translate,
                                    base_dir,
                                    users_path,
                                    options_actor,
                                    options_avatar_url).encode('utf-8')
            msglen = len(msg)
            self._set_headers('text/html', msglen,
                              cookie, calling_domain, False)
            self._write(msg)
            self.server.postreq_busy = False
            return

        # person options screen, unfollow button
        # See html_person_options followStr
        if '&submitUnfollow=' in options_confirm_params or \
           '&submitLeave=' in options_confirm_params:
            print('Unfollowing ' + options_actor)
            msg = \
                html_confirm_unfollow(self.server.translate,
                                      base_dir,
                                      users_path,
                                      options_actor,
                                      options_avatar_url).encode('utf-8')
            msglen = len(msg)
            self._set_headers('text/html', msglen,
                              cookie, calling_domain, False)
            self._write(msg)
            self.server.postreq_busy = False
            return

        # person options screen, DM button
        # See html_person_options
        if '&submitDM=' in options_confirm_params:
            if debug:
                print('Sending DM to ' + options_actor)
            report_path = path.replace('/personoptions', '') + '/newdm'

            access_keys = self.server.access_keys
            if '/users/' in path:
                nickname = path.split('/users/')[1]
                if '/' in nickname:
                    nickname = nickname.split('/')[0]
                if self.server.key_shortcuts.get(nickname):
                    access_keys = self.server.key_shortcuts[nickname]

            custom_submit_text = get_config_param(base_dir, 'customSubmitText')
            conversation_id = None
            reply_is_chat = False

            bold_reading = False
            if self.server.bold_reading.get(chooser_nickname):
                bold_reading = True

            languages_understood = \
                get_understood_languages(base_dir,
                                         http_prefix,
                                         chooser_nickname,
                                         self.server.domain_full,
                                         self.server.person_cache)

            msg = \
                html_new_post({}, False, self.server.translate,
                              base_dir,
                              http_prefix,
                              report_path, None,
                              [options_actor], None, None,
                              page_number, '',
                              chooser_nickname,
                              domain,
                              domain_full,
                              self.server.default_timeline,
                              self.server.newswire,
                              self.server.theme_name,
                              True, access_keys,
                              custom_submit_text,
                              conversation_id,
                              self.server.recent_posts_cache,
                              self.server.max_recent_posts,
                              curr_session,
                              self.server.cached_webfingers,
                              self.server.person_cache,
                              self.server.port,
                              None,
                              self.server.project_version,
                              self.server.yt_replace_domain,
                              self.server.twitter_replacement_domain,
                              self.server.show_published_date_only,
                              self.server.peertube_instances,
                              self.server.allow_local_network_access,
                              self.server.system_language,
                              languages_understood,
                              self.server.max_like_count,
                              self.server.signing_priv_key_pem,
                              self.server.cw_lists,
                              self.server.lists_enabled,
                              self.server.default_timeline,
                              reply_is_chat,
                              bold_reading,
                              self.server.dogwhistles,
                              self.server.min_images_for_accounts)
            if msg:
                msg = msg.encode('utf-8')
                msglen = len(msg)
                self._set_headers('text/html', msglen,
                                  cookie, calling_domain, False)
                self._write(msg)
            self.server.postreq_busy = False
            return

        # person options screen, Info button
        # See html_person_options
        if '&submitPersonInfo=' in options_confirm_params:
            if is_moderator(self.server.base_dir, chooser_nickname):
                if debug:
                    print('Showing info for ' + options_actor)
                signing_priv_key_pem = self.server.signing_priv_key_pem
                msg = \
                    html_account_info(self.server.translate,
                                      base_dir,
                                      http_prefix,
                                      chooser_nickname,
                                      domain,
                                      self.server.port,
                                      options_actor,
                                      self.server.debug,
                                      self.server.system_language,
                                      signing_priv_key_pem)
                if msg:
                    msg = msg.encode('utf-8')
                    msglen = len(msg)
                    self._set_headers('text/html', msglen,
                                      cookie, calling_domain, False)
                    self._write(msg)
                self.server.postreq_busy = False
                return
            self._404()
            return

        # person options screen, snooze button
        # See html_person_options
        if '&submitSnooze=' in options_confirm_params:
            users_path = path.split('/personoptions')[0]
            this_actor = http_prefix + '://' + domain_full + users_path
            if debug:
                print('Snoozing ' + options_actor + ' ' + this_actor)
            if '/users/' in this_actor:
                nickname = this_actor.split('/users/')[1]
                person_snooze(base_dir, nickname,
                              domain, options_actor)
                if calling_domain.endswith('.onion') and onion_domain:
                    this_actor = 'http://' + onion_domain + users_path
                elif (calling_domain.endswith('.i2p') and i2p_domain):
                    this_actor = 'http://' + i2p_domain + users_path
                actor_path_str = \
                    this_actor + '/' + self.server.default_timeline + \
                    '?page=' + str(page_number)
                self._redirect_headers(actor_path_str, cookie,
                                       calling_domain)
                self.server.postreq_busy = False
                return

        # person options screen, unsnooze button
        # See html_person_options
        if '&submitUnSnooze=' in options_confirm_params:
            users_path = path.split('/personoptions')[0]
            this_actor = http_prefix + '://' + domain_full + users_path
            if debug:
                print('Unsnoozing ' + options_actor + ' ' + this_actor)
            if '/users/' in this_actor:
                nickname = this_actor.split('/users/')[1]
                person_unsnooze(base_dir, nickname,
                                domain, options_actor)
                if calling_domain.endswith('.onion') and onion_domain:
                    this_actor = 'http://' + onion_domain + users_path
                elif (calling_domain.endswith('.i2p') and i2p_domain):
                    this_actor = 'http://' + i2p_domain + users_path
                actor_path_str = \
                    this_actor + '/' + self.server.default_timeline + \
                    '?page=' + str(page_number)
                self._redirect_headers(actor_path_str, cookie,
                                       calling_domain)
                self.server.postreq_busy = False
                return

        # person options screen, report button
        # See html_person_options
        if '&submitReport=' in options_confirm_params:
            if debug:
                print('Reporting ' + options_actor)
            report_path = \
                path.replace('/personoptions', '') + '/newreport'

            access_keys = self.server.access_keys
            if '/users/' in path:
                nickname = path.split('/users/')[1]
                if '/' in nickname:
                    nickname = nickname.split('/')[0]
                if self.server.key_shortcuts.get(nickname):
                    access_keys = self.server.key_shortcuts[nickname]

            custom_submit_text = get_config_param(base_dir, 'customSubmitText')
            conversation_id = None
            reply_is_chat = False

            bold_reading = False
            if self.server.bold_reading.get(chooser_nickname):
                bold_reading = True

            languages_understood = \
                get_understood_languages(base_dir,
                                         http_prefix,
                                         chooser_nickname,
                                         self.server.domain_full,
                                         self.server.person_cache)

            msg = \
                html_new_post({}, False, self.server.translate,
                              base_dir,
                              http_prefix,
                              report_path, None, [],
                              None, post_url, page_number, '',
                              chooser_nickname,
                              domain,
                              domain_full,
                              self.server.default_timeline,
                              self.server.newswire,
                              self.server.theme_name,
                              True, access_keys,
                              custom_submit_text,
                              conversation_id,
                              self.server.recent_posts_cache,
                              self.server.max_recent_posts,
                              curr_session,
                              self.server.cached_webfingers,
                              self.server.person_cache,
                              self.server.port,
                              None,
                              self.server.project_version,
                              self.server.yt_replace_domain,
                              self.server.twitter_replacement_domain,
                              self.server.show_published_date_only,
                              self.server.peertube_instances,
                              self.server.allow_local_network_access,
                              self.server.system_language,
                              languages_understood,
                              self.server.max_like_count,
                              self.server.signing_priv_key_pem,
                              self.server.cw_lists,
                              self.server.lists_enabled,
                              self.server.default_timeline,
                              reply_is_chat,
                              bold_reading,
                              self.server.dogwhistles,
                              self.server.min_images_for_accounts)
            if msg:
                msg = msg.encode('utf-8')
                msglen = len(msg)
                self._set_headers('text/html', msglen,
                                  cookie, calling_domain, False)
                self._write(msg)
            self.server.postreq_busy = False
            return

        # redirect back from person options screen
        if calling_domain.endswith('.onion') and onion_domain:
            origin_path_str = 'http://' + onion_domain + users_path
        elif calling_domain.endswith('.i2p') and i2p_domain:
            origin_path_str = 'http://' + i2p_domain + users_path
        self._redirect_headers(origin_path_str, cookie, calling_domain)
        self.server.postreq_busy = False
        return

    def _unfollow_confirm(self, calling_domain: str, cookie: str,
                          path: str, base_dir: str, http_prefix: str,
                          domain: str, domain_full: str, port: int,
                          onion_domain: str, i2p_domain: str,
                          debug: bool,
                          curr_session, proxy_type: str) -> None:
        """Confirm to unfollow
        """
        users_path = path.split('/unfollowconfirm')[0]
        origin_path_str = http_prefix + '://' + domain_full + users_path
        follower_nickname = get_nickname_from_actor(origin_path_str)
        if not follower_nickname:
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return

        length = int(self.headers['Content-length'])

        try:
            follow_confirm_params = self.rfile.read(length).decode('utf-8')
        except SocketError as ex:
            if ex.errno == errno.ECONNRESET:
                print('EX: POST follow_confirm_params ' +
                      'connection was reset')
            else:
                print('EX: POST follow_confirm_params socket error')
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        except ValueError as ex:
            print('EX: POST follow_confirm_params rfile.read failed, ' +
                  str(ex))
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return

        if '&submitYes=' in follow_confirm_params:
            following_actor = \
                urllib.parse.unquote_plus(follow_confirm_params)
            following_actor = following_actor.split('actor=')[1]
            if '&' in following_actor:
                following_actor = following_actor.split('&')[0]
            following_nickname = get_nickname_from_actor(following_actor)
            if not following_nickname:
                self.send_response(400)
                self.end_headers()
                self.server.postreq_busy = False
                return
            following_domain, following_port = \
                get_domain_from_actor(following_actor)
            following_domain_full = \
                get_full_domain(following_domain, following_port)
            if follower_nickname == following_nickname and \
               following_domain == domain and \
               following_port == port:
                if debug:
                    print('You cannot unfollow yourself!')
            else:
                if debug:
                    print(follower_nickname + ' stops following ' +
                          following_actor)
                follow_actor = \
                    local_actor_url(http_prefix,
                                    follower_nickname, domain_full)
                status_number, _ = get_status_number()
                follow_id = follow_actor + '/statuses/' + str(status_number)
                unfollow_json = {
                    '@context': 'https://www.w3.org/ns/activitystreams',
                    'id': follow_id + '/undo',
                    'type': 'Undo',
                    'actor': follow_actor,
                    'object': {
                        'id': follow_id,
                        'type': 'Follow',
                        'actor': follow_actor,
                        'object': following_actor
                    }
                }
                path_users_section = path.split('/users/')[1]
                self.post_to_nickname = path_users_section.split('/')[0]
                group_account = has_group_type(base_dir, following_actor,
                                               self.server.person_cache)
                unfollow_account(self.server.base_dir, self.post_to_nickname,
                                 self.server.domain,
                                 following_nickname, following_domain_full,
                                 self.server.debug, group_account)
                self._post_to_outbox_thread(unfollow_json,
                                            curr_session, proxy_type)

        if calling_domain.endswith('.onion') and onion_domain:
            origin_path_str = 'http://' + onion_domain + users_path
        elif (calling_domain.endswith('.i2p') and i2p_domain):
            origin_path_str = 'http://' + i2p_domain + users_path
        self._redirect_headers(origin_path_str, cookie, calling_domain)
        self.server.postreq_busy = False

    def _follow_confirm(self, calling_domain: str, cookie: str,
                        path: str, base_dir: str, http_prefix: str,
                        domain: str, domain_full: str, port: int,
                        onion_domain: str, i2p_domain: str,
                        debug: bool,
                        curr_session, proxy_type: str) -> None:
        """Confirm to follow
        """
        users_path = path.split('/followconfirm')[0]
        origin_path_str = http_prefix + '://' + domain_full + users_path
        follower_nickname = get_nickname_from_actor(origin_path_str)
        if not follower_nickname:
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return

        length = int(self.headers['Content-length'])

        try:
            follow_confirm_params = self.rfile.read(length).decode('utf-8')
        except SocketError as ex:
            if ex.errno == errno.ECONNRESET:
                print('EX: POST follow_confirm_params ' +
                      'connection was reset')
            else:
                print('EX: POST follow_confirm_params socket error')
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        except ValueError as ex:
            print('EX: POST follow_confirm_params rfile.read failed, ' +
                  str(ex))
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return

        if '&submitView=' in follow_confirm_params:
            following_actor = \
                urllib.parse.unquote_plus(follow_confirm_params)
            following_actor = following_actor.split('actor=')[1]
            if '&' in following_actor:
                following_actor = following_actor.split('&')[0]
            self._redirect_headers(following_actor, cookie, calling_domain)
            self.server.postreq_busy = False
            return

        if '&submitYes=' in follow_confirm_params:
            following_actor = \
                urllib.parse.unquote_plus(follow_confirm_params)
            following_actor = following_actor.split('actor=')[1]
            if '&' in following_actor:
                following_actor = following_actor.split('&')[0]
            following_nickname = get_nickname_from_actor(following_actor)
            if not following_nickname:
                self.send_response(400)
                self.end_headers()
                self.server.postreq_busy = False
                return
            following_domain, following_port = \
                get_domain_from_actor(following_actor)
            if follower_nickname == following_nickname and \
               following_domain == domain and \
               following_port == port:
                if debug:
                    print('You cannot follow yourself!')
            elif (following_nickname == 'news' and
                  following_domain == domain and
                  following_port == port):
                if debug:
                    print('You cannot follow the news actor')
            else:
                print('Sending follow request from ' +
                      follower_nickname + ' to ' + following_actor)
                if not self.server.signing_priv_key_pem:
                    print('Sending follow request with no signing key')

                curr_domain = domain
                curr_port = port
                curr_http_prefix = http_prefix
                curr_proxy_type = proxy_type
                if onion_domain:
                    if not curr_domain.endswith('.onion') and \
                       following_domain.endswith('.onion'):
                        curr_session = self.server.session_onion
                        curr_domain = onion_domain
                        curr_port = 80
                        following_port = 80
                        curr_http_prefix = 'http'
                        curr_proxy_type = 'tor'
                if i2p_domain:
                    if not curr_domain.endswith('.i2p') and \
                       following_domain.endswith('.i2p'):
                        curr_session = self.server.session_i2p
                        curr_domain = i2p_domain
                        curr_port = 80
                        following_port = 80
                        curr_http_prefix = 'http'
                        curr_proxy_type = 'i2p'

                curr_session = \
                    self._establish_session("follow request",
                                            curr_session,
                                            curr_proxy_type)

                send_follow_request(curr_session,
                                    base_dir, follower_nickname,
                                    domain, curr_domain, curr_port,
                                    curr_http_prefix,
                                    following_nickname,
                                    following_domain,
                                    following_actor,
                                    following_port, curr_http_prefix,
                                    False, self.server.federation_list,
                                    self.server.send_threads,
                                    self.server.postLog,
                                    self.server.cached_webfingers,
                                    self.server.person_cache, debug,
                                    self.server.project_version,
                                    self.server.signing_priv_key_pem,
                                    self.server.domain,
                                    self.server.onion_domain,
                                    self.server.i2p_domain)

        if '&submitUnblock=' in follow_confirm_params:
            blocking_actor = \
                urllib.parse.unquote_plus(follow_confirm_params)
            blocking_actor = blocking_actor.split('actor=')[1]
            if '&' in blocking_actor:
                blocking_actor = blocking_actor.split('&')[0]
            blocking_nickname = get_nickname_from_actor(blocking_actor)
            if not blocking_nickname:
                if calling_domain.endswith('.onion') and onion_domain:
                    origin_path_str = 'http://' + onion_domain + users_path
                elif (calling_domain.endswith('.i2p') and i2p_domain):
                    origin_path_str = 'http://' + i2p_domain + users_path
                print('WARN: unable to find blocked nickname in ' +
                      blocking_actor)
                self._redirect_headers(origin_path_str,
                                       cookie, calling_domain)
                self.server.postreq_busy = False
                return
            blocking_domain, blocking_port = \
                get_domain_from_actor(blocking_actor)
            blocking_domain_full = \
                get_full_domain(blocking_domain, blocking_port)
            if follower_nickname == blocking_nickname and \
               blocking_domain == domain and \
               blocking_port == port:
                if debug:
                    print('You cannot unblock yourself!')
            else:
                if debug:
                    print(follower_nickname + ' stops blocking ' +
                          blocking_actor)
                remove_block(base_dir,
                             follower_nickname, domain,
                             blocking_nickname, blocking_domain_full)
                if is_moderator(base_dir, follower_nickname):
                    remove_global_block(base_dir,
                                        blocking_nickname,
                                        blocking_domain_full)

        if calling_domain.endswith('.onion') and onion_domain:
            origin_path_str = 'http://' + onion_domain + users_path
        elif (calling_domain.endswith('.i2p') and i2p_domain):
            origin_path_str = 'http://' + i2p_domain + users_path
        self._redirect_headers(origin_path_str, cookie, calling_domain)
        self.server.postreq_busy = False

    def _block_confirm(self, calling_domain: str, cookie: str,
                       path: str, base_dir: str, http_prefix: str,
                       domain: str, domain_full: str, port: int,
                       onion_domain: str, i2p_domain: str,
                       debug: bool,
                       curr_session, proxy_type: str) -> None:
        """Confirms a block from the person options screen
        """
        users_path = path.split('/blockconfirm')[0]
        origin_path_str = http_prefix + '://' + domain_full + users_path
        blocker_nickname = get_nickname_from_actor(origin_path_str)
        if not blocker_nickname:
            if calling_domain.endswith('.onion') and onion_domain:
                origin_path_str = 'http://' + onion_domain + users_path
            elif (calling_domain.endswith('.i2p') and i2p_domain):
                origin_path_str = 'http://' + i2p_domain + users_path
            print('WARN: unable to find nickname in ' + origin_path_str)
            self._redirect_headers(origin_path_str,
                                   cookie, calling_domain)
            self.server.postreq_busy = False
            return

        length = int(self.headers['Content-length'])

        try:
            block_confirm_params = self.rfile.read(length).decode('utf-8')
        except SocketError as ex:
            if ex.errno == errno.ECONNRESET:
                print('EX: POST block_confirm_params ' +
                      'connection was reset')
            else:
                print('EX: POST block_confirm_params socket error')
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        except ValueError as ex:
            print('EX: POST block_confirm_params rfile.read failed, ' +
                  str(ex))
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return

        if '&submitYes=' in block_confirm_params:
            blocking_actor = \
                urllib.parse.unquote_plus(block_confirm_params)
            blocking_actor = blocking_actor.split('actor=')[1]
            if '&' in blocking_actor:
                blocking_actor = blocking_actor.split('&')[0]
            blocking_nickname = get_nickname_from_actor(blocking_actor)
            if not blocking_nickname:
                if calling_domain.endswith('.onion') and onion_domain:
                    origin_path_str = 'http://' + onion_domain + users_path
                elif (calling_domain.endswith('.i2p') and i2p_domain):
                    origin_path_str = 'http://' + i2p_domain + users_path
                print('WARN: unable to find nickname in ' + blocking_actor)
                self._redirect_headers(origin_path_str,
                                       cookie, calling_domain)
                self.server.postreq_busy = False
                return
            blocking_domain, blocking_port = \
                get_domain_from_actor(blocking_actor)
            blocking_domain_full = \
                get_full_domain(blocking_domain, blocking_port)
            if blocker_nickname == blocking_nickname and \
               blocking_domain == domain and \
               blocking_port == port:
                if debug:
                    print('You cannot block yourself!')
            else:
                print('Adding block by ' + blocker_nickname +
                      ' of ' + blocking_actor)
                if add_block(base_dir, blocker_nickname,
                             domain,
                             blocking_nickname,
                             blocking_domain_full):
                    # send block activity
                    self._send_block(http_prefix,
                                     blocker_nickname, domain_full,
                                     blocking_nickname, blocking_domain_full,
                                     curr_session, proxy_type)
        if calling_domain.endswith('.onion') and onion_domain:
            origin_path_str = 'http://' + onion_domain + users_path
        elif (calling_domain.endswith('.i2p') and i2p_domain):
            origin_path_str = 'http://' + i2p_domain + users_path
        self._redirect_headers(origin_path_str, cookie, calling_domain)
        self.server.postreq_busy = False

    def _unblock_confirm(self, calling_domain: str, cookie: str,
                         path: str, base_dir: str, http_prefix: str,
                         domain: str, domain_full: str, port: int,
                         onion_domain: str, i2p_domain: str,
                         debug: bool) -> None:
        """Confirms a unblock
        """
        users_path = path.split('/unblockconfirm')[0]
        origin_path_str = http_prefix + '://' + domain_full + users_path
        blocker_nickname = get_nickname_from_actor(origin_path_str)
        if not blocker_nickname:
            if calling_domain.endswith('.onion') and onion_domain:
                origin_path_str = 'http://' + onion_domain + users_path
            elif (calling_domain.endswith('.i2p') and i2p_domain):
                origin_path_str = 'http://' + i2p_domain + users_path
            print('WARN: unable to find nickname in ' + origin_path_str)
            self._redirect_headers(origin_path_str,
                                   cookie, calling_domain)
            self.server.postreq_busy = False
            return

        length = int(self.headers['Content-length'])

        try:
            block_confirm_params = self.rfile.read(length).decode('utf-8')
        except SocketError as ex:
            if ex.errno == errno.ECONNRESET:
                print('EX: POST block_confirm_params ' +
                      'connection was reset')
            else:
                print('EX: POST block_confirm_params socket error')
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        except ValueError as ex:
            print('EX: POST block_confirm_params rfile.read failed, ' +
                  str(ex))
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return

        if '&submitYes=' in block_confirm_params:
            blocking_actor = \
                urllib.parse.unquote_plus(block_confirm_params)
            blocking_actor = blocking_actor.split('actor=')[1]
            if '&' in blocking_actor:
                blocking_actor = blocking_actor.split('&')[0]
            blocking_nickname = get_nickname_from_actor(blocking_actor)
            if not blocking_nickname:
                if calling_domain.endswith('.onion') and onion_domain:
                    origin_path_str = 'http://' + onion_domain + users_path
                elif (calling_domain.endswith('.i2p') and i2p_domain):
                    origin_path_str = 'http://' + i2p_domain + users_path
                print('WARN: unable to find nickname in ' + blocking_actor)
                self._redirect_headers(origin_path_str,
                                       cookie, calling_domain)
                self.server.postreq_busy = False
                return
            blocking_domain, blocking_port = \
                get_domain_from_actor(blocking_actor)
            blocking_domain_full = \
                get_full_domain(blocking_domain, blocking_port)
            if blocker_nickname == blocking_nickname and \
               blocking_domain == domain and \
               blocking_port == port:
                if debug:
                    print('You cannot unblock yourself!')
            else:
                if debug:
                    print(blocker_nickname + ' stops blocking ' +
                          blocking_actor)
                remove_block(base_dir,
                             blocker_nickname, domain,
                             blocking_nickname, blocking_domain_full)
                if is_moderator(base_dir, blocker_nickname):
                    remove_global_block(base_dir,
                                        blocking_nickname,
                                        blocking_domain_full)
        if calling_domain.endswith('.onion') and onion_domain:
            origin_path_str = 'http://' + onion_domain + users_path
        elif (calling_domain.endswith('.i2p') and i2p_domain):
            origin_path_str = 'http://' + i2p_domain + users_path
        self._redirect_headers(origin_path_str,
                               cookie, calling_domain)
        self.server.postreq_busy = False

    def _receive_search_query(self, calling_domain: str, cookie: str,
                              authorized: bool, path: str,
                              base_dir: str, http_prefix: str,
                              domain: str, domain_full: str,
                              port: int, search_for_emoji: bool,
                              onion_domain: str, i2p_domain: str,
                              getreq_start_time, debug: bool,
                              curr_session, proxy_type: str) -> None:
        """Receive a search query
        """
        # get the page number
        page_number = 1
        if '/searchhandle?page=' in path:
            page_number_str = path.split('/searchhandle?page=')[1]
            if '#' in page_number_str:
                page_number_str = page_number_str.split('#')[0]
            if len(page_number_str) > 5:
                page_number_str = "1"
            if page_number_str.isdigit():
                page_number = int(page_number_str)
            path = path.split('?page=')[0]

        users_path = path.replace('/searchhandle', '')
        actor_str = self._get_instance_url(calling_domain) + users_path
        length = int(self.headers['Content-length'])
        try:
            search_params = self.rfile.read(length).decode('utf-8')
        except SocketError as ex:
            if ex.errno == errno.ECONNRESET:
                print('EX: POST search_params connection was reset')
            else:
                print('EX: POST search_params socket error')
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        except ValueError as ex:
            print('EX: POST search_params rfile.read failed, ' + str(ex))
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        if 'submitBack=' in search_params:
            # go back on search screen
            self._redirect_headers(actor_str + '/' +
                                   self.server.default_timeline,
                                   cookie, calling_domain)
            self.server.postreq_busy = False
            return
        if 'searchtext=' in search_params:
            search_str = search_params.split('searchtext=')[1]
            if '&' in search_str:
                search_str = search_str.split('&')[0]
            search_str = \
                urllib.parse.unquote_plus(search_str.strip())
            search_str = search_str.strip()
            # hashtags can be combined case
            if not search_str.startswith('#'):
                search_str = search_str.lower()
            print('search_str: ' + search_str)
            if search_for_emoji:
                search_str = ':' + search_str + ':'
            if search_str.startswith('#'):
                nickname = get_nickname_from_actor(actor_str)
                if not nickname:
                    self.send_response(400)
                    self.end_headers()
                    self.server.postreq_busy = False
                    return

                # hashtag search
                timezone = None
                if self.server.account_timezone.get(nickname):
                    timezone = \
                        self.server.account_timezone.get(nickname)
                bold_reading = False
                if self.server.bold_reading.get(nickname):
                    bold_reading = True
                hashtag_str = \
                    html_hashtag_search(nickname, domain, port,
                                        self.server.recent_posts_cache,
                                        self.server.max_recent_posts,
                                        self.server.translate,
                                        base_dir,
                                        search_str[1:], 1,
                                        MAX_POSTS_IN_HASHTAG_FEED,
                                        curr_session,
                                        self.server.cached_webfingers,
                                        self.server.person_cache,
                                        http_prefix,
                                        self.server.project_version,
                                        self.server.yt_replace_domain,
                                        self.server.twitter_replacement_domain,
                                        self.server.show_published_date_only,
                                        self.server.peertube_instances,
                                        self.server.allow_local_network_access,
                                        self.server.theme_name,
                                        self.server.system_language,
                                        self.server.max_like_count,
                                        self.server.signing_priv_key_pem,
                                        self.server.cw_lists,
                                        self.server.lists_enabled,
                                        timezone, bold_reading,
                                        self.server.dogwhistles,
                                        self.server.map_format,
                                        self.server.access_keys,
                                        'search',
                                        self.server.min_images_for_accounts)
                if hashtag_str:
                    msg = hashtag_str.encode('utf-8')
                    msglen = len(msg)
                    self._login_headers('text/html',
                                        msglen, calling_domain)
                    self._write(msg)
                    self.server.postreq_busy = False
                    return
            elif (search_str.startswith('*') or
                  search_str.endswith(' skill')):
                possible_endings = (
                    ' skill'
                )
                for poss_ending in possible_endings:
                    if search_str.endswith(poss_ending):
                        search_str = search_str.replace(poss_ending, '')
                        break
                # skill search
                search_str = search_str.replace('*', '').strip()
                nickname = get_nickname_from_actor(actor_str)
                skill_str = \
                    html_skills_search(actor_str,
                                       self.server.translate,
                                       base_dir,
                                       search_str,
                                       self.server.instance_only_skills_search,
                                       64, nickname, domain,
                                       self.server.theme_name,
                                       self.server.access_keys)
                if skill_str:
                    msg = skill_str.encode('utf-8')
                    msglen = len(msg)
                    self._login_headers('text/html',
                                        msglen, calling_domain)
                    self._write(msg)
                    self.server.postreq_busy = False
                    return
            elif (search_str.startswith("'") or
                  search_str.endswith(' history') or
                  search_str.endswith(' in sent') or
                  search_str.endswith(' in outbox') or
                  search_str.endswith(' in outgoing') or
                  search_str.endswith(' in sent items') or
                  search_str.endswith(' in sent posts') or
                  search_str.endswith(' in outgoing posts') or
                  search_str.endswith(' in my history') or
                  search_str.endswith(' in my outbox') or
                  search_str.endswith(' in my posts')):
                possible_endings = (
                    ' in my posts',
                    ' in my history',
                    ' in my outbox',
                    ' in sent posts',
                    ' in outgoing posts',
                    ' in sent items',
                    ' in history',
                    ' in outbox',
                    ' in outgoing',
                    ' in sent',
                    ' history'
                )
                for poss_ending in possible_endings:
                    if search_str.endswith(poss_ending):
                        search_str = search_str.replace(poss_ending, '')
                        break
                # your post history search
                nickname = get_nickname_from_actor(actor_str)
                if not nickname:
                    self.send_response(400)
                    self.end_headers()
                    self.server.postreq_busy = False
                    return
                search_str = search_str.replace("'", '', 1).strip()
                timezone = None
                if self.server.account_timezone.get(nickname):
                    timezone = \
                        self.server.account_timezone.get(nickname)
                bold_reading = False
                if self.server.bold_reading.get(nickname):
                    bold_reading = True
                history_str = \
                    html_history_search(self.server.translate,
                                        base_dir,
                                        http_prefix,
                                        nickname,
                                        domain,
                                        search_str,
                                        MAX_POSTS_IN_FEED,
                                        page_number,
                                        self.server.project_version,
                                        self.server.recent_posts_cache,
                                        self.server.max_recent_posts,
                                        curr_session,
                                        self.server.cached_webfingers,
                                        self.server.person_cache,
                                        port,
                                        self.server.yt_replace_domain,
                                        self.server.twitter_replacement_domain,
                                        self.server.show_published_date_only,
                                        self.server.peertube_instances,
                                        self.server.allow_local_network_access,
                                        self.server.theme_name, 'outbox',
                                        self.server.system_language,
                                        self.server.max_like_count,
                                        self.server.signing_priv_key_pem,
                                        self.server.cw_lists,
                                        self.server.lists_enabled,
                                        timezone, bold_reading,
                                        self.server.dogwhistles,
                                        self.server.access_keys,
                                        self.server.min_images_for_accounts)
                if history_str:
                    msg = history_str.encode('utf-8')
                    msglen = len(msg)
                    self._login_headers('text/html',
                                        msglen, calling_domain)
                    self._write(msg)
                    self.server.postreq_busy = False
                    return
            elif (search_str.startswith('-') or
                  search_str.endswith(' in my saved items') or
                  search_str.endswith(' in my saved posts') or
                  search_str.endswith(' in my bookmarks') or
                  search_str.endswith(' in my saved') or
                  search_str.endswith(' in my saves') or
                  search_str.endswith(' in saved posts') or
                  search_str.endswith(' in saved items') or
                  search_str.endswith(' in bookmarks') or
                  search_str.endswith(' in saved') or
                  search_str.endswith(' in saves') or
                  search_str.endswith(' bookmark')):
                possible_endings = (
                    ' in my bookmarks'
                    ' in my saved posts'
                    ' in my saved items'
                    ' in my saved'
                    ' in my saves'
                    ' in saved posts'
                    ' in saved items'
                    ' in saved'
                    ' in saves'
                    ' in bookmarks'
                    ' bookmark'
                )
                for poss_ending in possible_endings:
                    if search_str.endswith(poss_ending):
                        search_str = search_str.replace(poss_ending, '')
                        break
                # bookmark search
                nickname = get_nickname_from_actor(actor_str)
                if not nickname:
                    self.send_response(400)
                    self.end_headers()
                    self.server.postreq_busy = False
                    return
                search_str = search_str.replace('-', '', 1).strip()
                timezone = None
                if self.server.account_timezone.get(nickname):
                    timezone = \
                        self.server.account_timezone.get(nickname)
                bold_reading = False
                if self.server.bold_reading.get(nickname):
                    bold_reading = True
                bookmarks_str = \
                    html_history_search(self.server.translate,
                                        base_dir,
                                        http_prefix,
                                        nickname,
                                        domain,
                                        search_str,
                                        MAX_POSTS_IN_FEED,
                                        page_number,
                                        self.server.project_version,
                                        self.server.recent_posts_cache,
                                        self.server.max_recent_posts,
                                        curr_session,
                                        self.server.cached_webfingers,
                                        self.server.person_cache,
                                        port,
                                        self.server.yt_replace_domain,
                                        self.server.twitter_replacement_domain,
                                        self.server.show_published_date_only,
                                        self.server.peertube_instances,
                                        self.server.allow_local_network_access,
                                        self.server.theme_name, 'bookmarks',
                                        self.server.system_language,
                                        self.server.max_like_count,
                                        self.server.signing_priv_key_pem,
                                        self.server.cw_lists,
                                        self.server.lists_enabled,
                                        timezone, bold_reading,
                                        self.server.dogwhistles,
                                        self.server.access_keys,
                                        self.server.min_images_for_accounts)
                if bookmarks_str:
                    msg = bookmarks_str.encode('utf-8')
                    msglen = len(msg)
                    self._login_headers('text/html',
                                        msglen, calling_domain)
                    self._write(msg)
                    self.server.postreq_busy = False
                    return
            elif ('@' in search_str or
                  ('://' in search_str and
                   has_users_path(search_str))):
                if search_str.endswith(':') or \
                   search_str.endswith(';') or \
                   search_str.endswith('.'):
                    actor_str = \
                        self._get_instance_url(calling_domain) + users_path
                    self._redirect_headers(actor_str + '/search',
                                           cookie, calling_domain)
                    self.server.postreq_busy = False
                    return
                # profile search
                nickname = get_nickname_from_actor(actor_str)
                if not nickname:
                    self.send_response(400)
                    self.end_headers()
                    self.server.postreq_busy = False
                    return
                profile_path_str = path.replace('/searchhandle', '')

                # are we already following the searched for handle?
                if is_following_actor(base_dir, nickname, domain, search_str):
                    # get the actor
                    if not has_users_path(search_str):
                        search_nickname = get_nickname_from_actor(search_str)
                        if not search_nickname:
                            self.send_response(400)
                            self.end_headers()
                            self.server.postreq_busy = False
                            return
                        search_domain, search_port = \
                            get_domain_from_actor(search_str)
                        search_domain_full = \
                            get_full_domain(search_domain, search_port)
                        actor = \
                            local_actor_url(http_prefix, search_nickname,
                                            search_domain_full)
                    else:
                        actor = search_str

                    # establish the session
                    curr_proxy_type = proxy_type
                    if '.onion/' in actor:
                        curr_proxy_type = 'tor'
                        curr_session = self.server.session_onion
                    elif '.i2p/' in actor:
                        curr_proxy_type = 'i2p'
                        curr_session = self.server.session_i2p

                    curr_session = \
                        self._establish_session("handle search",
                                                curr_session,
                                                curr_proxy_type)
                    if not curr_session:
                        self.server.postreq_busy = False
                        return

                    # get the avatar url for the actor
                    avatar_url = \
                        get_avatar_image_url(curr_session,
                                             base_dir, http_prefix,
                                             actor,
                                             self.server.person_cache,
                                             None, True,
                                             self.server.signing_priv_key_pem)
                    profile_path_str += \
                        '?options=' + actor + ';1;' + avatar_url

                    self._show_person_options(calling_domain, profile_path_str,
                                              base_dir, http_prefix,
                                              domain, domain_full,
                                              getreq_start_time,
                                              onion_domain, i2p_domain,
                                              cookie, debug, authorized,
                                              curr_session)
                    return
                else:
                    show_published_date_only = \
                        self.server.show_published_date_only
                    allow_local_network_access = \
                        self.server.allow_local_network_access

                    access_keys = self.server.access_keys
                    if self.server.key_shortcuts.get(nickname):
                        access_keys = self.server.key_shortcuts[nickname]

                    signing_priv_key_pem = \
                        self.server.signing_priv_key_pem
                    twitter_replacement_domain = \
                        self.server.twitter_replacement_domain
                    peertube_instances = \
                        self.server.peertube_instances
                    yt_replace_domain = \
                        self.server.yt_replace_domain
                    cached_webfingers = \
                        self.server.cached_webfingers
                    recent_posts_cache = \
                        self.server.recent_posts_cache
                    timezone = None
                    if self.server.account_timezone.get(nickname):
                        timezone = \
                            self.server.account_timezone.get(nickname)

                    profile_handle = remove_eol(search_str).strip()

                    # establish the session
                    curr_proxy_type = proxy_type
                    if '.onion/' in profile_handle or \
                       profile_handle.endswith('.onion'):
                        curr_proxy_type = 'tor'
                        curr_session = self.server.session_onion
                    elif ('.i2p/' in profile_handle or
                          profile_handle.endswith('.i2p')):
                        curr_proxy_type = 'i2p'
                        curr_session = self.server.session_i2p

                    curr_session = \
                        self._establish_session("handle search",
                                                curr_session,
                                                curr_proxy_type)
                    if not curr_session:
                        self.server.postreq_busy = False
                        return

                    bold_reading = False
                    if self.server.bold_reading.get(nickname):
                        bold_reading = True

                    min_images_for_accounts = \
                        self.server.min_images_for_accounts
                    profile_str = \
                        html_profile_after_search(recent_posts_cache,
                                                  self.server.max_recent_posts,
                                                  self.server.translate,
                                                  base_dir,
                                                  profile_path_str,
                                                  http_prefix,
                                                  nickname,
                                                  domain,
                                                  port,
                                                  profile_handle,
                                                  curr_session,
                                                  cached_webfingers,
                                                  self.server.person_cache,
                                                  self.server.debug,
                                                  self.server.project_version,
                                                  yt_replace_domain,
                                                  twitter_replacement_domain,
                                                  show_published_date_only,
                                                  self.server.default_timeline,
                                                  peertube_instances,
                                                  allow_local_network_access,
                                                  self.server.theme_name,
                                                  access_keys,
                                                  self.server.system_language,
                                                  self.server.max_like_count,
                                                  signing_priv_key_pem,
                                                  self.server.cw_lists,
                                                  self.server.lists_enabled,
                                                  timezone,
                                                  self.server.onion_domain,
                                                  self.server.i2p_domain,
                                                  bold_reading,
                                                  self.server.dogwhistles,
                                                  min_images_for_accounts)
                if profile_str:
                    msg = profile_str.encode('utf-8')
                    msglen = len(msg)
                    self._login_headers('text/html',
                                        msglen, calling_domain)
                    self._write(msg)
                    self.server.postreq_busy = False
                    return
                actor_str = \
                    self._get_instance_url(calling_domain) + users_path
                self._redirect_headers(actor_str + '/search',
                                       cookie, calling_domain)
                self.server.postreq_busy = False
                return
            elif (search_str.startswith(':') or
                  search_str.endswith(' emoji')):
                # eg. "cat emoji"
                if search_str.endswith(' emoji'):
                    search_str = \
                        search_str.replace(' emoji', '')
                # emoji search
                nickname = get_nickname_from_actor(actor_str)
                emoji_str = \
                    html_search_emoji(self.server.translate,
                                      base_dir, search_str,
                                      nickname, domain,
                                      self.server.theme_name,
                                      self.server.access_keys)
                if emoji_str:
                    msg = emoji_str.encode('utf-8')
                    msglen = len(msg)
                    self._login_headers('text/html',
                                        msglen, calling_domain)
                    self._write(msg)
                    self.server.postreq_busy = False
                    return
            elif search_str.startswith('.'):
                # wanted items search
                shared_items_federated_domains = \
                    self.server.shared_items_federated_domains
                nickname = get_nickname_from_actor(actor_str)
                wanted_items_str = \
                    html_search_shared_items(self.server.translate,
                                             base_dir,
                                             search_str[1:], page_number,
                                             MAX_POSTS_IN_FEED,
                                             http_prefix,
                                             domain_full,
                                             actor_str, calling_domain,
                                             shared_items_federated_domains,
                                             'wanted', nickname, domain,
                                             self.server.theme_name,
                                             self.server.access_keys)
                if wanted_items_str:
                    msg = wanted_items_str.encode('utf-8')
                    msglen = len(msg)
                    self._login_headers('text/html',
                                        msglen, calling_domain)
                    self._write(msg)
                    self.server.postreq_busy = False
                    return
            else:
                # shared items search
                shared_items_federated_domains = \
                    self.server.shared_items_federated_domains
                nickname = get_nickname_from_actor(actor_str)
                shared_items_str = \
                    html_search_shared_items(self.server.translate,
                                             base_dir,
                                             search_str, page_number,
                                             MAX_POSTS_IN_FEED,
                                             http_prefix,
                                             domain_full,
                                             actor_str, calling_domain,
                                             shared_items_federated_domains,
                                             'shares', nickname, domain,
                                             self.server.theme_name,
                                             self.server.access_keys)
                if shared_items_str:
                    msg = shared_items_str.encode('utf-8')
                    msglen = len(msg)
                    self._login_headers('text/html',
                                        msglen, calling_domain)
                    self._write(msg)
                    self.server.postreq_busy = False
                    return
        actor_str = self._get_instance_url(calling_domain) + users_path
        self._redirect_headers(actor_str + '/' +
                               self.server.default_timeline,
                               cookie, calling_domain)
        self.server.postreq_busy = False

    def _receive_vote(self, calling_domain: str, cookie: str,
                      path: str, http_prefix: str, domain_full: str,
                      onion_domain: str, i2p_domain: str,
                      curr_session, proxy_type: str) -> None:
        """Receive a vote via POST
        """
        page_number = 1
        if '?page=' in path:
            page_number_str = path.split('?page=')[1]
            if '#' in page_number_str:
                page_number_str = page_number_str.split('#')[0]
            if len(page_number_str) > 5:
                page_number_str = "1"
            if page_number_str.isdigit():
                page_number = int(page_number_str)
            path = path.split('?page=')[0]

        # the actor who votes
        users_path = path.replace('/question', '')
        actor = http_prefix + '://' + domain_full + users_path
        nickname = get_nickname_from_actor(actor)
        if not nickname:
            if calling_domain.endswith('.onion') and onion_domain:
                actor = 'http://' + onion_domain + users_path
            elif (calling_domain.endswith('.i2p') and i2p_domain):
                actor = 'http://' + i2p_domain + users_path
            actor_path_str = \
                actor + '/' + self.server.default_timeline + \
                '?page=' + str(page_number)
            self._redirect_headers(actor_path_str,
                                   cookie, calling_domain)
            self.server.postreq_busy = False
            return

        # get the parameters
        length = int(self.headers['Content-length'])

        try:
            question_params = self.rfile.read(length).decode('utf-8')
        except SocketError as ex:
            if ex.errno == errno.ECONNRESET:
                print('EX: POST question_params connection was reset')
            else:
                print('EX: POST question_params socket error')
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        except ValueError as ex:
            print('EX: POST question_params rfile.read failed, ' + str(ex))
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return

        question_params = question_params.replace('+', ' ')
        question_params = question_params.replace('%3F', '')
        question_params = \
            urllib.parse.unquote_plus(question_params.strip())

        # post being voted on
        message_id = None
        if 'messageId=' in question_params:
            message_id = question_params.split('messageId=')[1]
            if '&' in message_id:
                message_id = message_id.split('&')[0]

        answer = None
        if 'answer=' in question_params:
            answer = question_params.split('answer=')[1]
            if '&' in answer:
                answer = answer.split('&')[0]

        self._send_reply_to_question(nickname, message_id, answer,
                                     curr_session, proxy_type)
        if calling_domain.endswith('.onion') and onion_domain:
            actor = 'http://' + onion_domain + users_path
        elif (calling_domain.endswith('.i2p') and i2p_domain):
            actor = 'http://' + i2p_domain + users_path
        actor_path_str = \
            actor + '/' + self.server.default_timeline + \
            '?page=' + str(page_number)
        self._redirect_headers(actor_path_str, cookie,
                               calling_domain)
        self.server.postreq_busy = False
        return

    def _receive_image(self, length: int, path: str, base_dir: str,
                       domain: str, debug: bool) -> None:
        """Receives an image via POST
        """
        if not self.outbox_authenticated:
            if debug:
                print('DEBUG: unauthenticated attempt to ' +
                      'post image to outbox')
            self.send_response(403)
            self.end_headers()
            self.server.postreq_busy = False
            return
        path_users_section = path.split('/users/')[1]
        if '/' not in path_users_section:
            self._404()
            self.server.postreq_busy = False
            return
        self.post_from_nickname = path_users_section.split('/')[0]
        accounts_dir = acct_dir(base_dir, self.post_from_nickname, domain)
        if not os.path.isdir(accounts_dir):
            self._404()
            self.server.postreq_busy = False
            return

        try:
            media_bytes = self.rfile.read(length)
        except SocketError as ex:
            if ex.errno == errno.ECONNRESET:
                print('EX: POST media_bytes ' +
                      'connection reset by peer')
            else:
                print('EX: POST media_bytes socket error')
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        except ValueError as ex:
            print('EX: POST media_bytes rfile.read failed, ' + str(ex))
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return

        media_filename_base = accounts_dir + '/upload'
        media_filename = \
            media_filename_base + '.' + \
            get_image_extension_from_mime_type(self.headers['Content-type'])
        try:
            with open(media_filename, 'wb') as av_file:
                av_file.write(media_bytes)
        except OSError:
            print('EX: unable to write ' + media_filename)
        if debug:
            print('DEBUG: image saved to ' + media_filename)
        self.send_response(201)
        self.end_headers()
        self.server.postreq_busy = False

    def _remove_share(self, calling_domain: str, cookie: str,
                      authorized: bool, path: str,
                      base_dir: str, http_prefix: str, domain_full: str,
                      onion_domain: str, i2p_domain: str) -> None:
        """Removes a shared item
        """
        users_path = path.split('/rmshare')[0]
        origin_path_str = http_prefix + '://' + domain_full + users_path

        length = int(self.headers['Content-length'])

        try:
            remove_share_confirm_params = \
                self.rfile.read(length).decode('utf-8')
        except SocketError as ex:
            if ex.errno == errno.ECONNRESET:
                print('EX: POST remove_share_confirm_params ' +
                      'connection was reset')
            else:
                print('EX: POST remove_share_confirm_params socket error')
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        except ValueError as ex:
            print('EX: POST remove_share_confirm_params ' +
                  'rfile.read failed, ' + str(ex))
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return

        if '&submitYes=' in remove_share_confirm_params and authorized:
            remove_share_confirm_params = \
                remove_share_confirm_params.replace('+', ' ').strip()
            remove_share_confirm_params = \
                urllib.parse.unquote_plus(remove_share_confirm_params)
            share_actor = remove_share_confirm_params.split('actor=')[1]
            if '&' in share_actor:
                share_actor = share_actor.split('&')[0]
            admin_nickname = get_config_param(base_dir, 'admin')
            admin_actor = \
                local_actor_url(http_prefix, admin_nickname, domain_full)
            actor = origin_path_str
            actor_nickname = get_nickname_from_actor(actor)
            if not actor_nickname:
                self.send_response(400)
                self.end_headers()
                self.server.postreq_busy = False
                return
            if actor == share_actor or actor == admin_actor or \
               is_moderator(base_dir, actor_nickname):
                item_id = remove_share_confirm_params.split('itemID=')[1]
                if '&' in item_id:
                    item_id = item_id.split('&')[0]
                share_nickname = get_nickname_from_actor(share_actor)
                if share_nickname:
                    share_domain, _ = \
                        get_domain_from_actor(share_actor)
                    remove_shared_item(base_dir,
                                       share_nickname, share_domain, item_id,
                                       'shares')

        if calling_domain.endswith('.onion') and onion_domain:
            origin_path_str = 'http://' + onion_domain + users_path
        elif (calling_domain.endswith('.i2p') and i2p_domain):
            origin_path_str = 'http://' + i2p_domain + users_path
        self._redirect_headers(origin_path_str + '/tlshares',
                               cookie, calling_domain)
        self.server.postreq_busy = False

    def _remove_wanted(self, calling_domain: str, cookie: str,
                       authorized: bool, path: str,
                       base_dir: str, http_prefix: str,
                       domain_full: str,
                       onion_domain: str, i2p_domain: str) -> None:
        """Removes a wanted item
        """
        users_path = path.split('/rmwanted')[0]
        origin_path_str = http_prefix + '://' + domain_full + users_path

        length = int(self.headers['Content-length'])

        try:
            remove_share_confirm_params = \
                self.rfile.read(length).decode('utf-8')
        except SocketError as ex:
            if ex.errno == errno.ECONNRESET:
                print('EX: POST remove_share_confirm_params ' +
                      'connection was reset')
            else:
                print('EX: POST remove_share_confirm_params socket error')
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        except ValueError as ex:
            print('EX: POST remove_share_confirm_params ' +
                  'rfile.read failed, ' + str(ex))
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return

        if '&submitYes=' in remove_share_confirm_params and authorized:
            remove_share_confirm_params = \
                remove_share_confirm_params.replace('+', ' ').strip()
            remove_share_confirm_params = \
                urllib.parse.unquote_plus(remove_share_confirm_params)
            share_actor = remove_share_confirm_params.split('actor=')[1]
            if '&' in share_actor:
                share_actor = share_actor.split('&')[0]
            admin_nickname = get_config_param(base_dir, 'admin')
            admin_actor = \
                local_actor_url(http_prefix, admin_nickname, domain_full)
            actor = origin_path_str
            actor_nickname = get_nickname_from_actor(actor)
            if not actor_nickname:
                self.send_response(400)
                self.end_headers()
                self.server.postreq_busy = False
                return
            if actor == share_actor or actor == admin_actor or \
               is_moderator(base_dir, actor_nickname):
                item_id = remove_share_confirm_params.split('itemID=')[1]
                if '&' in item_id:
                    item_id = item_id.split('&')[0]
                share_nickname = get_nickname_from_actor(share_actor)
                if share_nickname:
                    share_domain, _ = \
                        get_domain_from_actor(share_actor)
                    remove_shared_item(base_dir,
                                       share_nickname, share_domain, item_id,
                                       'wanted')

        if calling_domain.endswith('.onion') and onion_domain:
            origin_path_str = 'http://' + onion_domain + users_path
        elif (calling_domain.endswith('.i2p') and i2p_domain):
            origin_path_str = 'http://' + i2p_domain + users_path
        self._redirect_headers(origin_path_str + '/tlwanted',
                               cookie, calling_domain)
        self.server.postreq_busy = False

    def _receive_remove_post(self, calling_domain: str, cookie: str,
                             path: str, base_dir: str, http_prefix: str,
                             domain: str, domain_full: str,
                             onion_domain: str, i2p_domain: str,
                             curr_session, proxy_type: str) -> None:
        """Endpoint for removing posts after confirmation
        """
        page_number = 1
        users_path = path.split('/rmpost')[0]
        origin_path_str = \
            http_prefix + '://' + \
            domain_full + users_path

        length = int(self.headers['Content-length'])

        try:
            remove_post_confirm_params = \
                self.rfile.read(length).decode('utf-8')
        except SocketError as ex:
            if ex.errno == errno.ECONNRESET:
                print('EX: POST remove_post_confirm_params ' +
                      'connection was reset')
            else:
                print('EX: POST remove_post_confirm_params socket error')
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        except ValueError as ex:
            print('EX: POST remove_post_confirm_params ' +
                  'rfile.read failed, ' + str(ex))
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        if '&submitYes=' in remove_post_confirm_params:
            remove_post_confirm_params = \
                urllib.parse.unquote_plus(remove_post_confirm_params)
            remove_message_id = \
                remove_post_confirm_params.split('messageId=')[1]
            if '&' in remove_message_id:
                remove_message_id = remove_message_id.split('&')[0]
            print('remove_message_id: ' + remove_message_id)
            if 'pageNumber=' in remove_post_confirm_params:
                page_number_str = \
                    remove_post_confirm_params.split('pageNumber=')[1]
                if '&' in page_number_str:
                    page_number_str = page_number_str.split('&')[0]
                if len(page_number_str) > 5:
                    page_number_str = "1"
                if page_number_str.isdigit():
                    page_number = int(page_number_str)
            year_str = None
            if 'year=' in remove_post_confirm_params:
                year_str = remove_post_confirm_params.split('year=')[1]
                if '&' in year_str:
                    year_str = year_str.split('&')[0]
            month_str = None
            if 'month=' in remove_post_confirm_params:
                month_str = remove_post_confirm_params.split('month=')[1]
                if '&' in month_str:
                    month_str = month_str.split('&')[0]
            if '/statuses/' in remove_message_id:
                remove_post_actor = remove_message_id.split('/statuses/')[0]
            print('origin_path_str: ' + origin_path_str)
            print('remove_post_actor: ' + remove_post_actor)
            if origin_path_str in remove_post_actor:
                to_list = [
                    'https://www.w3.org/ns/activitystreams#Public',
                    remove_post_actor
                ]
                delete_json = {
                    "@context": "https://www.w3.org/ns/activitystreams",
                    'actor': remove_post_actor,
                    'object': remove_message_id,
                    'to': to_list,
                    'cc': [remove_post_actor + '/followers'],
                    'type': 'Delete'
                }
                self.post_to_nickname = \
                    get_nickname_from_actor(remove_post_actor)
                if self.post_to_nickname:
                    if month_str and year_str:
                        if len(month_str) <= 3 and \
                           len(year_str) <= 3 and \
                           month_str.isdigit() and \
                           year_str.isdigit():
                            year_int = int(year_str)
                            month_int = int(month_str)
                            remove_calendar_event(base_dir,
                                                  self.post_to_nickname,
                                                  domain, year_int,
                                                  month_int,
                                                  remove_message_id)
                    self._post_to_outbox_thread(delete_json,
                                                curr_session, proxy_type)
        if calling_domain.endswith('.onion') and onion_domain:
            origin_path_str = 'http://' + onion_domain + users_path
        elif (calling_domain.endswith('.i2p') and i2p_domain):
            origin_path_str = 'http://' + i2p_domain + users_path
        if page_number == 1:
            self._redirect_headers(origin_path_str + '/outbox', cookie,
                                   calling_domain)
        else:
            page_number_str = str(page_number)
            actor_path_str = \
                origin_path_str + '/outbox?page=' + page_number_str
            self._redirect_headers(actor_path_str,
                                   cookie, calling_domain)
        self.server.postreq_busy = False

    def _links_update(self, calling_domain: str, cookie: str,
                      path: str, base_dir: str, debug: bool,
                      default_timeline: str,
                      allow_local_network_access: bool) -> None:
        """Updates the left links column of the timeline
        """
        users_path = path.replace('/linksdata', '')
        users_path = users_path.replace('/editlinks', '')
        actor_str = self._get_instance_url(calling_domain) + users_path

        boundary = None
        if ' boundary=' in self.headers['Content-type']:
            boundary = self.headers['Content-type'].split('boundary=')[1]
            if ';' in boundary:
                boundary = boundary.split(';')[0]

        # get the nickname
        nickname = get_nickname_from_actor(actor_str)
        editor = None
        if nickname:
            editor = is_editor(base_dir, nickname)
        if not nickname or not editor:
            if not nickname:
                print('WARN: nickname not found in ' + actor_str)
            else:
                print('WARN: nickname is not a moderator' + actor_str)
            self._redirect_headers(actor_str, cookie, calling_domain)
            self.server.postreq_busy = False
            return

        if self.headers.get('Content-length'):
            length = int(self.headers['Content-length'])

            # check that the POST isn't too large
            if length > self.server.max_post_length:
                print('Maximum links data length exceeded ' + str(length))
                self._redirect_headers(actor_str, cookie, calling_domain)
                self.server.postreq_busy = False
                return

        try:
            # read the bytes of the http form POST
            post_bytes = self.rfile.read(length)
        except SocketError as ex:
            if ex.errno == errno.ECONNRESET:
                print('EX: connection was reset while ' +
                      'reading bytes from http form POST')
            else:
                print('EX: error while reading bytes ' +
                      'from http form POST')
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        except ValueError as ex:
            print('EX: failed to read bytes for POST, ' + str(ex))
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return

        links_filename = base_dir + '/accounts/links.txt'
        about_filename = base_dir + '/accounts/about.md'
        tos_filename = base_dir + '/accounts/tos.md'
        specification_filename = base_dir + '/accounts/activitypub.md'

        if not boundary:
            if b'--LYNX' in post_bytes:
                boundary = '--LYNX'

        if boundary:
            # extract all of the text fields into a dict
            fields = \
                extract_text_fields_in_post(post_bytes, boundary, debug)

            if fields.get('editedLinks'):
                links_str = fields['editedLinks']
                if fields.get('newColLink'):
                    if links_str:
                        if not links_str.endswith('\n'):
                            links_str += '\n'
                    links_str += fields['newColLink'] + '\n'
                try:
                    with open(links_filename, 'w+',
                              encoding='utf-8') as linksfile:
                        linksfile.write(links_str)
                except OSError:
                    print('EX: _links_update unable to write ' +
                          links_filename)
            else:
                if fields.get('newColLink'):
                    # the text area is empty but there is a new link added
                    links_str = fields['newColLink'] + '\n'
                    try:
                        with open(links_filename, 'w+',
                                  encoding='utf-8') as linksfile:
                            linksfile.write(links_str)
                    except OSError:
                        print('EX: _links_update unable to write ' +
                              links_filename)
                else:
                    if os.path.isfile(links_filename):
                        try:
                            os.remove(links_filename)
                        except OSError:
                            print('EX: _links_update unable to delete ' +
                                  links_filename)

            admin_nickname = \
                get_config_param(base_dir, 'admin')
            if nickname == admin_nickname:
                if fields.get('editedAbout'):
                    about_str = fields['editedAbout']
                    if not dangerous_markup(about_str,
                                            allow_local_network_access):
                        try:
                            with open(about_filename, 'w+',
                                      encoding='utf-8') as aboutfile:
                                aboutfile.write(about_str)
                        except OSError:
                            print('EX: unable to write about ' +
                                  about_filename)
                else:
                    if os.path.isfile(about_filename):
                        try:
                            os.remove(about_filename)
                        except OSError:
                            print('EX: _links_update unable to delete ' +
                                  about_filename)

                if fields.get('editedTOS'):
                    tos_str = fields['editedTOS']
                    if not dangerous_markup(tos_str,
                                            allow_local_network_access):
                        try:
                            with open(tos_filename, 'w+',
                                      encoding='utf-8') as tosfile:
                                tosfile.write(tos_str)
                        except OSError:
                            print('EX: unable to write TOS ' + tos_filename)
                else:
                    if os.path.isfile(tos_filename):
                        try:
                            os.remove(tos_filename)
                        except OSError:
                            print('EX: _links_update unable to delete ' +
                                  tos_filename)

                if fields.get('editedSpecification'):
                    specification_str = fields['editedSpecification']
                    try:
                        with open(specification_filename, 'w+',
                                  encoding='utf-8') as specificationfile:
                            specificationfile.write(specification_str)
                    except OSError:
                        print('EX: unable to write specification ' +
                              specification_filename)
                else:
                    if os.path.isfile(specification_filename):
                        try:
                            os.remove(specification_filename)
                        except OSError:
                            print('EX: _links_update unable to delete ' +
                                  specification_filename)

        # redirect back to the default timeline
        self._redirect_headers(actor_str + '/' + default_timeline,
                               cookie, calling_domain)
        self.server.postreq_busy = False

    def _set_hashtag_category(self, calling_domain: str, cookie: str,
                              path: str, base_dir: str,
                              domain: str, debug: bool,
                              system_language: str) -> None:
        """On the screen after selecting a hashtag from the swarm, this sets
        the category for that tag
        """
        users_path = path.replace('/sethashtagcategory', '')
        hashtag = ''
        if '/tags/' not in users_path:
            # no hashtag is specified within the path
            self._404()
            return
        hashtag = users_path.split('/tags/')[1].strip()
        hashtag = urllib.parse.unquote_plus(hashtag)
        if not hashtag:
            # no hashtag was given in the path
            self._404()
            return
        hashtag_filename = base_dir + '/tags/' + hashtag + '.txt'
        if not os.path.isfile(hashtag_filename):
            # the hashtag does not exist
            self._404()
            return
        users_path = users_path.split('/tags/')[0]
        actor_str = self._get_instance_url(calling_domain) + users_path
        tag_screen_str = actor_str + '/tags/' + hashtag

        boundary = None
        if ' boundary=' in self.headers['Content-type']:
            boundary = self.headers['Content-type'].split('boundary=')[1]
            if ';' in boundary:
                boundary = boundary.split(';')[0]

        # get the nickname
        nickname = get_nickname_from_actor(actor_str)
        editor = None
        if nickname:
            editor = is_editor(base_dir, nickname)
        if not hashtag or not editor:
            if not nickname:
                print('WARN: nickname not found in ' + actor_str)
            else:
                print('WARN: nickname is not a moderator' + actor_str)
            self._redirect_headers(tag_screen_str, cookie, calling_domain)
            self.server.postreq_busy = False
            return

        if self.headers.get('Content-length'):
            length = int(self.headers['Content-length'])

            # check that the POST isn't too large
            if length > self.server.max_post_length:
                print('Maximum links data length exceeded ' + str(length))
                self._redirect_headers(tag_screen_str, cookie, calling_domain)
                self.server.postreq_busy = False
                return

        try:
            # read the bytes of the http form POST
            post_bytes = self.rfile.read(length)
        except SocketError as ex:
            if ex.errno == errno.ECONNRESET:
                print('EX: connection was reset while ' +
                      'reading bytes from http form POST')
            else:
                print('EX: error while reading bytes ' +
                      'from http form POST')
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        except ValueError as ex:
            print('EX: failed to read bytes for POST, ' + str(ex))
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return

        if not boundary:
            if b'--LYNX' in post_bytes:
                boundary = '--LYNX'

        if boundary:
            # extract all of the text fields into a dict
            fields = \
                extract_text_fields_in_post(post_bytes, boundary, debug)

            if fields.get('hashtagCategory'):
                category_str = fields['hashtagCategory'].lower()
                if not is_blocked_hashtag(base_dir, category_str) and \
                   not is_filtered(base_dir, nickname, domain, category_str,
                                   system_language):
                    set_hashtag_category(base_dir, hashtag,
                                         category_str, False)
            else:
                category_filename = base_dir + '/tags/' + hashtag + '.category'
                if os.path.isfile(category_filename):
                    try:
                        os.remove(category_filename)
                    except OSError:
                        print('EX: _set_hashtag_category unable to delete ' +
                              category_filename)

        # redirect back to the default timeline
        self._redirect_headers(tag_screen_str,
                               cookie, calling_domain)
        self.server.postreq_busy = False

    def _newswire_update(self, calling_domain: str, cookie: str,
                         path: str, base_dir: str,
                         domain: str, debug: bool,
                         default_timeline: str) -> None:
        """Updates the right newswire column of the timeline
        """
        users_path = path.replace('/newswiredata', '')
        users_path = users_path.replace('/editnewswire', '')
        actor_str = self._get_instance_url(calling_domain) + users_path

        boundary = None
        if ' boundary=' in self.headers['Content-type']:
            boundary = self.headers['Content-type'].split('boundary=')[1]
            if ';' in boundary:
                boundary = boundary.split(';')[0]

        # get the nickname
        nickname = get_nickname_from_actor(actor_str)
        moderator = None
        if nickname:
            moderator = is_moderator(base_dir, nickname)
        if not nickname or not moderator:
            if not nickname:
                print('WARN: nickname not found in ' + actor_str)
            else:
                print('WARN: nickname is not a moderator' + actor_str)
            self._redirect_headers(actor_str, cookie, calling_domain)
            self.server.postreq_busy = False
            return

        if self.headers.get('Content-length'):
            length = int(self.headers['Content-length'])

            # check that the POST isn't too large
            if length > self.server.max_post_length:
                print('Maximum newswire data length exceeded ' + str(length))
                self._redirect_headers(actor_str, cookie, calling_domain)
                self.server.postreq_busy = False
                return

        try:
            # read the bytes of the http form POST
            post_bytes = self.rfile.read(length)
        except SocketError as ex:
            if ex.errno == errno.ECONNRESET:
                print('EX: connection was reset while ' +
                      'reading bytes from http form POST')
            else:
                print('EX: error while reading bytes ' +
                      'from http form POST')
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        except ValueError as ex:
            print('EX: failed to read bytes for POST, ' + str(ex))
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return

        newswire_filename = base_dir + '/accounts/newswire.txt'

        if not boundary:
            if b'--LYNX' in post_bytes:
                boundary = '--LYNX'

        if boundary:
            # extract all of the text fields into a dict
            fields = \
                extract_text_fields_in_post(post_bytes, boundary, debug)
            if fields.get('editedNewswire'):
                newswire_str = fields['editedNewswire']
                # append a new newswire entry
                if fields.get('newNewswireFeed'):
                    if newswire_str:
                        if not newswire_str.endswith('\n'):
                            newswire_str += '\n'
                    newswire_str += fields['newNewswireFeed'] + '\n'
                try:
                    with open(newswire_filename, 'w+',
                              encoding='utf-8') as newsfile:
                        newsfile.write(newswire_str)
                except OSError:
                    print('EX: unable to write ' + newswire_filename)
            else:
                if fields.get('newNewswireFeed'):
                    # the text area is empty but there is a new feed added
                    newswire_str = fields['newNewswireFeed'] + '\n'
                    try:
                        with open(newswire_filename, 'w+',
                                  encoding='utf-8') as newsfile:
                            newsfile.write(newswire_str)
                    except OSError:
                        print('EX: unable to write ' + newswire_filename)
                else:
                    # text area has been cleared and there is no new feed
                    if os.path.isfile(newswire_filename):
                        try:
                            os.remove(newswire_filename)
                        except OSError:
                            print('EX: _newswire_update unable to delete ' +
                                  newswire_filename)

            # save filtered words list for the newswire
            filter_newswire_filename = \
                base_dir + '/accounts/' + \
                'news@' + domain + '/filters.txt'
            if fields.get('filteredWordsNewswire'):
                try:
                    with open(filter_newswire_filename, 'w+',
                              encoding='utf-8') as filterfile:
                        filterfile.write(fields['filteredWordsNewswire'])
                except OSError:
                    print('EX: unable to write ' + filter_newswire_filename)
            else:
                if os.path.isfile(filter_newswire_filename):
                    try:
                        os.remove(filter_newswire_filename)
                    except OSError:
                        print('EX: _newswire_update unable to delete ' +
                              filter_newswire_filename)

            # save dogwhistle words list
            dogwhistles_filename = base_dir + '/accounts/dogwhistles.txt'
            if fields.get('dogwhistleWords'):
                try:
                    with open(dogwhistles_filename, 'w+',
                              encoding='utf-8') as fp_dogwhistles:
                        fp_dogwhistles.write(fields['dogwhistleWords'])
                except OSError:
                    print('EX: unable to write ' + dogwhistles_filename)
                self.server.dogwhistles = \
                    load_dogwhistles(dogwhistles_filename)
            else:
                # save an empty file
                try:
                    with open(dogwhistles_filename, 'w+',
                              encoding='utf-8') as fp_dogwhistles:
                        fp_dogwhistles.write('')
                except OSError:
                    print('EX: unable to write ' + dogwhistles_filename)
                self.server.dogwhistles = {}

            # save news tagging rules
            hashtag_rules_filename = \
                base_dir + '/accounts/hashtagrules.txt'
            if fields.get('hashtagRulesList'):
                try:
                    with open(hashtag_rules_filename, 'w+',
                              encoding='utf-8') as rulesfile:
                        rulesfile.write(fields['hashtagRulesList'])
                except OSError:
                    print('EX: unable to write ' + hashtag_rules_filename)
            else:
                if os.path.isfile(hashtag_rules_filename):
                    try:
                        os.remove(hashtag_rules_filename)
                    except OSError:
                        print('EX: _newswire_update unable to delete ' +
                              hashtag_rules_filename)

            newswire_tusted_filename = \
                base_dir + '/accounts/newswiretrusted.txt'
            if fields.get('trustedNewswire'):
                newswire_trusted = fields['trustedNewswire']
                if not newswire_trusted.endswith('\n'):
                    newswire_trusted += '\n'
                try:
                    with open(newswire_tusted_filename, 'w+',
                              encoding='utf-8') as trustfile:
                        trustfile.write(newswire_trusted)
                except OSError:
                    print('EX: unable to write ' + newswire_tusted_filename)
            else:
                if os.path.isfile(newswire_tusted_filename):
                    try:
                        os.remove(newswire_tusted_filename)
                    except OSError:
                        print('EX: _newswire_update unable to delete ' +
                              newswire_tusted_filename)

        # redirect back to the default timeline
        self._redirect_headers(actor_str + '/' + default_timeline,
                               cookie, calling_domain)
        self.server.postreq_busy = False

    def _citations_update(self, calling_domain: str, cookie: str,
                          path: str, base_dir: str,
                          domain: str, debug: bool,
                          newswire: {}) -> None:
        """Updates the citations for a blog post after hitting
        update button on the citations screen
        """
        users_path = path.replace('/citationsdata', '')
        actor_str = self._get_instance_url(calling_domain) + users_path
        nickname = get_nickname_from_actor(actor_str)
        if not nickname:
            self.server.postreq_busy = False
            return

        citations_filename = \
            acct_dir(base_dir, nickname, domain) + '/.citations.txt'
        # remove any existing citations file
        if os.path.isfile(citations_filename):
            try:
                os.remove(citations_filename)
            except OSError:
                print('EX: _citations_update unable to delete ' +
                      citations_filename)

        if newswire and \
           ' boundary=' in self.headers['Content-type']:
            boundary = self.headers['Content-type'].split('boundary=')[1]
            if ';' in boundary:
                boundary = boundary.split(';')[0]

            length = int(self.headers['Content-length'])

            # check that the POST isn't too large
            if length > self.server.max_post_length:
                print('Maximum citations data length exceeded ' + str(length))
                self._redirect_headers(actor_str, cookie, calling_domain)
                self.server.postreq_busy = False
                return

            try:
                # read the bytes of the http form POST
                post_bytes = self.rfile.read(length)
            except SocketError as ex:
                if ex.errno == errno.ECONNRESET:
                    print('EX: connection was reset while ' +
                          'reading bytes from http form ' +
                          'citation screen POST')
                else:
                    print('EX: error while reading bytes ' +
                          'from http form citations screen POST')
                self.send_response(400)
                self.end_headers()
                self.server.postreq_busy = False
                return
            except ValueError as ex:
                print('EX: failed to read bytes for ' +
                      'citations screen POST, ' + str(ex))
                self.send_response(400)
                self.end_headers()
                self.server.postreq_busy = False
                return

            # extract all of the text fields into a dict
            fields = \
                extract_text_fields_in_post(post_bytes, boundary, debug)
            print('citationstest: ' + str(fields))
            citations = []
            for ctr in range(0, 128):
                field_name = 'newswire' + str(ctr)
                if not fields.get(field_name):
                    continue
                citations.append(fields[field_name])

            if citations:
                citations_str = ''
                for citation_date in citations:
                    citations_str += citation_date + '\n'
                # save citations dates, so that they can be added when
                # reloading the newblog screen
                try:
                    with open(citations_filename, 'w+',
                              encoding='utf-8') as citfile:
                        citfile.write(citations_str)
                except OSError:
                    print('EX: unable to write ' + citations_filename)

        # redirect back to the default timeline
        self._redirect_headers(actor_str + '/newblog',
                               cookie, calling_domain)
        self.server.postreq_busy = False

    def _news_post_edit(self, calling_domain: str, cookie: str,
                        path: str, base_dir: str,
                        domain: str, debug: bool) -> None:
        """edits a news post after receiving POST
        """
        users_path = path.replace('/newseditdata', '')
        users_path = users_path.replace('/editnewspost', '')
        actor_str = self._get_instance_url(calling_domain) + users_path

        boundary = None
        if ' boundary=' in self.headers['Content-type']:
            boundary = self.headers['Content-type'].split('boundary=')[1]
            if ';' in boundary:
                boundary = boundary.split(';')[0]

        # get the nickname
        nickname = get_nickname_from_actor(actor_str)
        editor_role = None
        if nickname:
            editor_role = is_editor(base_dir, nickname)
        if not nickname or not editor_role:
            if not nickname:
                print('WARN: nickname not found in ' + actor_str)
            else:
                print('WARN: nickname is not an editor' + actor_str)
            if self.server.news_instance:
                self._redirect_headers(actor_str + '/tlfeatures',
                                       cookie, calling_domain)
            else:
                self._redirect_headers(actor_str + '/tlnews',
                                       cookie, calling_domain)
            self.server.postreq_busy = False
            return

        if self.headers.get('Content-length'):
            length = int(self.headers['Content-length'])

            # check that the POST isn't too large
            if length > self.server.max_post_length:
                print('Maximum news data length exceeded ' + str(length))
                if self.server.news_instance:
                    self._redirect_headers(actor_str + '/tlfeatures',
                                           cookie, calling_domain)
                else:
                    self._redirect_headers(actor_str + '/tlnews',
                                           cookie, calling_domain)
                self.server.postreq_busy = False
                return

        try:
            # read the bytes of the http form POST
            post_bytes = self.rfile.read(length)
        except SocketError as ex:
            if ex.errno == errno.ECONNRESET:
                print('EX: connection was reset while ' +
                      'reading bytes from http form POST')
            else:
                print('EX: error while reading bytes ' +
                      'from http form POST')
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        except ValueError as ex:
            print('EX: failed to read bytes for POST, ' + str(ex))
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return

        if not boundary:
            if b'--LYNX' in post_bytes:
                boundary = '--LYNX'

        if boundary:
            # extract all of the text fields into a dict
            fields = \
                extract_text_fields_in_post(post_bytes, boundary, debug)
            news_post_url = None
            news_post_title = None
            news_post_content = None
            if fields.get('newsPostUrl'):
                news_post_url = fields['newsPostUrl']
            if fields.get('newsPostTitle'):
                news_post_title = fields['newsPostTitle']
            if fields.get('editedNewsPost'):
                news_post_content = fields['editedNewsPost']

            if news_post_url and news_post_content and news_post_title:
                # load the post
                post_filename = \
                    locate_post(base_dir, nickname, domain,
                                news_post_url)
                if post_filename:
                    post_json_object = load_json(post_filename)
                    # update the content and title
                    post_json_object['object']['summary'] = \
                        news_post_title
                    post_json_object['object']['content'] = \
                        news_post_content
                    content_map = post_json_object['object']['contentMap']
                    content_map[self.server.system_language] = \
                        news_post_content
                    # update newswire
                    pub_date = post_json_object['object']['published']
                    published_date = \
                        datetime.datetime.strptime(pub_date,
                                                   "%Y-%m-%dT%H:%M:%SZ")
                    if self.server.newswire.get(str(published_date)):
                        self.server.newswire[published_date][0] = \
                            news_post_title
                        self.server.newswire[published_date][4] = \
                            first_paragraph_from_string(news_post_content)
                        # save newswire
                        newswire_state_filename = \
                            base_dir + '/accounts/.newswirestate.json'
                        try:
                            save_json(self.server.newswire,
                                      newswire_state_filename)
                        except BaseException as ex:
                            print('EX: saving newswire state, ' + str(ex))

                    # remove any previous cached news posts
                    news_id = \
                        remove_id_ending(post_json_object['object']['id'])
                    news_id = news_id.replace('/', '#')
                    clear_from_post_caches(base_dir,
                                           self.server.recent_posts_cache,
                                           news_id)

                    # save the news post
                    save_json(post_json_object, post_filename)

        # redirect back to the default timeline
        if self.server.news_instance:
            self._redirect_headers(actor_str + '/tlfeatures',
                                   cookie, calling_domain)
        else:
            self._redirect_headers(actor_str + '/tlnews',
                                   cookie, calling_domain)
        self.server.postreq_busy = False

    def _profile_edit(self, calling_domain: str, cookie: str,
                      path: str, base_dir: str, http_prefix: str,
                      domain: str, domain_full: str,
                      onion_domain: str, i2p_domain: str,
                      debug: bool, allow_local_network_access: bool,
                      system_language: str,
                      content_license_url: str,
                      curr_session, proxy_type: str) -> None:
        """Updates your user profile after editing via the Edit button
        on the profile screen
        """
        users_path = path.replace('/profiledata', '')
        users_path = users_path.replace('/editprofile', '')
        actor_str = self._get_instance_url(calling_domain) + users_path

        boundary = None
        if ' boundary=' in self.headers['Content-type']:
            boundary = self.headers['Content-type'].split('boundary=')[1]
            if ';' in boundary:
                boundary = boundary.split(';')[0]

        # get the nickname
        nickname = get_nickname_from_actor(actor_str)
        if not nickname:
            print('WARN: nickname not found in ' + actor_str)
            self._redirect_headers(actor_str, cookie, calling_domain)
            self.server.postreq_busy = False
            return

        if self.headers.get('Content-length'):
            length = int(self.headers['Content-length'])

            # check that the POST isn't too large
            if length > self.server.max_post_length:
                print('Maximum profile data length exceeded ' +
                      str(length))
                self._redirect_headers(actor_str, cookie, calling_domain)
                self.server.postreq_busy = False
                return

        try:
            # read the bytes of the http form POST
            post_bytes = self.rfile.read(length)
        except SocketError as ex:
            if ex.errno == errno.ECONNRESET:
                print('EX: connection was reset while ' +
                      'reading bytes from http form POST')
            else:
                print('EX: error while reading bytes ' +
                      'from http form POST')
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        except ValueError as ex:
            print('EX: failed to read bytes for POST, ' + str(ex))
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return

        admin_nickname = get_config_param(self.server.base_dir, 'admin')

        if not boundary:
            if b'--LYNX' in post_bytes:
                boundary = '--LYNX'

        if boundary:
            # get the various avatar, banner and background images
            actor_changed = True
            profile_media_types = (
                'avatar', 'image',
                'banner', 'search_banner',
                'instanceLogo',
                'left_col_image', 'right_col_image',
                'submitImportFollows',
                'submitImportTheme'
            )
            profile_media_types_uploaded = {}
            for m_type in profile_media_types:
                # some images can only be changed by the admin
                if m_type == 'instanceLogo':
                    if nickname != admin_nickname:
                        print('WARN: only the admin can change ' +
                              'instance logo')
                        continue

                if debug:
                    print('DEBUG: profile update extracting ' + m_type +
                          ' image, zip, csv or font from POST')
                media_bytes, post_bytes = \
                    extract_media_in_form_post(post_bytes, boundary, m_type)
                if media_bytes:
                    if debug:
                        print('DEBUG: profile update ' + m_type +
                              ' image, zip, csv or font was found. ' +
                              str(len(media_bytes)) + ' bytes')
                else:
                    if debug:
                        print('DEBUG: profile update, no ' + m_type +
                              ' image, zip, csv or font was found in POST')
                    continue

                # Note: a .temp extension is used here so that at no
                # time is an image with metadata publicly exposed,
                # even for a few mS
                if m_type == 'instanceLogo':
                    filename_base = \
                        base_dir + '/accounts/login.temp'
                elif m_type == 'submitImportTheme':
                    if not os.path.isdir(base_dir + '/imports'):
                        os.mkdir(base_dir + '/imports')
                    filename_base = \
                        base_dir + '/imports/newtheme.zip'
                    if os.path.isfile(filename_base):
                        try:
                            os.remove(filename_base)
                        except OSError:
                            print('EX: _profile_edit unable to delete ' +
                                  filename_base)
                elif m_type == 'submitImportFollows':
                    filename_base = \
                        acct_dir(base_dir, nickname, domain) + \
                        '/import_following.csv'
                else:
                    filename_base = \
                        acct_dir(base_dir, nickname, domain) + \
                        '/' + m_type + '.temp'

                filename, _ = \
                    save_media_in_form_post(media_bytes, debug,
                                            filename_base)
                if filename:
                    print('Profile update POST ' + m_type +
                          ' media, zip, csv or font filename is ' + filename)
                else:
                    print('Profile update, no ' + m_type +
                          ' media, zip, csv or font filename in POST')
                    continue

                if m_type == 'submitImportFollows':
                    if os.path.isfile(filename_base):
                        print(nickname + ' imported follows csv')
                    else:
                        print('WARN: failed to import follows from csv for ' +
                              nickname)
                    continue

                if m_type == 'submitImportTheme':
                    if nickname == admin_nickname or \
                       is_artist(base_dir, nickname):
                        if import_theme(base_dir, filename):
                            print(nickname + ' uploaded a theme')
                    else:
                        print('Only admin or artist can import a theme')
                    continue

                post_image_filename = filename.replace('.temp', '')
                if debug:
                    print('DEBUG: POST ' + m_type +
                          ' media removing metadata')
                # remove existing etag
                if os.path.isfile(post_image_filename + '.etag'):
                    try:
                        os.remove(post_image_filename + '.etag')
                    except OSError:
                        print('EX: _profile_edit unable to delete ' +
                              post_image_filename + '.etag')

                city = get_spoofed_city(self.server.city,
                                        base_dir, nickname, domain)

                if self.server.low_bandwidth:
                    convert_image_to_low_bandwidth(filename)
                process_meta_data(base_dir, nickname, domain,
                                  filename, post_image_filename, city,
                                  content_license_url)
                if os.path.isfile(post_image_filename):
                    print('profile update POST ' + m_type +
                          ' image, zip or font saved to ' +
                          post_image_filename)
                    if m_type != 'instanceLogo':
                        last_part_of_image_filename = \
                            post_image_filename.split('/')[-1]
                        profile_media_types_uploaded[m_type] = \
                            last_part_of_image_filename
                        actor_changed = True
                else:
                    print('ERROR: profile update POST ' + m_type +
                          ' image or font could not be saved to ' +
                          post_image_filename)

            post_bytes_str = post_bytes.decode('utf-8')
            redirect_path = ''
            check_name_and_bio = False
            on_final_welcome_screen = False
            if 'name="previewAvatar"' in post_bytes_str:
                redirect_path = '/welcome_profile'
            elif 'name="initialWelcomeScreen"' in post_bytes_str:
                redirect_path = '/welcome'
            elif 'name="finalWelcomeScreen"' in post_bytes_str:
                check_name_and_bio = True
                redirect_path = '/welcome_final'
            elif 'name="welcomeCompleteButton"' in post_bytes_str:
                redirect_path = '/' + self.server.default_timeline
                welcome_screen_is_complete(self.server.base_dir, nickname,
                                           self.server.domain)
                on_final_welcome_screen = True
            elif 'name="submitExportTheme"' in post_bytes_str:
                print('submitExportTheme')
                theme_download_path = actor_str
                if export_theme(self.server.base_dir,
                                self.server.theme_name):
                    theme_download_path += \
                        '/exports/' + self.server.theme_name + '.zip'
                print('submitExportTheme path=' + theme_download_path)
                self._redirect_headers(theme_download_path,
                                       cookie, calling_domain)
                self.server.postreq_busy = False
                return

            # extract all of the text fields into a dict
            fields = \
                extract_text_fields_in_post(post_bytes, boundary, debug)
            if debug:
                if fields:
                    print('DEBUG: profile update text ' +
                          'field extracted from POST ' + str(fields))
                else:
                    print('WARN: profile update, no text ' +
                          'fields could be extracted from POST')

            # load the json for the actor for this user
            actor_filename = \
                acct_dir(base_dir, nickname, domain) + '.json'
            if os.path.isfile(actor_filename):
                actor_json = load_json(actor_filename)
                if actor_json:
                    if not actor_json.get('discoverable'):
                        # discoverable in profile directory
                        # which isn't implemented in Epicyon
                        actor_json['discoverable'] = True
                        actor_changed = True
                    if actor_json.get('capabilityAcquisitionEndpoint'):
                        del actor_json['capabilityAcquisitionEndpoint']
                        actor_changed = True
                    # update the avatar/image url file extension
                    uploads = profile_media_types_uploaded.items()
                    for m_type, last_part in uploads:
                        rep_str = '/' + last_part
                        if m_type == 'avatar':
                            actor_url = actor_json['icon']['url']
                            last_part_of_url = actor_url.split('/')[-1]
                            srch_str = '/' + last_part_of_url
                            actor_url = actor_url.replace(srch_str, rep_str)
                            actor_json['icon']['url'] = actor_url
                            print('actor_url: ' + actor_url)
                            if '.' in actor_url:
                                img_ext = actor_url.split('.')[-1]
                                if img_ext == 'jpg':
                                    img_ext = 'jpeg'
                                actor_json['icon']['mediaType'] = \
                                    'image/' + img_ext
                        elif m_type == 'image':
                            last_part_of_url = \
                                actor_json['image']['url'].split('/')[-1]
                            srch_str = '/' + last_part_of_url
                            actor_json['image']['url'] = \
                                actor_json['image']['url'].replace(srch_str,
                                                                   rep_str)
                            if '.' in actor_json['image']['url']:
                                img_ext = \
                                    actor_json['image']['url'].split('.')[-1]
                                if img_ext == 'jpg':
                                    img_ext = 'jpeg'
                                actor_json['image']['mediaType'] = \
                                    'image/' + img_ext

                    # set skill levels
                    skill_ctr = 1
                    actor_skills_ctr = no_of_actor_skills(actor_json)
                    while skill_ctr < 10:
                        skill_name = \
                            fields.get('skillName' + str(skill_ctr))
                        if not skill_name:
                            skill_ctr += 1
                            continue
                        if is_filtered(base_dir, nickname, domain, skill_name,
                                       system_language):
                            skill_ctr += 1
                            continue
                        skill_value = \
                            fields.get('skillValue' + str(skill_ctr))
                        if not skill_value:
                            skill_ctr += 1
                            continue
                        if not actor_has_skill(actor_json, skill_name):
                            actor_changed = True
                        else:
                            if actor_skill_value(actor_json, skill_name) != \
                               int(skill_value):
                                actor_changed = True
                        set_actor_skill_level(actor_json,
                                              skill_name, int(skill_value))
                        skills_str = self.server.translate['Skills']
                        skills_str = skills_str.lower()
                        set_hashtag_category(base_dir, skill_name,
                                             skills_str, False)
                        skill_ctr += 1
                    if no_of_actor_skills(actor_json) != \
                       actor_skills_ctr:
                        actor_changed = True

                    # change password
                    if fields.get('password') and \
                       fields.get('passwordconfirm'):
                        fields['password'] = \
                            remove_eol(fields['password']).strip()
                        fields['passwordconfirm'] = \
                            remove_eol(fields['passwordconfirm']).strip()
                        if valid_password(fields['password']) and \
                           fields['password'] == fields['passwordconfirm']:
                            # set password
                            store_basic_credentials(base_dir, nickname,
                                                    fields['password'])

                    # reply interval in hours
                    if fields.get('replyhours'):
                        if fields['replyhours'].isdigit():
                            set_reply_interval_hours(base_dir,
                                                     nickname, domain,
                                                     fields['replyhours'])

                    # change city
                    if fields.get('cityDropdown'):
                        city_filename = \
                            acct_dir(base_dir, nickname, domain) + '/city.txt'
                        try:
                            with open(city_filename, 'w+',
                                      encoding='utf-8') as fp_city:
                                fp_city.write(fields['cityDropdown'])
                        except OSError:
                            print('EX: unable to write city ' + city_filename)

                    # change displayed name
                    if fields.get('displayNickname'):
                        if fields['displayNickname'] != actor_json['name']:
                            display_name = \
                                remove_html(fields['displayNickname'])
                            if not is_filtered(base_dir,
                                               nickname, domain,
                                               display_name,
                                               system_language):
                                actor_json['name'] = display_name
                            else:
                                actor_json['name'] = nickname
                                if check_name_and_bio:
                                    redirect_path = 'previewAvatar'
                            actor_changed = True
                    else:
                        if check_name_and_bio:
                            redirect_path = 'previewAvatar'

                    # change the theme from edit profile screen
                    if nickname == admin_nickname or \
                       is_artist(base_dir, nickname):
                        if fields.get('themeDropdown'):
                            if self.server.theme_name != \
                               fields['themeDropdown']:
                                self.server.theme_name = \
                                    fields['themeDropdown']
                                set_theme(base_dir, self.server.theme_name,
                                          domain, allow_local_network_access,
                                          system_language,
                                          self.server.dyslexic_font, True)
                                self.server.text_mode_banner = \
                                    get_text_mode_banner(self.server.base_dir)
                                self.server.iconsCache = {}
                                self.server.fontsCache = {}
                                self.server.css_cache = {}
                                self.server.show_publish_as_icon = \
                                    get_config_param(self.server.base_dir,
                                                     'showPublishAsIcon')
                                self.server.full_width_tl_button_header = \
                                    get_config_param(self.server.base_dir,
                                                     'fullWidthTlButtonHeader')
                                self.server.icons_as_buttons = \
                                    get_config_param(self.server.base_dir,
                                                     'iconsAsButtons')
                                self.server.rss_icon_at_top = \
                                    get_config_param(self.server.base_dir,
                                                     'rssIconAtTop')
                                self.server.publish_button_at_top = \
                                    get_config_param(self.server.base_dir,
                                                     'publishButtonAtTop')
                                set_news_avatar(base_dir,
                                                fields['themeDropdown'],
                                                http_prefix,
                                                domain, domain_full)

                    if nickname == admin_nickname:
                        # change media instance status
                        if fields.get('mediaInstance'):
                            self.server.media_instance = False
                            self.server.default_timeline = 'inbox'
                            if fields['mediaInstance'] == 'on':
                                self.server.media_instance = True
                                self.server.blogs_instance = False
                                self.server.news_instance = False
                                self.server.default_timeline = 'tlmedia'
                            set_config_param(base_dir, "mediaInstance",
                                             self.server.media_instance)
                            set_config_param(base_dir, "blogsInstance",
                                             self.server.blogs_instance)
                            set_config_param(base_dir, "newsInstance",
                                             self.server.news_instance)
                        else:
                            if self.server.media_instance:
                                self.server.media_instance = False
                                self.server.default_timeline = 'inbox'
                                set_config_param(base_dir, "mediaInstance",
                                                 self.server.media_instance)

                        # is this a news theme?
                        if is_news_theme_name(self.server.base_dir,
                                              self.server.theme_name):
                            fields['newsInstance'] = 'on'

                        # change news instance status
                        if fields.get('newsInstance'):
                            self.server.news_instance = False
                            self.server.default_timeline = 'inbox'
                            if fields['newsInstance'] == 'on':
                                self.server.news_instance = True
                                self.server.blogs_instance = False
                                self.server.media_instance = False
                                self.server.default_timeline = 'tlfeatures'
                            set_config_param(base_dir, "mediaInstance",
                                             self.server.media_instance)
                            set_config_param(base_dir, "blogsInstance",
                                             self.server.blogs_instance)
                            set_config_param(base_dir, "newsInstance",
                                             self.server.news_instance)
                        else:
                            if self.server.news_instance:
                                self.server.news_instance = False
                                self.server.default_timeline = 'inbox'
                                set_config_param(base_dir, "newsInstance",
                                                 self.server.media_instance)

                        # change blog instance status
                        if fields.get('blogsInstance'):
                            self.server.blogs_instance = False
                            self.server.default_timeline = 'inbox'
                            if fields['blogsInstance'] == 'on':
                                self.server.blogs_instance = True
                                self.server.media_instance = False
                                self.server.news_instance = False
                                self.server.default_timeline = 'tlblogs'
                            set_config_param(base_dir, "blogsInstance",
                                             self.server.blogs_instance)
                            set_config_param(base_dir, "mediaInstance",
                                             self.server.media_instance)
                            set_config_param(base_dir, "newsInstance",
                                             self.server.news_instance)
                        else:
                            if self.server.blogs_instance:
                                self.server.blogs_instance = False
                                self.server.default_timeline = 'inbox'
                                set_config_param(base_dir, "blogsInstance",
                                                 self.server.blogs_instance)

                        # change instance title
                        if fields.get('instanceTitle'):
                            curr_instance_title = \
                                get_config_param(base_dir, 'instanceTitle')
                            if fields['instanceTitle'] != curr_instance_title:
                                set_config_param(base_dir, 'instanceTitle',
                                                 fields['instanceTitle'])

                        # change YouTube alternate domain
                        if fields.get('ytdomain'):
                            curr_yt_domain = self.server.yt_replace_domain
                            if fields['ytdomain'] != curr_yt_domain:
                                new_yt_domain = fields['ytdomain']
                                if '://' in new_yt_domain:
                                    new_yt_domain = \
                                        new_yt_domain.split('://')[1]
                                if '/' in new_yt_domain:
                                    new_yt_domain = new_yt_domain.split('/')[0]
                                if '.' in new_yt_domain:
                                    set_config_param(base_dir, 'youtubedomain',
                                                     new_yt_domain)
                                    self.server.yt_replace_domain = \
                                        new_yt_domain
                        else:
                            set_config_param(base_dir, 'youtubedomain', '')
                            self.server.yt_replace_domain = None

                        # change twitter alternate domain
                        if fields.get('twitterdomain'):
                            curr_twitter_domain = \
                                self.server.twitter_replacement_domain
                            if fields['twitterdomain'] != curr_twitter_domain:
                                new_twitter_domain = fields['twitterdomain']
                                if '://' in new_twitter_domain:
                                    new_twitter_domain = \
                                        new_twitter_domain.split('://')[1]
                                if '/' in new_twitter_domain:
                                    new_twitter_domain = \
                                        new_twitter_domain.split('/')[0]
                                if '.' in new_twitter_domain:
                                    set_config_param(base_dir, 'twitterdomain',
                                                     new_twitter_domain)
                                    self.server.twitter_replacement_domain = \
                                        new_twitter_domain
                        else:
                            set_config_param(base_dir, 'twitterdomain', '')
                            self.server.twitter_replacement_domain = None

                        # change custom post submit button text
                        curr_custom_submit_text = \
                            get_config_param(base_dir, 'customSubmitText')
                        if fields.get('customSubmitText'):
                            if fields['customSubmitText'] != \
                               curr_custom_submit_text:
                                custom_text = fields['customSubmitText']
                                set_config_param(base_dir, 'customSubmitText',
                                                 custom_text)
                        else:
                            if curr_custom_submit_text:
                                set_config_param(base_dir, 'customSubmitText',
                                                 '')

                        # libretranslate URL
                        curr_libretranslate_url = \
                            get_config_param(base_dir,
                                             'libretranslateUrl')
                        if fields.get('libretranslateUrl'):
                            if fields['libretranslateUrl'] != \
                               curr_libretranslate_url:
                                lt_url = fields['libretranslateUrl']
                                if '://' in lt_url and \
                                   '.' in lt_url:
                                    set_config_param(base_dir,
                                                     'libretranslateUrl',
                                                     lt_url)
                        else:
                            if curr_libretranslate_url:
                                set_config_param(base_dir,
                                                 'libretranslateUrl', '')

                        # libretranslate API Key
                        curr_libretranslate_api_key = \
                            get_config_param(base_dir,
                                             'libretranslateApiKey')
                        if fields.get('libretranslateApiKey'):
                            if fields['libretranslateApiKey'] != \
                               curr_libretranslate_api_key:
                                lt_api_key = fields['libretranslateApiKey']
                                set_config_param(base_dir,
                                                 'libretranslateApiKey',
                                                 lt_api_key)
                        else:
                            if curr_libretranslate_api_key:
                                set_config_param(base_dir,
                                                 'libretranslateApiKey', '')

                        # change instance short description
                        if fields.get('contentLicenseUrl'):
                            if fields['contentLicenseUrl'] != \
                               self.server.content_license_url:
                                license_str = fields['contentLicenseUrl']
                                set_config_param(base_dir,
                                                 'contentLicenseUrl',
                                                 license_str)
                                self.server.content_license_url = \
                                    license_str
                        else:
                            license_str = \
                                'https://creativecommons.org/licenses/by/4.0'
                            set_config_param(base_dir,
                                             'contentLicenseUrl',
                                             license_str)
                            self.server.content_license_url = license_str

                        # change instance short description
                        curr_instance_description_short = \
                            get_config_param(base_dir,
                                             'instanceDescriptionShort')
                        if fields.get('instanceDescriptionShort'):
                            if fields['instanceDescriptionShort'] != \
                               curr_instance_description_short:
                                idesc = fields['instanceDescriptionShort']
                                set_config_param(base_dir,
                                                 'instanceDescriptionShort',
                                                 idesc)
                        else:
                            if curr_instance_description_short:
                                set_config_param(base_dir,
                                                 'instanceDescriptionShort',
                                                 '')

                        # change instance description
                        curr_instance_description = \
                            get_config_param(base_dir, 'instanceDescription')
                        if fields.get('instanceDescription'):
                            if fields['instanceDescription'] != \
                               curr_instance_description:
                                set_config_param(base_dir,
                                                 'instanceDescription',
                                                 fields['instanceDescription'])
                        else:
                            if curr_instance_description:
                                set_config_param(base_dir,
                                                 'instanceDescription', '')

                    # change email address
                    current_email_address = get_email_address(actor_json)
                    if fields.get('email'):
                        if fields['email'] != current_email_address:
                            set_email_address(actor_json, fields['email'])
                            actor_changed = True
                    else:
                        if current_email_address:
                            set_email_address(actor_json, '')
                            actor_changed = True

                    # change xmpp address
                    current_xmpp_address = get_xmpp_address(actor_json)
                    if fields.get('xmppAddress'):
                        if fields['xmppAddress'] != current_xmpp_address:
                            set_xmpp_address(actor_json,
                                             fields['xmppAddress'])
                            actor_changed = True
                    else:
                        if current_xmpp_address:
                            set_xmpp_address(actor_json, '')
                            actor_changed = True

                    # change matrix address
                    current_matrix_address = get_matrix_address(actor_json)
                    if fields.get('matrixAddress'):
                        if fields['matrixAddress'] != current_matrix_address:
                            set_matrix_address(actor_json,
                                               fields['matrixAddress'])
                            actor_changed = True
                    else:
                        if current_matrix_address:
                            set_matrix_address(actor_json, '')
                            actor_changed = True

                    # change SSB address
                    current_ssb_address = get_ssb_address(actor_json)
                    if fields.get('ssbAddress'):
                        if fields['ssbAddress'] != current_ssb_address:
                            set_ssb_address(actor_json,
                                            fields['ssbAddress'])
                            actor_changed = True
                    else:
                        if current_ssb_address:
                            set_ssb_address(actor_json, '')
                            actor_changed = True

                    # change blog address
                    current_blog_address = get_blog_address(actor_json)
                    if fields.get('blogAddress'):
                        if fields['blogAddress'] != current_blog_address:
                            set_blog_address(actor_json,
                                             fields['blogAddress'])
                            actor_changed = True
                        site_is_verified(curr_session,
                                         self.server.base_dir,
                                         self.server.http_prefix,
                                         nickname, domain,
                                         fields['blogAddress'],
                                         True,
                                         self.server.debug)
                    else:
                        if current_blog_address:
                            set_blog_address(actor_json, '')
                            actor_changed = True

                    # change Languages address
                    current_show_languages = get_actor_languages(actor_json)
                    if fields.get('showLanguages'):
                        if fields['showLanguages'] != current_show_languages:
                            set_actor_languages(actor_json,
                                                fields['showLanguages'])
                            actor_changed = True
                    else:
                        if current_show_languages:
                            set_actor_languages(actor_json, '')
                            actor_changed = True

                    # change time zone
                    timezone = \
                        get_account_timezone(base_dir, nickname, domain)
                    if fields.get('timeZone'):
                        if fields['timeZone'] != timezone:
                            set_account_timezone(base_dir,
                                                 nickname, domain,
                                                 fields['timeZone'])
                            self.server.account_timezone[nickname] = \
                                fields['timeZone']
                            actor_changed = True
                    else:
                        if timezone:
                            set_account_timezone(base_dir,
                                                 nickname, domain, '')
                            del self.server.account_timezone[nickname]
                            actor_changed = True

                    # set post expiry period in days
                    post_expiry_period_days = \
                        get_post_expiry_days(base_dir, nickname, domain)
                    if fields.get('postExpiryPeriod'):
                        if fields['postExpiryPeriod'] != \
                           str(post_expiry_period_days):
                            post_expiry_period_days = \
                                fields['postExpiryPeriod']
                            set_post_expiry_days(base_dir, nickname, domain,
                                                 post_expiry_period_days)
                            actor_changed = True
                    else:
                        if post_expiry_period_days > 0:
                            set_post_expiry_days(base_dir, nickname, domain, 0)
                            actor_changed = True

                    # change tox address
                    current_tox_address = get_tox_address(actor_json)
                    if fields.get('toxAddress'):
                        if fields['toxAddress'] != current_tox_address:
                            set_tox_address(actor_json,
                                            fields['toxAddress'])
                            actor_changed = True
                    else:
                        if current_tox_address:
                            set_tox_address(actor_json, '')
                            actor_changed = True

                    # change briar address
                    current_briar_address = get_briar_address(actor_json)
                    if fields.get('briarAddress'):
                        if fields['briarAddress'] != current_briar_address:
                            set_briar_address(actor_json,
                                              fields['briarAddress'])
                            actor_changed = True
                    else:
                        if current_briar_address:
                            set_briar_address(actor_json, '')
                            actor_changed = True

                    # change cwtch address
                    current_cwtch_address = get_cwtch_address(actor_json)
                    if fields.get('cwtchAddress'):
                        if fields['cwtchAddress'] != current_cwtch_address:
                            set_cwtch_address(actor_json,
                                              fields['cwtchAddress'])
                            actor_changed = True
                    else:
                        if current_cwtch_address:
                            set_cwtch_address(actor_json, '')
                            actor_changed = True

                    # change ntfy url
                    if fields.get('ntfyUrl'):
                        ntfy_url_file = \
                            base_dir + '/accounts/' + \
                            nickname + '@' + domain + '/.ntfy_url'
                        try:
                            with open(ntfy_url_file, 'w+',
                                      encoding='utf-8') as fp_ntfy:
                                fp_ntfy.write(fields['ntfyUrl'])
                        except OSError:
                            print('EX: unable to save ntfy url ' +
                                  ntfy_url_file)

                    # change ntfy topic
                    if fields.get('ntfyTopic'):
                        ntfy_topic_file = \
                            base_dir + '/accounts/' + \
                            nickname + '@' + domain + '/.ntfy_topic'
                        try:
                            with open(ntfy_topic_file, 'w+',
                                      encoding='utf-8') as fp_ntfy:
                                fp_ntfy.write(fields['ntfyTopic'])
                        except OSError:
                            print('EX: unable to save ntfy topic ' +
                                  ntfy_topic_file)

                    # change Enigma public key
                    currentenigma_pub_key = get_enigma_pub_key(actor_json)
                    if fields.get('enigmapubkey'):
                        if fields['enigmapubkey'] != currentenigma_pub_key:
                            set_enigma_pub_key(actor_json,
                                               fields['enigmapubkey'])
                            actor_changed = True
                    else:
                        if currentenigma_pub_key:
                            set_enigma_pub_key(actor_json, '')
                            actor_changed = True

                    # change PGP public key
                    currentpgp_pub_key = get_pgp_pub_key(actor_json)
                    if fields.get('pgp'):
                        if fields['pgp'] != currentpgp_pub_key:
                            set_pgp_pub_key(actor_json,
                                            fields['pgp'])
                            actor_changed = True
                    else:
                        if currentpgp_pub_key:
                            set_pgp_pub_key(actor_json, '')
                            actor_changed = True

                    # change PGP fingerprint
                    currentpgp_fingerprint = get_pgp_fingerprint(actor_json)
                    if fields.get('openpgp'):
                        if fields['openpgp'] != currentpgp_fingerprint:
                            set_pgp_fingerprint(actor_json,
                                                fields['openpgp'])
                            actor_changed = True
                    else:
                        if currentpgp_fingerprint:
                            set_pgp_fingerprint(actor_json, '')
                            actor_changed = True

                    # change donation link
                    current_donate_url = get_donation_url(actor_json)
                    if fields.get('donateUrl'):
                        if fields['donateUrl'] != current_donate_url:
                            set_donation_url(actor_json,
                                             fields['donateUrl'])
                            actor_changed = True
                    else:
                        if current_donate_url:
                            set_donation_url(actor_json, '')
                            actor_changed = True

                    # change website
                    current_website = \
                        get_website(actor_json, self.server.translate)
                    if fields.get('websiteUrl'):
                        if fields['websiteUrl'] != current_website:
                            set_website(actor_json,
                                        fields['websiteUrl'],
                                        self.server.translate)
                            actor_changed = True
                        site_is_verified(curr_session,
                                         self.server.base_dir,
                                         self.server.http_prefix,
                                         nickname, domain,
                                         fields['websiteUrl'],
                                         True,
                                         self.server.debug)
                    else:
                        if current_website:
                            set_website(actor_json, '', self.server.translate)
                            actor_changed = True

                    # change gemini link
                    current_gemini_link = \
                        get_gemini_link(actor_json, self.server.translate)
                    if fields.get('geminiLink'):
                        if fields['geminiLink'] != current_gemini_link:
                            set_gemini_link(actor_json,
                                            fields['geminiLink'],
                                            self.server.translate)
                            actor_changed = True
                    else:
                        if current_gemini_link:
                            set_gemini_link(actor_json, '',
                                            self.server.translate)
                            actor_changed = True

                    # account moved to new address
                    moved_to = ''
                    if actor_json.get('movedTo'):
                        moved_to = actor_json['movedTo']
                    if fields.get('movedTo'):
                        if fields['movedTo'] != moved_to and \
                           '://' in fields['movedTo'] and \
                           '.' in fields['movedTo']:
                            actor_json['movedTo'] = moved_to
                            actor_changed = True
                    else:
                        if moved_to:
                            del actor_json['movedTo']
                            actor_changed = True

                    # Other accounts (alsoKnownAs)
                    occupation_name = get_occupation_name(actor_json)
                    if fields.get('occupationName'):
                        fields['occupationName'] = \
                            remove_html(fields['occupationName'])
                        if occupation_name != \
                           fields['occupationName']:
                            set_occupation_name(actor_json,
                                                fields['occupationName'])
                            actor_changed = True
                    else:
                        if occupation_name:
                            set_occupation_name(actor_json, '')
                            actor_changed = True

                    # Other accounts (alsoKnownAs)
                    also_known_as = []
                    if actor_json.get('alsoKnownAs'):
                        also_known_as = actor_json['alsoKnownAs']
                    if fields.get('alsoKnownAs'):
                        also_known_as_str = ''
                        also_known_as_ctr = 0
                        for alt_actor in also_known_as:
                            if also_known_as_ctr > 0:
                                also_known_as_str += ', '
                            also_known_as_str += alt_actor
                            also_known_as_ctr += 1
                        if fields['alsoKnownAs'] != also_known_as_str and \
                           '://' in fields['alsoKnownAs'] and \
                           '@' not in fields['alsoKnownAs'] and \
                           '.' in fields['alsoKnownAs']:
                            if ';' in fields['alsoKnownAs']:
                                fields['alsoKnownAs'] = \
                                    fields['alsoKnownAs'].replace(';', ',')
                            new_also_known_as = \
                                fields['alsoKnownAs'].split(',')
                            also_known_as = []
                            for alt_actor in new_also_known_as:
                                alt_actor = alt_actor.strip()
                                if '://' in alt_actor and '.' in alt_actor:
                                    if alt_actor not in also_known_as:
                                        also_known_as.append(alt_actor)
                            actor_json['alsoKnownAs'] = also_known_as
                            actor_changed = True
                    else:
                        if also_known_as:
                            del actor_json['alsoKnownAs']
                            actor_changed = True

                    # change user bio
                    if fields.get('bio'):
                        if fields['bio'] != actor_json['summary']:
                            bio_str = remove_html(fields['bio'])
                            if not is_filtered(base_dir,
                                               nickname, domain, bio_str,
                                               system_language):
                                actor_tags = {}
                                actor_json['summary'] = \
                                    add_html_tags(base_dir,
                                                  http_prefix,
                                                  nickname,
                                                  domain_full,
                                                  bio_str, [], actor_tags,
                                                  self.server.translate)
                                if actor_tags:
                                    actor_json['tag'] = []
                                    for _, tag in actor_tags.items():
                                        actor_json['tag'].append(tag)
                                actor_changed = True
                            else:
                                if check_name_and_bio:
                                    redirect_path = 'previewAvatar'
                    else:
                        if check_name_and_bio:
                            redirect_path = 'previewAvatar'

                    admin_nickname = \
                        get_config_param(base_dir, 'admin')

                    if admin_nickname:
                        # whether to require jsonld signatures
                        # on all incoming posts
                        if path.startswith('/users/' +
                                           admin_nickname + '/'):
                            show_node_info_accounts = False
                            if fields.get('showNodeInfoAccounts'):
                                if fields['showNodeInfoAccounts'] == 'on':
                                    show_node_info_accounts = True
                            self.server.show_node_info_accounts = \
                                show_node_info_accounts
                            set_config_param(base_dir,
                                             "showNodeInfoAccounts",
                                             show_node_info_accounts)

                            show_node_info_version = False
                            if fields.get('showNodeInfoVersion'):
                                if fields['showNodeInfoVersion'] == 'on':
                                    show_node_info_version = True
                            self.server.show_node_info_version = \
                                show_node_info_version
                            set_config_param(base_dir,
                                             "showNodeInfoVersion",
                                             show_node_info_version)

                            verify_all_signatures = False
                            if fields.get('verifyallsignatures'):
                                if fields['verifyallsignatures'] == 'on':
                                    verify_all_signatures = True
                            self.server.verify_all_signatures = \
                                verify_all_signatures
                            set_config_param(base_dir, "verifyAllSignatures",
                                             verify_all_signatures)

                            broch_mode = False
                            if fields.get('brochMode'):
                                if fields['brochMode'] == 'on':
                                    broch_mode = True
                            curr_broch_mode = \
                                get_config_param(base_dir, "brochMode")
                            if broch_mode != curr_broch_mode:
                                set_broch_mode(self.server.base_dir,
                                               self.server.domain_full,
                                               broch_mode)
                                set_config_param(base_dir, 'brochMode',
                                                 broch_mode)

                            # shared item federation domains
                            si_domain_updated = False
                            fed_domains_variable = \
                                "sharedItemsFederatedDomains"
                            fed_domains_str = \
                                get_config_param(base_dir,
                                                 fed_domains_variable)
                            if not fed_domains_str:
                                fed_domains_str = ''
                            shared_items_form_str = ''
                            if fields.get('shareDomainList'):
                                shared_it_list = \
                                    fed_domains_str.split(',')
                                for shared_federated_domain in shared_it_list:
                                    shared_items_form_str += \
                                        shared_federated_domain.strip() + '\n'

                                share_domain_list = fields['shareDomainList']
                                if share_domain_list != \
                                   shared_items_form_str:
                                    shared_items_form_str2 = \
                                        share_domain_list.replace('\n', ',')
                                    shared_items_field = \
                                        "sharedItemsFederatedDomains"
                                    set_config_param(base_dir,
                                                     shared_items_field,
                                                     shared_items_form_str2)
                                    si_domain_updated = True
                            else:
                                if fed_domains_str:
                                    shared_items_field = \
                                        "sharedItemsFederatedDomains"
                                    set_config_param(base_dir,
                                                     shared_items_field,
                                                     '')
                                    si_domain_updated = True
                            if si_domain_updated:
                                si_domains = shared_items_form_str.split('\n')
                                si_tokens = \
                                    self.server.shared_item_federation_tokens
                                self.server.shared_items_federated_domains = \
                                    si_domains
                                domain_full = self.server.domain_full
                                base_dir = \
                                    self.server.base_dir
                                self.server.shared_item_federation_tokens = \
                                    merge_shared_item_tokens(base_dir,
                                                             domain_full,
                                                             si_domains,
                                                             si_tokens)

                        # change moderators list
                        set_roles_from_list(base_dir, domain, admin_nickname,
                                            'moderators', 'moderator', fields,
                                            path, 'moderators.txt')

                        # change site editors list
                        set_roles_from_list(base_dir, domain, admin_nickname,
                                            'editors', 'editor', fields,
                                            path, 'editors.txt')

                        # change site devops list
                        set_roles_from_list(base_dir, domain, admin_nickname,
                                            'devopslist', 'devops', fields,
                                            path, 'devops.txt')

                        # change site counselors list
                        set_roles_from_list(base_dir, domain, admin_nickname,
                                            'counselors', 'counselor', fields,
                                            path, 'counselors.txt')

                        # change site artists list
                        set_roles_from_list(base_dir, domain, admin_nickname,
                                            'artists', 'artist', fields,
                                            path, 'artists.txt')

                    # remove scheduled posts
                    if fields.get('removeScheduledPosts'):
                        if fields['removeScheduledPosts'] == 'on':
                            remove_scheduled_posts(base_dir,
                                                   nickname, domain)

                    # approve followers
                    if on_final_welcome_screen:
                        # Default setting created via the welcome screen
                        actor_json['manuallyApprovesFollowers'] = True
                        actor_changed = True
                    else:
                        approve_followers = False
                        if fields.get('approveFollowers'):
                            if fields['approveFollowers'] == 'on':
                                approve_followers = True
                        if approve_followers != \
                           actor_json['manuallyApprovesFollowers']:
                            actor_json['manuallyApprovesFollowers'] = \
                                approve_followers
                            actor_changed = True

                    # reject spam actors
                    reject_spam_actors = False
                    if fields.get('rejectSpamActors'):
                        if fields['rejectSpamActors'] == 'on':
                            reject_spam_actors = True
                    curr_reject_spam_actors = False
                    actor_spam_filter_filename = \
                        acct_dir(base_dir, nickname, domain) + \
                        '/.reject_spam_actors'
                    if os.path.isfile(actor_spam_filter_filename):
                        curr_reject_spam_actors = True
                    if reject_spam_actors != curr_reject_spam_actors:
                        if reject_spam_actors:
                            try:
                                with open(actor_spam_filter_filename, 'w+',
                                          encoding='utf-8') as fp_spam:
                                    fp_spam.write('\n')
                            except OSError:
                                print('EX: unable to write reject spam actors')
                        else:
                            try:
                                os.remove(actor_spam_filter_filename)
                            except OSError:
                                print('EX: ' +
                                      'unable to remove reject spam actors')

                    # keep DMs during post expiry
                    expire_keep_dms = False
                    if fields.get('expiryKeepDMs'):
                        if fields['expiryKeepDMs'] == 'on':
                            expire_keep_dms = True
                    curr_keep_dms = \
                        get_post_expiry_keep_dms(base_dir, nickname, domain)
                    if curr_keep_dms != expire_keep_dms:
                        set_post_expiry_keep_dms(base_dir, nickname, domain,
                                                 expire_keep_dms)
                        actor_changed = True

                    # remove a custom font
                    if fields.get('removeCustomFont'):
                        if (fields['removeCustomFont'] == 'on' and
                            (is_artist(base_dir, nickname) or
                             path.startswith('/users/' +
                                             admin_nickname + '/'))):
                            font_ext = ('woff', 'woff2', 'otf', 'ttf')
                            for ext in font_ext:
                                if os.path.isfile(base_dir +
                                                  '/fonts/custom.' + ext):
                                    try:
                                        os.remove(base_dir +
                                                  '/fonts/custom.' + ext)
                                    except OSError:
                                        print('EX: _profile_edit ' +
                                              'unable to delete ' +
                                              base_dir +
                                              '/fonts/custom.' + ext)
                                if os.path.isfile(base_dir +
                                                  '/fonts/custom.' + ext +
                                                  '.etag'):
                                    try:
                                        os.remove(base_dir +
                                                  '/fonts/custom.' + ext +
                                                  '.etag')
                                    except OSError:
                                        print('EX: _profile_edit ' +
                                              'unable to delete ' +
                                              base_dir + '/fonts/custom.' +
                                              ext + '.etag')
                            curr_theme = get_theme(base_dir)
                            if curr_theme:
                                self.server.theme_name = curr_theme
                                allow_local_network_access = \
                                    self.server.allow_local_network_access
                                set_theme(base_dir, curr_theme, domain,
                                          allow_local_network_access,
                                          system_language,
                                          self.server.dyslexic_font, False)
                                self.server.text_mode_banner = \
                                    get_text_mode_banner(base_dir)
                                self.server.iconsCache = {}
                                self.server.fontsCache = {}
                                self.server.show_publish_as_icon = \
                                    get_config_param(base_dir,
                                                     'showPublishAsIcon')
                                self.server.full_width_tl_button_header = \
                                    get_config_param(base_dir,
                                                     'fullWidthTimeline' +
                                                     'ButtonHeader')
                                self.server.icons_as_buttons = \
                                    get_config_param(base_dir,
                                                     'iconsAsButtons')
                                self.server.rss_icon_at_top = \
                                    get_config_param(base_dir,
                                                     'rssIconAtTop')
                                self.server.publish_button_at_top = \
                                    get_config_param(base_dir,
                                                     'publishButtonAtTop')

                    # only receive DMs from accounts you follow
                    follow_dms_filename = \
                        acct_dir(base_dir, nickname, domain) + '/.followDMs'
                    if on_final_welcome_screen:
                        # initial default setting created via
                        # the welcome screen
                        try:
                            with open(follow_dms_filename, 'w+',
                                      encoding='utf-8') as ffile:
                                ffile.write('\n')
                        except OSError:
                            print('EX: unable to write follow DMs ' +
                                  follow_dms_filename)
                        actor_changed = True
                    else:
                        follow_dms_active = False
                        if fields.get('followDMs'):
                            if fields['followDMs'] == 'on':
                                follow_dms_active = True
                                try:
                                    with open(follow_dms_filename, 'w+',
                                              encoding='utf-8') as ffile:
                                        ffile.write('\n')
                                except OSError:
                                    print('EX: unable to write follow DMs 2 ' +
                                          follow_dms_filename)
                        if not follow_dms_active:
                            if os.path.isfile(follow_dms_filename):
                                try:
                                    os.remove(follow_dms_filename)
                                except OSError:
                                    print('EX: _profile_edit ' +
                                          'unable to delete ' +
                                          follow_dms_filename)

                    # remove Twitter retweets
                    remove_twitter_filename = \
                        acct_dir(base_dir, nickname, domain) + \
                        '/.removeTwitter'
                    remove_twitter_active = False
                    if fields.get('removeTwitter'):
                        if fields['removeTwitter'] == 'on':
                            remove_twitter_active = True
                            try:
                                with open(remove_twitter_filename, 'w+',
                                          encoding='utf-8') as rfile:
                                    rfile.write('\n')
                            except OSError:
                                print('EX: unable to write remove twitter ' +
                                      remove_twitter_filename)
                    if not remove_twitter_active:
                        if os.path.isfile(remove_twitter_filename):
                            try:
                                os.remove(remove_twitter_filename)
                            except OSError:
                                print('EX: _profile_edit ' +
                                      'unable to delete ' +
                                      remove_twitter_filename)

                    # hide Like button
                    hide_like_button_file = \
                        acct_dir(base_dir, nickname, domain) + \
                        '/.hideLikeButton'
                    notify_likes_filename = \
                        acct_dir(base_dir, nickname, domain) + \
                        '/.notifyLikes'
                    hide_like_button_active = False
                    if fields.get('hideLikeButton'):
                        if fields['hideLikeButton'] == 'on':
                            hide_like_button_active = True
                            try:
                                with open(hide_like_button_file, 'w+',
                                          encoding='utf-8') as rfil:
                                    rfil.write('\n')
                            except OSError:
                                print('EX: unable to write hide like ' +
                                      hide_like_button_file)
                            # remove notify likes selection
                            if os.path.isfile(notify_likes_filename):
                                try:
                                    os.remove(notify_likes_filename)
                                except OSError:
                                    print('EX: _profile_edit ' +
                                          'unable to delete ' +
                                          notify_likes_filename)
                    if not hide_like_button_active:
                        if os.path.isfile(hide_like_button_file):
                            try:
                                os.remove(hide_like_button_file)
                            except OSError:
                                print('EX: _profile_edit ' +
                                      'unable to delete ' +
                                      hide_like_button_file)

                    # Minimize all images from edit profile screen
                    minimize_all_images = False
                    if fields.get('minimizeAllImages'):
                        if fields['minimizeAllImages'] == 'on':
                            minimize_all_images = True
                            min_img_acct = self.server.min_images_for_accounts
                            set_minimize_all_images(base_dir,
                                                    nickname, domain,
                                                    True, min_img_acct)
                            print('min_images_for_accounts: ' +
                                  str(min_img_acct))
                    if not minimize_all_images:
                        min_img_acct = self.server.min_images_for_accounts
                        set_minimize_all_images(base_dir,
                                                nickname, domain,
                                                False, min_img_acct)
                        print('min_images_for_accounts: ' +
                              str(min_img_acct))

                    # hide Reaction button
                    hide_reaction_button_file = \
                        acct_dir(base_dir, nickname, domain) + \
                        '/.hideReactionButton'
                    notify_reactions_filename = \
                        acct_dir(base_dir, nickname, domain) + \
                        '/.notifyReactions'
                    hide_reaction_button_active = False
                    if fields.get('hideReactionButton'):
                        if fields['hideReactionButton'] == 'on':
                            hide_reaction_button_active = True
                            try:
                                with open(hide_reaction_button_file, 'w+',
                                          encoding='utf-8') as rfile:
                                    rfile.write('\n')
                            except OSError:
                                print('EX: unable to write hide reaction ' +
                                      hide_reaction_button_file)
                            # remove notify Reaction selection
                            if os.path.isfile(notify_reactions_filename):
                                try:
                                    os.remove(notify_reactions_filename)
                                except OSError:
                                    print('EX: _profile_edit ' +
                                          'unable to delete ' +
                                          notify_reactions_filename)
                    if not hide_reaction_button_active:
                        if os.path.isfile(hide_reaction_button_file):
                            try:
                                os.remove(hide_reaction_button_file)
                            except OSError:
                                print('EX: _profile_edit ' +
                                      'unable to delete ' +
                                      hide_reaction_button_file)

                    # bold reading checkbox
                    bold_reading_filename = \
                        acct_dir(base_dir, nickname, domain) + \
                        '/.boldReading'
                    bold_reading = False
                    if fields.get('boldReading'):
                        if fields['boldReading'] == 'on':
                            bold_reading = True
                            self.server.bold_reading[nickname] = True
                            try:
                                with open(bold_reading_filename, 'w+',
                                          encoding='utf-8') as rfile:
                                    rfile.write('\n')
                            except OSError:
                                print('EX: unable to write bold reading ' +
                                      bold_reading_filename)
                    if not bold_reading:
                        if self.server.bold_reading.get(nickname):
                            del self.server.bold_reading[nickname]
                        if os.path.isfile(bold_reading_filename):
                            try:
                                os.remove(bold_reading_filename)
                            except OSError:
                                print('EX: _profile_edit ' +
                                      'unable to delete ' +
                                      bold_reading_filename)

                    # notify about new Likes
                    if on_final_welcome_screen:
                        # default setting from welcome screen
                        try:
                            with open(notify_likes_filename, 'w+',
                                      encoding='utf-8') as rfile:
                                rfile.write('\n')
                        except OSError:
                            print('EX: unable to write notify likes ' +
                                  notify_likes_filename)
                        actor_changed = True
                    else:
                        notify_likes_active = False
                        if fields.get('notifyLikes'):
                            if fields['notifyLikes'] == 'on' and \
                               not hide_like_button_active:
                                notify_likes_active = True
                                try:
                                    with open(notify_likes_filename, 'w+',
                                              encoding='utf-8') as rfile:
                                        rfile.write('\n')
                                except OSError:
                                    print('EX: unable to write notify likes ' +
                                          notify_likes_filename)
                        if not notify_likes_active:
                            if os.path.isfile(notify_likes_filename):
                                try:
                                    os.remove(notify_likes_filename)
                                except OSError:
                                    print('EX: _profile_edit ' +
                                          'unable to delete ' +
                                          notify_likes_filename)

                    notify_reactions_filename = \
                        acct_dir(base_dir, nickname, domain) + \
                        '/.notifyReactions'
                    if on_final_welcome_screen:
                        # default setting from welcome screen
                        notify_react_filename = notify_reactions_filename
                        try:
                            with open(notify_react_filename, 'w+',
                                      encoding='utf-8') as rfile:
                                rfile.write('\n')
                        except OSError:
                            print('EX: unable to write notify reactions ' +
                                  notify_reactions_filename)
                        actor_changed = True
                    else:
                        notify_reactions_active = False
                        if fields.get('notifyReactions'):
                            if fields['notifyReactions'] == 'on' and \
                               not hide_reaction_button_active:
                                notify_reactions_active = True
                                try:
                                    with open(notify_reactions_filename, 'w+',
                                              encoding='utf-8') as rfile:
                                        rfile.write('\n')
                                except OSError:
                                    print('EX: unable to write ' +
                                          'notify reactions ' +
                                          notify_reactions_filename)
                        if not notify_reactions_active:
                            if os.path.isfile(notify_reactions_filename):
                                try:
                                    os.remove(notify_reactions_filename)
                                except OSError:
                                    print('EX: _profile_edit ' +
                                          'unable to delete ' +
                                          notify_reactions_filename)

                    # this account is a bot
                    if fields.get('isBot'):
                        if fields['isBot'] == 'on' and \
                           actor_json.get('type'):
                            if actor_json['type'] != 'Service':
                                actor_json['type'] = 'Service'
                                actor_changed = True
                    else:
                        # this account is a group
                        if fields.get('isGroup'):
                            if fields['isGroup'] == 'on' and \
                               actor_json.get('type'):
                                if actor_json['type'] != 'Group':
                                    # only allow admin to create groups
                                    if path.startswith('/users/' +
                                                       admin_nickname + '/'):
                                        actor_json['type'] = 'Group'
                                        actor_changed = True
                        else:
                            # this account is a person (default)
                            if actor_json.get('type'):
                                if actor_json['type'] != 'Person':
                                    actor_json['type'] = 'Person'
                                    actor_changed = True

                    # grayscale theme
                    if path.startswith('/users/' + admin_nickname + '/') or \
                       is_artist(base_dir, nickname):
                        grayscale = False
                        if fields.get('grayscale'):
                            if fields['grayscale'] == 'on':
                                grayscale = True
                        if grayscale:
                            enable_grayscale(base_dir)
                        else:
                            disable_grayscale(base_dir)

                    # dyslexic font
                    if path.startswith('/users/' + admin_nickname + '/') or \
                       is_artist(base_dir, nickname):
                        dyslexic_font = False
                        if fields.get('dyslexicFont'):
                            if fields['dyslexicFont'] == 'on':
                                dyslexic_font = True
                        if dyslexic_font != self.server.dyslexic_font:
                            self.server.dyslexic_font = dyslexic_font
                            set_config_param(base_dir, 'dyslexicFont',
                                             self.server.dyslexic_font)
                            set_theme(base_dir,
                                      self.server.theme_name,
                                      self.server.domain,
                                      self.server.allow_local_network_access,
                                      self.server.system_language,
                                      self.server.dyslexic_font, False)

                    # low bandwidth images checkbox
                    if path.startswith('/users/' + admin_nickname + '/') or \
                       is_artist(base_dir, nickname):
                        curr_low_bandwidth = \
                            get_config_param(base_dir, 'lowBandwidth')
                        low_bandwidth = False
                        if fields.get('lowBandwidth'):
                            if fields['lowBandwidth'] == 'on':
                                low_bandwidth = True
                        if curr_low_bandwidth != low_bandwidth:
                            set_config_param(base_dir, 'lowBandwidth',
                                             low_bandwidth)
                            self.server.low_bandwidth = low_bandwidth

                    # save filtered words list
                    filter_filename = \
                        acct_dir(base_dir, nickname, domain) + \
                        '/filters.txt'
                    if fields.get('filteredWords'):
                        try:
                            with open(filter_filename, 'w+',
                                      encoding='utf-8') as filterfile:
                                filterfile.write(fields['filteredWords'])
                        except OSError:
                            print('EX: unable to write filter ' +
                                  filter_filename)
                    else:
                        if os.path.isfile(filter_filename):
                            try:
                                os.remove(filter_filename)
                            except OSError:
                                print('EX: _profile_edit ' +
                                      'unable to delete filter ' +
                                      filter_filename)

                    # save filtered words within bio list
                    filter_bio_filename = \
                        acct_dir(base_dir, nickname, domain) + \
                        '/filters_bio.txt'
                    if fields.get('filteredWordsBio'):
                        try:
                            with open(filter_bio_filename, 'w+',
                                      encoding='utf-8') as filterfile:
                                filterfile.write(fields['filteredWordsBio'])
                        except OSError:
                            print('EX: unable to write bio filter ' +
                                  filter_bio_filename)
                    else:
                        if os.path.isfile(filter_bio_filename):
                            try:
                                os.remove(filter_bio_filename)
                            except OSError:
                                print('EX: _profile_edit ' +
                                      'unable to delete bio filter ' +
                                      filter_bio_filename)

                    # word replacements
                    switch_filename = \
                        acct_dir(base_dir, nickname, domain) + \
                        '/replacewords.txt'
                    if fields.get('switchwords'):
                        try:
                            with open(switch_filename, 'w+',
                                      encoding='utf-8') as switchfile:
                                switchfile.write(fields['switchwords'])
                        except OSError:
                            print('EX: unable to write switches ' +
                                  switch_filename)
                    else:
                        if os.path.isfile(switch_filename):
                            try:
                                os.remove(switch_filename)
                            except OSError:
                                print('EX: _profile_edit ' +
                                      'unable to delete ' +
                                      switch_filename)

                    # autogenerated tags
                    auto_tags_filename = \
                        acct_dir(base_dir, nickname, domain) + \
                        '/autotags.txt'
                    if fields.get('autoTags'):
                        try:
                            with open(auto_tags_filename, 'w+',
                                      encoding='utf-8') as autofile:
                                autofile.write(fields['autoTags'])
                        except OSError:
                            print('EX: unable to write auto tags ' +
                                  auto_tags_filename)
                    else:
                        if os.path.isfile(auto_tags_filename):
                            try:
                                os.remove(auto_tags_filename)
                            except OSError:
                                print('EX: _profile_edit ' +
                                      'unable to delete ' +
                                      auto_tags_filename)

                    # autogenerated content warnings
                    auto_cw_filename = \
                        acct_dir(base_dir, nickname, domain) + \
                        '/autocw.txt'
                    if fields.get('autoCW'):
                        try:
                            with open(auto_cw_filename, 'w+',
                                      encoding='utf-8') as auto_cw_file:
                                auto_cw_file.write(fields['autoCW'])
                        except OSError:
                            print('EX: unable to write auto CW ' +
                                  auto_cw_filename)
                    else:
                        if os.path.isfile(auto_cw_filename):
                            try:
                                os.remove(auto_cw_filename)
                            except OSError:
                                print('EX: _profile_edit ' +
                                      'unable to delete ' +
                                      auto_cw_filename)

                    # save blocked accounts list
                    blocked_filename = \
                        acct_dir(base_dir, nickname, domain) + \
                        '/blocking.txt'
                    if fields.get('blocked'):
                        try:
                            with open(blocked_filename, 'w+',
                                      encoding='utf-8') as blockedfile:
                                blockedfile.write(fields['blocked'])
                        except OSError:
                            print('EX: unable to write blocked accounts ' +
                                  blocked_filename)
                    else:
                        if os.path.isfile(blocked_filename):
                            try:
                                os.remove(blocked_filename)
                            except OSError:
                                print('EX: _profile_edit ' +
                                      'unable to delete ' +
                                      blocked_filename)

                    # Save DM allowed instances list.
                    # The allow list for incoming DMs,
                    # if the .followDMs flag file exists
                    dm_allowed_instances_filename = \
                        acct_dir(base_dir, nickname, domain) + \
                        '/dmAllowedInstances.txt'
                    if fields.get('dmAllowedInstances'):
                        try:
                            with open(dm_allowed_instances_filename, 'w+',
                                      encoding='utf-8') as afile:
                                afile.write(fields['dmAllowedInstances'])
                        except OSError:
                            print('EX: unable to write allowed DM instances ' +
                                  dm_allowed_instances_filename)
                    else:
                        if os.path.isfile(dm_allowed_instances_filename):
                            try:
                                os.remove(dm_allowed_instances_filename)
                            except OSError:
                                print('EX: _profile_edit ' +
                                      'unable to delete ' +
                                      dm_allowed_instances_filename)

                    # save allowed instances list
                    # This is the account level allow list
                    allowed_instances_filename = \
                        acct_dir(base_dir, nickname, domain) + \
                        '/allowedinstances.txt'
                    if fields.get('allowedInstances'):
                        inst_filename = allowed_instances_filename
                        try:
                            with open(inst_filename, 'w+',
                                      encoding='utf-8') as afile:
                                afile.write(fields['allowedInstances'])
                        except OSError:
                            print('EX: unable to write allowed instances ' +
                                  allowed_instances_filename)
                    else:
                        if os.path.isfile(allowed_instances_filename):
                            try:
                                os.remove(allowed_instances_filename)
                            except OSError:
                                print('EX: _profile_edit ' +
                                      'unable to delete ' +
                                      allowed_instances_filename)

                    if is_moderator(self.server.base_dir, nickname):
                        # set selected content warning lists
                        new_lists_enabled = ''
                        for name, _ in self.server.cw_lists.items():
                            list_var_name = get_cw_list_variable(name)
                            if fields.get(list_var_name):
                                if fields[list_var_name] == 'on':
                                    if new_lists_enabled:
                                        new_lists_enabled += ', ' + name
                                    else:
                                        new_lists_enabled += name
                        if new_lists_enabled != self.server.lists_enabled:
                            self.server.lists_enabled = new_lists_enabled
                            set_config_param(self.server.base_dir,
                                             "listsEnabled",
                                             new_lists_enabled)

                        # save blocked user agents
                        user_agents_blocked = []
                        if fields.get('userAgentsBlockedStr'):
                            user_agents_blocked_str = \
                                fields['userAgentsBlockedStr']
                            user_agents_blocked_list = \
                                user_agents_blocked_str.split('\n')
                            for uagent in user_agents_blocked_list:
                                if uagent in user_agents_blocked:
                                    continue
                                user_agents_blocked.append(uagent.strip())
                        if str(self.server.user_agents_blocked) != \
                           str(user_agents_blocked):
                            self.server.user_agents_blocked = \
                                user_agents_blocked
                            user_agents_blocked_str = ''
                            for uagent in user_agents_blocked:
                                if user_agents_blocked_str:
                                    user_agents_blocked_str += ','
                                user_agents_blocked_str += uagent
                            set_config_param(base_dir, 'userAgentsBlocked',
                                             user_agents_blocked_str)

                        # save allowed web crawlers
                        crawlers_allowed = []
                        if fields.get('crawlersAllowedStr'):
                            crawlers_allowed_str = \
                                fields['crawlersAllowedStr']
                            crawlers_allowed_list = \
                                crawlers_allowed_str.split('\n')
                            for uagent in crawlers_allowed_list:
                                if uagent in crawlers_allowed:
                                    continue
                                crawlers_allowed.append(uagent.strip())
                        if str(self.server.crawlers_allowed) != \
                           str(crawlers_allowed):
                            self.server.crawlers_allowed = \
                                crawlers_allowed
                            crawlers_allowed_str = ''
                            for uagent in crawlers_allowed:
                                if crawlers_allowed_str:
                                    crawlers_allowed_str += ','
                                crawlers_allowed_str += uagent
                            set_config_param(base_dir, 'crawlersAllowed',
                                             crawlers_allowed_str)

                        # save peertube instances list
                        peertube_instances_file = \
                            base_dir + '/accounts/peertube.txt'
                        if fields.get('ptInstances'):
                            self.server.peertube_instances.clear()
                            try:
                                with open(peertube_instances_file, 'w+',
                                          encoding='utf-8') as afile:
                                    afile.write(fields['ptInstances'])
                            except OSError:
                                print('EX: unable to write peertube ' +
                                      peertube_instances_file)
                            pt_instances_list = \
                                fields['ptInstances'].split('\n')
                            if pt_instances_list:
                                for url in pt_instances_list:
                                    url = url.strip()
                                    if not url:
                                        continue
                                    if url in self.server.peertube_instances:
                                        continue
                                    self.server.peertube_instances.append(url)
                        else:
                            if os.path.isfile(peertube_instances_file):
                                try:
                                    os.remove(peertube_instances_file)
                                except OSError:
                                    print('EX: _profile_edit ' +
                                          'unable to delete ' +
                                          peertube_instances_file)
                            self.server.peertube_instances.clear()

                    # save git project names list
                    git_projects_filename = \
                        acct_dir(base_dir, nickname, domain) + \
                        '/gitprojects.txt'
                    if fields.get('gitProjects'):
                        try:
                            with open(git_projects_filename, 'w+',
                                      encoding='utf-8') as afile:
                                afile.write(fields['gitProjects'].lower())
                        except OSError:
                            print('EX: unable to write git ' +
                                  git_projects_filename)
                    else:
                        if os.path.isfile(git_projects_filename):
                            try:
                                os.remove(git_projects_filename)
                            except OSError:
                                print('EX: _profile_edit ' +
                                      'unable to delete ' +
                                      git_projects_filename)

                    # save actor json file within accounts
                    if actor_changed:
                        # update the context for the actor
                        actor_json['@context'] = [
                            'https://www.w3.org/ns/activitystreams',
                            'https://w3id.org/security/v1',
                            get_default_person_context()
                        ]
                        if actor_json.get('nomadicLocations'):
                            del actor_json['nomadicLocations']
                        if not actor_json.get('featured'):
                            actor_json['featured'] = \
                                actor_json['id'] + '/collections/featured'
                        if not actor_json.get('featuredTags'):
                            actor_json['featuredTags'] = \
                                actor_json['id'] + '/collections/tags'
                        randomize_actor_images(actor_json)
                        add_actor_update_timestamp(actor_json)
                        # save the actor
                        save_json(actor_json, actor_filename)
                        webfinger_update(base_dir,
                                         nickname, domain,
                                         onion_domain, i2p_domain,
                                         self.server.cached_webfingers)
                        # also copy to the actors cache and
                        # person_cache in memory
                        store_person_in_cache(base_dir,
                                              actor_json['id'], actor_json,
                                              self.server.person_cache,
                                              True)
                        # clear any cached images for this actor
                        id_str = actor_json['id'].replace('/', '-')
                        remove_avatar_from_cache(base_dir, id_str)
                        # save the actor to the cache
                        actor_cache_filename = \
                            base_dir + '/cache/actors/' + \
                            actor_json['id'].replace('/', '#') + '.json'
                        save_json(actor_json, actor_cache_filename)
                        # send profile update to followers
                        update_actor_json = get_actor_update_json(actor_json)
                        print('Sending actor update: ' +
                              str(update_actor_json))
                        self._post_to_outbox(update_actor_json,
                                             self.server.project_version,
                                             nickname,
                                             curr_session, proxy_type)

                    # deactivate the account
                    if fields.get('deactivateThisAccount'):
                        if fields['deactivateThisAccount'] == 'on':
                            deactivate_account(base_dir,
                                               nickname, domain)
                            self._clear_login_details(nickname,
                                                      calling_domain)
                            self.server.postreq_busy = False
                            return

        # redirect back to the profile screen
        self._redirect_headers(actor_str + redirect_path,
                               cookie, calling_domain)
        self.server.postreq_busy = False

    def _progressive_web_app_manifest(self, base_dir: str,
                                      calling_domain: str,
                                      referer_domain: str,
                                      getreq_start_time) -> None:
        """gets the PWA manifest
        """
        css_filename = base_dir + '/epicyon.css'
        pwa_theme_color, pwa_theme_background_color = \
            get_pwa_theme_colors(css_filename)

        app1 = "https://f-droid.org/en/packages/eu.siacs.conversations"
        app2 = "https://staging.f-droid.org/en/packages/im.vector.app"
        app3 = \
            "https://f-droid.org/en/packages/" + \
            "com.stoutner.privacybrowser.standard"
        manifest = {
            "name": "Epicyon",
            "short_name": "Epicyon",
            "start_url": "/index.html",
            "display": "standalone",
            "background_color": pwa_theme_background_color,
            "theme_color": pwa_theme_color,
            "orientation": "portrait-primary",
            "categories": ["microblog", "fediverse", "activitypub"],
            "screenshots": [
                {
                    "src": "/mobile.jpg",
                    "sizes": "418x851",
                    "type": "image/jpeg"
                },
                {
                    "src": "/mobile_person.jpg",
                    "sizes": "429x860",
                    "type": "image/jpeg"
                },
                {
                    "src": "/mobile_search.jpg",
                    "sizes": "422x861",
                    "type": "image/jpeg"
                }
            ],
            "icons": [
                {
                    "src": "/logo72.png",
                    "type": "image/png",
                    "sizes": "72x72"
                },
                {
                    "src": "/logo96.png",
                    "type": "image/png",
                    "sizes": "96x96"
                },
                {
                    "src": "/logo128.png",
                    "type": "image/png",
                    "sizes": "128x128"
                },
                {
                    "src": "/logo144.png",
                    "type": "image/png",
                    "sizes": "144x144"
                },
                {
                    "src": "/logo150.png",
                    "type": "image/png",
                    "sizes": "150x150"
                },
                {
                    "src": "/apple-touch-icon.png",
                    "type": "image/png",
                    "sizes": "180x180"
                },
                {
                    "src": "/logo192.png",
                    "type": "image/png",
                    "sizes": "192x192"
                },
                {
                    "src": "/logo256.png",
                    "type": "image/png",
                    "sizes": "256x256"
                },
                {
                    "src": "/logo512.png",
                    "type": "image/png",
                    "sizes": "512x512"
                }
            ],
            "related_applications": [
                {
                    "platform": "fdroid",
                    "url": app1
                },
                {
                    "platform": "fdroid",
                    "url": app2
                },
                {
                    "platform": "fdroid",
                    "url": app3
                }
            ]
        }
        msg_str = json.dumps(manifest, ensure_ascii=False)
        msg_str = self._convert_domains(calling_domain,
                                        referer_domain,
                                        msg_str)
        msg = msg_str.encode('utf-8')

        msglen = len(msg)
        protocol_str = \
            get_json_content_from_accept(self.headers['Accept'])
        self._set_headers(protocol_str, msglen,
                          None, calling_domain, False)
        self._write(msg)
        if self.server.debug:
            print('Sent manifest: ' + calling_domain)
        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_progressive_web_app_manifest',
                            self.server.debug)

    def _browser_config(self, calling_domain: str, referer_domain: str,
                        getreq_start_time) -> None:
        """Used by MS Windows to put an icon on the desktop if you
        link to a website
        """
        xml_str = \
            '<?xml version="1.0" encoding="utf-8"?>\n' + \
            '<browserconfig>\n' + \
            '  <msapplication>\n' + \
            '    <tile>\n' + \
            '      <square150x150logo src="/logo150.png"/>\n' + \
            '      <TileColor>#eeeeee</TileColor>\n' + \
            '    </tile>\n' + \
            '  </msapplication>\n' + \
            '</browserconfig>'

        msg_str = json.dumps(xml_str, ensure_ascii=False)
        msg_str = self._convert_domains(calling_domain,
                                        referer_domain,
                                        msg_str)
        msg = msg_str.encode('utf-8')
        msglen = len(msg)
        self._set_headers('application/xrd+xml', msglen,
                          None, calling_domain, False)
        self._write(msg)
        if self.server.debug:
            print('Sent browserconfig: ' + calling_domain)
        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_browser_config',
                            self.server.debug)

    def _get_favicon(self, calling_domain: str,
                     base_dir: str, debug: bool,
                     fav_filename: str) -> None:
        """Return the site favicon or default newswire favicon
        """
        fav_type = 'image/x-icon'
        if self._has_accept(calling_domain):
            if 'image/webp' in self.headers['Accept']:
                fav_type = 'image/webp'
                fav_filename = fav_filename.split('.')[0] + '.webp'
            if 'image/avif' in self.headers['Accept']:
                fav_type = 'image/avif'
                fav_filename = fav_filename.split('.')[0] + '.avif'
            if 'image/heic' in self.headers['Accept']:
                fav_type = 'image/heic'
                fav_filename = fav_filename.split('.')[0] + '.heic'
            if 'image/jxl' in self.headers['Accept']:
                fav_type = 'image/jxl'
                fav_filename = fav_filename.split('.')[0] + '.jxl'
        if not self.server.theme_name:
            self.theme_name = get_config_param(base_dir, 'theme')
        if not self.server.theme_name:
            self.server.theme_name = 'default'
        # custom favicon
        favicon_filename = \
            base_dir + '/theme/' + self.server.theme_name + \
            '/icons/' + fav_filename
        if not fav_filename.endswith('.ico'):
            if not os.path.isfile(favicon_filename):
                if fav_filename.endswith('.webp'):
                    fav_filename = fav_filename.replace('.webp', '.ico')
                elif fav_filename.endswith('.avif'):
                    fav_filename = fav_filename.replace('.avif', '.ico')
                elif fav_filename.endswith('.heic'):
                    fav_filename = fav_filename.replace('.heic', '.ico')
                elif fav_filename.endswith('.jxl'):
                    fav_filename = fav_filename.replace('.jxl', '.ico')
        if not os.path.isfile(favicon_filename):
            # default favicon
            favicon_filename = \
                base_dir + '/theme/default/icons/' + fav_filename
        if self._etag_exists(favicon_filename):
            # The file has not changed
            if debug:
                print('favicon icon has not changed: ' + calling_domain)
            self._304()
            return
        if self.server.iconsCache.get(fav_filename):
            fav_binary = self.server.iconsCache[fav_filename]
            self._set_headers_etag(favicon_filename,
                                   fav_type,
                                   fav_binary, None,
                                   self.server.domain_full,
                                   False, None)
            self._write(fav_binary)
            if debug:
                print('Sent favicon from cache: ' + calling_domain)
            return
        if os.path.isfile(favicon_filename):
            fav_binary = None
            try:
                with open(favicon_filename, 'rb') as fav_file:
                    fav_binary = fav_file.read()
            except OSError:
                print('EX: unable to read favicon ' + favicon_filename)
            if fav_binary:
                self._set_headers_etag(favicon_filename,
                                       fav_type,
                                       fav_binary, None,
                                       self.server.domain_full,
                                       False, None)
                self._write(fav_binary)
                self.server.iconsCache[fav_filename] = fav_binary
                if self.server.debug:
                    print('Sent favicon from file: ' + calling_domain)
                return
        if debug:
            print('favicon not sent: ' + calling_domain)
        self._404()

    def _get_speaker(self, calling_domain: str, referer_domain: str,
                     path: str, base_dir: str, domain: str) -> None:
        """Returns the speaker file used for TTS and
        accessed via c2s
        """
        nickname = path.split('/users/')[1]
        if '/' in nickname:
            nickname = nickname.split('/')[0]
        speaker_filename = \
            acct_dir(base_dir, nickname, domain) + '/speaker.json'
        if not os.path.isfile(speaker_filename):
            self._404()
            return

        speaker_json = load_json(speaker_filename)
        msg_str = json.dumps(speaker_json, ensure_ascii=False)
        msg_str = self._convert_domains(calling_domain,
                                        referer_domain,
                                        msg_str)
        msg = msg_str.encode('utf-8')
        msglen = len(msg)
        protocol_str = \
            get_json_content_from_accept(self.headers['Accept'])
        self._set_headers(protocol_str, msglen,
                          None, calling_domain, False)
        self._write(msg)

    def _get_exported_theme(self, path: str, base_dir: str,
                            domain_full: str) -> None:
        """Returns an exported theme zip file
        """
        filename = path.split('/exports/', 1)[1]
        filename = base_dir + '/exports/' + filename
        if os.path.isfile(filename):
            export_binary = None
            try:
                with open(filename, 'rb') as fp_exp:
                    export_binary = fp_exp.read()
            except OSError:
                print('EX: unable to read theme export ' + filename)
            if export_binary:
                export_type = 'application/zip'
                self._set_headers_etag(filename, export_type,
                                       export_binary, None,
                                       domain_full, False, None)
                self._write(export_binary)
        self._404()

    def _get_fonts(self, calling_domain: str, path: str,
                   base_dir: str, debug: bool,
                   getreq_start_time) -> None:
        """Returns a font
        """
        font_str = path.split('/fonts/')[1]
        if font_str.endswith('.otf') or \
           font_str.endswith('.ttf') or \
           font_str.endswith('.woff') or \
           font_str.endswith('.woff2'):
            if font_str.endswith('.otf'):
                font_type = 'font/otf'
            elif font_str.endswith('.ttf'):
                font_type = 'font/ttf'
            elif font_str.endswith('.woff'):
                font_type = 'font/woff'
            else:
                font_type = 'font/woff2'
            font_filename = \
                base_dir + '/fonts/' + font_str
            if self._etag_exists(font_filename):
                # The file has not changed
                self._304()
                return
            if self.server.fontsCache.get(font_str):
                font_binary = self.server.fontsCache[font_str]
                self._set_headers_etag(font_filename,
                                       font_type,
                                       font_binary, None,
                                       self.server.domain_full, False, None)
                self._write(font_binary)
                if debug:
                    print('font sent from cache: ' +
                          path + ' ' + calling_domain)
                fitness_performance(getreq_start_time, self.server.fitness,
                                    '_GET', '_get_fonts cache',
                                    debug)
                return
            if os.path.isfile(font_filename):
                font_binary = None
                try:
                    with open(font_filename, 'rb') as fontfile:
                        font_binary = fontfile.read()
                except OSError:
                    print('EX: unable to load font ' + font_filename)
                if font_binary:
                    self._set_headers_etag(font_filename,
                                           font_type,
                                           font_binary, None,
                                           self.server.domain_full,
                                           False, None)
                    self._write(font_binary)
                    self.server.fontsCache[font_str] = font_binary
                if debug:
                    print('font sent from file: ' +
                          path + ' ' + calling_domain)
                fitness_performance(getreq_start_time, self.server.fitness,
                                    '_GET', '_get_fonts', debug)
                return
        if debug:
            print('font not found: ' + path + ' ' + calling_domain)
        self._404()

    def _get_rss2feed(self, calling_domain: str, path: str,
                      base_dir: str, http_prefix: str,
                      domain: str, port: int, proxy_type: str,
                      getreq_start_time, debug: bool,
                      curr_session) -> None:
        """Returns an RSS2 feed for the blog
        """
        nickname = path.split('/blog/')[1]
        if '/' in nickname:
            nickname = nickname.split('/')[0]
        if not nickname.startswith('rss.'):
            account_dir = acct_dir(self.server.base_dir, nickname, domain)
            if os.path.isdir(account_dir):
                curr_session = \
                    self._establish_session("RSS request",
                                            curr_session,
                                            proxy_type)
                if not curr_session:
                    return

                msg = \
                    html_blog_page_rss2(base_dir,
                                        http_prefix,
                                        self.server.translate,
                                        nickname,
                                        domain,
                                        port,
                                        MAX_POSTS_IN_RSS_FEED, 1,
                                        True,
                                        self.server.system_language)
                if msg is not None:
                    msg = msg.encode('utf-8')
                    msglen = len(msg)
                    self._set_headers('text/xml', msglen,
                                      None, calling_domain, True)
                    self._write(msg)
                    if debug:
                        print('Sent rss2 feed: ' +
                              path + ' ' + calling_domain)
                    fitness_performance(getreq_start_time, self.server.fitness,
                                        '_GET', '_get_rss2feed',
                                        debug)
                    return
        if debug:
            print('Failed to get rss2 feed: ' +
                  path + ' ' + calling_domain)
        self._404()

    def _get_rss2site(self, calling_domain: str, path: str,
                      base_dir: str, http_prefix: str,
                      domain_full: str, port: int, proxy_type: str,
                      translate: {},
                      getreq_start_time,
                      debug: bool,
                      curr_session) -> None:
        """Returns an RSS2 feed for all blogs on this instance
        """
        curr_session = \
            self._establish_session("get_rss2site",
                                    curr_session,
                                    proxy_type)
        if not curr_session:
            self._404()
            return

        msg = ''
        for _, dirs, _ in os.walk(base_dir + '/accounts'):
            for acct in dirs:
                if not is_account_dir(acct):
                    continue
                nickname = acct.split('@')[0]
                domain = acct.split('@')[1]
                msg += \
                    html_blog_page_rss2(base_dir,
                                        http_prefix,
                                        self.server.translate,
                                        nickname,
                                        domain,
                                        port,
                                        MAX_POSTS_IN_RSS_FEED, 1,
                                        False,
                                        self.server.system_language)
            break
        if msg:
            msg = rss2header(http_prefix,
                             'news', domain_full,
                             'Site', translate) + msg + rss2footer()

            msg = msg.encode('utf-8')
            msglen = len(msg)
            self._set_headers('text/xml', msglen,
                              None, calling_domain, True)
            self._write(msg)
            if debug:
                print('Sent rss2 feed: ' +
                      path + ' ' + calling_domain)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', '_get_rss2site',
                                debug)
            return
        if debug:
            print('Failed to get rss2 feed: ' +
                  path + ' ' + calling_domain)
        self._404()

    def _get_newswire_feed(self, calling_domain: str, path: str,
                           proxy_type: str, getreq_start_time,
                           debug: bool, curr_session) -> None:
        """Returns the newswire feed
        """
        curr_session = \
            self._establish_session("get_newswire_feed",
                                    curr_session,
                                    proxy_type)
        if not curr_session:
            self._404()
            return

        msg = get_rs_sfrom_dict(self.server.base_dir, self.server.newswire,
                                self.server.http_prefix,
                                self.server.domain_full,
                                'Newswire', self.server.translate)
        if msg:
            msg = msg.encode('utf-8')
            msglen = len(msg)
            self._set_headers('text/xml', msglen,
                              None, calling_domain, True)
            self._write(msg)
            if debug:
                print('Sent rss2 newswire feed: ' +
                      path + ' ' + calling_domain)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', '_get_newswire_feed',
                                debug)
            return
        if debug:
            print('Failed to get rss2 newswire feed: ' +
                  path + ' ' + calling_domain)
        self._404()

    def _get_hashtag_categories_feed(self, calling_domain: str, path: str,
                                     base_dir: str, proxy_type: str,
                                     getreq_start_time,
                                     debug: bool,
                                     curr_session) -> None:
        """Returns the hashtag categories feed
        """
        curr_session = \
            self._establish_session("get_hashtag_categories_feed",
                                    curr_session, proxy_type)
        if not curr_session:
            self._404()
            return

        hashtag_categories = None
        msg = \
            get_hashtag_categories_feed(base_dir, hashtag_categories)
        if msg:
            msg = msg.encode('utf-8')
            msglen = len(msg)
            self._set_headers('text/xml', msglen,
                              None, calling_domain, True)
            self._write(msg)
            if debug:
                print('Sent rss2 categories feed: ' +
                      path + ' ' + calling_domain)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', '_get_hashtag_categories_feed', debug)
            return
        if debug:
            print('Failed to get rss2 categories feed: ' +
                  path + ' ' + calling_domain)
        self._404()

    def _get_rss3feed(self, calling_domain: str, path: str,
                      base_dir: str, http_prefix: str,
                      domain: str, port: int, proxy_type: str,
                      getreq_start_time,
                      debug: bool, system_language: str,
                      curr_session) -> None:
        """Returns an RSS3 feed
        """
        nickname = path.split('/blog/')[1]
        if '/' in nickname:
            nickname = nickname.split('/')[0]
        if not nickname.startswith('rss.'):
            account_dir = acct_dir(base_dir, nickname, domain)
            if os.path.isdir(account_dir):
                curr_session = \
                    self._establish_session("get_rss3feed",
                                            curr_session, proxy_type)
                if not curr_session:
                    self._404()
                    return
                msg = \
                    html_blog_page_rss3(base_dir, http_prefix,
                                        nickname, domain, port,
                                        MAX_POSTS_IN_RSS_FEED, 1,
                                        system_language)
                if msg is not None:
                    msg = msg.encode('utf-8')
                    msglen = len(msg)
                    self._set_headers('text/plain; charset=utf-8',
                                      msglen, None, calling_domain, True)
                    self._write(msg)
                    if self.server.debug:
                        print('Sent rss3 feed: ' +
                              path + ' ' + calling_domain)
                    fitness_performance(getreq_start_time, self.server.fitness,
                                        '_GET', '_get_rss3feed', debug)
                    return
        if debug:
            print('Failed to get rss3 feed: ' +
                  path + ' ' + calling_domain)
        self._404()

    def _show_person_options(self, calling_domain: str, path: str,
                             base_dir: str, http_prefix: str,
                             domain: str, domain_full: str,
                             getreq_start_time,
                             onion_domain: str, i2p_domain: str,
                             cookie: str, debug: bool,
                             authorized: bool,
                             curr_session) -> None:
        """Show person options screen
        """
        back_to_path = ''
        options_str = path.split('?options=')[1]
        origin_path_str = path.split('?options=')[0]
        if ';' in options_str and '/users/news/' not in path:
            page_number = 1
            options_list = options_str.split(';')
            options_actor = options_list[0]
            options_page_number = 1
            if len(options_list) > 1:
                options_page_number = options_list[1]
            options_profile_url = ''
            if len(options_list) > 2:
                options_profile_url = options_list[2]
            if '.' in options_profile_url and \
               options_profile_url.startswith('/members/'):
                ext = options_profile_url.split('.')[-1]
                options_profile_url = options_profile_url.split('/members/')[1]
                options_profile_url = \
                    options_profile_url.replace('.' + ext, '')
                options_profile_url = \
                    '/users/' + options_profile_url + '/avatar.' + ext
                back_to_path = 'moderation'
            if len(options_page_number) > 5:
                options_page_number = "1"
            if options_page_number.isdigit():
                page_number = int(options_page_number)
            options_link = None
            if len(options_list) > 3:
                options_link = options_list[3]
            is_group = False
            donate_url = None
            website_url = None
            gemini_link = None
            enigma_pub_key = None
            pgp_pub_key = None
            pgp_fingerprint = None
            xmpp_address = None
            matrix_address = None
            blog_address = None
            tox_address = None
            briar_address = None
            cwtch_address = None
            ssb_address = None
            email_address = None
            locked_account = False
            also_known_as = None
            moved_to = ''
            actor_json = \
                get_person_from_cache(base_dir,
                                      options_actor,
                                      self.server.person_cache)
            if actor_json:
                if actor_json.get('movedTo'):
                    moved_to = actor_json['movedTo']
                    if '"' in moved_to:
                        moved_to = moved_to.split('"')[1]
                if actor_json.get('type'):
                    if actor_json['type'] == 'Group':
                        is_group = True
                locked_account = get_locked_account(actor_json)
                donate_url = get_donation_url(actor_json)
                website_url = get_website(actor_json, self.server.translate)
                gemini_link = get_gemini_link(actor_json,
                                              self.server.translate)
                xmpp_address = get_xmpp_address(actor_json)
                matrix_address = get_matrix_address(actor_json)
                ssb_address = get_ssb_address(actor_json)
                blog_address = get_blog_address(actor_json)
                tox_address = get_tox_address(actor_json)
                briar_address = get_briar_address(actor_json)
                cwtch_address = get_cwtch_address(actor_json)
                email_address = get_email_address(actor_json)
                enigma_pub_key = get_enigma_pub_key(actor_json)
                pgp_pub_key = get_pgp_pub_key(actor_json)
                pgp_fingerprint = get_pgp_fingerprint(actor_json)
                if actor_json.get('alsoKnownAs'):
                    also_known_as = actor_json['alsoKnownAs']

            access_keys = self.server.access_keys
            nickname = 'instance'
            if '/users/' in path:
                nickname = path.split('/users/')[1]
                if '/' in nickname:
                    nickname = nickname.split('/')[0]
                if self.server.key_shortcuts.get(nickname):
                    access_keys = self.server.key_shortcuts[nickname]

            if curr_session:
                # because this is slow, do it in a separate thread
                if self.server.thrCheckActor.get(nickname):
                    # kill existing thread
                    self.server.thrCheckActor[nickname].kill()

                self.server.thrCheckActor[nickname] = \
                    thread_with_trace(target=check_for_changed_actor,
                                      args=(curr_session,
                                            self.server.base_dir,
                                            self.server.http_prefix,
                                            self.server.domain_full,
                                            options_actor, options_profile_url,
                                            self.server.person_cache,
                                            self.server.check_actor_timeout),
                                      daemon=True)
                begin_thread(self.server.thrCheckActor[nickname],
                             '_show_person_options')

            msg = \
                html_person_options(self.server.default_timeline,
                                    self.server.translate,
                                    base_dir, domain,
                                    domain_full,
                                    origin_path_str,
                                    options_actor,
                                    options_profile_url,
                                    options_link,
                                    page_number, donate_url, website_url,
                                    gemini_link,
                                    xmpp_address, matrix_address,
                                    ssb_address, blog_address,
                                    tox_address, briar_address,
                                    cwtch_address,
                                    enigma_pub_key,
                                    pgp_pub_key, pgp_fingerprint,
                                    email_address,
                                    self.server.dormant_months,
                                    back_to_path,
                                    locked_account,
                                    moved_to, also_known_as,
                                    self.server.text_mode_banner,
                                    self.server.news_instance,
                                    authorized,
                                    access_keys, is_group,
                                    self.server.theme_name)
            if msg:
                msg = msg.encode('utf-8')
                msglen = len(msg)
                self._set_headers('text/html', msglen,
                                  cookie, calling_domain, False)
                self._write(msg)
                fitness_performance(getreq_start_time, self.server.fitness,
                                    '_GET', '_show_person_options', debug)
            else:
                self._404()
            return

        if '/users/news/' in path:
            self._redirect_headers(origin_path_str + '/tlfeatures',
                                   cookie, calling_domain)
            return

        if calling_domain.endswith('.onion') and onion_domain:
            origin_path_str_absolute = \
                'http://' + onion_domain + origin_path_str
        elif calling_domain.endswith('.i2p') and i2p_domain:
            origin_path_str_absolute = \
                'http://' + i2p_domain + origin_path_str
        else:
            origin_path_str_absolute = \
                http_prefix + '://' + domain_full + origin_path_str
        self._redirect_headers(origin_path_str_absolute, cookie,
                               calling_domain)

    def _show_media(self, path: str, base_dir: str,
                    getreq_start_time) -> None:
        """Returns a media file
        """
        if is_image_file(path) or \
           path_is_video(path) or \
           path_is_audio(path):
            media_str = path.split('/media/')[1]
            media_filename = base_dir + '/media/' + media_str
            if os.path.isfile(media_filename):
                if self._etag_exists(media_filename):
                    # The file has not changed
                    self._304()
                    return

                media_file_type = media_file_mime_type(media_filename)

                media_tm = os.path.getmtime(media_filename)
                last_modified_time = datetime.datetime.fromtimestamp(media_tm)
                last_modified_time_str = \
                    last_modified_time.strftime('%a, %d %b %Y %H:%M:%S GMT')

                media_binary = None
                try:
                    with open(media_filename, 'rb') as av_file:
                        media_binary = av_file.read()
                except OSError:
                    print('EX: unable to read media binary ' + media_filename)
                if media_binary:
                    self._set_headers_etag(media_filename, media_file_type,
                                           media_binary, None,
                                           None, True,
                                           last_modified_time_str)
                    self._write(media_binary)
                fitness_performance(getreq_start_time, self.server.fitness,
                                    '_GET', '_show_media', self.server.debug)
                return
        self._404()

    def _get_ontology(self, calling_domain: str,
                      path: str, base_dir: str,
                      getreq_start_time) -> None:
        """Returns an ontology file
        """
        if '.owl' in path or '.rdf' in path or '.json' in path:
            if '/ontologies/' in path:
                ontology_str = path.split('/ontologies/')[1].replace('#', '')
            else:
                ontology_str = path.split('/data/')[1].replace('#', '')
            ontology_filename = None
            ontology_file_type = 'application/rdf+xml'
            if ontology_str.startswith('DFC_'):
                ontology_filename = base_dir + '/ontology/DFC/' + ontology_str
            else:
                ontology_str = ontology_str.replace('/data/', '')
                ontology_filename = base_dir + '/ontology/' + ontology_str
            if ontology_str.endswith('.json'):
                ontology_file_type = 'application/ld+json'
            if os.path.isfile(ontology_filename):
                ontology_file = None
                try:
                    with open(ontology_filename, 'r',
                              encoding='utf-8') as fp_ont:
                        ontology_file = fp_ont.read()
                except OSError:
                    print('EX: unable to read ontology ' + ontology_filename)
                if ontology_file:
                    ontology_file = \
                        ontology_file.replace('static.datafoodconsortium.org',
                                              calling_domain)
                    if not calling_domain.endswith('.i2p') and \
                       not calling_domain.endswith('.onion'):
                        ontology_file = \
                            ontology_file.replace('http://' +
                                                  calling_domain,
                                                  'https://' +
                                                  calling_domain)
                    msg = ontology_file.encode('utf-8')
                    msglen = len(msg)
                    self._set_headers(ontology_file_type, msglen,
                                      None, calling_domain, False)
                    self._write(msg)
                fitness_performance(getreq_start_time, self.server.fitness,
                                    '_GET', '_get_ontology', self.server.debug)
                return
        self._404()

    def _show_emoji(self, path: str,
                    base_dir: str, getreq_start_time) -> None:
        """Returns an emoji image
        """
        if is_image_file(path):
            emoji_str = path.split('/emoji/')[1]
            emoji_filename = base_dir + '/emoji/' + emoji_str
            if not os.path.isfile(emoji_filename):
                emoji_filename = base_dir + '/emojicustom/' + emoji_str
            if os.path.isfile(emoji_filename):
                if self._etag_exists(emoji_filename):
                    # The file has not changed
                    self._304()
                    return

                media_image_type = get_image_mime_type(emoji_filename)
                media_binary = None
                try:
                    with open(emoji_filename, 'rb') as av_file:
                        media_binary = av_file.read()
                except OSError:
                    print('EX: unable to read emoji image ' + emoji_filename)
                if media_binary:
                    self._set_headers_etag(emoji_filename,
                                           media_image_type,
                                           media_binary, None,
                                           self.server.domain_full,
                                           False, None)
                    self._write(media_binary)
                fitness_performance(getreq_start_time, self.server.fitness,
                                    '_GET', '_show_emoji', self.server.debug)
                return
        self._404()

    def _show_icon(self, path: str,
                   base_dir: str, getreq_start_time) -> None:
        """Shows an icon
        """
        if not path.endswith('.png'):
            self._404()
            return
        media_str = path.split('/icons/')[1]
        if '/' not in media_str:
            if not self.server.theme_name:
                theme = 'default'
            else:
                theme = self.server.theme_name
            icon_filename = media_str
        else:
            theme = media_str.split('/')[0]
            icon_filename = media_str.split('/')[1]
        media_filename = \
            base_dir + '/theme/' + theme + '/icons/' + icon_filename
        if self._etag_exists(media_filename):
            # The file has not changed
            self._304()
            return
        if self.server.iconsCache.get(media_str):
            media_binary = self.server.iconsCache[media_str]
            mime_type_str = media_file_mime_type(media_filename)
            self._set_headers_etag(media_filename,
                                   mime_type_str,
                                   media_binary, None,
                                   self.server.domain_full,
                                   False, None)
            self._write(media_binary)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', '_show_icon', self.server.debug)
            return
        if os.path.isfile(media_filename):
            media_binary = None
            try:
                with open(media_filename, 'rb') as av_file:
                    media_binary = av_file.read()
            except OSError:
                print('EX: unable to read icon image ' + media_filename)
            if media_binary:
                mime_type = media_file_mime_type(media_filename)
                self._set_headers_etag(media_filename,
                                       mime_type,
                                       media_binary, None,
                                       self.server.domain_full,
                                       False, None)
                self._write(media_binary)
                self.server.iconsCache[media_str] = media_binary
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', '_show_icon', self.server.debug)
            return
        self._404()

    def _show_specification_image(self, path: str,
                                  base_dir: str, getreq_start_time) -> None:
        """Shows an image within the ActivityPub specification document
        """
        image_filename = path.split('/', 1)[1]
        if '/' in image_filename:
            self._404()
            return
        media_filename = \
            base_dir + '/specification/' + image_filename
        if self._etag_exists(media_filename):
            # The file has not changed
            self._304()
            return
        if self.server.iconsCache.get(media_filename):
            media_binary = self.server.iconsCache[media_filename]
            mime_type_str = media_file_mime_type(media_filename)
            self._set_headers_etag(media_filename,
                                   mime_type_str,
                                   media_binary, None,
                                   self.server.domain_full,
                                   False, None)
            self._write(media_binary)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', '_show_specification_image',
                                self.server.debug)
            return
        if os.path.isfile(media_filename):
            media_binary = None
            try:
                with open(media_filename, 'rb') as av_file:
                    media_binary = av_file.read()
            except OSError:
                print('EX: unable to read specification image ' +
                      media_filename)
            if media_binary:
                mime_type = media_file_mime_type(media_filename)
                self._set_headers_etag(media_filename,
                                       mime_type,
                                       media_binary, None,
                                       self.server.domain_full,
                                       False, None)
                self._write(media_binary)
                self.server.iconsCache[media_filename] = media_binary
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', '_show_specification_image',
                                self.server.debug)
            return
        self._404()

    def _show_manual_image(self, path: str,
                           base_dir: str, getreq_start_time) -> None:
        """Shows an image within the manual
        """
        image_filename = path.split('/', 1)[1]
        if '/' in image_filename:
            self._404()
            return
        media_filename = \
            base_dir + '/manual/' + image_filename
        if self._etag_exists(media_filename):
            # The file has not changed
            self._304()
            return
        if self.server.iconsCache.get(media_filename):
            media_binary = self.server.iconsCache[media_filename]
            mime_type_str = media_file_mime_type(media_filename)
            self._set_headers_etag(media_filename,
                                   mime_type_str,
                                   media_binary, None,
                                   self.server.domain_full,
                                   False, None)
            self._write(media_binary)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', '_show_manual_image',
                                self.server.debug)
            return
        if os.path.isfile(media_filename):
            media_binary = None
            try:
                with open(media_filename, 'rb') as av_file:
                    media_binary = av_file.read()
            except OSError:
                print('EX: unable to read manual image ' +
                      media_filename)
            if media_binary:
                mime_type = media_file_mime_type(media_filename)
                self._set_headers_etag(media_filename,
                                       mime_type,
                                       media_binary, None,
                                       self.server.domain_full,
                                       False, None)
                self._write(media_binary)
                self.server.iconsCache[media_filename] = media_binary
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', '_show_manual_image',
                                self.server.debug)
            return
        self._404()

    def _show_help_screen_image(self, path: str,
                                base_dir: str, getreq_start_time) -> None:
        """Shows a help screen image
        """
        if not is_image_file(path):
            return
        media_str = path.split('/helpimages/')[1]
        if '/' not in media_str:
            if not self.server.theme_name:
                theme = 'default'
            else:
                theme = self.server.theme_name
            icon_filename = media_str
        else:
            theme = media_str.split('/')[0]
            icon_filename = media_str.split('/')[1]
        media_filename = \
            base_dir + '/theme/' + theme + '/helpimages/' + icon_filename
        # if there is no theme-specific help image then use the default one
        if not os.path.isfile(media_filename):
            media_filename = \
                base_dir + '/theme/default/helpimages/' + icon_filename
        if self._etag_exists(media_filename):
            # The file has not changed
            self._304()
            return
        if os.path.isfile(media_filename):
            media_binary = None
            try:
                with open(media_filename, 'rb') as av_file:
                    media_binary = av_file.read()
            except OSError:
                print('EX: unable to read help image ' + media_filename)
            if media_binary:
                mime_type = media_file_mime_type(media_filename)
                self._set_headers_etag(media_filename,
                                       mime_type,
                                       media_binary, None,
                                       self.server.domain_full,
                                       False, None)
                self._write(media_binary)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', '_show_help_screen_image',
                                self.server.debug)
            return
        self._404()

    def _show_cached_favicon(self, referer_domain: str, path: str,
                             base_dir: str, getreq_start_time) -> None:
        """Shows a favicon image obtained from the cache
        """
        fav_file = path.replace('/favicons/', '')
        fav_filename = base_dir + urllib.parse.unquote_plus(path)
        print('showCachedFavicon: ' + fav_filename)
        if self.server.favicons_cache.get(fav_file):
            media_binary = self.server.favicons_cache[fav_file]
            mime_type = media_file_mime_type(fav_filename)
            self._set_headers_etag(fav_filename,
                                   mime_type,
                                   media_binary, None,
                                   referer_domain,
                                   False, None)
            self._write(media_binary)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', '_show_cached_favicon2',
                                self.server.debug)
            return
        if not os.path.isfile(fav_filename):
            self._404()
            return
        if self._etag_exists(fav_filename):
            # The file has not changed
            self._304()
            return
        media_binary = None
        try:
            with open(fav_filename, 'rb') as av_file:
                media_binary = av_file.read()
        except OSError:
            print('EX: unable to read cached favicon ' + fav_filename)
        if media_binary:
            mime_type = media_file_mime_type(fav_filename)
            self._set_headers_etag(fav_filename,
                                   mime_type,
                                   media_binary, None,
                                   referer_domain,
                                   False, None)
            self._write(media_binary)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', '_show_cached_favicon',
                                self.server.debug)
            self.server.favicons_cache[fav_file] = media_binary
            return
        self._404()

    def _show_cached_avatar(self, referer_domain: str, path: str,
                            base_dir: str, getreq_start_time) -> None:
        """Shows an avatar image obtained from the cache
        """
        media_filename = base_dir + '/cache' + path
        if os.path.isfile(media_filename):
            if self._etag_exists(media_filename):
                # The file has not changed
                self._304()
                return
            media_binary = None
            try:
                with open(media_filename, 'rb') as av_file:
                    media_binary = av_file.read()
            except OSError:
                print('EX: unable to read cached avatar ' + media_filename)
            if media_binary:
                mime_type = media_file_mime_type(media_filename)
                self._set_headers_etag(media_filename,
                                       mime_type,
                                       media_binary, None,
                                       referer_domain,
                                       False, None)
                self._write(media_binary)
                fitness_performance(getreq_start_time, self.server.fitness,
                                    '_GET', '_show_cached_avatar',
                                    self.server.debug)
                return
        self._404()

    def _hashtag_search(self, calling_domain: str,
                        path: str, cookie: str,
                        base_dir: str, http_prefix: str,
                        domain: str, domain_full: str, port: int,
                        onion_domain: str, i2p_domain: str,
                        getreq_start_time,
                        curr_session) -> None:
        """Return the result of a hashtag search
        """
        page_number = 1
        if '?page=' in path:
            page_number_str = path.split('?page=')[1]
            if '#' in page_number_str:
                page_number_str = page_number_str.split('#')[0]
            if len(page_number_str) > 5:
                page_number_str = "1"
            if page_number_str.isdigit():
                page_number = int(page_number_str)
        hashtag = path.split('/tags/')[1]
        if '?page=' in hashtag:
            hashtag = hashtag.split('?page=')[0]
        hashtag = urllib.parse.unquote_plus(hashtag)
        if is_blocked_hashtag(base_dir, hashtag):
            print('BLOCK: hashtag #' + hashtag)
            msg = html_hashtag_blocked(base_dir,
                                       self.server.translate).encode('utf-8')
            msglen = len(msg)
            self._login_headers('text/html', msglen, calling_domain)
            self._write(msg)
            return
        nickname = None
        if '/users/' in path:
            nickname = path.split('/users/')[1]
            if '/' in nickname:
                nickname = nickname.split('/')[0]
            if '?' in nickname:
                nickname = nickname.split('?')[0]
        timezone = None
        if self.server.account_timezone.get(nickname):
            timezone = \
                self.server.account_timezone.get(nickname)
        bold_reading = False
        if self.server.bold_reading.get(nickname):
            bold_reading = True
        hashtag_str = \
            html_hashtag_search(nickname, domain, port,
                                self.server.recent_posts_cache,
                                self.server.max_recent_posts,
                                self.server.translate,
                                base_dir, hashtag, page_number,
                                MAX_POSTS_IN_HASHTAG_FEED,
                                curr_session,
                                self.server.cached_webfingers,
                                self.server.person_cache,
                                http_prefix,
                                self.server.project_version,
                                self.server.yt_replace_domain,
                                self.server.twitter_replacement_domain,
                                self.server.show_published_date_only,
                                self.server.peertube_instances,
                                self.server.allow_local_network_access,
                                self.server.theme_name,
                                self.server.system_language,
                                self.server.max_like_count,
                                self.server.signing_priv_key_pem,
                                self.server.cw_lists,
                                self.server.lists_enabled,
                                timezone, bold_reading,
                                self.server.dogwhistles,
                                self.server.map_format,
                                self.server.access_keys,
                                'search',
                                self.server.min_images_for_accounts)
        if hashtag_str:
            msg = hashtag_str.encode('utf-8')
            msglen = len(msg)
            self._set_headers('text/html', msglen,
                              cookie, calling_domain, False)
            self._write(msg)
        else:
            origin_path_str = path.split('/tags/')[0]
            origin_path_str_absolute = \
                http_prefix + '://' + domain_full + origin_path_str
            if calling_domain.endswith('.onion') and onion_domain:
                origin_path_str_absolute = \
                    'http://' + onion_domain + origin_path_str
            elif (calling_domain.endswith('.i2p') and onion_domain):
                origin_path_str_absolute = \
                    'http://' + i2p_domain + origin_path_str
            self._redirect_headers(origin_path_str_absolute + '/search',
                                   cookie, calling_domain)
        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_hashtag_search',
                            self.server.debug)

    def _hashtag_search_rss2(self, calling_domain: str,
                             path: str, cookie: str,
                             base_dir: str, http_prefix: str,
                             domain: str, domain_full: str, port: int,
                             onion_domain: str, i2p_domain: str,
                             getreq_start_time,
                             curr_session) -> None:
        """Return an RSS 2 feed for a hashtag
        """
        hashtag = path.split('/tags/rss2/')[1]
        if is_blocked_hashtag(base_dir, hashtag):
            self._400()
            return
        nickname = None
        if '/users/' in path:
            actor = \
                http_prefix + '://' + domain_full + path
            nickname = \
                get_nickname_from_actor(actor)
        hashtag_str = \
            rss_hashtag_search(nickname,
                               domain, port,
                               self.server.recent_posts_cache,
                               self.server.max_recent_posts,
                               self.server.translate,
                               base_dir, hashtag,
                               MAX_POSTS_IN_FEED, curr_session,
                               self.server.cached_webfingers,
                               self.server.person_cache,
                               http_prefix,
                               self.server.project_version,
                               self.server.yt_replace_domain,
                               self.server.twitter_replacement_domain,
                               self.server.system_language)
        if hashtag_str:
            msg = hashtag_str.encode('utf-8')
            msglen = len(msg)
            self._set_headers('text/xml', msglen,
                              cookie, calling_domain, False)
            self._write(msg)
        else:
            origin_path_str = path.split('/tags/rss2/')[0]
            origin_path_str_absolute = \
                http_prefix + '://' + domain_full + origin_path_str
            if calling_domain.endswith('.onion') and onion_domain:
                origin_path_str_absolute = \
                    'http://' + onion_domain + origin_path_str
            elif (calling_domain.endswith('.i2p') and onion_domain):
                origin_path_str_absolute = \
                    'http://' + i2p_domain + origin_path_str
            self._redirect_headers(origin_path_str_absolute + '/search',
                                   cookie, calling_domain)
        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_hashtag_search_rss2',
                            self.server.debug)

    def _announce_button(self, calling_domain: str, path: str,
                         base_dir: str,
                         cookie: str, proxy_type: str,
                         http_prefix: str,
                         domain: str, domain_full: str, port: int,
                         onion_domain: str, i2p_domain: str,
                         getreq_start_time,
                         repeat_private: bool,
                         debug: bool,
                         curr_session) -> None:
        """The announce/repeat button was pressed on a post
        """
        page_number = 1
        repeat_url = path.split('?repeat=')[1]
        if '?' in repeat_url:
            repeat_url = repeat_url.split('?')[0]
        timeline_bookmark = ''
        if '?bm=' in path:
            timeline_bookmark = path.split('?bm=')[1]
            if '?' in timeline_bookmark:
                timeline_bookmark = timeline_bookmark.split('?')[0]
            timeline_bookmark = '#' + timeline_bookmark
        if '?page=' in path:
            page_number_str = path.split('?page=')[1]
            if '?' in page_number_str:
                page_number_str = page_number_str.split('?')[0]
            if '#' in page_number_str:
                page_number_str = page_number_str.split('#')[0]
            if len(page_number_str) > 5:
                page_number_str = "1"
            if page_number_str.isdigit():
                page_number = int(page_number_str)
        timeline_str = 'inbox'
        if '?tl=' in path:
            timeline_str = path.split('?tl=')[1]
            if '?' in timeline_str:
                timeline_str = timeline_str.split('?')[0]
        actor = path.split('?repeat=')[0]
        self.post_to_nickname = get_nickname_from_actor(actor)
        if not self.post_to_nickname:
            print('WARN: unable to find nickname in ' + actor)
            actor_absolute = self._get_instance_url(calling_domain) + actor
            actor_path_str = \
                actor_absolute + '/' + timeline_str + \
                '?page=' + str(page_number)
            self._redirect_headers(actor_path_str, cookie,
                                   calling_domain)
            return

        if onion_domain:
            if '.onion/' in actor:
                curr_session = self.server.session_onion
                proxy_type = 'tor'
        if i2p_domain:
            if '.onion/' in actor:
                curr_session = self.server.session_i2p
                proxy_type = 'i2p'

        curr_session = \
            self._establish_session("announceButton",
                                    curr_session, proxy_type)
        if not curr_session:
            self._404()
            return
        self.server.actorRepeat = path.split('?actor=')[1]
        announce_to_str = \
            local_actor_url(http_prefix, self.post_to_nickname,
                            domain_full) + \
            '/followers'
        if not repeat_private:
            announce_to_str = 'https://www.w3.org/ns/activitystreams#Public'
        announce_json = \
            create_announce(curr_session,
                            base_dir,
                            self.server.federation_list,
                            self.post_to_nickname,
                            domain, port,
                            announce_to_str,
                            None, http_prefix,
                            repeat_url, False, False,
                            self.server.send_threads,
                            self.server.postLog,
                            self.server.person_cache,
                            self.server.cached_webfingers,
                            debug,
                            self.server.project_version,
                            self.server.signing_priv_key_pem,
                            self.server.domain,
                            onion_domain,
                            i2p_domain)
        announce_filename = None
        if announce_json:
            # save the announce straight to the outbox
            # This is because the subsequent send is within a separate thread
            # but the html still needs to be generated before this call ends
            announce_id = remove_id_ending(announce_json['id'])
            announce_filename = \
                save_post_to_box(base_dir, http_prefix, announce_id,
                                 self.post_to_nickname, domain_full,
                                 announce_json, 'outbox')

            # clear the icon from the cache so that it gets updated
            if self.server.iconsCache.get('repeat.png'):
                del self.server.iconsCache['repeat.png']

            # send out the announce within a separate thread
            self._post_to_outbox(announce_json,
                                 self.server.project_version,
                                 self.post_to_nickname,
                                 curr_session, proxy_type)

            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', '_announce_button postToOutboxThread',
                                self.server.debug)

        # generate the html for the announce
        if announce_json and announce_filename:
            if debug:
                print('Generating html post for announce')
            cached_post_filename = \
                get_cached_post_filename(base_dir, self.post_to_nickname,
                                         domain, announce_json)
            if debug:
                print('Announced post json: ' + str(announce_json))
                print('Announced post nickname: ' +
                      self.post_to_nickname + ' ' + domain)
                print('Announced post cache: ' + str(cached_post_filename))
            show_individual_post_icons = True
            manually_approve_followers = \
                follower_approval_active(base_dir,
                                         self.post_to_nickname, domain)
            show_repeats = not is_dm(announce_json)
            timezone = None
            if self.server.account_timezone.get(self.post_to_nickname):
                timezone = \
                    self.server.account_timezone.get(self.post_to_nickname)
            mitm = False
            if os.path.isfile(announce_filename.replace('.json', '') +
                              '.mitm'):
                mitm = True
            bold_reading = False
            if self.server.bold_reading.get(self.post_to_nickname):
                bold_reading = True
            minimize_all_images = False
            if self.post_to_nickname in self.server.min_images_for_accounts:
                minimize_all_images = True
            individual_post_as_html(self.server.signing_priv_key_pem, False,
                                    self.server.recent_posts_cache,
                                    self.server.max_recent_posts,
                                    self.server.translate,
                                    page_number, base_dir,
                                    curr_session,
                                    self.server.cached_webfingers,
                                    self.server.person_cache,
                                    self.post_to_nickname, domain,
                                    self.server.port, announce_json,
                                    None, True,
                                    self.server.allow_deletion,
                                    http_prefix, self.server.project_version,
                                    timeline_str,
                                    self.server.yt_replace_domain,
                                    self.server.twitter_replacement_domain,
                                    self.server.show_published_date_only,
                                    self.server.peertube_instances,
                                    self.server.allow_local_network_access,
                                    self.server.theme_name,
                                    self.server.system_language,
                                    self.server.max_like_count,
                                    show_repeats,
                                    show_individual_post_icons,
                                    manually_approve_followers,
                                    False, True, False,
                                    self.server.cw_lists,
                                    self.server.lists_enabled,
                                    timezone, mitm, bold_reading,
                                    self.server.dogwhistles,
                                    minimize_all_images)

        actor_absolute = self._get_instance_url(calling_domain) + actor
        actor_path_str = \
            actor_absolute + '/' + timeline_str + '?page=' + \
            str(page_number) + timeline_bookmark
        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_announce_button',
                            self.server.debug)
        self._redirect_headers(actor_path_str, cookie, calling_domain)

    def _announce_button_undo(self, calling_domain: str, path: str,
                              base_dir: str, cookie: str, proxy_type: str,
                              http_prefix: str, domain: str, domain_full: str,
                              onion_domain: str, i2p_domain: str,
                              getreq_start_time, debug: bool,
                              recent_posts_cache: {}, curr_session) -> None:
        """Undo announce/repeat button was pressed
        """
        page_number = 1

        # the post which was referenced by the announce post
        repeat_url = path.split('?unrepeat=')[1]
        if '?' in repeat_url:
            repeat_url = repeat_url.split('?')[0]

        timeline_bookmark = ''
        if '?bm=' in path:
            timeline_bookmark = path.split('?bm=')[1]
            if '?' in timeline_bookmark:
                timeline_bookmark = timeline_bookmark.split('?')[0]
            timeline_bookmark = '#' + timeline_bookmark
        if '?page=' in path:
            page_number_str = path.split('?page=')[1]
            if '?' in page_number_str:
                page_number_str = page_number_str.split('?')[0]
            if '#' in page_number_str:
                page_number_str = page_number_str.split('#')[0]
            if len(page_number_str) > 5:
                page_number_str = "1"
            if page_number_str.isdigit():
                page_number = int(page_number_str)
        timeline_str = 'inbox'
        if '?tl=' in path:
            timeline_str = path.split('?tl=')[1]
            if '?' in timeline_str:
                timeline_str = timeline_str.split('?')[0]
        actor = path.split('?unrepeat=')[0]
        self.post_to_nickname = get_nickname_from_actor(actor)
        if not self.post_to_nickname:
            print('WARN: unable to find nickname in ' + actor)
            actor_absolute = self._get_instance_url(calling_domain) + actor
            actor_path_str = \
                actor_absolute + '/' + timeline_str + '?page=' + \
                str(page_number)
            self._redirect_headers(actor_path_str, cookie,
                                   calling_domain)
            return

        if onion_domain:
            if '.onion/' in actor:
                curr_session = self.server.session_onion
                proxy_type = 'tor'
        if i2p_domain:
            if '.onion/' in actor:
                curr_session = self.server.session_i2p
                proxy_type = 'i2p'

        curr_session = \
            self._establish_session("undoAnnounceButton",
                                    curr_session, proxy_type)
        if not curr_session:
            self._404()
            return
        undo_announce_actor = \
            http_prefix + '://' + domain_full + \
            '/users/' + self.post_to_nickname
        un_repeat_to_str = 'https://www.w3.org/ns/activitystreams#Public'
        new_undo_announce = {
            "@context": "https://www.w3.org/ns/activitystreams",
            'actor': undo_announce_actor,
            'type': 'Undo',
            'cc': [undo_announce_actor + '/followers'],
            'to': [un_repeat_to_str],
            'object': {
                'actor': undo_announce_actor,
                'cc': [undo_announce_actor + '/followers'],
                'object': repeat_url,
                'to': [un_repeat_to_str],
                'type': 'Announce'
            }
        }
        # clear the icon from the cache so that it gets updated
        if self.server.iconsCache.get('repeat_inactive.png'):
            del self.server.iconsCache['repeat_inactive.png']

        # delete the announce post
        if '?unannounce=' in path:
            announce_url = path.split('?unannounce=')[1]
            if '?' in announce_url:
                announce_url = announce_url.split('?')[0]
            post_filename = None
            nickname = get_nickname_from_actor(announce_url)
            if nickname:
                if domain_full + '/users/' + nickname + '/' in announce_url:
                    post_filename = \
                        locate_post(base_dir, nickname, domain, announce_url)
            if post_filename:
                delete_post(base_dir, http_prefix,
                            nickname, domain, post_filename,
                            debug, recent_posts_cache, True)

        self._post_to_outbox(new_undo_announce,
                             self.server.project_version,
                             self.post_to_nickname,
                             curr_session, proxy_type)

        actor_absolute = self._get_instance_url(calling_domain) + actor
        actor_path_str = \
            actor_absolute + '/' + timeline_str + '?page=' + \
            str(page_number) + timeline_bookmark
        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_undo_announce_button',
                            self.server.debug)
        self._redirect_headers(actor_path_str, cookie, calling_domain)

    def _follow_approve_button(self, calling_domain: str, path: str,
                               cookie: str,
                               base_dir: str, http_prefix: str,
                               domain: str, domain_full: str, port: int,
                               onion_domain: str, i2p_domain: str,
                               getreq_start_time,
                               proxy_type: str, debug: bool,
                               curr_session) -> None:
        """Follow approve button was pressed
        """
        origin_path_str = path.split('/followapprove=')[0]
        follower_nickname = origin_path_str.replace('/users/', '')
        following_handle = path.split('/followapprove=')[1]
        if '://' in following_handle:
            handle_nickname = get_nickname_from_actor(following_handle)
            if not handle_nickname:
                self._404()
                return
            handle_domain, handle_port = \
                get_domain_from_actor(following_handle)
            following_handle = \
                handle_nickname + '@' + \
                get_full_domain(handle_domain, handle_port)
        if '@' in following_handle:
            if self.server.onion_domain:
                if following_handle.endswith('.onion'):
                    curr_session = self.server.session_onion
                    proxy_type = 'tor'
                    port = 80
            if self.server.i2p_domain:
                if following_handle.endswith('.i2p'):
                    curr_session = self.server.session_i2p
                    proxy_type = 'i2p'
                    port = 80

            curr_session = \
                self._establish_session("follow_approve_button",
                                        curr_session, proxy_type)
            if not curr_session:
                print('WARN: unable to establish session ' +
                      'when approving follow request')
                self._404()
                return
            signing_priv_key_pem = \
                self.server.signing_priv_key_pem
            manual_approve_follow_request_thread(self.server.session,
                                                 self.server.session_onion,
                                                 self.server.session_i2p,
                                                 self.server.onion_domain,
                                                 self.server.i2p_domain,
                                                 base_dir, http_prefix,
                                                 follower_nickname,
                                                 domain, port,
                                                 following_handle,
                                                 self.server.federation_list,
                                                 self.server.send_threads,
                                                 self.server.postLog,
                                                 self.server.cached_webfingers,
                                                 self.server.person_cache,
                                                 debug,
                                                 self.server.project_version,
                                                 signing_priv_key_pem,
                                                 proxy_type)
        origin_path_str_absolute = \
            http_prefix + '://' + domain_full + origin_path_str
        if calling_domain.endswith('.onion') and onion_domain:
            origin_path_str_absolute = \
                'http://' + onion_domain + origin_path_str
        elif (calling_domain.endswith('.i2p') and i2p_domain):
            origin_path_str_absolute = \
                'http://' + i2p_domain + origin_path_str
        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_follow_approve_button',
                            self.server.debug)
        self._redirect_headers(origin_path_str_absolute,
                               cookie, calling_domain)

    def _newswire_vote(self, calling_domain: str, path: str,
                       cookie: str,
                       base_dir: str, http_prefix: str,
                       domain_full: str,
                       onion_domain: str, i2p_domain: str,
                       getreq_start_time,
                       newswire: {}):
        """Vote for a newswire item
        """
        origin_path_str = path.split('/newswirevote=')[0]
        date_str = \
            path.split('/newswirevote=')[1].replace('T', ' ')
        date_str = date_str.replace(' 00:00', '').replace('+00:00', '')
        date_str = urllib.parse.unquote_plus(date_str) + '+00:00'
        nickname = \
            urllib.parse.unquote_plus(origin_path_str.split('/users/')[1])
        if '/' in nickname:
            nickname = nickname.split('/')[0]
        print('Newswire item date: ' + date_str)
        if newswire.get(date_str):
            if is_moderator(base_dir, nickname):
                newswire_item = newswire[date_str]
                print('Voting on newswire item: ' + str(newswire_item))
                votes_index = 2
                filename_index = 3
                if 'vote:' + nickname not in newswire_item[votes_index]:
                    newswire_item[votes_index].append('vote:' + nickname)
                    filename = newswire_item[filename_index]
                    newswire_state_filename = \
                        base_dir + '/accounts/.newswirestate.json'
                    try:
                        save_json(newswire, newswire_state_filename)
                    except BaseException as ex:
                        print('EX: saving newswire state, ' + str(ex))
                    if filename:
                        save_json(newswire_item[votes_index],
                                  filename + '.votes')
        else:
            print('No newswire item with date: ' + date_str + ' ' +
                  str(newswire))

        origin_path_str_absolute = \
            http_prefix + '://' + domain_full + origin_path_str + '/' + \
            self.server.default_timeline
        if calling_domain.endswith('.onion') and onion_domain:
            origin_path_str_absolute = \
                'http://' + onion_domain + origin_path_str
        elif (calling_domain.endswith('.i2p') and i2p_domain):
            origin_path_str_absolute = \
                'http://' + i2p_domain + origin_path_str
        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_newswire_vote',
                            self.server.debug)
        self._redirect_headers(origin_path_str_absolute,
                               cookie, calling_domain)

    def _newswire_unvote(self, calling_domain: str, path: str,
                         cookie: str, base_dir: str, http_prefix: str,
                         domain_full: str,
                         onion_domain: str, i2p_domain: str,
                         getreq_start_time, debug: bool,
                         newswire: {}):
        """Remove vote for a newswire item
        """
        origin_path_str = path.split('/newswireunvote=')[0]
        date_str = \
            path.split('/newswireunvote=')[1].replace('T', ' ')
        date_str = date_str.replace(' 00:00', '').replace('+00:00', '')
        date_str = urllib.parse.unquote_plus(date_str) + '+00:00'
        nickname = \
            urllib.parse.unquote_plus(origin_path_str.split('/users/')[1])
        if '/' in nickname:
            nickname = nickname.split('/')[0]
        if newswire.get(date_str):
            if is_moderator(base_dir, nickname):
                votes_index = 2
                filename_index = 3
                newswire_item = newswire[date_str]
                if 'vote:' + nickname in newswire_item[votes_index]:
                    newswire_item[votes_index].remove('vote:' + nickname)
                    filename = newswire_item[filename_index]
                    newswire_state_filename = \
                        base_dir + '/accounts/.newswirestate.json'
                    try:
                        save_json(newswire, newswire_state_filename)
                    except BaseException as ex:
                        print('EX: saving newswire state, ' + str(ex))
                    if filename:
                        save_json(newswire_item[votes_index],
                                  filename + '.votes')
        else:
            print('No newswire item with date: ' + date_str + ' ' +
                  str(newswire))

        origin_path_str_absolute = \
            http_prefix + '://' + domain_full + origin_path_str + '/' + \
            self.server.default_timeline
        if calling_domain.endswith('.onion') and onion_domain:
            origin_path_str_absolute = \
                'http://' + onion_domain + origin_path_str
        elif (calling_domain.endswith('.i2p') and i2p_domain):
            origin_path_str_absolute = \
                'http://' + i2p_domain + origin_path_str
        self._redirect_headers(origin_path_str_absolute,
                               cookie, calling_domain)
        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_newswire_unvote', debug)

    def _follow_deny_button(self, calling_domain: str, path: str,
                            cookie: str, base_dir: str, http_prefix: str,
                            domain: str, domain_full: str, port: int,
                            onion_domain: str, i2p_domain: str,
                            getreq_start_time, debug: bool) -> None:
        """Follow deny button was pressed
        """
        origin_path_str = path.split('/followdeny=')[0]
        follower_nickname = origin_path_str.replace('/users/', '')
        following_handle = path.split('/followdeny=')[1]
        if '://' in following_handle:
            handle_nickname = get_nickname_from_actor(following_handle)
            if not handle_nickname:
                self._404()
                return
            handle_domain, handle_port = \
                get_domain_from_actor(following_handle)
            following_handle = \
                handle_nickname + '@' + \
                get_full_domain(handle_domain, handle_port)
        if '@' in following_handle:
            manual_deny_follow_request_thread(self.server.session,
                                              self.server.session_onion,
                                              self.server.session_i2p,
                                              onion_domain,
                                              i2p_domain,
                                              base_dir, http_prefix,
                                              follower_nickname,
                                              domain, port,
                                              following_handle,
                                              self.server.federation_list,
                                              self.server.send_threads,
                                              self.server.postLog,
                                              self.server.cached_webfingers,
                                              self.server.person_cache,
                                              debug,
                                              self.server.project_version,
                                              self.server.signing_priv_key_pem)
        origin_path_str_absolute = \
            http_prefix + '://' + domain_full + origin_path_str
        if calling_domain.endswith('.onion') and onion_domain:
            origin_path_str_absolute = \
                'http://' + onion_domain + origin_path_str
        elif calling_domain.endswith('.i2p') and i2p_domain:
            origin_path_str_absolute = \
                'http://' + i2p_domain + origin_path_str
        self._redirect_headers(origin_path_str_absolute,
                               cookie, calling_domain)
        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_follow_deny_button',
                            self.server.debug)

    def _like_button(self, calling_domain: str, path: str,
                     base_dir: str, http_prefix: str,
                     domain: str, domain_full: str,
                     onion_domain: str, i2p_domain: str,
                     getreq_start_time,
                     proxy_type: str, cookie: str,
                     debug: str,
                     curr_session) -> None:
        """Press the like button
        """
        page_number = 1
        like_url = path.split('?like=')[1]
        if '?' in like_url:
            like_url = like_url.split('?')[0]
        timeline_bookmark = ''
        if '?bm=' in path:
            timeline_bookmark = path.split('?bm=')[1]
            if '?' in timeline_bookmark:
                timeline_bookmark = timeline_bookmark.split('?')[0]
            timeline_bookmark = '#' + timeline_bookmark
        actor = path.split('?like=')[0]
        if '?page=' in path:
            page_number_str = path.split('?page=')[1]
            if '?' in page_number_str:
                page_number_str = page_number_str.split('?')[0]
            if '#' in page_number_str:
                page_number_str = page_number_str.split('#')[0]
            if len(page_number_str) > 5:
                page_number_str = "1"
            if page_number_str.isdigit():
                page_number = int(page_number_str)
        timeline_str = 'inbox'
        if '?tl=' in path:
            timeline_str = path.split('?tl=')[1]
            if '?' in timeline_str:
                timeline_str = timeline_str.split('?')[0]

        self.post_to_nickname = get_nickname_from_actor(actor)
        if not self.post_to_nickname:
            print('WARN: unable to find nickname in ' + actor)
            actor_absolute = self._get_instance_url(calling_domain) + actor
            actor_path_str = \
                actor_absolute + '/' + timeline_str + \
                '?page=' + str(page_number) + timeline_bookmark
            self._redirect_headers(actor_path_str, cookie,
                                   calling_domain)
            return

        if onion_domain:
            if '.onion/' in actor:
                curr_session = self.server.session_onion
                proxy_type = 'tor'
        if i2p_domain:
            if '.onion/' in actor:
                curr_session = self.server.session_i2p
                proxy_type = 'i2p'

        curr_session = \
            self._establish_session("likeButton",
                                    curr_session, proxy_type)
        if not curr_session:
            self._404()
            return
        like_actor = \
            local_actor_url(http_prefix, self.post_to_nickname, domain_full)
        actor_liked = path.split('?actor=')[1]
        if '?' in actor_liked:
            actor_liked = actor_liked.split('?')[0]

        # if this is an announce then send the like to the original post
        orig_actor, orig_post_url, orig_filename = \
            get_original_post_from_announce_url(like_url, base_dir,
                                                self.post_to_nickname, domain)
        like_url2 = like_url
        liked_post_filename = orig_filename
        if orig_actor and orig_post_url:
            actor_liked = orig_actor
            like_url2 = orig_post_url
            liked_post_filename = None

        like_json = {
            "@context": "https://www.w3.org/ns/activitystreams",
            'type': 'Like',
            'actor': like_actor,
            'to': [actor_liked],
            'object': like_url2
        }

        # send out the like to followers
        self._post_to_outbox(like_json, self.server.project_version, None,
                             curr_session, proxy_type)

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_like_button postToOutbox',
                            self.server.debug)

        print('Locating liked post ' + like_url)
        # directly like the post file
        if not liked_post_filename:
            liked_post_filename = \
                locate_post(base_dir, self.post_to_nickname, domain, like_url)
        if liked_post_filename:
            recent_posts_cache = self.server.recent_posts_cache
            liked_post_json = load_json(liked_post_filename, 0, 1)
            if orig_filename and orig_post_url:
                update_likes_collection(recent_posts_cache,
                                        base_dir, liked_post_filename,
                                        like_url, like_actor,
                                        self.post_to_nickname,
                                        domain, debug, liked_post_json)
                like_url = orig_post_url
                liked_post_filename = orig_filename
            if debug:
                print('Updating likes for ' + liked_post_filename)
            update_likes_collection(recent_posts_cache,
                                    base_dir, liked_post_filename, like_url,
                                    like_actor, self.post_to_nickname, domain,
                                    debug, None)
            if debug:
                print('Regenerating html post for changed likes collection')
            # clear the icon from the cache so that it gets updated
            if liked_post_json:
                cached_post_filename = \
                    get_cached_post_filename(base_dir, self.post_to_nickname,
                                             domain, liked_post_json)
                if debug:
                    print('Liked post json: ' + str(liked_post_json))
                    print('Liked post nickname: ' +
                          self.post_to_nickname + ' ' + domain)
                    print('Liked post cache: ' + str(cached_post_filename))
                show_individual_post_icons = True
                manually_approve_followers = \
                    follower_approval_active(base_dir,
                                             self.post_to_nickname, domain)
                show_repeats = not is_dm(liked_post_json)
                timezone = None
                if self.server.account_timezone.get(self.post_to_nickname):
                    timezone = \
                        self.server.account_timezone.get(self.post_to_nickname)
                mitm = False
                if os.path.isfile(liked_post_filename.replace('.json', '') +
                                  '.mitm'):
                    mitm = True
                bold_reading = False
                if self.server.bold_reading.get(self.post_to_nickname):
                    bold_reading = True
                minimize_all_images = False
                if self.post_to_nickname in \
                   self.server.min_images_for_accounts:
                    minimize_all_images = True
                individual_post_as_html(self.server.signing_priv_key_pem,
                                        False,
                                        self.server.recent_posts_cache,
                                        self.server.max_recent_posts,
                                        self.server.translate,
                                        page_number, base_dir,
                                        curr_session,
                                        self.server.cached_webfingers,
                                        self.server.person_cache,
                                        self.post_to_nickname, domain,
                                        self.server.port, liked_post_json,
                                        None, True,
                                        self.server.allow_deletion,
                                        http_prefix,
                                        self.server.project_version,
                                        timeline_str,
                                        self.server.yt_replace_domain,
                                        self.server.twitter_replacement_domain,
                                        self.server.show_published_date_only,
                                        self.server.peertube_instances,
                                        self.server.allow_local_network_access,
                                        self.server.theme_name,
                                        self.server.system_language,
                                        self.server.max_like_count,
                                        show_repeats,
                                        show_individual_post_icons,
                                        manually_approve_followers,
                                        False, True, False,
                                        self.server.cw_lists,
                                        self.server.lists_enabled,
                                        timezone, mitm, bold_reading,
                                        self.server.dogwhistles,
                                        minimize_all_images)
            else:
                print('WARN: Liked post not found: ' + liked_post_filename)
            # clear the icon from the cache so that it gets updated
            if self.server.iconsCache.get('like.png'):
                del self.server.iconsCache['like.png']
        else:
            print('WARN: unable to locate file for liked post ' +
                  like_url)

        actor_absolute = self._get_instance_url(calling_domain) + actor
        actor_path_str = \
            actor_absolute + '/' + timeline_str + \
            '?page=' + str(page_number) + timeline_bookmark
        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_like_button',
                            self.server.debug)
        self._redirect_headers(actor_path_str, cookie,
                               calling_domain)

    def _undo_like_button(self, calling_domain: str, path: str,
                          base_dir: str, http_prefix: str,
                          domain: str, domain_full: str,
                          onion_domain: str, i2p_domain: str,
                          getreq_start_time,
                          proxy_type: str, cookie: str,
                          debug: str,
                          curr_session) -> None:
        """A button is pressed to undo
        """
        page_number = 1
        like_url = path.split('?unlike=')[1]
        if '?' in like_url:
            like_url = like_url.split('?')[0]
        timeline_bookmark = ''
        if '?bm=' in path:
            timeline_bookmark = path.split('?bm=')[1]
            if '?' in timeline_bookmark:
                timeline_bookmark = timeline_bookmark.split('?')[0]
            timeline_bookmark = '#' + timeline_bookmark
        if '?page=' in path:
            page_number_str = path.split('?page=')[1]
            if '?' in page_number_str:
                page_number_str = page_number_str.split('?')[0]
            if '#' in page_number_str:
                page_number_str = page_number_str.split('#')[0]
            if len(page_number_str) > 5:
                page_number_str = "1"
            if page_number_str.isdigit():
                page_number = int(page_number_str)
        timeline_str = 'inbox'
        if '?tl=' in path:
            timeline_str = path.split('?tl=')[1]
            if '?' in timeline_str:
                timeline_str = timeline_str.split('?')[0]
        actor = path.split('?unlike=')[0]
        self.post_to_nickname = get_nickname_from_actor(actor)
        if not self.post_to_nickname:
            print('WARN: unable to find nickname in ' + actor)
            actor_absolute = self._get_instance_url(calling_domain) + actor
            actor_path_str = \
                actor_absolute + '/' + timeline_str + \
                '?page=' + str(page_number)
            self._redirect_headers(actor_path_str, cookie,
                                   calling_domain)
            return

        if onion_domain:
            if '.onion/' in actor:
                curr_session = self.server.session_onion
                proxy_type = 'tor'
        if i2p_domain:
            if '.onion/' in actor:
                curr_session = self.server.session_i2p
                proxy_type = 'i2p'

        curr_session = \
            self._establish_session("undoLikeButton",
                                    curr_session, proxy_type)
        if not curr_session:
            self._404()
            return
        undo_actor = \
            local_actor_url(http_prefix, self.post_to_nickname, domain_full)
        actor_liked = path.split('?actor=')[1]
        if '?' in actor_liked:
            actor_liked = actor_liked.split('?')[0]

        # if this is an announce then send the like to the original post
        orig_actor, orig_post_url, orig_filename = \
            get_original_post_from_announce_url(like_url, base_dir,
                                                self.post_to_nickname, domain)
        like_url2 = like_url
        liked_post_filename = orig_filename
        if orig_actor and orig_post_url:
            actor_liked = orig_actor
            like_url2 = orig_post_url
            liked_post_filename = None

        undo_like_json = {
            "@context": "https://www.w3.org/ns/activitystreams",
            'type': 'Undo',
            'actor': undo_actor,
            'to': [actor_liked],
            'object': {
                'type': 'Like',
                'actor': undo_actor,
                'to': [actor_liked],
                'object': like_url2
            }
        }

        # send out the undo like to followers
        self._post_to_outbox(undo_like_json,
                             self.server.project_version, None,
                             curr_session, proxy_type)

        # directly undo the like within the post file
        if not liked_post_filename:
            liked_post_filename = locate_post(base_dir, self.post_to_nickname,
                                              domain, like_url)
        if liked_post_filename:
            recent_posts_cache = self.server.recent_posts_cache
            liked_post_json = load_json(liked_post_filename, 0, 1)
            if orig_filename and orig_post_url:
                undo_likes_collection_entry(recent_posts_cache,
                                            base_dir, liked_post_filename,
                                            like_url, undo_actor,
                                            domain, debug,
                                            liked_post_json)
                like_url = orig_post_url
                liked_post_filename = orig_filename
            if debug:
                print('Removing likes for ' + liked_post_filename)
            undo_likes_collection_entry(recent_posts_cache,
                                        base_dir,
                                        liked_post_filename, like_url,
                                        undo_actor, domain, debug, None)
            if debug:
                print('Regenerating html post for changed likes collection')
            if liked_post_json:
                show_individual_post_icons = True
                manually_approve_followers = \
                    follower_approval_active(base_dir,
                                             self.post_to_nickname, domain)
                show_repeats = not is_dm(liked_post_json)
                timezone = None
                if self.server.account_timezone.get(self.post_to_nickname):
                    timezone = \
                        self.server.account_timezone.get(self.post_to_nickname)
                mitm = False
                if os.path.isfile(liked_post_filename.replace('.json', '') +
                                  '.mitm'):
                    mitm = True
                bold_reading = False
                if self.server.bold_reading.get(self.post_to_nickname):
                    bold_reading = True
                minimize_all_images = False
                if self.post_to_nickname in \
                   self.server.min_images_for_accounts:
                    minimize_all_images = True
                individual_post_as_html(self.server.signing_priv_key_pem,
                                        False,
                                        self.server.recent_posts_cache,
                                        self.server.max_recent_posts,
                                        self.server.translate,
                                        page_number, base_dir,
                                        curr_session,
                                        self.server.cached_webfingers,
                                        self.server.person_cache,
                                        self.post_to_nickname, domain,
                                        self.server.port, liked_post_json,
                                        None, True,
                                        self.server.allow_deletion,
                                        http_prefix,
                                        self.server.project_version,
                                        timeline_str,
                                        self.server.yt_replace_domain,
                                        self.server.twitter_replacement_domain,
                                        self.server.show_published_date_only,
                                        self.server.peertube_instances,
                                        self.server.allow_local_network_access,
                                        self.server.theme_name,
                                        self.server.system_language,
                                        self.server.max_like_count,
                                        show_repeats,
                                        show_individual_post_icons,
                                        manually_approve_followers,
                                        False, True, False,
                                        self.server.cw_lists,
                                        self.server.lists_enabled,
                                        timezone, mitm, bold_reading,
                                        self.server.dogwhistles,
                                        minimize_all_images)
            else:
                print('WARN: Unliked post not found: ' + liked_post_filename)
            # clear the icon from the cache so that it gets updated
            if self.server.iconsCache.get('like_inactive.png'):
                del self.server.iconsCache['like_inactive.png']
        actor_absolute = self._get_instance_url(calling_domain) + actor
        actor_path_str = \
            actor_absolute + '/' + timeline_str + \
            '?page=' + str(page_number) + timeline_bookmark
        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_undo_like_button',
                            self.server.debug)
        self._redirect_headers(actor_path_str, cookie,
                               calling_domain)

    def _reaction_button(self, calling_domain: str, path: str,
                         base_dir: str, http_prefix: str,
                         domain: str, domain_full: str,
                         onion_domain: str, i2p_domain: str,
                         getreq_start_time,
                         proxy_type: str, cookie: str,
                         debug: str,
                         curr_session) -> None:
        """Press an emoji reaction button
        Note that this is not the emoji reaction selection icon at the
        bottom of the post
        """
        page_number = 1
        reaction_url = path.split('?react=')[1]
        if '?' in reaction_url:
            reaction_url = reaction_url.split('?')[0]
        timeline_bookmark = ''
        if '?bm=' in path:
            timeline_bookmark = path.split('?bm=')[1]
            if '?' in timeline_bookmark:
                timeline_bookmark = timeline_bookmark.split('?')[0]
            timeline_bookmark = '#' + timeline_bookmark
        actor = path.split('?react=')[0]
        if '?page=' in path:
            page_number_str = path.split('?page=')[1]
            if '?' in page_number_str:
                page_number_str = page_number_str.split('?')[0]
            if '#' in page_number_str:
                page_number_str = page_number_str.split('#')[0]
            if len(page_number_str) > 5:
                page_number_str = "1"
            if page_number_str.isdigit():
                page_number = int(page_number_str)
        timeline_str = 'inbox'
        if '?tl=' in path:
            timeline_str = path.split('?tl=')[1]
            if '?' in timeline_str:
                timeline_str = timeline_str.split('?')[0]
        emoji_content_encoded = None
        if '?emojreact=' in path:
            emoji_content_encoded = path.split('?emojreact=')[1]
            if '?' in emoji_content_encoded:
                emoji_content_encoded = emoji_content_encoded.split('?')[0]
        if not emoji_content_encoded:
            print('WARN: no emoji reaction ' + actor)
            actor_absolute = self._get_instance_url(calling_domain) + actor
            actor_path_str = \
                actor_absolute + '/' + timeline_str + \
                '?page=' + str(page_number) + timeline_bookmark
            self._redirect_headers(actor_path_str, cookie,
                                   calling_domain)
            return
        emoji_content = urllib.parse.unquote_plus(emoji_content_encoded)
        self.post_to_nickname = get_nickname_from_actor(actor)
        if not self.post_to_nickname:
            print('WARN: unable to find nickname in ' + actor)
            actor_absolute = self._get_instance_url(calling_domain) + actor
            actor_path_str = \
                actor_absolute + '/' + timeline_str + \
                '?page=' + str(page_number) + timeline_bookmark
            self._redirect_headers(actor_path_str, cookie,
                                   calling_domain)
            return

        if onion_domain:
            if '.onion/' in actor:
                curr_session = self.server.session_onion
                proxy_type = 'tor'
        if i2p_domain:
            if '.onion/' in actor:
                curr_session = self.server.session_i2p
                proxy_type = 'i2p'

        curr_session = \
            self._establish_session("reactionButton",
                                    curr_session, proxy_type)
        if not curr_session:
            self._404()
            return
        reaction_actor = \
            local_actor_url(http_prefix, self.post_to_nickname, domain_full)
        actor_reaction = path.split('?actor=')[1]
        if '?' in actor_reaction:
            actor_reaction = actor_reaction.split('?')[0]

        # if this is an announce then send the emoji reaction
        # to the original post
        orig_actor, orig_post_url, orig_filename = \
            get_original_post_from_announce_url(reaction_url, base_dir,
                                                self.post_to_nickname, domain)
        reaction_url2 = reaction_url
        reaction_post_filename = orig_filename
        if orig_actor and orig_post_url:
            actor_reaction = orig_actor
            reaction_url2 = orig_post_url
            reaction_post_filename = None

        reaction_json = {
            "@context": "https://www.w3.org/ns/activitystreams",
            'type': 'EmojiReact',
            'actor': reaction_actor,
            'to': [actor_reaction],
            'object': reaction_url2,
            'content': emoji_content
        }

        # send out the emoji reaction to followers
        self._post_to_outbox(reaction_json, self.server.project_version, None,
                             curr_session, proxy_type)

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_reaction_button postToOutbox',
                            self.server.debug)

        print('Locating emoji reaction post ' + reaction_url)
        # directly emoji reaction the post file
        if not reaction_post_filename:
            reaction_post_filename = \
                locate_post(base_dir, self.post_to_nickname, domain,
                            reaction_url)
        if reaction_post_filename:
            recent_posts_cache = self.server.recent_posts_cache
            reaction_post_json = load_json(reaction_post_filename, 0, 1)
            if orig_filename and orig_post_url:
                update_reaction_collection(recent_posts_cache,
                                           base_dir, reaction_post_filename,
                                           reaction_url,
                                           reaction_actor,
                                           self.post_to_nickname,
                                           domain, debug, reaction_post_json,
                                           emoji_content)
                reaction_url = orig_post_url
                reaction_post_filename = orig_filename
            if debug:
                print('Updating emoji reaction for ' + reaction_post_filename)
            update_reaction_collection(recent_posts_cache,
                                       base_dir, reaction_post_filename,
                                       reaction_url,
                                       reaction_actor,
                                       self.post_to_nickname, domain,
                                       debug, None, emoji_content)
            if debug:
                print('Regenerating html post for changed ' +
                      'emoji reaction collection')
            # clear the icon from the cache so that it gets updated
            if reaction_post_json:
                cached_post_filename = \
                    get_cached_post_filename(base_dir, self.post_to_nickname,
                                             domain, reaction_post_json)
                if debug:
                    print('Reaction post json: ' + str(reaction_post_json))
                    print('Reaction post nickname: ' +
                          self.post_to_nickname + ' ' + domain)
                    print('Reaction post cache: ' + str(cached_post_filename))
                show_individual_post_icons = True
                manually_approve_followers = \
                    follower_approval_active(base_dir,
                                             self.post_to_nickname, domain)
                show_repeats = not is_dm(reaction_post_json)
                timezone = None
                if self.server.account_timezone.get(self.post_to_nickname):
                    timezone = \
                        self.server.account_timezone.get(self.post_to_nickname)
                mitm = False
                if os.path.isfile(reaction_post_filename.replace('.json', '') +
                                  '.mitm'):
                    mitm = True
                bold_reading = False
                if self.server.bold_reading.get(self.post_to_nickname):
                    bold_reading = True
                minimize_all_images = False
                if self.post_to_nickname in \
                   self.server.min_images_for_accounts:
                    minimize_all_images = True
                individual_post_as_html(self.server.signing_priv_key_pem,
                                        False,
                                        self.server.recent_posts_cache,
                                        self.server.max_recent_posts,
                                        self.server.translate,
                                        page_number, base_dir,
                                        curr_session,
                                        self.server.cached_webfingers,
                                        self.server.person_cache,
                                        self.post_to_nickname, domain,
                                        self.server.port, reaction_post_json,
                                        None, True,
                                        self.server.allow_deletion,
                                        http_prefix,
                                        self.server.project_version,
                                        timeline_str,
                                        self.server.yt_replace_domain,
                                        self.server.twitter_replacement_domain,
                                        self.server.show_published_date_only,
                                        self.server.peertube_instances,
                                        self.server.allow_local_network_access,
                                        self.server.theme_name,
                                        self.server.system_language,
                                        self.server.max_like_count,
                                        show_repeats,
                                        show_individual_post_icons,
                                        manually_approve_followers,
                                        False, True, False,
                                        self.server.cw_lists,
                                        self.server.lists_enabled,
                                        timezone, mitm, bold_reading,
                                        self.server.dogwhistles,
                                        minimize_all_images)
            else:
                print('WARN: Emoji reaction post not found: ' +
                      reaction_post_filename)
        else:
            print('WARN: unable to locate file for emoji reaction post ' +
                  reaction_url)

        actor_absolute = self._get_instance_url(calling_domain) + actor
        actor_path_str = \
            actor_absolute + '/' + timeline_str + \
            '?page=' + str(page_number) + timeline_bookmark
        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_reaction_button',
                            self.server.debug)
        self._redirect_headers(actor_path_str, cookie,
                               calling_domain)

    def _undo_reaction_button(self, calling_domain: str, path: str,
                              base_dir: str, http_prefix: str,
                              domain: str, domain_full: str,
                              onion_domain: str, i2p_domain: str,
                              getreq_start_time,
                              proxy_type: str, cookie: str,
                              debug: str,
                              curr_session) -> None:
        """A button is pressed to undo emoji reaction
        """
        page_number = 1
        reaction_url = path.split('?unreact=')[1]
        if '?' in reaction_url:
            reaction_url = reaction_url.split('?')[0]
        timeline_bookmark = ''
        if '?bm=' in path:
            timeline_bookmark = path.split('?bm=')[1]
            if '?' in timeline_bookmark:
                timeline_bookmark = timeline_bookmark.split('?')[0]
            timeline_bookmark = '#' + timeline_bookmark
        if '?page=' in path:
            page_number_str = path.split('?page=')[1]
            if '?' in page_number_str:
                page_number_str = page_number_str.split('?')[0]
            if '#' in page_number_str:
                page_number_str = page_number_str.split('#')[0]
            if len(page_number_str) > 5:
                page_number_str = "1"
            if page_number_str.isdigit():
                page_number = int(page_number_str)
        timeline_str = 'inbox'
        if '?tl=' in path:
            timeline_str = path.split('?tl=')[1]
            if '?' in timeline_str:
                timeline_str = timeline_str.split('?')[0]
        actor = path.split('?unreact=')[0]
        self.post_to_nickname = get_nickname_from_actor(actor)
        if not self.post_to_nickname:
            print('WARN: unable to find nickname in ' + actor)
            actor_absolute = self._get_instance_url(calling_domain) + actor
            actor_path_str = \
                actor_absolute + '/' + timeline_str + \
                '?page=' + str(page_number)
            self._redirect_headers(actor_path_str, cookie,
                                   calling_domain)
            return
        emoji_content_encoded = None
        if '?emojreact=' in path:
            emoji_content_encoded = path.split('?emojreact=')[1]
            if '?' in emoji_content_encoded:
                emoji_content_encoded = emoji_content_encoded.split('?')[0]
        if not emoji_content_encoded:
            print('WARN: no emoji reaction ' + actor)
            actor_absolute = self._get_instance_url(calling_domain) + actor
            actor_path_str = \
                actor_absolute + '/' + timeline_str + \
                '?page=' + str(page_number) + timeline_bookmark
            self._redirect_headers(actor_path_str, cookie,
                                   calling_domain)
            return
        emoji_content = urllib.parse.unquote_plus(emoji_content_encoded)

        if onion_domain:
            if '.onion/' in actor:
                curr_session = self.server.session_onion
                proxy_type = 'tor'
        if i2p_domain:
            if '.onion/' in actor:
                curr_session = self.server.session_i2p
                proxy_type = 'i2p'

        curr_session = \
            self._establish_session("undoReactionButton",
                                    curr_session, proxy_type)
        if not curr_session:
            self._404()
            return
        undo_actor = \
            local_actor_url(http_prefix, self.post_to_nickname, domain_full)
        actor_reaction = path.split('?actor=')[1]
        if '?' in actor_reaction:
            actor_reaction = actor_reaction.split('?')[0]

        # if this is an announce then send the emoji reaction
        # to the original post
        orig_actor, orig_post_url, orig_filename = \
            get_original_post_from_announce_url(reaction_url, base_dir,
                                                self.post_to_nickname, domain)
        reaction_url2 = reaction_url
        reaction_post_filename = orig_filename
        if orig_actor and orig_post_url:
            actor_reaction = orig_actor
            reaction_url2 = orig_post_url
            reaction_post_filename = None

        undo_reaction_json = {
            "@context": "https://www.w3.org/ns/activitystreams",
            'type': 'Undo',
            'actor': undo_actor,
            'to': [actor_reaction],
            'object': {
                'type': 'EmojiReact',
                'actor': undo_actor,
                'to': [actor_reaction],
                'object': reaction_url2
            }
        }

        # send out the undo emoji reaction to followers
        self._post_to_outbox(undo_reaction_json,
                             self.server.project_version, None,
                             curr_session, proxy_type)

        # directly undo the emoji reaction within the post file
        if not reaction_post_filename:
            reaction_post_filename = \
                locate_post(base_dir, self.post_to_nickname, domain,
                            reaction_url)
        if reaction_post_filename:
            recent_posts_cache = self.server.recent_posts_cache
            reaction_post_json = load_json(reaction_post_filename, 0, 1)
            if orig_filename and orig_post_url:
                undo_reaction_collection_entry(recent_posts_cache,
                                               base_dir,
                                               reaction_post_filename,
                                               reaction_url,
                                               undo_actor, domain, debug,
                                               reaction_post_json,
                                               emoji_content)
                reaction_url = orig_post_url
                reaction_post_filename = orig_filename
            if debug:
                print('Removing emoji reaction for ' + reaction_post_filename)
            undo_reaction_collection_entry(recent_posts_cache,
                                           base_dir, reaction_post_filename,
                                           reaction_url,
                                           undo_actor, domain, debug,
                                           reaction_post_json, emoji_content)
            if debug:
                print('Regenerating html post for changed ' +
                      'emoji reaction collection')
            if reaction_post_json:
                show_individual_post_icons = True
                manually_approve_followers = \
                    follower_approval_active(base_dir,
                                             self.post_to_nickname, domain)
                show_repeats = not is_dm(reaction_post_json)
                timezone = None
                if self.server.account_timezone.get(self.post_to_nickname):
                    timezone = \
                        self.server.account_timezone.get(self.post_to_nickname)
                mitm = False
                if os.path.isfile(reaction_post_filename.replace('.json', '') +
                                  '.mitm'):
                    mitm = True
                bold_reading = False
                if self.server.bold_reading.get(self.post_to_nickname):
                    bold_reading = True
                minimize_all_images = False
                if self.post_to_nickname in \
                   self.server.min_images_for_accounts:
                    minimize_all_images = True
                individual_post_as_html(self.server.signing_priv_key_pem,
                                        False,
                                        self.server.recent_posts_cache,
                                        self.server.max_recent_posts,
                                        self.server.translate,
                                        page_number, base_dir,
                                        curr_session,
                                        self.server.cached_webfingers,
                                        self.server.person_cache,
                                        self.post_to_nickname, domain,
                                        self.server.port, reaction_post_json,
                                        None, True,
                                        self.server.allow_deletion,
                                        http_prefix,
                                        self.server.project_version,
                                        timeline_str,
                                        self.server.yt_replace_domain,
                                        self.server.twitter_replacement_domain,
                                        self.server.show_published_date_only,
                                        self.server.peertube_instances,
                                        self.server.allow_local_network_access,
                                        self.server.theme_name,
                                        self.server.system_language,
                                        self.server.max_like_count,
                                        show_repeats,
                                        show_individual_post_icons,
                                        manually_approve_followers,
                                        False, True, False,
                                        self.server.cw_lists,
                                        self.server.lists_enabled,
                                        timezone, mitm, bold_reading,
                                        self.server.dogwhistles,
                                        minimize_all_images)
            else:
                print('WARN: Unreaction post not found: ' +
                      reaction_post_filename)

        actor_absolute = self._get_instance_url(calling_domain) + actor
        actor_path_str = \
            actor_absolute + '/' + timeline_str + \
            '?page=' + str(page_number) + timeline_bookmark
        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_undo_reaction_button',
                            self.server.debug)
        self._redirect_headers(actor_path_str, cookie, calling_domain)

    def _reaction_picker(self, calling_domain: str, path: str,
                         base_dir: str, http_prefix: str,
                         domain: str, port: int,
                         getreq_start_time, cookie: str,
                         debug: str, curr_session) -> None:
        """Press the emoji reaction picker icon at the bottom of the post
        """
        page_number = 1
        reaction_url = path.split('?selreact=')[1]
        if '?' in reaction_url:
            reaction_url = reaction_url.split('?')[0]
        timeline_bookmark = ''
        if '?bm=' in path:
            timeline_bookmark = path.split('?bm=')[1]
            if '?' in timeline_bookmark:
                timeline_bookmark = timeline_bookmark.split('?')[0]
            timeline_bookmark = '#' + timeline_bookmark
        actor = path.split('?selreact=')[0]
        if '?page=' in path:
            page_number_str = path.split('?page=')[1]
            if '?' in page_number_str:
                page_number_str = page_number_str.split('?')[0]
            if '#' in page_number_str:
                page_number_str = page_number_str.split('#')[0]
            if len(page_number_str) > 5:
                page_number_str = "1"
            if page_number_str.isdigit():
                page_number = int(page_number_str)
        timeline_str = 'inbox'
        if '?tl=' in path:
            timeline_str = path.split('?tl=')[1]
            if '?' in timeline_str:
                timeline_str = timeline_str.split('?')[0]
        self.post_to_nickname = get_nickname_from_actor(actor)
        if not self.post_to_nickname:
            print('WARN: unable to find nickname in ' + actor)
            actor_absolute = self._get_instance_url(calling_domain) + actor
            actor_path_str = \
                actor_absolute + '/' + timeline_str + \
                '?page=' + str(page_number) + timeline_bookmark
            self._redirect_headers(actor_path_str, cookie, calling_domain)
            return

        post_json_object = None
        reaction_post_filename = \
            locate_post(base_dir,
                        self.post_to_nickname, domain, reaction_url)
        if reaction_post_filename:
            post_json_object = load_json(reaction_post_filename)
        if not reaction_post_filename or not post_json_object:
            print('WARN: unable to locate reaction post ' + reaction_url)
            actor_absolute = self._get_instance_url(calling_domain) + actor
            actor_path_str = \
                actor_absolute + '/' + timeline_str + \
                '?page=' + str(page_number) + timeline_bookmark
            self._redirect_headers(actor_path_str, cookie, calling_domain)
            return

        timezone = None
        if self.server.account_timezone.get(self.post_to_nickname):
            timezone = \
                self.server.account_timezone.get(self.post_to_nickname)

        bold_reading = False
        if self.server.bold_reading.get(self.post_to_nickname):
            bold_reading = True

        msg = \
            html_emoji_reaction_picker(self.server.recent_posts_cache,
                                       self.server.max_recent_posts,
                                       self.server.translate,
                                       base_dir,
                                       curr_session,
                                       self.server.cached_webfingers,
                                       self.server.person_cache,
                                       self.post_to_nickname,
                                       domain, port, post_json_object,
                                       http_prefix,
                                       self.server.project_version,
                                       self.server.yt_replace_domain,
                                       self.server.twitter_replacement_domain,
                                       self.server.show_published_date_only,
                                       self.server.peertube_instances,
                                       self.server.allow_local_network_access,
                                       self.server.theme_name,
                                       self.server.system_language,
                                       self.server.max_like_count,
                                       self.server.signing_priv_key_pem,
                                       self.server.cw_lists,
                                       self.server.lists_enabled,
                                       timeline_str, page_number,
                                       timezone, bold_reading,
                                       self.server.dogwhistles,
                                       self.server.min_images_for_accounts)
        msg = msg.encode('utf-8')
        msglen = len(msg)
        self._set_headers('text/html', msglen,
                          cookie, calling_domain, False)
        self._write(msg)
        fitness_performance(getreq_start_time,
                            self.server.fitness,
                            '_GET', '_reaction_picker',
                            debug)

    def _bookmark_button(self, calling_domain: str, path: str,
                         base_dir: str, http_prefix: str,
                         domain: str, domain_full: str, port: int,
                         onion_domain: str, i2p_domain: str,
                         getreq_start_time,
                         proxy_type: str, cookie: str,
                         debug: str,
                         curr_session) -> None:
        """Bookmark button was pressed
        """
        page_number = 1
        bookmark_url = path.split('?bookmark=')[1]
        if '?' in bookmark_url:
            bookmark_url = bookmark_url.split('?')[0]
        timeline_bookmark = ''
        if '?bm=' in path:
            timeline_bookmark = path.split('?bm=')[1]
            if '?' in timeline_bookmark:
                timeline_bookmark = timeline_bookmark.split('?')[0]
            timeline_bookmark = '#' + timeline_bookmark
        actor = path.split('?bookmark=')[0]
        if '?page=' in path:
            page_number_str = path.split('?page=')[1]
            if '?' in page_number_str:
                page_number_str = page_number_str.split('?')[0]
            if '#' in page_number_str:
                page_number_str = page_number_str.split('#')[0]
            if len(page_number_str) > 5:
                page_number_str = "1"
            if page_number_str.isdigit():
                page_number = int(page_number_str)
        timeline_str = 'inbox'
        if '?tl=' in path:
            timeline_str = path.split('?tl=')[1]
            if '?' in timeline_str:
                timeline_str = timeline_str.split('?')[0]

        self.post_to_nickname = get_nickname_from_actor(actor)
        if not self.post_to_nickname:
            print('WARN: unable to find nickname in ' + actor)
            actor_absolute = self._get_instance_url(calling_domain) + actor
            actor_path_str = \
                actor_absolute + '/' + timeline_str + \
                '?page=' + str(page_number)
            self._redirect_headers(actor_path_str, cookie,
                                   calling_domain)
            return

        if onion_domain:
            if '.onion/' in actor:
                curr_session = self.server.session_onion
                proxy_type = 'tor'
        if i2p_domain:
            if '.onion/' in actor:
                curr_session = self.server.session_i2p
                proxy_type = 'i2p'

        curr_session = \
            self._establish_session("bookmarkButton",
                                    curr_session, proxy_type)
        if not curr_session:
            self._404()
            return
        bookmark_actor = \
            local_actor_url(http_prefix, self.post_to_nickname, domain_full)
        cc_list = []
        bookmark_post(self.server.recent_posts_cache,
                      base_dir, self.server.federation_list,
                      self.post_to_nickname, domain, port,
                      cc_list, http_prefix, bookmark_url, bookmark_actor,
                      debug)
        # clear the icon from the cache so that it gets updated
        if self.server.iconsCache.get('bookmark.png'):
            del self.server.iconsCache['bookmark.png']
        bookmark_filename = \
            locate_post(base_dir, self.post_to_nickname, domain, bookmark_url)
        if bookmark_filename:
            print('Regenerating html post for changed bookmark')
            bookmark_post_json = load_json(bookmark_filename, 0, 1)
            if bookmark_post_json:
                cached_post_filename = \
                    get_cached_post_filename(base_dir, self.post_to_nickname,
                                             domain, bookmark_post_json)
                print('Bookmarked post json: ' + str(bookmark_post_json))
                print('Bookmarked post nickname: ' +
                      self.post_to_nickname + ' ' + domain)
                print('Bookmarked post cache: ' + str(cached_post_filename))
                show_individual_post_icons = True
                manually_approve_followers = \
                    follower_approval_active(base_dir,
                                             self.post_to_nickname, domain)
                show_repeats = not is_dm(bookmark_post_json)
                timezone = None
                if self.server.account_timezone.get(self.post_to_nickname):
                    timezone = \
                        self.server.account_timezone.get(self.post_to_nickname)
                mitm = False
                if os.path.isfile(bookmark_filename.replace('.json', '') +
                                  '.mitm'):
                    mitm = True
                bold_reading = False
                if self.server.bold_reading.get(self.post_to_nickname):
                    bold_reading = True
                minimize_all_images = False
                if self.post_to_nickname in \
                   self.server.min_images_for_accounts:
                    minimize_all_images = True
                individual_post_as_html(self.server.signing_priv_key_pem,
                                        False,
                                        self.server.recent_posts_cache,
                                        self.server.max_recent_posts,
                                        self.server.translate,
                                        page_number, base_dir,
                                        curr_session,
                                        self.server.cached_webfingers,
                                        self.server.person_cache,
                                        self.post_to_nickname, domain,
                                        self.server.port, bookmark_post_json,
                                        None, True,
                                        self.server.allow_deletion,
                                        http_prefix,
                                        self.server.project_version,
                                        timeline_str,
                                        self.server.yt_replace_domain,
                                        self.server.twitter_replacement_domain,
                                        self.server.show_published_date_only,
                                        self.server.peertube_instances,
                                        self.server.allow_local_network_access,
                                        self.server.theme_name,
                                        self.server.system_language,
                                        self.server.max_like_count,
                                        show_repeats,
                                        show_individual_post_icons,
                                        manually_approve_followers,
                                        False, True, False,
                                        self.server.cw_lists,
                                        self.server.lists_enabled,
                                        timezone, mitm, bold_reading,
                                        self.server.dogwhistles,
                                        minimize_all_images)
            else:
                print('WARN: Bookmarked post not found: ' + bookmark_filename)
        # self._post_to_outbox(bookmark_json,
        # self.server.project_version, None,
        # curr_session, proxy_type)
        actor_absolute = self._get_instance_url(calling_domain) + actor
        actor_path_str = \
            actor_absolute + '/' + timeline_str + \
            '?page=' + str(page_number) + timeline_bookmark
        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_bookmark_button',
                            debug)
        self._redirect_headers(actor_path_str, cookie,
                               calling_domain)

    def _undo_bookmark_button(self, calling_domain: str, path: str,
                              base_dir: str, http_prefix: str,
                              domain: str, domain_full: str, port: int,
                              onion_domain: str, i2p_domain: str,
                              getreq_start_time,
                              proxy_type: str, cookie: str,
                              debug: str,
                              curr_session) -> None:
        """Button pressed to undo a bookmark
        """
        page_number = 1
        bookmark_url = path.split('?unbookmark=')[1]
        if '?' in bookmark_url:
            bookmark_url = bookmark_url.split('?')[0]
        timeline_bookmark = ''
        if '?bm=' in path:
            timeline_bookmark = path.split('?bm=')[1]
            if '?' in timeline_bookmark:
                timeline_bookmark = timeline_bookmark.split('?')[0]
            timeline_bookmark = '#' + timeline_bookmark
        if '?page=' in path:
            page_number_str = path.split('?page=')[1]
            if '?' in page_number_str:
                page_number_str = page_number_str.split('?')[0]
            if '#' in page_number_str:
                page_number_str = page_number_str.split('#')[0]
            if len(page_number_str) > 5:
                page_number_str = "1"
            if page_number_str.isdigit():
                page_number = int(page_number_str)
        timeline_str = 'inbox'
        if '?tl=' in path:
            timeline_str = path.split('?tl=')[1]
            if '?' in timeline_str:
                timeline_str = timeline_str.split('?')[0]
        actor = path.split('?unbookmark=')[0]
        self.post_to_nickname = get_nickname_from_actor(actor)
        if not self.post_to_nickname:
            print('WARN: unable to find nickname in ' + actor)
            actor_absolute = self._get_instance_url(calling_domain) + actor
            actor_path_str = \
                actor_absolute + '/' + timeline_str + \
                '?page=' + str(page_number)
            self._redirect_headers(actor_path_str, cookie,
                                   calling_domain)
            return

        if onion_domain:
            if '.onion/' in actor:
                curr_session = self.server.session_onion
                proxy_type = 'tor'
        if i2p_domain:
            if '.onion/' in actor:
                curr_session = self.server.session_i2p
                proxy_type = 'i2p'

        curr_session = \
            self._establish_session("undo_bookmarkButton",
                                    curr_session, proxy_type)
        if not curr_session:
            self._404()
            return
        undo_actor = \
            local_actor_url(http_prefix, self.post_to_nickname, domain_full)
        cc_list = []
        undo_bookmark_post(self.server.recent_posts_cache,
                           base_dir, self.server.federation_list,
                           self.post_to_nickname,
                           domain, port, cc_list, http_prefix,
                           bookmark_url, undo_actor, debug)
        # clear the icon from the cache so that it gets updated
        if self.server.iconsCache.get('bookmark_inactive.png'):
            del self.server.iconsCache['bookmark_inactive.png']
        # self._post_to_outbox(undo_bookmark_json,
        #                    self.server.project_version, None,
        #                    curr_session, proxy_type)
        bookmark_filename = \
            locate_post(base_dir, self.post_to_nickname, domain, bookmark_url)
        if bookmark_filename:
            print('Regenerating html post for changed unbookmark')
            bookmark_post_json = load_json(bookmark_filename, 0, 1)
            if bookmark_post_json:
                cached_post_filename = \
                    get_cached_post_filename(base_dir, self.post_to_nickname,
                                             domain, bookmark_post_json)
                print('Unbookmarked post json: ' + str(bookmark_post_json))
                print('Unbookmarked post nickname: ' +
                      self.post_to_nickname + ' ' + domain)
                print('Unbookmarked post cache: ' + str(cached_post_filename))
                show_individual_post_icons = True
                manually_approve_followers = \
                    follower_approval_active(base_dir,
                                             self.post_to_nickname, domain)
                show_repeats = not is_dm(bookmark_post_json)
                timezone = None
                if self.server.account_timezone.get(self.post_to_nickname):
                    timezone = \
                        self.server.account_timezone.get(self.post_to_nickname)
                mitm = False
                if os.path.isfile(bookmark_filename.replace('.json', '') +
                                  '.mitm'):
                    mitm = True
                bold_reading = False
                if self.server.bold_reading.get(self.post_to_nickname):
                    bold_reading = True
                minimize_all_images = False
                if self.post_to_nickname in \
                   self.server.min_images_for_accounts:
                    minimize_all_images = True
                individual_post_as_html(self.server.signing_priv_key_pem,
                                        False,
                                        self.server.recent_posts_cache,
                                        self.server.max_recent_posts,
                                        self.server.translate,
                                        page_number, base_dir,
                                        curr_session,
                                        self.server.cached_webfingers,
                                        self.server.person_cache,
                                        self.post_to_nickname, domain,
                                        self.server.port, bookmark_post_json,
                                        None, True,
                                        self.server.allow_deletion,
                                        http_prefix,
                                        self.server.project_version,
                                        timeline_str,
                                        self.server.yt_replace_domain,
                                        self.server.twitter_replacement_domain,
                                        self.server.show_published_date_only,
                                        self.server.peertube_instances,
                                        self.server.allow_local_network_access,
                                        self.server.theme_name,
                                        self.server.system_language,
                                        self.server.max_like_count,
                                        show_repeats,
                                        show_individual_post_icons,
                                        manually_approve_followers,
                                        False, True, False,
                                        self.server.cw_lists,
                                        self.server.lists_enabled,
                                        timezone, mitm, bold_reading,
                                        self.server.dogwhistles,
                                        minimize_all_images)
            else:
                print('WARN: Unbookmarked post not found: ' +
                      bookmark_filename)
        actor_absolute = self._get_instance_url(calling_domain) + actor
        actor_path_str = \
            actor_absolute + '/' + timeline_str + \
            '?page=' + str(page_number) + timeline_bookmark
        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_undo_bookmark_button',
                            self.server.debug)
        self._redirect_headers(actor_path_str, cookie,
                               calling_domain)

    def _delete_button(self, calling_domain: str, path: str,
                       base_dir: str, http_prefix: str,
                       domain_full: str,
                       onion_domain: str, i2p_domain: str,
                       getreq_start_time,
                       proxy_type: str, cookie: str,
                       debug: str, curr_session) -> None:
        """Delete button is pressed on a post
        """
        if not cookie:
            print('ERROR: no cookie given when deleting ' + path)
            self._400()
            return
        page_number = 1
        if '?page=' in path:
            page_number_str = path.split('?page=')[1]
            if '?' in page_number_str:
                page_number_str = page_number_str.split('?')[0]
            if '#' in page_number_str:
                page_number_str = page_number_str.split('#')[0]
            if len(page_number_str) > 5:
                page_number_str = "1"
            if page_number_str.isdigit():
                page_number = int(page_number_str)
        delete_url = path.split('?delete=')[1]
        if '?' in delete_url:
            delete_url = delete_url.split('?')[0]
        timeline_str = self.server.default_timeline
        if '?tl=' in path:
            timeline_str = path.split('?tl=')[1]
            if '?' in timeline_str:
                timeline_str = timeline_str.split('?')[0]
        users_path = path.split('?delete=')[0]
        actor = \
            http_prefix + '://' + domain_full + users_path
        if self.server.allow_deletion or \
           delete_url.startswith(actor):
            if self.server.debug:
                print('DEBUG: delete_url=' + delete_url)
                print('DEBUG: actor=' + actor)
            if actor not in delete_url:
                # You can only delete your own posts
                if calling_domain.endswith('.onion') and onion_domain:
                    actor = 'http://' + onion_domain + users_path
                elif calling_domain.endswith('.i2p') and i2p_domain:
                    actor = 'http://' + i2p_domain + users_path
                self._redirect_headers(actor + '/' + timeline_str,
                                       cookie, calling_domain)
                return
            self.post_to_nickname = get_nickname_from_actor(actor)
            if not self.post_to_nickname:
                print('WARN: unable to find nickname in ' + actor)
                if calling_domain.endswith('.onion') and onion_domain:
                    actor = 'http://' + onion_domain + users_path
                elif calling_domain.endswith('.i2p') and i2p_domain:
                    actor = 'http://' + i2p_domain + users_path
                self._redirect_headers(actor + '/' + timeline_str,
                                       cookie, calling_domain)
                return

            if onion_domain:
                if '.onion/' in actor:
                    curr_session = self.server.session_onion
                    proxy_type = 'tor'
            if i2p_domain:
                if '.onion/' in actor:
                    curr_session = self.server.session_i2p
                    proxy_type = 'i2p'

            curr_session = \
                self._establish_session("deleteButton",
                                        curr_session, proxy_type)
            if not curr_session:
                self._404()
                return

            delete_str = \
                html_confirm_delete(self.server,
                                    self.server.recent_posts_cache,
                                    self.server.max_recent_posts,
                                    self.server.translate, page_number,
                                    curr_session, base_dir,
                                    delete_url, http_prefix,
                                    self.server.project_version,
                                    self.server.cached_webfingers,
                                    self.server.person_cache, calling_domain,
                                    self.server.yt_replace_domain,
                                    self.server.twitter_replacement_domain,
                                    self.server.show_published_date_only,
                                    self.server.peertube_instances,
                                    self.server.allow_local_network_access,
                                    self.server.theme_name,
                                    self.server.system_language,
                                    self.server.max_like_count,
                                    self.server.signing_priv_key_pem,
                                    self.server.cw_lists,
                                    self.server.lists_enabled,
                                    self.server.dogwhistles,
                                    self.server.min_images_for_accounts)
            if delete_str:
                delete_str_len = len(delete_str)
                self._set_headers('text/html', delete_str_len,
                                  cookie, calling_domain, False)
                self._write(delete_str.encode('utf-8'))
                self.server.getreq_busy = False
                return
        if calling_domain.endswith('.onion') and onion_domain:
            actor = 'http://' + onion_domain + users_path
        elif (calling_domain.endswith('.i2p') and i2p_domain):
            actor = 'http://' + i2p_domain + users_path
        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_delete_button',
                            debug)
        self._redirect_headers(actor + '/' + timeline_str,
                               cookie, calling_domain)

    def _mute_button(self, calling_domain: str, path: str,
                     base_dir: str, http_prefix: str,
                     domain: str, domain_full: str, port: int,
                     onion_domain: str, i2p_domain: str,
                     getreq_start_time, cookie: str,
                     debug: str, curr_session):
        """Mute button is pressed
        """
        mute_url = path.split('?mute=')[1]
        if '?' in mute_url:
            mute_url = mute_url.split('?')[0]
        timeline_bookmark = ''
        if '?bm=' in path:
            timeline_bookmark = path.split('?bm=')[1]
            if '?' in timeline_bookmark:
                timeline_bookmark = timeline_bookmark.split('?')[0]
            timeline_bookmark = '#' + timeline_bookmark
        timeline_str = self.server.default_timeline
        if '?tl=' in path:
            timeline_str = path.split('?tl=')[1]
            if '?' in timeline_str:
                timeline_str = timeline_str.split('?')[0]
        page_number = 1
        if '?page=' in path:
            page_number_str = path.split('?page=')[1]
            if '?' in page_number_str:
                page_number_str = page_number_str.split('?')[0]
            if '#' in page_number_str:
                page_number_str = page_number_str.split('#')[0]
            if len(page_number_str) > 5:
                page_number_str = "1"
            if page_number_str.isdigit():
                page_number = int(page_number_str)
        actor = \
            http_prefix + '://' + domain_full + path.split('?mute=')[0]
        nickname = get_nickname_from_actor(actor)
        if not nickname:
            self._404()
            return
        mute_post(base_dir, nickname, domain, port,
                  http_prefix, mute_url,
                  self.server.recent_posts_cache, debug)
        mute_filename = \
            locate_post(base_dir, nickname, domain, mute_url)
        if mute_filename:
            print('mute_post: Regenerating html post for changed mute status')
            mute_post_json = load_json(mute_filename, 0, 1)
            if mute_post_json:
                cached_post_filename = \
                    get_cached_post_filename(base_dir, nickname,
                                             domain, mute_post_json)
                print('mute_post: Muted post json: ' + str(mute_post_json))
                print('mute_post: Muted post nickname: ' +
                      nickname + ' ' + domain)
                print('mute_post: Muted post cache: ' +
                      str(cached_post_filename))
                show_individual_post_icons = True
                manually_approve_followers = \
                    follower_approval_active(base_dir,
                                             nickname, domain)
                show_repeats = not is_dm(mute_post_json)
                show_public_only = False
                store_to_cache = True
                use_cache_only = False
                allow_downloads = False
                show_avatar_options = True
                avatar_url = None
                timezone = None
                if self.server.account_timezone.get(nickname):
                    timezone = \
                        self.server.account_timezone.get(nickname)
                mitm = False
                if os.path.isfile(mute_filename.replace('.json', '') +
                                  '.mitm'):
                    mitm = True
                bold_reading = False
                if self.server.bold_reading.get(nickname):
                    bold_reading = True
                minimize_all_images = False
                if nickname in self.server.min_images_for_accounts:
                    minimize_all_images = True
                individual_post_as_html(self.server.signing_priv_key_pem,
                                        allow_downloads,
                                        self.server.recent_posts_cache,
                                        self.server.max_recent_posts,
                                        self.server.translate,
                                        page_number, base_dir,
                                        curr_session,
                                        self.server.cached_webfingers,
                                        self.server.person_cache,
                                        nickname, domain,
                                        self.server.port, mute_post_json,
                                        avatar_url, show_avatar_options,
                                        self.server.allow_deletion,
                                        http_prefix,
                                        self.server.project_version,
                                        timeline_str,
                                        self.server.yt_replace_domain,
                                        self.server.twitter_replacement_domain,
                                        self.server.show_published_date_only,
                                        self.server.peertube_instances,
                                        self.server.allow_local_network_access,
                                        self.server.theme_name,
                                        self.server.system_language,
                                        self.server.max_like_count,
                                        show_repeats,
                                        show_individual_post_icons,
                                        manually_approve_followers,
                                        show_public_only, store_to_cache,
                                        use_cache_only,
                                        self.server.cw_lists,
                                        self.server.lists_enabled,
                                        timezone, mitm, bold_reading,
                                        self.server.dogwhistles,
                                        minimize_all_images)
            else:
                print('WARN: Muted post not found: ' + mute_filename)

        if calling_domain.endswith('.onion') and onion_domain:
            actor = \
                'http://' + onion_domain + \
                path.split('?mute=')[0]
        elif (calling_domain.endswith('.i2p') and i2p_domain):
            actor = \
                'http://' + i2p_domain + \
                path.split('?mute=')[0]
        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_mute_button', self.server.debug)
        self._redirect_headers(actor + '/' +
                               timeline_str + timeline_bookmark,
                               cookie, calling_domain)

    def _undo_mute_button(self, calling_domain: str, path: str,
                          base_dir: str, http_prefix: str,
                          domain: str, domain_full: str, port: int,
                          onion_domain: str, i2p_domain: str,
                          getreq_start_time, cookie: str,
                          debug: str, curr_session):
        """Undo mute button is pressed
        """
        mute_url = path.split('?unmute=')[1]
        if '?' in mute_url:
            mute_url = mute_url.split('?')[0]
        timeline_bookmark = ''
        if '?bm=' in path:
            timeline_bookmark = path.split('?bm=')[1]
            if '?' in timeline_bookmark:
                timeline_bookmark = timeline_bookmark.split('?')[0]
            timeline_bookmark = '#' + timeline_bookmark
        timeline_str = self.server.default_timeline
        if '?tl=' in path:
            timeline_str = path.split('?tl=')[1]
            if '?' in timeline_str:
                timeline_str = timeline_str.split('?')[0]
        page_number = 1
        if '?page=' in path:
            page_number_str = path.split('?page=')[1]
            if '?' in page_number_str:
                page_number_str = page_number_str.split('?')[0]
            if '#' in page_number_str:
                page_number_str = page_number_str.split('#')[0]
            if len(page_number_str) > 5:
                page_number_str = "1"
            if page_number_str.isdigit():
                page_number = int(page_number_str)
        actor = \
            http_prefix + '://' + domain_full + path.split('?unmute=')[0]
        nickname = get_nickname_from_actor(actor)
        if not nickname:
            self._404()
            return
        unmute_post(base_dir, nickname, domain, port,
                    http_prefix, mute_url,
                    self.server.recent_posts_cache, debug)
        mute_filename = \
            locate_post(base_dir, nickname, domain, mute_url)
        if mute_filename:
            print('unmute_post: ' +
                  'Regenerating html post for changed unmute status')
            mute_post_json = load_json(mute_filename, 0, 1)
            if mute_post_json:
                cached_post_filename = \
                    get_cached_post_filename(base_dir, nickname,
                                             domain, mute_post_json)
                print('unmute_post: Unmuted post json: ' + str(mute_post_json))
                print('unmute_post: Unmuted post nickname: ' +
                      nickname + ' ' + domain)
                print('unmute_post: Unmuted post cache: ' +
                      str(cached_post_filename))
                show_individual_post_icons = True
                manually_approve_followers = \
                    follower_approval_active(base_dir, nickname, domain)
                show_repeats = not is_dm(mute_post_json)
                show_public_only = False
                store_to_cache = True
                use_cache_only = False
                allow_downloads = False
                show_avatar_options = True
                avatar_url = None
                timezone = None
                if self.server.account_timezone.get(nickname):
                    timezone = \
                        self.server.account_timezone.get(nickname)
                mitm = False
                if os.path.isfile(mute_filename.replace('.json', '') +
                                  '.mitm'):
                    mitm = True
                bold_reading = False
                if self.server.bold_reading.get(nickname):
                    bold_reading = True
                minimize_all_images = False
                if nickname in self.server.min_images_for_accounts:
                    minimize_all_images = True
                individual_post_as_html(self.server.signing_priv_key_pem,
                                        allow_downloads,
                                        self.server.recent_posts_cache,
                                        self.server.max_recent_posts,
                                        self.server.translate,
                                        page_number, base_dir,
                                        curr_session,
                                        self.server.cached_webfingers,
                                        self.server.person_cache,
                                        nickname, domain,
                                        self.server.port, mute_post_json,
                                        avatar_url, show_avatar_options,
                                        self.server.allow_deletion,
                                        http_prefix,
                                        self.server.project_version,
                                        timeline_str,
                                        self.server.yt_replace_domain,
                                        self.server.twitter_replacement_domain,
                                        self.server.show_published_date_only,
                                        self.server.peertube_instances,
                                        self.server.allow_local_network_access,
                                        self.server.theme_name,
                                        self.server.system_language,
                                        self.server.max_like_count,
                                        show_repeats,
                                        show_individual_post_icons,
                                        manually_approve_followers,
                                        show_public_only, store_to_cache,
                                        use_cache_only,
                                        self.server.cw_lists,
                                        self.server.lists_enabled,
                                        timezone, mitm, bold_reading,
                                        self.server.dogwhistles,
                                        minimize_all_images)
            else:
                print('WARN: Unmuted post not found: ' + mute_filename)
        if calling_domain.endswith('.onion') and onion_domain:
            actor = \
                'http://' + onion_domain + path.split('?unmute=')[0]
        elif calling_domain.endswith('.i2p') and i2p_domain:
            actor = \
                'http://' + i2p_domain + path.split('?unmute=')[0]
        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_undo_mute_button', self.server.debug)
        self._redirect_headers(actor + '/' + timeline_str +
                               timeline_bookmark,
                               cookie, calling_domain)

    def _show_replies_to_post(self, authorized: bool,
                              calling_domain: str, referer_domain: str,
                              path: str, base_dir: str, http_prefix: str,
                              domain: str, domain_full: str, port: int,
                              getreq_start_time,
                              proxy_type: str, cookie: str,
                              debug: str, curr_session) -> bool:
        """Shows the replies to a post
        """
        if not ('/statuses/' in path and '/users/' in path):
            return False

        named_status = path.split('/users/')[1]
        if '/' not in named_status:
            return False

        post_sections = named_status.split('/')
        if len(post_sections) < 4:
            return False

        if not post_sections[3].startswith('replies'):
            return False
        nickname = post_sections[0]
        status_number = post_sections[2]
        if not (len(status_number) > 10 and status_number.isdigit()):
            return False

        boxname = 'outbox'
        # get the replies file
        post_dir = \
            acct_dir(base_dir, nickname, domain) + '/' + boxname
        post_replies_filename = \
            post_dir + '/' + \
            http_prefix + ':##' + domain_full + '#users#' + \
            nickname + '#statuses#' + status_number + '.replies'
        if not os.path.isfile(post_replies_filename):
            # There are no replies,
            # so show empty collection
            context_str = \
                'https://www.w3.org/ns/activitystreams'

            first_str = \
                local_actor_url(http_prefix, nickname, domain_full) + \
                '/statuses/' + status_number + '/replies?page=true'

            id_str = \
                local_actor_url(http_prefix, nickname, domain_full) + \
                '/statuses/' + status_number + '/replies'

            last_str = \
                local_actor_url(http_prefix, nickname, domain_full) + \
                '/statuses/' + status_number + '/replies?page=true'

            replies_json = {
                '@context': context_str,
                'first': first_str,
                'id': id_str,
                'last': last_str,
                'totalItems': 0,
                'type': 'OrderedCollection'
            }

            if self._request_http():
                curr_session = \
                    self._establish_session("showRepliesToPost",
                                            curr_session, proxy_type)
                if not curr_session:
                    self._404()
                    return True
                recent_posts_cache = self.server.recent_posts_cache
                max_recent_posts = self.server.max_recent_posts
                translate = self.server.translate
                cached_webfingers = self.server.cached_webfingers
                person_cache = self.server.person_cache
                project_version = self.server.project_version
                yt_domain = self.server.yt_replace_domain
                twitter_replacement_domain = \
                    self.server.twitter_replacement_domain
                peertube_instances = self.server.peertube_instances
                timezone = None
                if self.server.account_timezone.get(nickname):
                    timezone = \
                        self.server.account_timezone.get(nickname)
                bold_reading = False
                if self.server.bold_reading.get(nickname):
                    bold_reading = True
                msg = \
                    html_post_replies(recent_posts_cache,
                                      max_recent_posts,
                                      translate,
                                      base_dir,
                                      curr_session,
                                      cached_webfingers,
                                      person_cache,
                                      nickname,
                                      domain,
                                      port,
                                      replies_json,
                                      http_prefix,
                                      project_version,
                                      yt_domain,
                                      twitter_replacement_domain,
                                      self.server.show_published_date_only,
                                      peertube_instances,
                                      self.server.allow_local_network_access,
                                      self.server.theme_name,
                                      self.server.system_language,
                                      self.server.max_like_count,
                                      self.server.signing_priv_key_pem,
                                      self.server.cw_lists,
                                      self.server.lists_enabled,
                                      timezone, bold_reading,
                                      self.server.dogwhistles,
                                      self.server.min_images_for_accounts)
                msg = msg.encode('utf-8')
                msglen = len(msg)
                self._set_headers('text/html', msglen,
                                  cookie, calling_domain, False)
                self._write(msg)
                fitness_performance(getreq_start_time, self.server.fitness,
                                    '_GET', '_show_replies_to_post',
                                    debug)
            else:
                if self._secure_mode(curr_session, proxy_type):
                    msg_str = json.dumps(replies_json, ensure_ascii=False)
                    msg_str = self._convert_domains(calling_domain,
                                                    referer_domain,
                                                    msg_str)
                    msg = msg_str.encode('utf-8')
                    protocol_str = \
                        get_json_content_from_accept(self.headers['Accept'])
                    msglen = len(msg)
                    self._set_headers(protocol_str, msglen, None,
                                      calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time, self.server.fitness,
                                        '_GET', '_show_replies_to_post json',
                                        debug)
                else:
                    self._404()
            return True
        else:
            # replies exist. Itterate through the
            # text file containing message ids
            context_str = 'https://www.w3.org/ns/activitystreams'

            id_str = \
                local_actor_url(http_prefix, nickname, domain_full) + \
                '/statuses/' + status_number + '?page=true'

            part_of_str = \
                local_actor_url(http_prefix, nickname, domain_full) + \
                '/statuses/' + status_number

            replies_json = {
                '@context': context_str,
                'id': id_str,
                'orderedItems': [
                ],
                'partOf': part_of_str,
                'type': 'OrderedCollectionPage'
            }

            # populate the items list with replies
            populate_replies_json(base_dir, nickname, domain,
                                  post_replies_filename,
                                  authorized, replies_json)

            # send the replies json
            if self._request_http():
                curr_session = \
                    self._establish_session("showRepliesToPost2",
                                            curr_session, proxy_type)
                if not curr_session:
                    self._404()
                    return True
                recent_posts_cache = self.server.recent_posts_cache
                max_recent_posts = self.server.max_recent_posts
                translate = self.server.translate
                cached_webfingers = self.server.cached_webfingers
                person_cache = self.server.person_cache
                project_version = self.server.project_version
                yt_domain = self.server.yt_replace_domain
                twitter_replacement_domain = \
                    self.server.twitter_replacement_domain
                peertube_instances = self.server.peertube_instances
                timezone = None
                if self.server.account_timezone.get(nickname):
                    timezone = \
                        self.server.account_timezone.get(nickname)
                bold_reading = False
                if self.server.bold_reading.get(nickname):
                    bold_reading = True
                msg = \
                    html_post_replies(recent_posts_cache,
                                      max_recent_posts,
                                      translate,
                                      base_dir,
                                      curr_session,
                                      cached_webfingers,
                                      person_cache,
                                      nickname,
                                      domain,
                                      port,
                                      replies_json,
                                      http_prefix,
                                      project_version,
                                      yt_domain,
                                      twitter_replacement_domain,
                                      self.server.show_published_date_only,
                                      peertube_instances,
                                      self.server.allow_local_network_access,
                                      self.server.theme_name,
                                      self.server.system_language,
                                      self.server.max_like_count,
                                      self.server.signing_priv_key_pem,
                                      self.server.cw_lists,
                                      self.server.lists_enabled,
                                      timezone, bold_reading,
                                      self.server.dogwhistles,
                                      self.server.min_images_for_accounts)
                msg = msg.encode('utf-8')
                msglen = len(msg)
                self._set_headers('text/html', msglen,
                                  cookie, calling_domain, False)
                self._write(msg)
                fitness_performance(getreq_start_time, self.server.fitness,
                                    '_GET', '_show_replies_to_post',
                                    debug)
            else:
                if self._secure_mode(curr_session, proxy_type):
                    msg_str = json.dumps(replies_json, ensure_ascii=False)
                    msg_str = self._convert_domains(calling_domain,
                                                    referer_domain,
                                                    msg_str)
                    msg = msg_str.encode('utf-8')
                    protocol_str = \
                        get_json_content_from_accept(self.headers['Accept'])
                    msglen = len(msg)
                    self._set_headers(protocol_str, msglen,
                                      None, calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time, self.server.fitness,
                                        '_GET', '_show_replies_to_post json',
                                        debug)
                else:
                    self._404()
            return True
        return False

    def _show_roles(self, calling_domain: str, referer_domain: str,
                    path: str, base_dir: str, http_prefix: str,
                    domain: str, getreq_start_time,
                    proxy_type: str, cookie: str, debug: str,
                    curr_session) -> bool:
        """Show roles within profile screen
        """
        named_status = path.split('/users/')[1]
        if '/' not in named_status:
            return False

        post_sections = named_status.split('/')
        nickname = post_sections[0]
        actor_filename = acct_dir(base_dir, nickname, domain) + '.json'
        if not os.path.isfile(actor_filename):
            return False

        actor_json = load_json(actor_filename)
        if not actor_json:
            return False

        if actor_json.get('hasOccupation'):
            if self._request_http():
                get_person = \
                    person_lookup(domain, path.replace('/roles', ''),
                                  base_dir)
                if get_person:
                    default_timeline = \
                        self.server.default_timeline
                    recent_posts_cache = \
                        self.server.recent_posts_cache
                    cached_webfingers = \
                        self.server.cached_webfingers
                    yt_replace_domain = \
                        self.server.yt_replace_domain
                    twitter_replacement_domain = \
                        self.server.twitter_replacement_domain
                    icons_as_buttons = \
                        self.server.icons_as_buttons

                    access_keys = self.server.access_keys
                    if self.server.key_shortcuts.get(nickname):
                        access_keys = self.server.key_shortcuts[nickname]

                    roles_list = get_actor_roles_list(actor_json)
                    city = \
                        get_spoofed_city(self.server.city,
                                         base_dir, nickname, domain)
                    shared_items_federated_domains = \
                        self.server.shared_items_federated_domains

                    timezone = None
                    if self.server.account_timezone.get(nickname):
                        timezone = \
                            self.server.account_timezone.get(nickname)
                    bold_reading = False
                    if self.server.bold_reading.get(nickname):
                        bold_reading = True
                    msg = \
                        html_profile(self.server.signing_priv_key_pem,
                                     self.server.rss_icon_at_top,
                                     icons_as_buttons,
                                     default_timeline,
                                     recent_posts_cache,
                                     self.server.max_recent_posts,
                                     self.server.translate,
                                     self.server.project_version,
                                     base_dir, http_prefix, True,
                                     get_person, 'roles',
                                     curr_session,
                                     cached_webfingers,
                                     self.server.person_cache,
                                     yt_replace_domain,
                                     twitter_replacement_domain,
                                     self.server.show_published_date_only,
                                     self.server.newswire,
                                     self.server.theme_name,
                                     self.server.dormant_months,
                                     self.server.peertube_instances,
                                     self.server.allow_local_network_access,
                                     self.server.text_mode_banner,
                                     self.server.debug,
                                     access_keys, city,
                                     self.server.system_language,
                                     self.server.max_like_count,
                                     shared_items_federated_domains,
                                     roles_list,
                                     None, None, self.server.cw_lists,
                                     self.server.lists_enabled,
                                     self.server.content_license_url,
                                     timezone, bold_reading)
                    msg = msg.encode('utf-8')
                    msglen = len(msg)
                    self._set_headers('text/html', msglen,
                                      cookie, calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time, self.server.fitness,
                                        '_GET', '_show_roles', debug)
            else:
                if self._secure_mode(curr_session, proxy_type):
                    roles_list = get_actor_roles_list(actor_json)
                    msg_str = json.dumps(roles_list, ensure_ascii=False)
                    msg_str = self._convert_domains(calling_domain,
                                                    referer_domain,
                                                    msg_str)
                    msg = msg_str.encode('utf-8')
                    msglen = len(msg)
                    protocol_str = \
                        get_json_content_from_accept(self.headers['Accept'])
                    self._set_headers(protocol_str, msglen,
                                      None, calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time, self.server.fitness,
                                        '_GET', '_show_roles json', debug)
                else:
                    self._404()
            return True
        return False

    def _show_skills(self, calling_domain: str, referer_domain: str,
                     path: str, base_dir: str, http_prefix: str,
                     domain: str, getreq_start_time, proxy_type: str,
                     cookie: str, debug: str, curr_session) -> bool:
        """Show skills on the profile screen
        """
        named_status = path.split('/users/')[1]
        if '/' in named_status:
            post_sections = named_status.split('/')
            nickname = post_sections[0]
            actor_filename = acct_dir(base_dir, nickname, domain) + '.json'
            if os.path.isfile(actor_filename):
                actor_json = load_json(actor_filename)
                if actor_json:
                    if no_of_actor_skills(actor_json) > 0:
                        if self._request_http():
                            get_person = \
                                person_lookup(domain,
                                              path.replace('/skills', ''),
                                              base_dir)
                            if get_person:
                                default_timeline =  \
                                    self.server.default_timeline
                                recent_posts_cache = \
                                    self.server.recent_posts_cache
                                cached_webfingers = \
                                    self.server.cached_webfingers
                                yt_replace_domain = \
                                    self.server.yt_replace_domain
                                twitter_replacement_domain = \
                                    self.server.twitter_replacement_domain
                                show_published_date_only = \
                                    self.server.show_published_date_only
                                icons_as_buttons = \
                                    self.server.icons_as_buttons
                                allow_local_network_access = \
                                    self.server.allow_local_network_access
                                access_keys = self.server.access_keys
                                if self.server.key_shortcuts.get(nickname):
                                    access_keys = \
                                        self.server.key_shortcuts[nickname]
                                actor_skills_list = \
                                    get_occupation_skills(actor_json)
                                skills = \
                                    get_skills_from_list(actor_skills_list)
                                city = get_spoofed_city(self.server.city,
                                                        base_dir,
                                                        nickname, domain)
                                shared_items_fed_domains = \
                                    self.server.shared_items_federated_domains
                                signing_priv_key_pem = \
                                    self.server.signing_priv_key_pem
                                content_license_url = \
                                    self.server.content_license_url
                                peertube_instances = \
                                    self.server.peertube_instances
                                timezone = None
                                nick = nickname
                                if self.server.account_timezone.get(nick):
                                    timezone = \
                                        self.server.account_timezone.get(nick)
                                bold_reading = False
                                if self.server.bold_reading.get(nick):
                                    bold_reading = True
                                msg = \
                                    html_profile(signing_priv_key_pem,
                                                 self.server.rss_icon_at_top,
                                                 icons_as_buttons,
                                                 default_timeline,
                                                 recent_posts_cache,
                                                 self.server.max_recent_posts,
                                                 self.server.translate,
                                                 self.server.project_version,
                                                 base_dir, http_prefix, True,
                                                 get_person, 'skills',
                                                 curr_session,
                                                 cached_webfingers,
                                                 self.server.person_cache,
                                                 yt_replace_domain,
                                                 twitter_replacement_domain,
                                                 show_published_date_only,
                                                 self.server.newswire,
                                                 self.server.theme_name,
                                                 self.server.dormant_months,
                                                 peertube_instances,
                                                 allow_local_network_access,
                                                 self.server.text_mode_banner,
                                                 self.server.debug,
                                                 access_keys, city,
                                                 self.server.system_language,
                                                 self.server.max_like_count,
                                                 shared_items_fed_domains,
                                                 skills,
                                                 None, None,
                                                 self.server.cw_lists,
                                                 self.server.lists_enabled,
                                                 content_license_url,
                                                 timezone, bold_reading)
                                msg = msg.encode('utf-8')
                                msglen = len(msg)
                                self._set_headers('text/html', msglen,
                                                  cookie, calling_domain,
                                                  False)
                                self._write(msg)
                                fitness_performance(getreq_start_time,
                                                    self.server.fitness,
                                                    '_GET', '_show_skills',
                                                    self.server.debug)
                        else:
                            if self._secure_mode(curr_session,
                                                 proxy_type):
                                actor_skills_list = \
                                    get_occupation_skills(actor_json)
                                skills = \
                                    get_skills_from_list(actor_skills_list)
                                msg_str = json.dumps(skills,
                                                     ensure_ascii=False)
                                msg_str = self._convert_domains(calling_domain,
                                                                referer_domain,
                                                                msg_str)
                                msg = msg_str.encode('utf-8')
                                msglen = len(msg)
                                accept_str = self.headers['Accept']
                                protocol_str = \
                                    get_json_content_from_accept(accept_str)
                                self._set_headers(protocol_str, msglen, None,
                                                  calling_domain, False)
                                self._write(msg)
                                fitness_performance(getreq_start_time,
                                                    self.server.fitness,
                                                    '_GET',
                                                    '_show_skills json',
                                                    debug)
                            else:
                                self._404()
                        return True
        actor = path.replace('/skills', '')
        actor_absolute = self._get_instance_url(calling_domain) + actor
        self._redirect_headers(actor_absolute, cookie, calling_domain)
        return True

    def _show_individual_at_post(self, ssml_getreq: bool, authorized: bool,
                                 calling_domain: str, referer_domain: str,
                                 path: str,
                                 base_dir: str, http_prefix: str,
                                 domain: str, domain_full: str, port: int,
                                 onion_domain: str, i2p_domain: str,
                                 getreq_start_time,
                                 proxy_type: str, cookie: str,
                                 debug: str,
                                 curr_session) -> bool:
        """get an individual post from the path /@nickname/statusnumber
        """
        if '/@' not in path:
            return False

        liked_by = None
        if '?likedBy=' in path:
            liked_by = path.split('?likedBy=')[1].strip()
            if '?' in liked_by:
                liked_by = liked_by.split('?')[0]
            path = path.split('?likedBy=')[0]

        react_by = None
        react_emoji = None
        if '?reactBy=' in path:
            react_by = path.split('?reactBy=')[1].strip()
            if ';' in react_by:
                react_by = react_by.split(';')[0]
            if ';emoj=' in path:
                react_emoji = path.split(';emoj=')[1].strip()
                if ';' in react_emoji:
                    react_emoji = react_emoji.split(';')[0]
            path = path.split('?reactBy=')[0]

        named_status = path.split('/@')[1]
        if '/' not in named_status:
            # show actor
            nickname = named_status
            return False

        post_sections = named_status.split('/')
        if len(post_sections) != 2:
            return False
        nickname = post_sections[0]
        status_number = post_sections[1]
        if len(status_number) <= 10 or not status_number.isdigit():
            return False

        if ssml_getreq:
            ssml_filename = \
                acct_dir(base_dir, nickname, domain) + '/outbox/' + \
                http_prefix + ':##' + domain_full + '#users#' + nickname + \
                '#statuses#' + status_number + '.ssml'
            if not os.path.isfile(ssml_filename):
                ssml_filename = \
                    acct_dir(base_dir, nickname, domain) + '/postcache/' + \
                    http_prefix + ':##' + domain_full + '#users#' + \
                    nickname + '#statuses#' + status_number + '.ssml'
            if not os.path.isfile(ssml_filename):
                self._404()
                return True
            ssml_str = None
            try:
                with open(ssml_filename, 'r', encoding='utf-8') as fp_ssml:
                    ssml_str = fp_ssml.read()
            except OSError:
                pass
            if ssml_str:
                msg = ssml_str.encode('utf-8')
                msglen = len(msg)
                self._set_headers('application/ssml+xml', msglen,
                                  cookie, calling_domain, False)
                self._write(msg)
                return True
            self._404()
            return True

        post_filename = \
            acct_dir(base_dir, nickname, domain) + '/outbox/' + \
            http_prefix + ':##' + domain_full + '#users#' + nickname + \
            '#statuses#' + status_number + '.json'

        include_create_wrapper = False
        if post_sections[-1] == 'activity':
            include_create_wrapper = True

        result = self._show_post_from_file(post_filename, liked_by,
                                           react_by, react_emoji,
                                           authorized, calling_domain,
                                           referer_domain,
                                           base_dir, http_prefix, nickname,
                                           domain, port,
                                           getreq_start_time,
                                           proxy_type, cookie, debug,
                                           include_create_wrapper,
                                           curr_session)
        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_show_individual_at_post',
                            self.server.debug)
        return result

    def _show_likers_of_post(self, authorized: bool,
                             calling_domain: str, path: str,
                             base_dir: str, http_prefix: str,
                             domain: str, port: int,
                             getreq_start_time, cookie: str,
                             debug: str, curr_session) -> bool:
        """Show the likers of a post
        """
        if not authorized:
            return False
        if '?likers=' not in path:
            return False
        if '/users/' not in path:
            return False
        nickname = path.split('/users/')[1]
        if '?' in nickname:
            nickname = nickname.split('?')[0]
        post_url = path.split('?likers=')[1]
        if '?' in post_url:
            post_url = post_url.split('?')[0]
        post_url = post_url.replace('--', '/')

        bold_reading = False
        if self.server.bold_reading.get(nickname):
            bold_reading = True

        msg = \
            html_likers_of_post(base_dir, nickname, domain, port,
                                post_url, self.server.translate,
                                http_prefix,
                                self.server.theme_name,
                                self.server.access_keys,
                                self.server.recent_posts_cache,
                                self.server.max_recent_posts,
                                curr_session,
                                self.server.cached_webfingers,
                                self.server.person_cache,
                                self.server.project_version,
                                self.server.yt_replace_domain,
                                self.server.twitter_replacement_domain,
                                self.server.show_published_date_only,
                                self.server.peertube_instances,
                                self.server.allow_local_network_access,
                                self.server.system_language,
                                self.server.max_like_count,
                                self.server.signing_priv_key_pem,
                                self.server.cw_lists,
                                self.server.lists_enabled,
                                'inbox', self.server.default_timeline,
                                bold_reading,
                                self.server.dogwhistles,
                                self.server.min_images_for_accounts)
        if not msg:
            self._404()
            return True
        msg = msg.encode('utf-8')
        msglen = len(msg)
        self._set_headers('text/html', msglen,
                          cookie, calling_domain, False)
        self._write(msg)
        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_show_likers_of_post',
                            debug)
        return True

    def _show_announcers_of_post(self, authorized: bool,
                                 calling_domain: str, path: str,
                                 base_dir: str, http_prefix: str,
                                 domain: str, port: int,
                                 getreq_start_time, cookie: str,
                                 debug: str, curr_session) -> bool:
        """Show the announcers of a post
        """
        if not authorized:
            return False
        if '?announcers=' not in path:
            return False
        if '/users/' not in path:
            return False
        nickname = path.split('/users/')[1]
        if '?' in nickname:
            nickname = nickname.split('?')[0]
        post_url = path.split('?announcers=')[1]
        if '?' in post_url:
            post_url = post_url.split('?')[0]
        post_url = post_url.replace('--', '/')

        bold_reading = False
        if self.server.bold_reading.get(nickname):
            bold_reading = True

        # note that the likers function is reused, but with 'shares'
        msg = \
            html_likers_of_post(base_dir, nickname, domain, port,
                                post_url, self.server.translate,
                                http_prefix,
                                self.server.theme_name,
                                self.server.access_keys,
                                self.server.recent_posts_cache,
                                self.server.max_recent_posts,
                                curr_session,
                                self.server.cached_webfingers,
                                self.server.person_cache,
                                self.server.project_version,
                                self.server.yt_replace_domain,
                                self.server.twitter_replacement_domain,
                                self.server.show_published_date_only,
                                self.server.peertube_instances,
                                self.server.allow_local_network_access,
                                self.server.system_language,
                                self.server.max_like_count,
                                self.server.signing_priv_key_pem,
                                self.server.cw_lists,
                                self.server.lists_enabled,
                                'inbox', self.server.default_timeline,
                                bold_reading, self.server.dogwhistles,
                                self.server.min_images_for_accounts,
                                'shares')
        if not msg:
            self._404()
            return True
        msg = msg.encode('utf-8')
        msglen = len(msg)
        self._set_headers('text/html', msglen,
                          cookie, calling_domain, False)
        self._write(msg)
        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_show_announcers_of_post',
                            debug)
        return True

    def _show_post_from_file(self, post_filename: str, liked_by: str,
                             react_by: str, react_emoji: str,
                             authorized: bool,
                             calling_domain: str, referer_domain: str,
                             base_dir: str, http_prefix: str, nickname: str,
                             domain: str, port: int,
                             getreq_start_time,
                             proxy_type: str, cookie: str,
                             debug: str, include_create_wrapper: bool,
                             curr_session) -> bool:
        """Shows an individual post from its filename
        """
        if not os.path.isfile(post_filename):
            self._404()
            self.server.getreq_busy = False
            return True

        post_json_object = load_json(post_filename)
        if not post_json_object:
            self.send_response(429)
            self.end_headers()
            self.server.getreq_busy = False
            return True

        # Only authorized viewers get to see likes on posts
        # Otherwize marketers could gain more social graph info
        if not authorized:
            pjo = post_json_object
            if not is_public_post(pjo):
                self._404()
                self.server.getreq_busy = False
                return True
            remove_post_interactions(pjo, True)
        if self._request_http():
            timezone = None
            if self.server.account_timezone.get(nickname):
                timezone = \
                    self.server.account_timezone.get(nickname)

            mitm = False
            if os.path.isfile(post_filename.replace('.json', '') +
                              '.mitm'):
                mitm = True

            bold_reading = False
            if self.server.bold_reading.get(nickname):
                bold_reading = True

            msg = \
                html_individual_post(self.server.recent_posts_cache,
                                     self.server.max_recent_posts,
                                     self.server.translate,
                                     base_dir,
                                     curr_session,
                                     self.server.cached_webfingers,
                                     self.server.person_cache,
                                     nickname, domain, port,
                                     authorized,
                                     post_json_object,
                                     http_prefix,
                                     self.server.project_version,
                                     liked_by, react_by, react_emoji,
                                     self.server.yt_replace_domain,
                                     self.server.twitter_replacement_domain,
                                     self.server.show_published_date_only,
                                     self.server.peertube_instances,
                                     self.server.allow_local_network_access,
                                     self.server.theme_name,
                                     self.server.system_language,
                                     self.server.max_like_count,
                                     self.server.signing_priv_key_pem,
                                     self.server.cw_lists,
                                     self.server.lists_enabled,
                                     timezone, mitm, bold_reading,
                                     self.server.dogwhistles,
                                     self.server.min_images_for_accounts)
            msg = msg.encode('utf-8')
            msglen = len(msg)
            self._set_headers('text/html', msglen,
                              cookie, calling_domain, False)
            self._write(msg)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', '_show_post_from_file',
                                debug)
        else:
            if self._secure_mode(curr_session, proxy_type):
                if not include_create_wrapper and \
                   post_json_object['type'] == 'Create' and \
                   has_object_dict(post_json_object):
                    unwrapped_json = post_json_object['object']
                    unwrapped_json['@context'] = \
                        get_individual_post_context()
                    msg_str = json.dumps(unwrapped_json,
                                         ensure_ascii=False)
                else:
                    msg_str = json.dumps(post_json_object,
                                         ensure_ascii=False)
                msg_str = self._convert_domains(calling_domain,
                                                referer_domain,
                                                msg_str)
                msg = msg_str.encode('utf-8')
                msglen = len(msg)
                protocol_str = \
                    get_json_content_from_accept(self.headers['Accept'])
                self._set_headers(protocol_str, msglen,
                                  None, calling_domain, False)
                self._write(msg)
                fitness_performance(getreq_start_time, self.server.fitness,
                                    '_GET', '_show_post_from_file json',
                                    debug)
            else:
                self._404()
        self.server.getreq_busy = False
        return True

    def _show_individual_post(self, ssml_getreq: bool, authorized: bool,
                              calling_domain: str, referer_domain: str,
                              path: str,
                              base_dir: str, http_prefix: str,
                              domain: str, domain_full: str, port: int,
                              getreq_start_time,
                              proxy_type: str, cookie: str,
                              debug: str,
                              curr_session) -> bool:
        """Shows an individual post
        """
        liked_by = None
        if '?likedBy=' in path:
            liked_by = path.split('?likedBy=')[1].strip()
            if '?' in liked_by:
                liked_by = liked_by.split('?')[0]
            path = path.split('?likedBy=')[0]

        react_by = None
        react_emoji = None
        if '?reactBy=' in path:
            react_by = path.split('?reactBy=')[1].strip()
            if ';' in react_by:
                react_by = react_by.split(';')[0]
            if ';emoj=' in path:
                react_emoji = path.split(';emoj=')[1].strip()
                if ';' in react_emoji:
                    react_emoji = react_emoji.split(';')[0]
            path = path.split('?reactBy=')[0]

        named_status = path.split('/users/')[1]
        if '/' not in named_status:
            return False
        post_sections = named_status.split('/')
        if len(post_sections) < 3:
            return False
        nickname = post_sections[0]
        status_number = post_sections[2]
        if len(status_number) <= 10 or (not status_number.isdigit()):
            return False

        if ssml_getreq:
            ssml_filename = \
                acct_dir(base_dir, nickname, domain) + '/outbox/' + \
                http_prefix + ':##' + domain_full + '#users#' + nickname + \
                '#statuses#' + status_number + '.ssml'
            if not os.path.isfile(ssml_filename):
                ssml_filename = \
                    acct_dir(base_dir, nickname, domain) + '/postcache/' + \
                    http_prefix + ':##' + domain_full + '#users#' + \
                    nickname + '#statuses#' + status_number + '.ssml'
            if not os.path.isfile(ssml_filename):
                self._404()
                return True
            ssml_str = None
            try:
                with open(ssml_filename, 'r', encoding='utf-8') as fp_ssml:
                    ssml_str = fp_ssml.read()
            except OSError:
                pass
            if ssml_str:
                msg = ssml_str.encode('utf-8')
                msglen = len(msg)
                self._set_headers('application/ssml+xml', msglen,
                                  cookie, calling_domain, False)
                self._write(msg)
                return True
            self._404()
            return True

        post_filename = \
            acct_dir(base_dir, nickname, domain) + '/outbox/' + \
            http_prefix + ':##' + domain_full + '#users#' + nickname + \
            '#statuses#' + status_number + '.json'

        include_create_wrapper = False
        if post_sections[-1] == 'activity':
            include_create_wrapper = True

        result = self._show_post_from_file(post_filename, liked_by,
                                           react_by, react_emoji,
                                           authorized, calling_domain,
                                           referer_domain,
                                           base_dir, http_prefix, nickname,
                                           domain, port,
                                           getreq_start_time,
                                           proxy_type, cookie, debug,
                                           include_create_wrapper,
                                           curr_session)
        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_show_individual_post',
                            self.server.debug)
        return result

    def _show_notify_post(self, authorized: bool,
                          calling_domain: str, referer_domain: str,
                          path: str,
                          base_dir: str, http_prefix: str,
                          domain: str, port: int,
                          getreq_start_time,
                          proxy_type: str, cookie: str,
                          debug: str,
                          curr_session) -> bool:
        """Shows an individual post from an account which you are following
        and where you have the notify checkbox set on person options
        """
        liked_by = None
        react_by = None
        react_emoji = None
        post_id = path.split('?notifypost=')[1].strip()
        post_id = post_id.replace('-', '/')
        path = path.split('?notifypost=')[0]
        nickname = path.split('/users/')[1]
        if '/' in nickname:
            return False
        replies = False

        post_filename = locate_post(base_dir, nickname, domain,
                                    post_id, replies)
        if not post_filename:
            return False

        include_create_wrapper = False
        if path.endswith('/activity'):
            include_create_wrapper = True

        result = self._show_post_from_file(post_filename, liked_by,
                                           react_by, react_emoji,
                                           authorized, calling_domain,
                                           referer_domain,
                                           base_dir, http_prefix, nickname,
                                           domain, port,
                                           getreq_start_time,
                                           proxy_type, cookie, debug,
                                           include_create_wrapper,
                                           curr_session)
        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_show_notify_post',
                            self.server.debug)
        return result

    def _show_inbox(self, authorized: bool,
                    calling_domain: str, referer_domain: str,
                    path: str,
                    base_dir: str, http_prefix: str,
                    domain: str, port: int,
                    getreq_start_time,
                    cookie: str, debug: str,
                    recent_posts_cache: {}, curr_session,
                    default_timeline: str,
                    max_recent_posts: int,
                    translate: {},
                    cached_webfingers: {},
                    person_cache: {},
                    allow_deletion: bool,
                    project_version: str,
                    yt_replace_domain: str,
                    twitter_replacement_domain: str,
                    ua_str: str) -> bool:
        """Shows the inbox timeline
        """
        if '/users/' in path:
            if authorized:
                inbox_feed = \
                    person_box_json(recent_posts_cache,
                                    base_dir,
                                    domain,
                                    port,
                                    path,
                                    http_prefix,
                                    MAX_POSTS_IN_FEED, 'inbox',
                                    authorized,
                                    0,
                                    self.server.positive_voting,
                                    self.server.voting_time_mins)
                if inbox_feed:
                    if getreq_start_time:
                        fitness_performance(getreq_start_time,
                                            self.server.fitness,
                                            '_GET', '_show_inbox',
                                            self.server.debug)
                    if self._request_http():
                        nickname = path.replace('/users/', '')
                        nickname = nickname.replace('/inbox', '')
                        page_number = 1
                        if '?page=' in nickname:
                            page_number = nickname.split('?page=')[1]
                            nickname = nickname.split('?page=')[0]
                            if len(page_number) > 5:
                                page_number = "1"
                            if page_number.isdigit():
                                page_number = int(page_number)
                            else:
                                page_number = 1
                        if 'page=' not in path:
                            # if no page was specified then show the first
                            inbox_feed = \
                                person_box_json(recent_posts_cache,
                                                base_dir,
                                                domain,
                                                port,
                                                path + '?page=1',
                                                http_prefix,
                                                MAX_POSTS_IN_FEED, 'inbox',
                                                authorized,
                                                0,
                                                self.server.positive_voting,
                                                self.server.voting_time_mins)
                            if getreq_start_time:
                                fitness_performance(getreq_start_time,
                                                    self.server.fitness,
                                                    '_GET', '_show_inbox2',
                                                    self.server.debug)
                        full_width_tl_button_header = \
                            self.server.full_width_tl_button_header
                        minimal_nick = is_minimal(base_dir, domain, nickname)

                        access_keys = self.server.access_keys
                        if self.server.key_shortcuts.get(nickname):
                            access_keys = \
                                self.server.key_shortcuts[nickname]
                        shared_items_federated_domains = \
                            self.server.shared_items_federated_domains
                        allow_local_network_access = \
                            self.server.allow_local_network_access
                        timezone = None
                        if self.server.account_timezone.get(nickname):
                            timezone = \
                                self.server.account_timezone.get(nickname)
                        bold_reading = False
                        if self.server.bold_reading.get(nickname):
                            bold_reading = True
                        msg = \
                            html_inbox(default_timeline,
                                       recent_posts_cache,
                                       max_recent_posts,
                                       translate,
                                       page_number, MAX_POSTS_IN_FEED,
                                       curr_session,
                                       base_dir,
                                       cached_webfingers,
                                       person_cache,
                                       nickname,
                                       domain,
                                       port,
                                       inbox_feed,
                                       allow_deletion,
                                       http_prefix,
                                       project_version,
                                       minimal_nick,
                                       yt_replace_domain,
                                       twitter_replacement_domain,
                                       self.server.show_published_date_only,
                                       self.server.newswire,
                                       self.server.positive_voting,
                                       self.server.show_publish_as_icon,
                                       full_width_tl_button_header,
                                       self.server.icons_as_buttons,
                                       self.server.rss_icon_at_top,
                                       self.server.publish_button_at_top,
                                       authorized,
                                       self.server.theme_name,
                                       self.server.peertube_instances,
                                       allow_local_network_access,
                                       self.server.text_mode_banner,
                                       access_keys,
                                       self.server.system_language,
                                       self.server.max_like_count,
                                       shared_items_federated_domains,
                                       self.server.signing_priv_key_pem,
                                       self.server.cw_lists,
                                       self.server.lists_enabled,
                                       timezone, bold_reading,
                                       self.server.dogwhistles,
                                       ua_str,
                                       self.server.min_images_for_accounts)
                        if getreq_start_time:
                            fitness_performance(getreq_start_time,
                                                self.server.fitness,
                                                '_GET', '_show_inbox3',
                                                self.server.debug)
                        if msg:
                            msg_str = msg
                            msg_str = self._convert_domains(calling_domain,
                                                            referer_domain,
                                                            msg_str)
                            msg = msg_str.encode('utf-8')
                            msglen = len(msg)
                            self._set_headers('text/html', msglen,
                                              cookie, calling_domain, False)
                            self._write(msg)

                        if getreq_start_time:
                            fitness_performance(getreq_start_time,
                                                self.server.fitness,
                                                '_GET', '_show_inbox4',
                                                self.server.debug)
                    else:
                        # don't need authorized fetch here because
                        # there is already the authorization check
                        msg_str = json.dumps(inbox_feed, ensure_ascii=False)
                        msg_str = self._convert_domains(calling_domain,
                                                        referer_domain,
                                                        msg_str)
                        msg = msg_str.encode('utf-8')
                        msglen = len(msg)
                        accept_str = self.headers['Accept']
                        protocol_str = \
                            get_json_content_from_accept(accept_str)
                        self._set_headers(protocol_str, msglen,
                                          None, calling_domain, False)
                        self._write(msg)
                        fitness_performance(getreq_start_time,
                                            self.server.fitness,
                                            '_GET', '_show_inbox5',
                                            self.server.debug)
                    return True
            else:
                if debug:
                    nickname = path.replace('/users/', '')
                    nickname = nickname.replace('/inbox', '')
                    print('DEBUG: ' + nickname +
                          ' was not authorized to access ' + path)
        if path != '/inbox':
            # not the shared inbox
            if debug:
                print('DEBUG: GET access to inbox is unauthorized')
            self.send_response(405)
            self.end_headers()
            return True
        return False

    def _show_dms(self, authorized: bool,
                  calling_domain: str, referer_domain: str,
                  path: str, base_dir: str, http_prefix: str,
                  domain: str, port: int,
                  getreq_start_time,
                  cookie: str, debug: str,
                  curr_session, ua_str: str) -> bool:
        """Shows the DMs timeline
        """
        if '/users/' in path:
            if authorized:
                inbox_dm_feed = \
                    person_box_json(self.server.recent_posts_cache,
                                    base_dir,
                                    domain,
                                    port,
                                    path,
                                    http_prefix,
                                    MAX_POSTS_IN_FEED, 'dm',
                                    authorized,
                                    0, self.server.positive_voting,
                                    self.server.voting_time_mins)
                if inbox_dm_feed:
                    if self._request_http():
                        nickname = path.replace('/users/', '')
                        nickname = nickname.replace('/dm', '')
                        page_number = 1
                        if '?page=' in nickname:
                            page_number = nickname.split('?page=')[1]
                            nickname = nickname.split('?page=')[0]
                            if len(page_number) > 5:
                                page_number = "1"
                            if page_number.isdigit():
                                page_number = int(page_number)
                            else:
                                page_number = 1
                        if 'page=' not in path:
                            # if no page was specified then show the first
                            inbox_dm_feed = \
                                person_box_json(self.server.recent_posts_cache,
                                                base_dir,
                                                domain,
                                                port,
                                                path + '?page=1',
                                                http_prefix,
                                                MAX_POSTS_IN_FEED, 'dm',
                                                authorized,
                                                0,
                                                self.server.positive_voting,
                                                self.server.voting_time_mins)
                        full_width_tl_button_header = \
                            self.server.full_width_tl_button_header
                        minimal_nick = is_minimal(base_dir, domain, nickname)

                        access_keys = self.server.access_keys
                        if self.server.key_shortcuts.get(nickname):
                            access_keys = \
                                self.server.key_shortcuts[nickname]

                        shared_items_federated_domains = \
                            self.server.shared_items_federated_domains
                        allow_local_network_access = \
                            self.server.allow_local_network_access
                        twitter_replacement_domain = \
                            self.server.twitter_replacement_domain
                        show_published_date_only = \
                            self.server.show_published_date_only
                        timezone = None
                        if self.server.account_timezone.get(nickname):
                            timezone = \
                                self.server.account_timezone.get(nickname)
                        bold_reading = False
                        if self.server.bold_reading.get(nickname):
                            bold_reading = True
                        msg = \
                            html_inbox_dms(self.server.default_timeline,
                                           self.server.recent_posts_cache,
                                           self.server.max_recent_posts,
                                           self.server.translate,
                                           page_number, MAX_POSTS_IN_FEED,
                                           curr_session,
                                           base_dir,
                                           self.server.cached_webfingers,
                                           self.server.person_cache,
                                           nickname,
                                           domain,
                                           port,
                                           inbox_dm_feed,
                                           self.server.allow_deletion,
                                           http_prefix,
                                           self.server.project_version,
                                           minimal_nick,
                                           self.server.yt_replace_domain,
                                           twitter_replacement_domain,
                                           show_published_date_only,
                                           self.server.newswire,
                                           self.server.positive_voting,
                                           self.server.show_publish_as_icon,
                                           full_width_tl_button_header,
                                           self.server.icons_as_buttons,
                                           self.server.rss_icon_at_top,
                                           self.server.publish_button_at_top,
                                           authorized, self.server.theme_name,
                                           self.server.peertube_instances,
                                           allow_local_network_access,
                                           self.server.text_mode_banner,
                                           access_keys,
                                           self.server.system_language,
                                           self.server.max_like_count,
                                           shared_items_federated_domains,
                                           self.server.signing_priv_key_pem,
                                           self.server.cw_lists,
                                           self.server.lists_enabled,
                                           timezone, bold_reading,
                                           self.server.dogwhistles, ua_str,
                                           self.server.min_images_for_accounts)
                        msg = msg.encode('utf-8')
                        msglen = len(msg)
                        self._set_headers('text/html', msglen,
                                          cookie, calling_domain, False)
                        self._write(msg)
                        fitness_performance(getreq_start_time,
                                            self.server.fitness,
                                            '_GET', '_show_dms',
                                            self.server.debug)
                    else:
                        # don't need authorized fetch here because
                        # there is already the authorization check
                        msg_str = \
                            json.dumps(inbox_dm_feed, ensure_ascii=False)
                        msg_str = self._convert_domains(calling_domain,
                                                        referer_domain,
                                                        msg_str)
                        msg = msg_str.encode('utf-8')
                        msglen = len(msg)
                        accept_str = self.headers['Accept']
                        protocol_str = \
                            get_json_content_from_accept(accept_str)
                        self._set_headers(protocol_str, msglen,
                                          None, calling_domain, False)
                        self._write(msg)
                        fitness_performance(getreq_start_time,
                                            self.server.fitness,
                                            '_GET', '_show_dms json',
                                            self.server.debug)
                    return True
            else:
                if debug:
                    nickname = path.replace('/users/', '')
                    nickname = nickname.replace('/dm', '')
                    print('DEBUG: ' + nickname +
                          ' was not authorized to access ' + path)
        if path != '/dm':
            # not the DM inbox
            if debug:
                print('DEBUG: GET access to DM timeline is unauthorized')
            self.send_response(405)
            self.end_headers()
            return True
        return False

    def _show_replies(self, authorized: bool,
                      calling_domain: str, referer_domain: str,
                      path: str,
                      base_dir: str, http_prefix: str,
                      domain: str, port: int,
                      getreq_start_time,
                      cookie: str, debug: str,
                      curr_session, ua_str: str) -> bool:
        """Shows the replies timeline
        """
        if '/users/' in path:
            if authorized:
                inbox_replies_feed = \
                    person_box_json(self.server.recent_posts_cache,
                                    base_dir,
                                    domain,
                                    port,
                                    path,
                                    http_prefix,
                                    MAX_POSTS_IN_FEED, 'tlreplies',
                                    True,
                                    0, self.server.positive_voting,
                                    self.server.voting_time_mins)
                if not inbox_replies_feed:
                    inbox_replies_feed = []
                if self._request_http():
                    nickname = path.replace('/users/', '')
                    nickname = nickname.replace('/tlreplies', '')
                    page_number = 1
                    if '?page=' in nickname:
                        page_number = nickname.split('?page=')[1]
                        nickname = nickname.split('?page=')[0]
                        if len(page_number) > 5:
                            page_number = "1"
                        if page_number.isdigit():
                            page_number = int(page_number)
                        else:
                            page_number = 1
                    if 'page=' not in path:
                        # if no page was specified then show the first
                        inbox_replies_feed = \
                            person_box_json(self.server.recent_posts_cache,
                                            base_dir,
                                            domain,
                                            port,
                                            path + '?page=1',
                                            http_prefix,
                                            MAX_POSTS_IN_FEED, 'tlreplies',
                                            True,
                                            0, self.server.positive_voting,
                                            self.server.voting_time_mins)
                    full_width_tl_button_header = \
                        self.server.full_width_tl_button_header
                    minimal_nick = is_minimal(base_dir, domain, nickname)

                    access_keys = self.server.access_keys
                    if self.server.key_shortcuts.get(nickname):
                        access_keys = \
                            self.server.key_shortcuts[nickname]

                    shared_items_federated_domains = \
                        self.server.shared_items_federated_domains
                    allow_local_network_access = \
                        self.server.allow_local_network_access
                    twitter_replacement_domain = \
                        self.server.twitter_replacement_domain
                    show_published_date_only = \
                        self.server.show_published_date_only
                    timezone = None
                    if self.server.account_timezone.get(nickname):
                        timezone = \
                            self.server.account_timezone.get(nickname)
                    bold_reading = False
                    if self.server.bold_reading.get(nickname):
                        bold_reading = True
                    msg = \
                        html_inbox_replies(self.server.default_timeline,
                                           self.server.recent_posts_cache,
                                           self.server.max_recent_posts,
                                           self.server.translate,
                                           page_number, MAX_POSTS_IN_FEED,
                                           curr_session,
                                           base_dir,
                                           self.server.cached_webfingers,
                                           self.server.person_cache,
                                           nickname,
                                           domain,
                                           port,
                                           inbox_replies_feed,
                                           self.server.allow_deletion,
                                           http_prefix,
                                           self.server.project_version,
                                           minimal_nick,
                                           self.server.yt_replace_domain,
                                           twitter_replacement_domain,
                                           show_published_date_only,
                                           self.server.newswire,
                                           self.server.positive_voting,
                                           self.server.show_publish_as_icon,
                                           full_width_tl_button_header,
                                           self.server.icons_as_buttons,
                                           self.server.rss_icon_at_top,
                                           self.server.publish_button_at_top,
                                           authorized, self.server.theme_name,
                                           self.server.peertube_instances,
                                           allow_local_network_access,
                                           self.server.text_mode_banner,
                                           access_keys,
                                           self.server.system_language,
                                           self.server.max_like_count,
                                           shared_items_federated_domains,
                                           self.server.signing_priv_key_pem,
                                           self.server.cw_lists,
                                           self.server.lists_enabled,
                                           timezone, bold_reading,
                                           self.server.dogwhistles,
                                           ua_str,
                                           self.server.min_images_for_accounts)
                    msg = msg.encode('utf-8')
                    msglen = len(msg)
                    self._set_headers('text/html', msglen,
                                      cookie, calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time,
                                        self.server.fitness,
                                        '_GET', '_show_replies',
                                        self.server.debug)
                else:
                    # don't need authorized fetch here because there is
                    # already the authorization check
                    msg_str = json.dumps(inbox_replies_feed,
                                         ensure_ascii=False)
                    msg_str = self._convert_domains(calling_domain,
                                                    referer_domain,
                                                    msg_str)
                    msg = msg_str.encode('utf-8')
                    msglen = len(msg)
                    accept_str = self.headers['Accept']
                    protocol_str = \
                        get_json_content_from_accept(accept_str)
                    self._set_headers(protocol_str, msglen,
                                      None, calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time,
                                        self.server.fitness,
                                        '_GET', '_show_replies json',
                                        self.server.debug)
                return True
            if debug:
                nickname = path.replace('/users/', '')
                nickname = nickname.replace('/tlreplies', '')
                print('DEBUG: ' + nickname +
                      ' was not authorized to access ' + path)
        if path != '/tlreplies':
            # not the replies inbox
            if debug:
                print('DEBUG: GET access to inbox is unauthorized')
            self.send_response(405)
            self.end_headers()
            return True
        return False

    def _show_media_timeline(self, authorized: bool,
                             calling_domain: str, referer_domain: str,
                             path: str,
                             base_dir: str, http_prefix: str,
                             domain: str, port: int,
                             getreq_start_time,
                             cookie: str, debug: str,
                             curr_session, ua_str: str) -> bool:
        """Shows the media timeline
        """
        if '/users/' in path:
            if authorized:
                inbox_media_feed = \
                    person_box_json(self.server.recent_posts_cache,
                                    base_dir,
                                    domain,
                                    port,
                                    path,
                                    http_prefix,
                                    MAX_POSTS_IN_MEDIA_FEED, 'tlmedia',
                                    True,
                                    0, self.server.positive_voting,
                                    self.server.voting_time_mins)
                if not inbox_media_feed:
                    inbox_media_feed = []
                if self._request_http():
                    nickname = path.replace('/users/', '')
                    nickname = nickname.replace('/tlmedia', '')
                    page_number = 1
                    if '?page=' in nickname:
                        page_number = nickname.split('?page=')[1]
                        nickname = nickname.split('?page=')[0]
                        if len(page_number) > 5:
                            page_number = "1"
                        if page_number.isdigit():
                            page_number = int(page_number)
                        else:
                            page_number = 1
                    if 'page=' not in path:
                        # if no page was specified then show the first
                        inbox_media_feed = \
                            person_box_json(self.server.recent_posts_cache,
                                            base_dir,
                                            domain,
                                            port,
                                            path + '?page=1',
                                            http_prefix,
                                            MAX_POSTS_IN_MEDIA_FEED, 'tlmedia',
                                            True,
                                            0, self.server.positive_voting,
                                            self.server.voting_time_mins)
                    full_width_tl_button_header = \
                        self.server.full_width_tl_button_header
                    minimal_nick = is_minimal(base_dir, domain, nickname)

                    access_keys = self.server.access_keys
                    if self.server.key_shortcuts.get(nickname):
                        access_keys = \
                            self.server.key_shortcuts[nickname]
                    fed_domains = \
                        self.server.shared_items_federated_domains
                    allow_local_network_access = \
                        self.server.allow_local_network_access
                    twitter_replacement_domain = \
                        self.server.twitter_replacement_domain
                    timezone = None
                    if self.server.account_timezone.get(nickname):
                        timezone = \
                            self.server.account_timezone.get(nickname)
                    bold_reading = False
                    if self.server.bold_reading.get(nickname):
                        bold_reading = True
                    msg = \
                        html_inbox_media(self.server.default_timeline,
                                         self.server.recent_posts_cache,
                                         self.server.max_recent_posts,
                                         self.server.translate,
                                         page_number, MAX_POSTS_IN_MEDIA_FEED,
                                         curr_session,
                                         base_dir,
                                         self.server.cached_webfingers,
                                         self.server.person_cache,
                                         nickname,
                                         domain,
                                         port,
                                         inbox_media_feed,
                                         self.server.allow_deletion,
                                         http_prefix,
                                         self.server.project_version,
                                         minimal_nick,
                                         self.server.yt_replace_domain,
                                         twitter_replacement_domain,
                                         self.server.show_published_date_only,
                                         self.server.newswire,
                                         self.server.positive_voting,
                                         self.server.show_publish_as_icon,
                                         full_width_tl_button_header,
                                         self.server.icons_as_buttons,
                                         self.server.rss_icon_at_top,
                                         self.server.publish_button_at_top,
                                         authorized,
                                         self.server.theme_name,
                                         self.server.peertube_instances,
                                         allow_local_network_access,
                                         self.server.text_mode_banner,
                                         access_keys,
                                         self.server.system_language,
                                         self.server.max_like_count,
                                         fed_domains,
                                         self.server.signing_priv_key_pem,
                                         self.server.cw_lists,
                                         self.server.lists_enabled,
                                         timezone, bold_reading,
                                         self.server.dogwhistles, ua_str,
                                         self.server.min_images_for_accounts)
                    msg = msg.encode('utf-8')
                    msglen = len(msg)
                    self._set_headers('text/html', msglen,
                                      cookie, calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time,
                                        self.server.fitness,
                                        '_GET', '_show_media_timeline',
                                        self.server.debug)
                else:
                    # don't need authorized fetch here because there is
                    # already the authorization check
                    msg_str = json.dumps(inbox_media_feed,
                                         ensure_ascii=False)
                    msg_str = self._convert_domains(calling_domain,
                                                    referer_domain,
                                                    msg_str)
                    msg = msg_str.encode('utf-8')
                    msglen = len(msg)
                    accept_str = self.headers['Accept']
                    protocol_str = \
                        get_json_content_from_accept(accept_str)
                    self._set_headers(protocol_str, msglen,
                                      None, calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time,
                                        self.server.fitness,
                                        '_GET', '_show_media_timeline json',
                                        self.server.debug)
                return True
            if debug:
                nickname = path.replace('/users/', '')
                nickname = nickname.replace('/tlmedia', '')
                print('DEBUG: ' + nickname +
                      ' was not authorized to access ' + path)
        if path != '/tlmedia':
            # not the media inbox
            if debug:
                print('DEBUG: GET access to inbox is unauthorized')
            self.send_response(405)
            self.end_headers()
            return True
        return False

    def _show_blogs_timeline(self, authorized: bool,
                             calling_domain: str, referer_domain: str,
                             path: str,
                             base_dir: str, http_prefix: str,
                             domain: str, port: int,
                             getreq_start_time,
                             cookie: str, debug: str,
                             curr_session, ua_str: str) -> bool:
        """Shows the blogs timeline
        """
        if '/users/' in path:
            if authorized:
                inbox_blogs_feed = \
                    person_box_json(self.server.recent_posts_cache,
                                    base_dir,
                                    domain,
                                    port,
                                    path,
                                    http_prefix,
                                    MAX_POSTS_IN_BLOGS_FEED, 'tlblogs',
                                    True,
                                    0, self.server.positive_voting,
                                    self.server.voting_time_mins)
                if not inbox_blogs_feed:
                    inbox_blogs_feed = []
                if self._request_http():
                    nickname = path.replace('/users/', '')
                    nickname = nickname.replace('/tlblogs', '')
                    page_number = 1
                    if '?page=' in nickname:
                        page_number = nickname.split('?page=')[1]
                        nickname = nickname.split('?page=')[0]
                        if len(page_number) > 5:
                            page_number = "1"
                        if page_number.isdigit():
                            page_number = int(page_number)
                        else:
                            page_number = 1
                    if 'page=' not in path:
                        # if no page was specified then show the first
                        inbox_blogs_feed = \
                            person_box_json(self.server.recent_posts_cache,
                                            base_dir,
                                            domain,
                                            port,
                                            path + '?page=1',
                                            http_prefix,
                                            MAX_POSTS_IN_BLOGS_FEED, 'tlblogs',
                                            True,
                                            0, self.server.positive_voting,
                                            self.server.voting_time_mins)
                    full_width_tl_button_header = \
                        self.server.full_width_tl_button_header
                    minimal_nick = is_minimal(base_dir, domain, nickname)

                    access_keys = self.server.access_keys
                    if self.server.key_shortcuts.get(nickname):
                        access_keys = \
                            self.server.key_shortcuts[nickname]
                    fed_domains = \
                        self.server.shared_items_federated_domains
                    allow_local_network_access = \
                        self.server.allow_local_network_access
                    twitter_replacement_domain = \
                        self.server.twitter_replacement_domain
                    timezone = None
                    if self.server.account_timezone.get(nickname):
                        timezone = \
                            self.server.account_timezone.get(nickname)
                    bold_reading = False
                    if self.server.bold_reading.get(nickname):
                        bold_reading = True
                    msg = \
                        html_inbox_blogs(self.server.default_timeline,
                                         self.server.recent_posts_cache,
                                         self.server.max_recent_posts,
                                         self.server.translate,
                                         page_number, MAX_POSTS_IN_BLOGS_FEED,
                                         curr_session,
                                         base_dir,
                                         self.server.cached_webfingers,
                                         self.server.person_cache,
                                         nickname,
                                         domain,
                                         port,
                                         inbox_blogs_feed,
                                         self.server.allow_deletion,
                                         http_prefix,
                                         self.server.project_version,
                                         minimal_nick,
                                         self.server.yt_replace_domain,
                                         twitter_replacement_domain,
                                         self.server.show_published_date_only,
                                         self.server.newswire,
                                         self.server.positive_voting,
                                         self.server.show_publish_as_icon,
                                         full_width_tl_button_header,
                                         self.server.icons_as_buttons,
                                         self.server.rss_icon_at_top,
                                         self.server.publish_button_at_top,
                                         authorized,
                                         self.server.theme_name,
                                         self.server.peertube_instances,
                                         allow_local_network_access,
                                         self.server.text_mode_banner,
                                         access_keys,
                                         self.server.system_language,
                                         self.server.max_like_count,
                                         fed_domains,
                                         self.server.signing_priv_key_pem,
                                         self.server.cw_lists,
                                         self.server.lists_enabled,
                                         timezone, bold_reading,
                                         self.server.dogwhistles, ua_str,
                                         self.server.min_images_for_accounts)
                    msg = msg.encode('utf-8')
                    msglen = len(msg)
                    self._set_headers('text/html', msglen,
                                      cookie, calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time,
                                        self.server.fitness,
                                        '_GET', '_show_blogs_timeline',
                                        self.server.debug)
                else:
                    # don't need authorized fetch here because there is
                    # already the authorization check
                    msg_str = json.dumps(inbox_blogs_feed,
                                         ensure_ascii=False)
                    msg_str = self._convert_domains(calling_domain,
                                                    referer_domain,
                                                    msg_str)
                    msg = msg_str.encode('utf-8')
                    msglen = len(msg)
                    accept_str = self.headers['Accept']
                    protocol_str = \
                        get_json_content_from_accept(accept_str)
                    self._set_headers(protocol_str, msglen,
                                      None, calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time,
                                        self.server.fitness,
                                        '_GET', '_show_blogs_timeline json',
                                        self.server.debug)
                return True
            if debug:
                nickname = path.replace('/users/', '')
                nickname = nickname.replace('/tlblogs', '')
                print('DEBUG: ' + nickname +
                      ' was not authorized to access ' + path)
        if path != '/tlblogs':
            # not the blogs inbox
            if debug:
                print('DEBUG: GET access to blogs is unauthorized')
            self.send_response(405)
            self.end_headers()
            return True
        return False

    def _show_news_timeline(self, authorized: bool,
                            calling_domain: str, referer_domain: str,
                            path: str,
                            base_dir: str, http_prefix: str,
                            domain: str, port: int,
                            getreq_start_time,
                            cookie: str, debug: str,
                            curr_session, ua_str: str) -> bool:
        """Shows the news timeline
        """
        if '/users/' in path:
            if authorized:
                inbox_news_feed = \
                    person_box_json(self.server.recent_posts_cache,
                                    base_dir,
                                    domain,
                                    port,
                                    path,
                                    http_prefix,
                                    MAX_POSTS_IN_NEWS_FEED, 'tlnews',
                                    True,
                                    self.server.newswire_votes_threshold,
                                    self.server.positive_voting,
                                    self.server.voting_time_mins)
                if not inbox_news_feed:
                    inbox_news_feed = []
                if self._request_http():
                    nickname = path.replace('/users/', '')
                    nickname = nickname.replace('/tlnews', '')
                    page_number = 1
                    if '?page=' in nickname:
                        page_number = nickname.split('?page=')[1]
                        nickname = nickname.split('?page=')[0]
                        if len(page_number) > 5:
                            page_number = "1"
                        if page_number.isdigit():
                            page_number = int(page_number)
                        else:
                            page_number = 1
                    if 'page=' not in path:
                        newswire_votes_threshold = \
                            self.server.newswire_votes_threshold
                        # if no page was specified then show the first
                        inbox_news_feed = \
                            person_box_json(self.server.recent_posts_cache,
                                            base_dir,
                                            domain,
                                            port,
                                            path + '?page=1',
                                            http_prefix,
                                            MAX_POSTS_IN_BLOGS_FEED, 'tlnews',
                                            True,
                                            newswire_votes_threshold,
                                            self.server.positive_voting,
                                            self.server.voting_time_mins)
                    curr_nickname = path.split('/users/')[1]
                    if '/' in curr_nickname:
                        curr_nickname = curr_nickname.split('/')[0]
                    moderator = is_moderator(base_dir, curr_nickname)
                    editor = is_editor(base_dir, curr_nickname)
                    artist = is_artist(base_dir, curr_nickname)
                    full_width_tl_button_header = \
                        self.server.full_width_tl_button_header
                    minimal_nick = is_minimal(base_dir, domain, nickname)

                    access_keys = self.server.access_keys
                    if self.server.key_shortcuts.get(nickname):
                        access_keys = \
                            self.server.key_shortcuts[nickname]
                    fed_domains = \
                        self.server.shared_items_federated_domains

                    timezone = None
                    if self.server.account_timezone.get(nickname):
                        timezone = \
                            self.server.account_timezone.get(nickname)
                    bold_reading = False
                    if self.server.bold_reading.get(nickname):
                        bold_reading = True
                    msg = \
                        html_inbox_news(self.server.default_timeline,
                                        self.server.recent_posts_cache,
                                        self.server.max_recent_posts,
                                        self.server.translate,
                                        page_number, MAX_POSTS_IN_NEWS_FEED,
                                        curr_session,
                                        base_dir,
                                        self.server.cached_webfingers,
                                        self.server.person_cache,
                                        nickname,
                                        domain,
                                        port,
                                        inbox_news_feed,
                                        self.server.allow_deletion,
                                        http_prefix,
                                        self.server.project_version,
                                        minimal_nick,
                                        self.server.yt_replace_domain,
                                        self.server.twitter_replacement_domain,
                                        self.server.show_published_date_only,
                                        self.server.newswire,
                                        moderator, editor, artist,
                                        self.server.positive_voting,
                                        self.server.show_publish_as_icon,
                                        full_width_tl_button_header,
                                        self.server.icons_as_buttons,
                                        self.server.rss_icon_at_top,
                                        self.server.publish_button_at_top,
                                        authorized,
                                        self.server.theme_name,
                                        self.server.peertube_instances,
                                        self.server.allow_local_network_access,
                                        self.server.text_mode_banner,
                                        access_keys,
                                        self.server.system_language,
                                        self.server.max_like_count,
                                        fed_domains,
                                        self.server.signing_priv_key_pem,
                                        self.server.cw_lists,
                                        self.server.lists_enabled,
                                        timezone, bold_reading,
                                        self.server.dogwhistles, ua_str,
                                        self.server.min_images_for_accounts)
                    msg = msg.encode('utf-8')
                    msglen = len(msg)
                    self._set_headers('text/html', msglen,
                                      cookie, calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time,
                                        self.server.fitness,
                                        '_GET', '_show_news_timeline',
                                        self.server.debug)
                else:
                    # don't need authorized fetch here because there is
                    # already the authorization check
                    msg_str = json.dumps(inbox_news_feed,
                                         ensure_ascii=False)
                    msg_str = self._convert_domains(calling_domain,
                                                    referer_domain,
                                                    msg_str)
                    msg = msg_str.encode('utf-8')
                    msglen = len(msg)
                    accept_str = self.headers['Accept']
                    protocol_str = \
                        get_json_content_from_accept(accept_str)
                    self._set_headers(protocol_str, msglen,
                                      None, calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time,
                                        self.server.fitness,
                                        '_GET', '_show_news_timeline json',
                                        self.server.debug)
                return True
            if debug:
                nickname = 'news'
                print('DEBUG: ' + nickname +
                      ' was not authorized to access ' + path)
        if path != '/tlnews':
            # not the news inbox
            if debug:
                print('DEBUG: GET access to news is unauthorized')
            self.send_response(405)
            self.end_headers()
            return True
        return False

    def _show_features_timeline(self, authorized: bool,
                                calling_domain: str, referer_domain: str,
                                path: str, base_dir: str, http_prefix: str,
                                domain: str, port: int,
                                getreq_start_time,
                                cookie: str, debug: str,
                                curr_session, ua_str: str) -> bool:
        """Shows the features timeline (all local blogs)
        """
        if '/users/' in path:
            if authorized:
                inbox_features_feed = \
                    person_box_json(self.server.recent_posts_cache,
                                    base_dir,
                                    domain,
                                    port,
                                    path,
                                    http_prefix,
                                    MAX_POSTS_IN_NEWS_FEED, 'tlfeatures',
                                    True,
                                    self.server.newswire_votes_threshold,
                                    self.server.positive_voting,
                                    self.server.voting_time_mins)
                if not inbox_features_feed:
                    inbox_features_feed = []
                if self._request_http():
                    nickname = path.replace('/users/', '')
                    nickname = nickname.replace('/tlfeatures', '')
                    page_number = 1
                    if '?page=' in nickname:
                        page_number = nickname.split('?page=')[1]
                        nickname = nickname.split('?page=')[0]
                        if len(page_number) > 5:
                            page_number = "1"
                        if page_number.isdigit():
                            page_number = int(page_number)
                        else:
                            page_number = 1
                    if 'page=' not in path:
                        newswire_votes_threshold = \
                            self.server.newswire_votes_threshold
                        # if no page was specified then show the first
                        inbox_features_feed = \
                            person_box_json(self.server.recent_posts_cache,
                                            base_dir,
                                            domain,
                                            port,
                                            path + '?page=1',
                                            http_prefix,
                                            MAX_POSTS_IN_BLOGS_FEED,
                                            'tlfeatures',
                                            True,
                                            newswire_votes_threshold,
                                            self.server.positive_voting,
                                            self.server.voting_time_mins)
                    curr_nickname = path.split('/users/')[1]
                    if '/' in curr_nickname:
                        curr_nickname = curr_nickname.split('/')[0]
                    full_width_tl_button_header = \
                        self.server.full_width_tl_button_header
                    minimal_nick = is_minimal(base_dir, domain, nickname)

                    access_keys = self.server.access_keys
                    if self.server.key_shortcuts.get(nickname):
                        access_keys = \
                            self.server.key_shortcuts[nickname]

                    shared_items_federated_domains = \
                        self.server.shared_items_federated_domains
                    allow_local_network_access = \
                        self.server.allow_local_network_access
                    twitter_replacement_domain = \
                        self.server.twitter_replacement_domain
                    show_published_date_only = \
                        self.server.show_published_date_only
                    timezone = None
                    if self.server.account_timezone.get(nickname):
                        timezone = \
                            self.server.account_timezone.get(nickname)
                    bold_reading = False
                    if self.server.bold_reading.get(nickname):
                        bold_reading = True
                    min_images_for_accounts = \
                        self.server.min_images_for_accounts
                    msg = \
                        html_inbox_features(self.server.default_timeline,
                                            self.server.recent_posts_cache,
                                            self.server.max_recent_posts,
                                            self.server.translate,
                                            page_number,
                                            MAX_POSTS_IN_BLOGS_FEED,
                                            curr_session,
                                            base_dir,
                                            self.server.cached_webfingers,
                                            self.server.person_cache,
                                            nickname,
                                            domain,
                                            port,
                                            inbox_features_feed,
                                            self.server.allow_deletion,
                                            http_prefix,
                                            self.server.project_version,
                                            minimal_nick,
                                            self.server.yt_replace_domain,
                                            twitter_replacement_domain,
                                            show_published_date_only,
                                            self.server.newswire,
                                            self.server.positive_voting,
                                            self.server.show_publish_as_icon,
                                            full_width_tl_button_header,
                                            self.server.icons_as_buttons,
                                            self.server.rss_icon_at_top,
                                            self.server.publish_button_at_top,
                                            authorized,
                                            self.server.theme_name,
                                            self.server.peertube_instances,
                                            allow_local_network_access,
                                            self.server.text_mode_banner,
                                            access_keys,
                                            self.server.system_language,
                                            self.server.max_like_count,
                                            shared_items_federated_domains,
                                            self.server.signing_priv_key_pem,
                                            self.server.cw_lists,
                                            self.server.lists_enabled,
                                            timezone, bold_reading,
                                            self.server.dogwhistles, ua_str,
                                            min_images_for_accounts)
                    msg = msg.encode('utf-8')
                    msglen = len(msg)
                    self._set_headers('text/html', msglen,
                                      cookie, calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time,
                                        self.server.fitness,
                                        '_GET', '_show_features_timeline',
                                        self.server.debug)
                else:
                    # don't need authorized fetch here because there is
                    # already the authorization check
                    msg_str = json.dumps(inbox_features_feed,
                                         ensure_ascii=False)
                    msg_str = self._convert_domains(calling_domain,
                                                    referer_domain,
                                                    msg_str)
                    msg = msg_str.encode('utf-8')
                    msglen = len(msg)
                    accept_str = self.headers['Accept']
                    protocol_str = \
                        get_json_content_from_accept(accept_str)
                    self._set_headers(protocol_str, msglen,
                                      None, calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time,
                                        self.server.fitness,
                                        '_GET', '_show_features_timeline json',
                                        self.server.debug)
                return True
            if debug:
                nickname = 'news'
                print('DEBUG: ' + nickname +
                      ' was not authorized to access ' + path)
        if path != '/tlfeatures':
            # not the features inbox
            if debug:
                print('DEBUG: GET access to features is unauthorized')
            self.send_response(405)
            self.end_headers()
            return True
        return False

    def _show_shares_timeline(self, authorized: bool,
                              calling_domain: str, path: str,
                              base_dir: str, http_prefix: str,
                              domain: str, port: int,
                              getreq_start_time,
                              cookie: str, debug: str,
                              curr_session, ua_str: str) -> bool:
        """Shows the shares timeline
        """
        if '/users/' in path:
            if authorized:
                if self._request_http():
                    nickname = path.replace('/users/', '')
                    nickname = nickname.replace('/tlshares', '')
                    page_number = 1
                    if '?page=' in nickname:
                        page_number = nickname.split('?page=')[1]
                        nickname = nickname.split('?page=')[0]
                        if len(page_number) > 5:
                            page_number = "1"
                        if page_number.isdigit():
                            page_number = int(page_number)
                        else:
                            page_number = 1

                    access_keys = self.server.access_keys
                    if self.server.key_shortcuts.get(nickname):
                        access_keys = \
                            self.server.key_shortcuts[nickname]
                    full_width_tl_button_header = \
                        self.server.full_width_tl_button_header

                    timezone = None
                    if self.server.account_timezone.get(nickname):
                        timezone = \
                            self.server.account_timezone.get(nickname)
                    bold_reading = False
                    if self.server.bold_reading.get(nickname):
                        bold_reading = True
                    msg = \
                        html_shares(self.server.default_timeline,
                                    self.server.recent_posts_cache,
                                    self.server.max_recent_posts,
                                    self.server.translate,
                                    page_number, MAX_POSTS_IN_FEED,
                                    curr_session,
                                    base_dir,
                                    self.server.cached_webfingers,
                                    self.server.person_cache,
                                    nickname,
                                    domain,
                                    port,
                                    self.server.allow_deletion,
                                    http_prefix,
                                    self.server.project_version,
                                    self.server.yt_replace_domain,
                                    self.server.twitter_replacement_domain,
                                    self.server.show_published_date_only,
                                    self.server.newswire,
                                    self.server.positive_voting,
                                    self.server.show_publish_as_icon,
                                    full_width_tl_button_header,
                                    self.server.icons_as_buttons,
                                    self.server.rss_icon_at_top,
                                    self.server.publish_button_at_top,
                                    authorized, self.server.theme_name,
                                    self.server.peertube_instances,
                                    self.server.allow_local_network_access,
                                    self.server.text_mode_banner,
                                    access_keys,
                                    self.server.system_language,
                                    self.server.max_like_count,
                                    self.server.shared_items_federated_domains,
                                    self.server.signing_priv_key_pem,
                                    self.server.cw_lists,
                                    self.server.lists_enabled, timezone,
                                    bold_reading, self.server.dogwhistles,
                                    ua_str,
                                    self.server.min_images_for_accounts)
                    msg = msg.encode('utf-8')
                    msglen = len(msg)
                    self._set_headers('text/html', msglen,
                                      cookie, calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time,
                                        self.server.fitness,
                                        '_GET', '_show_shares_timeline',
                                        self.server.debug)
                    return True
        # not the shares timeline
        if debug:
            print('DEBUG: GET access to shares timeline is unauthorized')
        self.send_response(405)
        self.end_headers()
        return True

    def _show_wanted_timeline(self, authorized: bool,
                              calling_domain: str, path: str,
                              base_dir: str, http_prefix: str,
                              domain: str, port: int,
                              getreq_start_time,
                              cookie: str, debug: str,
                              curr_session, ua_str: str) -> bool:
        """Shows the wanted timeline
        """
        if '/users/' in path:
            if authorized:
                if self._request_http():
                    nickname = path.replace('/users/', '')
                    nickname = nickname.replace('/tlwanted', '')
                    page_number = 1
                    if '?page=' in nickname:
                        page_number = nickname.split('?page=')[1]
                        nickname = nickname.split('?page=')[0]
                        if len(page_number) > 5:
                            page_number = "1"
                        if page_number.isdigit():
                            page_number = int(page_number)
                        else:
                            page_number = 1

                    access_keys = self.server.access_keys
                    if self.server.key_shortcuts.get(nickname):
                        access_keys = \
                            self.server.key_shortcuts[nickname]
                    full_width_tl_button_header = \
                        self.server.full_width_tl_button_header
                    timezone = None
                    if self.server.account_timezone.get(nickname):
                        timezone = \
                            self.server.account_timezone.get(nickname)
                    bold_reading = False
                    if self.server.bold_reading.get(nickname):
                        bold_reading = True
                    msg = \
                        html_wanted(self.server.default_timeline,
                                    self.server.recent_posts_cache,
                                    self.server.max_recent_posts,
                                    self.server.translate,
                                    page_number, MAX_POSTS_IN_FEED,
                                    curr_session,
                                    base_dir,
                                    self.server.cached_webfingers,
                                    self.server.person_cache,
                                    nickname,
                                    domain,
                                    port,
                                    self.server.allow_deletion,
                                    http_prefix,
                                    self.server.project_version,
                                    self.server.yt_replace_domain,
                                    self.server.twitter_replacement_domain,
                                    self.server.show_published_date_only,
                                    self.server.newswire,
                                    self.server.positive_voting,
                                    self.server.show_publish_as_icon,
                                    full_width_tl_button_header,
                                    self.server.icons_as_buttons,
                                    self.server.rss_icon_at_top,
                                    self.server.publish_button_at_top,
                                    authorized, self.server.theme_name,
                                    self.server.peertube_instances,
                                    self.server.allow_local_network_access,
                                    self.server.text_mode_banner,
                                    access_keys,
                                    self.server.system_language,
                                    self.server.max_like_count,
                                    self.server.shared_items_federated_domains,
                                    self.server.signing_priv_key_pem,
                                    self.server.cw_lists,
                                    self.server.lists_enabled,
                                    timezone, bold_reading,
                                    self.server.dogwhistles, ua_str,
                                    self.server.min_images_for_accounts)
                    msg = msg.encode('utf-8')
                    msglen = len(msg)
                    self._set_headers('text/html', msglen,
                                      cookie, calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time,
                                        self.server.fitness,
                                        '_GET', '_show_wanted_timeline',
                                        self.server.debug)
                    return True
        # not the shares timeline
        if debug:
            print('DEBUG: GET access to wanted timeline is unauthorized')
        self.send_response(405)
        self.end_headers()
        return True

    def _show_bookmarks_timeline(self, authorized: bool,
                                 calling_domain: str, referer_domain: str,
                                 path: str,
                                 base_dir: str, http_prefix: str,
                                 domain: str, port: int,
                                 getreq_start_time,
                                 cookie: str, debug: str,
                                 curr_session, ua_str: str) -> bool:
        """Shows the bookmarks timeline
        """
        if '/users/' in path:
            if authorized:
                bookmarks_feed = \
                    person_box_json(self.server.recent_posts_cache,
                                    base_dir,
                                    domain,
                                    port,
                                    path,
                                    http_prefix,
                                    MAX_POSTS_IN_FEED, 'tlbookmarks',
                                    authorized,
                                    0, self.server.positive_voting,
                                    self.server.voting_time_mins)
                if bookmarks_feed:
                    if self._request_http():
                        nickname = path.replace('/users/', '')
                        nickname = nickname.replace('/tlbookmarks', '')
                        nickname = nickname.replace('/bookmarks', '')
                        page_number = 1
                        if '?page=' in nickname:
                            page_number = nickname.split('?page=')[1]
                            nickname = nickname.split('?page=')[0]
                            if len(page_number) > 5:
                                page_number = "1"
                            if page_number.isdigit():
                                page_number = int(page_number)
                            else:
                                page_number = 1
                        if 'page=' not in path:
                            # if no page was specified then show the first
                            bookmarks_feed = \
                                person_box_json(self.server.recent_posts_cache,
                                                base_dir,
                                                domain,
                                                port,
                                                path + '?page=1',
                                                http_prefix,
                                                MAX_POSTS_IN_FEED,
                                                'tlbookmarks',
                                                authorized,
                                                0, self.server.positive_voting,
                                                self.server.voting_time_mins)
                        full_width_tl_button_header = \
                            self.server.full_width_tl_button_header
                        minimal_nick = is_minimal(base_dir, domain, nickname)

                        access_keys = self.server.access_keys
                        if self.server.key_shortcuts.get(nickname):
                            access_keys = \
                                self.server.key_shortcuts[nickname]

                        shared_items_federated_domains = \
                            self.server.shared_items_federated_domains
                        allow_local_network_access = \
                            self.server.allow_local_network_access
                        twitter_replacement_domain = \
                            self.server.twitter_replacement_domain
                        show_published_date_only = \
                            self.server.show_published_date_only
                        timezone = None
                        if self.server.account_timezone.get(nickname):
                            timezone = \
                                self.server.account_timezone.get(nickname)
                        bold_reading = False
                        if self.server.bold_reading.get(nickname):
                            bold_reading = True
                        msg = \
                            html_bookmarks(self.server.default_timeline,
                                           self.server.recent_posts_cache,
                                           self.server.max_recent_posts,
                                           self.server.translate,
                                           page_number, MAX_POSTS_IN_FEED,
                                           curr_session,
                                           base_dir,
                                           self.server.cached_webfingers,
                                           self.server.person_cache,
                                           nickname,
                                           domain,
                                           port,
                                           bookmarks_feed,
                                           self.server.allow_deletion,
                                           http_prefix,
                                           self.server.project_version,
                                           minimal_nick,
                                           self.server.yt_replace_domain,
                                           twitter_replacement_domain,
                                           show_published_date_only,
                                           self.server.newswire,
                                           self.server.positive_voting,
                                           self.server.show_publish_as_icon,
                                           full_width_tl_button_header,
                                           self.server.icons_as_buttons,
                                           self.server.rss_icon_at_top,
                                           self.server.publish_button_at_top,
                                           authorized,
                                           self.server.theme_name,
                                           self.server.peertube_instances,
                                           allow_local_network_access,
                                           self.server.text_mode_banner,
                                           access_keys,
                                           self.server.system_language,
                                           self.server.max_like_count,
                                           shared_items_federated_domains,
                                           self.server.signing_priv_key_pem,
                                           self.server.cw_lists,
                                           self.server.lists_enabled,
                                           timezone, bold_reading,
                                           self.server.dogwhistles, ua_str,
                                           self.server.min_images_for_accounts)
                        msg = msg.encode('utf-8')
                        msglen = len(msg)
                        self._set_headers('text/html', msglen,
                                          cookie, calling_domain, False)
                        self._write(msg)
                        fitness_performance(getreq_start_time,
                                            self.server.fitness,
                                            '_GET', '_show_bookmarks_timeline',
                                            self.server.debug)
                    else:
                        # don't need authorized fetch here because
                        # there is already the authorization check
                        msg_str = json.dumps(bookmarks_feed,
                                             ensure_ascii=False)
                        msg_str = self._convert_domains(calling_domain,
                                                        referer_domain,
                                                        msg_str)
                        msg = msg_str.encode('utf-8')
                        msglen = len(msg)
                        accept_str = self.headers['Accept']
                        protocol_str = \
                            get_json_content_from_accept(accept_str)
                        self._set_headers(protocol_str, msglen,
                                          None, calling_domain, False)
                        self._write(msg)
                        fitness_performance(getreq_start_time,
                                            self.server.fitness,
                                            '_GET',
                                            '_show_bookmarks_timeline json',
                                            self.server.debug)
                    return True
            else:
                if debug:
                    nickname = path.replace('/users/', '')
                    nickname = nickname.replace('/tlbookmarks', '')
                    nickname = nickname.replace('/bookmarks', '')
                    print('DEBUG: ' + nickname +
                          ' was not authorized to access ' + path)
        if debug:
            print('DEBUG: GET access to bookmarks is unauthorized')
        self.send_response(405)
        self.end_headers()
        return True

    def _show_outbox_timeline(self, authorized: bool,
                              calling_domain: str, referer_domain: str,
                              path: str,
                              base_dir: str, http_prefix: str,
                              domain: str, port: int,
                              getreq_start_time,
                              cookie: str, debug: str,
                              curr_session, ua_str: str,
                              proxy_type: str) -> bool:
        """Shows the outbox timeline
        """
        # get outbox feed for a person
        outbox_feed = \
            person_box_json(self.server.recent_posts_cache,
                            base_dir, domain, port, path,
                            http_prefix, MAX_POSTS_IN_FEED, 'outbox',
                            authorized,
                            self.server.newswire_votes_threshold,
                            self.server.positive_voting,
                            self.server.voting_time_mins)
        if outbox_feed:
            nickname = \
                path.replace('/users/', '').replace('/outbox', '')
            page_number = 0
            if '?page=' in nickname:
                page_number = nickname.split('?page=')[1]
                nickname = nickname.split('?page=')[0]
                if len(page_number) > 5:
                    page_number = "1"
                if page_number.isdigit():
                    page_number = int(page_number)
                else:
                    page_number = 1
            else:
                if self._request_http():
                    page_number = 1
            if authorized and page_number >= 1:
                # if a page wasn't specified then show the first one
                page_str = '?page=' + str(page_number)
                outbox_feed = \
                    person_box_json(self.server.recent_posts_cache,
                                    base_dir, domain, port,
                                    path + page_str,
                                    http_prefix,
                                    MAX_POSTS_IN_FEED, 'outbox',
                                    authorized,
                                    self.server.newswire_votes_threshold,
                                    self.server.positive_voting,
                                    self.server.voting_time_mins)
            else:
                page_number = 1

            if self._request_http():
                full_width_tl_button_header = \
                    self.server.full_width_tl_button_header
                minimal_nick = is_minimal(base_dir, domain, nickname)

                access_keys = self.server.access_keys
                if self.server.key_shortcuts.get(nickname):
                    access_keys = \
                        self.server.key_shortcuts[nickname]

                timezone = None
                if self.server.account_timezone.get(nickname):
                    timezone = \
                        self.server.account_timezone.get(nickname)
                bold_reading = False
                if self.server.bold_reading.get(nickname):
                    bold_reading = True
                msg = \
                    html_outbox(self.server.default_timeline,
                                self.server.recent_posts_cache,
                                self.server.max_recent_posts,
                                self.server.translate,
                                page_number, MAX_POSTS_IN_FEED,
                                curr_session,
                                base_dir,
                                self.server.cached_webfingers,
                                self.server.person_cache,
                                nickname, domain, port,
                                outbox_feed,
                                self.server.allow_deletion,
                                http_prefix,
                                self.server.project_version,
                                minimal_nick,
                                self.server.yt_replace_domain,
                                self.server.twitter_replacement_domain,
                                self.server.show_published_date_only,
                                self.server.newswire,
                                self.server.positive_voting,
                                self.server.show_publish_as_icon,
                                full_width_tl_button_header,
                                self.server.icons_as_buttons,
                                self.server.rss_icon_at_top,
                                self.server.publish_button_at_top,
                                authorized,
                                self.server.theme_name,
                                self.server.peertube_instances,
                                self.server.allow_local_network_access,
                                self.server.text_mode_banner,
                                access_keys,
                                self.server.system_language,
                                self.server.max_like_count,
                                self.server.shared_items_federated_domains,
                                self.server.signing_priv_key_pem,
                                self.server.cw_lists,
                                self.server.lists_enabled,
                                timezone, bold_reading,
                                self.server.dogwhistles, ua_str,
                                self.server.min_images_for_accounts)
                msg = msg.encode('utf-8')
                msglen = len(msg)
                self._set_headers('text/html', msglen,
                                  cookie, calling_domain, False)
                self._write(msg)
                fitness_performance(getreq_start_time,
                                    self.server.fitness,
                                    '_GET', '_show_outbox_timeline',
                                    debug)
            else:
                if self._secure_mode(curr_session, proxy_type):
                    msg_str = json.dumps(outbox_feed,
                                         ensure_ascii=False)
                    msg_str = self._convert_domains(calling_domain,
                                                    referer_domain,
                                                    msg_str)
                    msg = msg_str.encode('utf-8')
                    msglen = len(msg)
                    accept_str = self.headers['Accept']
                    protocol_str = \
                        get_json_content_from_accept(accept_str)
                    self._set_headers(protocol_str, msglen,
                                      None, calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time,
                                        self.server.fitness,
                                        '_GET', '_show_outbox_timeline json',
                                        debug)
                else:
                    self._404()
            return True
        return False

    def _show_mod_timeline(self, authorized: bool,
                           calling_domain: str, referer_domain: str,
                           path: str, base_dir: str, http_prefix: str,
                           domain: str, port: int, getreq_start_time,
                           cookie: str, debug: str,
                           curr_session, ua_str: str) -> bool:
        """Shows the moderation timeline
        """
        if '/users/' in path:
            if authorized:
                moderation_feed = \
                    person_box_json(self.server.recent_posts_cache,
                                    base_dir,
                                    domain,
                                    port,
                                    path,
                                    http_prefix,
                                    MAX_POSTS_IN_FEED, 'moderation',
                                    True,
                                    0, self.server.positive_voting,
                                    self.server.voting_time_mins)
                if moderation_feed:
                    if self._request_http():
                        nickname = path.replace('/users/', '')
                        nickname = nickname.replace('/moderation', '')
                        page_number = 1
                        if '?page=' in nickname:
                            page_number = nickname.split('?page=')[1]
                            nickname = nickname.split('?page=')[0]
                            if len(page_number) > 5:
                                page_number = "1"
                            if page_number.isdigit():
                                page_number = int(page_number)
                            else:
                                page_number = 1
                        if 'page=' not in path:
                            # if no page was specified then show the first
                            moderation_feed = \
                                person_box_json(self.server.recent_posts_cache,
                                                base_dir,
                                                domain,
                                                port,
                                                path + '?page=1',
                                                http_prefix,
                                                MAX_POSTS_IN_FEED,
                                                'moderation',
                                                True,
                                                0, self.server.positive_voting,
                                                self.server.voting_time_mins)
                        full_width_tl_button_header = \
                            self.server.full_width_tl_button_header
                        moderation_action_str = ''

                        access_keys = self.server.access_keys
                        if self.server.key_shortcuts.get(nickname):
                            access_keys = \
                                self.server.key_shortcuts[nickname]

                        shared_items_federated_domains = \
                            self.server.shared_items_federated_domains
                        twitter_replacement_domain = \
                            self.server.twitter_replacement_domain
                        allow_local_network_access = \
                            self.server.allow_local_network_access
                        show_published_date_only = \
                            self.server.show_published_date_only
                        timezone = None
                        if self.server.account_timezone.get(nickname):
                            timezone = \
                                self.server.account_timezone.get(nickname)
                        bold_reading = False
                        if self.server.bold_reading.get(nickname):
                            bold_reading = True
                        min_images_for_accounts = \
                            self.server.min_images_for_accounts
                        msg = \
                            html_moderation(self.server.default_timeline,
                                            self.server.recent_posts_cache,
                                            self.server.max_recent_posts,
                                            self.server.translate,
                                            page_number, MAX_POSTS_IN_FEED,
                                            curr_session,
                                            base_dir,
                                            self.server.cached_webfingers,
                                            self.server.person_cache,
                                            nickname,
                                            domain,
                                            port,
                                            moderation_feed,
                                            True,
                                            http_prefix,
                                            self.server.project_version,
                                            self.server.yt_replace_domain,
                                            twitter_replacement_domain,
                                            show_published_date_only,
                                            self.server.newswire,
                                            self.server.positive_voting,
                                            self.server.show_publish_as_icon,
                                            full_width_tl_button_header,
                                            self.server.icons_as_buttons,
                                            self.server.rss_icon_at_top,
                                            self.server.publish_button_at_top,
                                            authorized, moderation_action_str,
                                            self.server.theme_name,
                                            self.server.peertube_instances,
                                            allow_local_network_access,
                                            self.server.text_mode_banner,
                                            access_keys,
                                            self.server.system_language,
                                            self.server.max_like_count,
                                            shared_items_federated_domains,
                                            self.server.signing_priv_key_pem,
                                            self.server.cw_lists,
                                            self.server.lists_enabled,
                                            timezone, bold_reading,
                                            self.server.dogwhistles,
                                            ua_str,
                                            min_images_for_accounts)
                        msg = msg.encode('utf-8')
                        msglen = len(msg)
                        self._set_headers('text/html', msglen,
                                          cookie, calling_domain, False)
                        self._write(msg)
                        fitness_performance(getreq_start_time,
                                            self.server.fitness,
                                            '_GET', '_show_mod_timeline',
                                            self.server.debug)
                    else:
                        # don't need authorized fetch here because
                        # there is already the authorization check
                        msg_str = json.dumps(moderation_feed,
                                             ensure_ascii=False)
                        msg_str = self._convert_domains(calling_domain,
                                                        referer_domain,
                                                        msg_str)
                        msg = msg_str.encode('utf-8')
                        msglen = len(msg)
                        accept_str = self.headers['Accept']
                        protocol_str = \
                            get_json_content_from_accept(accept_str)
                        self._set_headers(protocol_str, msglen,
                                          None, calling_domain, False)
                        self._write(msg)
                        fitness_performance(getreq_start_time,
                                            self.server.fitness,
                                            '_GET', '_show_mod_timeline json',
                                            self.server.debug)
                    return True
            else:
                if debug:
                    nickname = path.replace('/users/', '')
                    nickname = nickname.replace('/moderation', '')
                    print('DEBUG: ' + nickname +
                          ' was not authorized to access ' + path)
        if debug:
            print('DEBUG: GET access to moderation feed is unauthorized')
        self.send_response(405)
        self.end_headers()
        return True

    def _show_shares_feed(self, authorized: bool,
                          calling_domain: str, referer_domain: str,
                          path: str, base_dir: str, http_prefix: str,
                          domain: str, port: int, getreq_start_time,
                          proxy_type: str, cookie: str,
                          debug: str, shares_file_type: str,
                          curr_session) -> bool:
        """Shows the shares feed
        """
        shares = \
            get_shares_feed_for_person(base_dir, domain, port, path,
                                       http_prefix, shares_file_type,
                                       SHARES_PER_PAGE)
        if shares:
            if self._request_http():
                page_number = 1
                if '?page=' not in path:
                    search_path = path
                    # get a page of shares, not the summary
                    shares = \
                        get_shares_feed_for_person(base_dir, domain, port,
                                                   path + '?page=true',
                                                   http_prefix,
                                                   shares_file_type,
                                                   SHARES_PER_PAGE)
                else:
                    page_number_str = path.split('?page=')[1]
                    if '#' in page_number_str:
                        page_number_str = page_number_str.split('#')[0]
                    if len(page_number_str) > 5:
                        page_number_str = "1"
                    if page_number_str.isdigit():
                        page_number = int(page_number_str)
                    search_path = path.split('?page=')[0]
                search_path2 = search_path.replace('/' + shares_file_type, '')
                get_person = person_lookup(domain, search_path2, base_dir)
                if get_person:
                    curr_session = \
                        self._establish_session("show_shares_feed",
                                                curr_session, proxy_type)
                    if not curr_session:
                        self._404()
                        self.server.getreq_busy = False
                        return True

                    access_keys = self.server.access_keys
                    if '/users/' in path:
                        nickname = path.split('/users/')[1]
                        if '/' in nickname:
                            nickname = nickname.split('/')[0]
                        if self.server.key_shortcuts.get(nickname):
                            access_keys = \
                                self.server.key_shortcuts[nickname]

                    city = get_spoofed_city(self.server.city,
                                            base_dir, nickname, domain)
                    shared_items_federated_domains = \
                        self.server.shared_items_federated_domains
                    timezone = None
                    if self.server.account_timezone.get(nickname):
                        timezone = \
                            self.server.account_timezone.get(nickname)
                    bold_reading = False
                    if self.server.bold_reading.get(nickname):
                        bold_reading = True
                    msg = \
                        html_profile(self.server.signing_priv_key_pem,
                                     self.server.rss_icon_at_top,
                                     self.server.icons_as_buttons,
                                     self.server.default_timeline,
                                     self.server.recent_posts_cache,
                                     self.server.max_recent_posts,
                                     self.server.translate,
                                     self.server.project_version,
                                     base_dir, http_prefix,
                                     authorized,
                                     get_person, shares_file_type,
                                     curr_session,
                                     self.server.cached_webfingers,
                                     self.server.person_cache,
                                     self.server.yt_replace_domain,
                                     self.server.twitter_replacement_domain,
                                     self.server.show_published_date_only,
                                     self.server.newswire,
                                     self.server.theme_name,
                                     self.server.dormant_months,
                                     self.server.peertube_instances,
                                     self.server.allow_local_network_access,
                                     self.server.text_mode_banner,
                                     self.server.debug,
                                     access_keys, city,
                                     self.server.system_language,
                                     self.server.max_like_count,
                                     shared_items_federated_domains,
                                     shares,
                                     page_number, SHARES_PER_PAGE,
                                     self.server.cw_lists,
                                     self.server.lists_enabled,
                                     self.server.content_license_url,
                                     timezone, bold_reading)
                    msg = msg.encode('utf-8')
                    msglen = len(msg)
                    self._set_headers('text/html', msglen,
                                      cookie, calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time,
                                        self.server.fitness,
                                        '_GET', '_show_shares_feed',
                                        debug)
                    self.server.getreq_busy = False
                    return True
            else:
                if self._secure_mode(curr_session, proxy_type):
                    msg_str = json.dumps(shares,
                                         ensure_ascii=False)
                    msg_str = self._convert_domains(calling_domain,
                                                    referer_domain,
                                                    msg_str)
                    msg = msg_str.encode('utf-8')
                    msglen = len(msg)
                    accept_str = self.headers['Accept']
                    protocol_str = \
                        get_json_content_from_accept(accept_str)
                    self._set_headers(protocol_str, msglen,
                                      None, calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time,
                                        self.server.fitness,
                                        '_GET', '_show_shares_feed json',
                                        debug)
                else:
                    self._404()
                return True
        return False

    def _show_following_feed(self, authorized: bool,
                             calling_domain: str, referer_domain: str,
                             path: str, base_dir: str, http_prefix: str,
                             domain: str, port: int, getreq_start_time,
                             proxy_type: str, cookie: str,
                             debug: str, curr_session) -> bool:
        """Shows the following feed
        """
        following = \
            get_following_feed(base_dir, domain, port, path,
                               http_prefix, authorized, FOLLOWS_PER_PAGE,
                               'following')
        if following:
            if self._request_http():
                page_number = 1
                if '?page=' not in path:
                    search_path = path
                    # get a page of following, not the summary
                    following = \
                        get_following_feed(base_dir,
                                           domain,
                                           port,
                                           path + '?page=true',
                                           http_prefix,
                                           authorized, FOLLOWS_PER_PAGE)
                else:
                    page_number_str = path.split('?page=')[1]
                    if '#' in page_number_str:
                        page_number_str = page_number_str.split('#')[0]
                    if len(page_number_str) > 5:
                        page_number_str = "1"
                    if page_number_str.isdigit():
                        page_number = int(page_number_str)
                    search_path = path.split('?page=')[0]
                get_person = \
                    person_lookup(domain,
                                  search_path.replace('/following', ''),
                                  base_dir)
                if get_person:
                    curr_session = \
                        self._establish_session("show_following_feed",
                                                curr_session, proxy_type)
                    if not curr_session:
                        self._404()
                        return True

                    access_keys = self.server.access_keys
                    city = None
                    timezone = None
                    if '/users/' in path:
                        nickname = path.split('/users/')[1]
                        if '/' in nickname:
                            nickname = nickname.split('/')[0]
                        if self.server.key_shortcuts.get(nickname):
                            access_keys = \
                                self.server.key_shortcuts[nickname]

                        city = get_spoofed_city(self.server.city,
                                                base_dir, nickname, domain)
                        if self.server.account_timezone.get(nickname):
                            timezone = \
                                self.server.account_timezone.get(nickname)
                    content_license_url = \
                        self.server.content_license_url
                    shared_items_federated_domains = \
                        self.server.shared_items_federated_domains
                    bold_reading = False
                    if self.server.bold_reading.get(nickname):
                        bold_reading = True
                    msg = \
                        html_profile(self.server.signing_priv_key_pem,
                                     self.server.rss_icon_at_top,
                                     self.server.icons_as_buttons,
                                     self.server.default_timeline,
                                     self.server.recent_posts_cache,
                                     self.server.max_recent_posts,
                                     self.server.translate,
                                     self.server.project_version,
                                     base_dir, http_prefix,
                                     authorized,
                                     get_person, 'following',
                                     curr_session,
                                     self.server.cached_webfingers,
                                     self.server.person_cache,
                                     self.server.yt_replace_domain,
                                     self.server.twitter_replacement_domain,
                                     self.server.show_published_date_only,
                                     self.server.newswire,
                                     self.server.theme_name,
                                     self.server.dormant_months,
                                     self.server.peertube_instances,
                                     self.server.allow_local_network_access,
                                     self.server.text_mode_banner,
                                     self.server.debug,
                                     access_keys, city,
                                     self.server.system_language,
                                     self.server.max_like_count,
                                     shared_items_federated_domains,
                                     following,
                                     page_number,
                                     FOLLOWS_PER_PAGE,
                                     self.server.cw_lists,
                                     self.server.lists_enabled,
                                     content_license_url,
                                     timezone, bold_reading).encode('utf-8')
                    msglen = len(msg)
                    self._set_headers('text/html',
                                      msglen, cookie, calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time,
                                        self.server.fitness,
                                        '_GET', '_show_following_feed',
                                        debug)
                    return True
            else:
                if self._secure_mode(curr_session, proxy_type):
                    msg_str = json.dumps(following,
                                         ensure_ascii=False)
                    msg_str = self._convert_domains(calling_domain,
                                                    referer_domain,
                                                    msg_str)
                    msg = msg_str.encode('utf-8')
                    msglen = len(msg)
                    accept_str = self.headers['Accept']
                    protocol_str = \
                        get_json_content_from_accept(accept_str)
                    self._set_headers(protocol_str, msglen,
                                      None, calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time,
                                        self.server.fitness,
                                        '_GET', '_show_following_feed json',
                                        debug)
                else:
                    self._404()
                return True
        return False

    def _show_followers_feed(self, authorized: bool,
                             calling_domain: str, referer_domain: str,
                             path: str, base_dir: str, http_prefix: str,
                             domain: str, port: int, getreq_start_time,
                             proxy_type: str, cookie: str,
                             debug: str, curr_session) -> bool:
        """Shows the followers feed
        """
        followers = \
            get_following_feed(base_dir, domain, port, path, http_prefix,
                               authorized, FOLLOWS_PER_PAGE, 'followers')
        if followers:
            if self._request_http():
                page_number = 1
                if '?page=' not in path:
                    search_path = path
                    # get a page of followers, not the summary
                    followers = \
                        get_following_feed(base_dir,
                                           domain,
                                           port,
                                           path + '?page=1',
                                           http_prefix,
                                           authorized, FOLLOWS_PER_PAGE,
                                           'followers')
                else:
                    page_number_str = path.split('?page=')[1]
                    if '#' in page_number_str:
                        page_number_str = page_number_str.split('#')[0]
                    if len(page_number_str) > 5:
                        page_number_str = "1"
                    if page_number_str.isdigit():
                        page_number = int(page_number_str)
                    search_path = path.split('?page=')[0]
                get_person = \
                    person_lookup(domain,
                                  search_path.replace('/followers', ''),
                                  base_dir)
                if get_person:
                    curr_session = \
                        self._establish_session("show_followers_feed",
                                                curr_session, proxy_type)
                    if not curr_session:
                        self._404()
                        return True

                    access_keys = self.server.access_keys
                    city = None
                    timezone = None
                    if '/users/' in path:
                        nickname = path.split('/users/')[1]
                        if '/' in nickname:
                            nickname = nickname.split('/')[0]
                        if self.server.key_shortcuts.get(nickname):
                            access_keys = \
                                self.server.key_shortcuts[nickname]

                        city = get_spoofed_city(self.server.city,
                                                base_dir, nickname, domain)
                        if self.server.account_timezone.get(nickname):
                            timezone = \
                                self.server.account_timezone.get(nickname)
                    content_license_url = \
                        self.server.content_license_url
                    shared_items_federated_domains = \
                        self.server.shared_items_federated_domains
                    bold_reading = False
                    if self.server.bold_reading.get(nickname):
                        bold_reading = True
                    msg = \
                        html_profile(self.server.signing_priv_key_pem,
                                     self.server.rss_icon_at_top,
                                     self.server.icons_as_buttons,
                                     self.server.default_timeline,
                                     self.server.recent_posts_cache,
                                     self.server.max_recent_posts,
                                     self.server.translate,
                                     self.server.project_version,
                                     base_dir,
                                     http_prefix,
                                     authorized,
                                     get_person, 'followers',
                                     curr_session,
                                     self.server.cached_webfingers,
                                     self.server.person_cache,
                                     self.server.yt_replace_domain,
                                     self.server.twitter_replacement_domain,
                                     self.server.show_published_date_only,
                                     self.server.newswire,
                                     self.server.theme_name,
                                     self.server.dormant_months,
                                     self.server.peertube_instances,
                                     self.server.allow_local_network_access,
                                     self.server.text_mode_banner,
                                     self.server.debug,
                                     access_keys, city,
                                     self.server.system_language,
                                     self.server.max_like_count,
                                     shared_items_federated_domains,
                                     followers,
                                     page_number,
                                     FOLLOWS_PER_PAGE,
                                     self.server.cw_lists,
                                     self.server.lists_enabled,
                                     content_license_url,
                                     timezone, bold_reading).encode('utf-8')
                    msglen = len(msg)
                    self._set_headers('text/html', msglen,
                                      cookie, calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time,
                                        self.server.fitness,
                                        '_GET', '_show_followers_feed',
                                        debug)
                    return True
            else:
                if self._secure_mode(curr_session, proxy_type):
                    msg_str = json.dumps(followers,
                                         ensure_ascii=False)
                    msg_str = self._convert_domains(calling_domain,
                                                    referer_domain,
                                                    msg_str)
                    msg = msg_str.encode('utf-8')
                    msglen = len(msg)
                    accept_str = self.headers['Accept']
                    protocol_str = \
                        get_json_content_from_accept(accept_str)
                    self._set_headers(protocol_str, msglen,
                                      None, calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time,
                                        self.server.fitness,
                                        '_GET', '_show_followers_feed json',
                                        debug)
                else:
                    self._404()
            return True
        return False

    def _get_featured_collection(self, calling_domain: str,
                                 referer_domain: str,
                                 base_dir: str,
                                 http_prefix: str,
                                 nickname: str, domain: str,
                                 domain_full: str,
                                 system_language: str) -> None:
        """Returns the featured posts collections in
        actor/collections/featured
        """
        featured_collection = \
            json_pin_post(base_dir, http_prefix,
                          nickname, domain, domain_full, system_language)
        msg_str = json.dumps(featured_collection,
                             ensure_ascii=False)
        msg_str = self._convert_domains(calling_domain,
                                        referer_domain,
                                        msg_str)
        msg = msg_str.encode('utf-8')
        msglen = len(msg)
        accept_str = self.headers['Accept']
        protocol_str = \
            get_json_content_from_accept(accept_str)
        self._set_headers(protocol_str, msglen,
                          None, calling_domain, False)
        self._write(msg)

    def _get_featured_tags_collection(self, calling_domain: str,
                                      referer_domain: str,
                                      path: str,
                                      http_prefix: str,
                                      domain_full: str):
        """Returns the featured tags collections in
        actor/collections/featuredTags
        TODO add ability to set a featured tags
        """
        post_context = get_individual_post_context()
        featured_tags_collection = {
            '@context': post_context,
            'id': http_prefix + '://' + domain_full + path,
            'orderedItems': [],
            'totalItems': 0,
            'type': 'OrderedCollection'
        }
        msg_str = json.dumps(featured_tags_collection,
                             ensure_ascii=False)
        msg_str = self._convert_domains(calling_domain,
                                        referer_domain,
                                        msg_str)
        msg = msg_str.encode('utf-8')
        msglen = len(msg)
        accept_str = self.headers['Accept']
        protocol_str = \
            get_json_content_from_accept(accept_str)
        self._set_headers(protocol_str, msglen,
                          None, calling_domain, False)
        self._write(msg)

    def _show_person_profile(self, authorized: bool,
                             calling_domain: str,
                             referer_domain: str, path: str,
                             base_dir: str, http_prefix: str,
                             domain: str,
                             onion_domain: str, i2p_domain: str,
                             getreq_start_time,
                             proxy_type: str, cookie: str,
                             debug: str,
                             curr_session) -> bool:
        """Shows the profile for a person
        """
        # look up a person
        actor_json = person_lookup(domain, path, base_dir)
        if not actor_json:
            return False
        add_alternate_domains(actor_json, domain, onion_domain, i2p_domain)
        if self._request_http():
            curr_session = \
                self._establish_session("showPersonProfile",
                                        curr_session, proxy_type)
            if not curr_session:
                self._404()
                return True

            access_keys = self.server.access_keys
            city = None
            timezone = None
            if '/users/' in path:
                nickname = path.split('/users/')[1]
                if '/' in nickname:
                    nickname = nickname.split('/')[0]
                if self.server.key_shortcuts.get(nickname):
                    access_keys = \
                        self.server.key_shortcuts[nickname]

                city = get_spoofed_city(self.server.city,
                                        base_dir, nickname, domain)
                if self.server.account_timezone.get(nickname):
                    timezone = \
                        self.server.account_timezone.get(nickname)
            bold_reading = False
            if self.server.bold_reading.get(nickname):
                bold_reading = True
            msg = \
                html_profile(self.server.signing_priv_key_pem,
                             self.server.rss_icon_at_top,
                             self.server.icons_as_buttons,
                             self.server.default_timeline,
                             self.server.recent_posts_cache,
                             self.server.max_recent_posts,
                             self.server.translate,
                             self.server.project_version,
                             base_dir, http_prefix, authorized,
                             actor_json, 'posts', curr_session,
                             self.server.cached_webfingers,
                             self.server.person_cache,
                             self.server.yt_replace_domain,
                             self.server.twitter_replacement_domain,
                             self.server.show_published_date_only,
                             self.server.newswire,
                             self.server.theme_name,
                             self.server.dormant_months,
                             self.server.peertube_instances,
                             self.server.allow_local_network_access,
                             self.server.text_mode_banner,
                             self.server.debug,
                             access_keys, city,
                             self.server.system_language,
                             self.server.max_like_count,
                             self.server.shared_items_federated_domains,
                             None, None, None,
                             self.server.cw_lists,
                             self.server.lists_enabled,
                             self.server.content_license_url,
                             timezone, bold_reading).encode('utf-8')
            msglen = len(msg)
            self._set_headers('text/html', msglen,
                              cookie, calling_domain, False)
            self._write(msg)
            fitness_performance(getreq_start_time,
                                self.server.fitness,
                                '_GET', '_show_person_profile',
                                debug)
            if self.server.debug:
                print('DEBUG: html actor sent')
        else:
            if self._secure_mode(curr_session, proxy_type):
                accept_str = self.headers['Accept']
                msg_str = json.dumps(actor_json, ensure_ascii=False)
                msg_str = self._convert_domains(calling_domain,
                                                referer_domain,
                                                msg_str)
                msg = msg_str.encode('utf-8')
                msglen = len(msg)
                if 'application/ld+json' in accept_str:
                    self._set_headers('application/ld+json', msglen,
                                      cookie, calling_domain, False)
                elif 'application/jrd+json' in accept_str:
                    self._set_headers('application/jrd+json', msglen,
                                      cookie, calling_domain, False)
                else:
                    self._set_headers('application/activity+json', msglen,
                                      cookie, calling_domain, False)
                self._write(msg)
                fitness_performance(getreq_start_time,
                                    self.server.fitness,
                                    '_GET', '_show_person_profile json',
                                    self.server.debug)
                if self.server.debug:
                    print('DEBUG: json actor sent')
            else:
                self._404()
        return True

    def _show_instance_actor(self, calling_domain: str,
                             referer_domain: str, path: str,
                             base_dir: str, http_prefix: str,
                             domain: str, domain_full: str,
                             onion_domain: str, i2p_domain: str,
                             getreq_start_time,
                             cookie: str, debug: str,
                             enable_shared_inbox: bool) -> bool:
        """Shows the instance actor
        """
        if debug:
            print('Instance actor requested by ' + calling_domain)
        if self._request_http():
            self._404()
            return False
        actor_json = person_lookup(domain, path, base_dir)
        if not actor_json:
            print('ERROR: no instance actor found')
            self._404()
            return False
        accept_str = self.headers['Accept']
        if onion_domain and calling_domain.endswith('.onion'):
            actor_domain_url = 'http://' + onion_domain
        elif i2p_domain and calling_domain.endswith('.i2p'):
            actor_domain_url = 'http://' + i2p_domain
        else:
            actor_domain_url = http_prefix + '://' + domain_full
        actor_url = actor_domain_url + '/users/Actor'
        remove_fields = (
            'icon', 'image', 'tts', 'shares',
            'alsoKnownAs', 'hasOccupation', 'featured',
            'featuredTags', 'discoverable', 'published',
            'devices'
        )
        for rfield in remove_fields:
            if rfield in actor_json:
                del actor_json[rfield]
        actor_json['endpoints'] = {}
        if enable_shared_inbox:
            actor_json['endpoints'] = {
                'sharedInbox': actor_domain_url + '/inbox'
            }
        actor_json['name'] = 'ACTOR'
        actor_json['preferredUsername'] = domain_full
        actor_json['id'] = actor_domain_url + '/actor'
        actor_json['type'] = 'Application'
        actor_json['summary'] = 'Instance Actor'
        actor_json['publicKey']['id'] = actor_domain_url + '/actor#main-key'
        actor_json['publicKey']['owner'] = actor_domain_url + '/actor'
        actor_json['url'] = actor_domain_url + '/actor'
        actor_json['inbox'] = actor_url + '/inbox'
        actor_json['followers'] = actor_url + '/followers'
        actor_json['following'] = actor_url + '/following'
        msg_str = json.dumps(actor_json, ensure_ascii=False)
        msg_str = self._convert_domains(calling_domain,
                                        referer_domain,
                                        msg_str)
        msg = msg_str.encode('utf-8')
        msglen = len(msg)
        if 'application/ld+json' in accept_str:
            self._set_headers('application/ld+json', msglen,
                              cookie, calling_domain, False)
        elif 'application/jrd+json' in accept_str:
            self._set_headers('application/jrd+json', msglen,
                              cookie, calling_domain, False)
        else:
            self._set_headers('application/activity+json', msglen,
                              cookie, calling_domain, False)
        self._write(msg)
        fitness_performance(getreq_start_time,
                            self.server.fitness,
                            '_GET', '_show_instance_actor',
                            debug)
        return True

    def _show_blog_page(self, authorized: bool,
                        calling_domain: str, path: str,
                        base_dir: str, http_prefix: str,
                        domain: str, port: int,
                        getreq_start_time,
                        proxy_type: str, cookie: str,
                        translate: {}, debug: str,
                        curr_session) -> bool:
        """Shows a blog page
        """
        page_number = 1
        nickname = path.split('/blog/')[1]
        if '/' in nickname:
            nickname = nickname.split('/')[0]
        if '?' in nickname:
            nickname = nickname.split('?')[0]
        if '?page=' in path:
            page_number_str = path.split('?page=')[1]
            if '?' in page_number_str:
                page_number_str = page_number_str.split('?')[0]
            if '#' in page_number_str:
                page_number_str = page_number_str.split('#')[0]
            if len(page_number_str) > 5:
                page_number_str = "1"
            if page_number_str.isdigit():
                page_number = int(page_number_str)
                if page_number < 1:
                    page_number = 1
                elif page_number > 10:
                    page_number = 10
        curr_session = \
            self._establish_session("showBlogPage",
                                    curr_session, proxy_type)
        if not curr_session:
            self._404()
            self.server.getreq_busy = False
            return True
        msg = html_blog_page(authorized,
                             curr_session,
                             base_dir,
                             http_prefix,
                             translate,
                             nickname,
                             domain, port,
                             MAX_POSTS_IN_BLOGS_FEED, page_number,
                             self.server.peertube_instances,
                             self.server.system_language,
                             self.server.person_cache,
                             debug)
        if msg is not None:
            msg = msg.encode('utf-8')
            msglen = len(msg)
            self._set_headers('text/html', msglen,
                              cookie, calling_domain, False)
            self._write(msg)
            fitness_performance(getreq_start_time,
                                self.server.fitness,
                                '_GET', '_show_blog_page',
                                debug)
            return True
        self._404()
        return True

    def _redirect_to_login_screen(self, calling_domain: str, path: str,
                                  http_prefix: str, domain_full: str,
                                  onion_domain: str, i2p_domain: str,
                                  getreq_start_time,
                                  authorized: bool, debug: bool):
        """Redirects to the login screen if necessary
        """
        divert_to_login_screen = False
        if '/media/' not in path and \
           '/ontologies/' not in path and \
           '/data/' not in path and \
           '/sharefiles/' not in path and \
           '/statuses/' not in path and \
           '/emoji/' not in path and \
           '/tags/' not in path and \
           '/tagmaps/' not in path and \
           '/avatars/' not in path and \
           '/favicons/' not in path and \
           '/headers/' not in path and \
           '/fonts/' not in path and \
           '/icons/' not in path:
            divert_to_login_screen = True
            if path.startswith('/users/'):
                nick_str = path.split('/users/')[1]
                if '/' not in nick_str and '?' not in nick_str:
                    divert_to_login_screen = False
                else:
                    if path.endswith('/following') or \
                       path.endswith('/followers') or \
                       path.endswith('/skills') or \
                       path.endswith('/roles') or \
                       path.endswith('/wanted') or \
                       path.endswith('/shares'):
                        divert_to_login_screen = False

        if divert_to_login_screen and not authorized:
            divert_path = '/login'
            if self.server.news_instance:
                # for news instances if not logged in then show the
                # front page
                divert_path = '/users/news'
            # if debug:
            print('DEBUG: divert_to_login_screen=' +
                  str(divert_to_login_screen))
            print('DEBUG: authorized=' + str(authorized))
            print('DEBUG: path=' + path)
            if calling_domain.endswith('.onion') and onion_domain:
                self._redirect_headers('http://' +
                                       onion_domain + divert_path,
                                       None, calling_domain)
            elif calling_domain.endswith('.i2p') and i2p_domain:
                self._redirect_headers('http://' +
                                       i2p_domain + divert_path,
                                       None, calling_domain)
            else:
                self._redirect_headers(http_prefix + '://' +
                                       domain_full +
                                       divert_path, None, calling_domain)
            fitness_performance(getreq_start_time,
                                self.server.fitness,
                                '_GET', '_redirect_to_login_screen',
                                debug)
            return True
        return False

    def _get_style_sheet(self, base_dir: str, calling_domain: str, path: str,
                         getreq_start_time) -> bool:
        """Returns the content of a css file
        """
        # get the last part of the path
        # eg. /my/path/file.css becomes file.css
        if '/' in path:
            path = path.split('/')[-1]
        path = base_dir + '/' + path
        css = None
        if self.server.css_cache.get(path):
            css = self.server.css_cache[path]
        elif os.path.isfile(path):
            tries = 0
            while tries < 5:
                try:
                    css = get_css(self.server.base_dir, path)
                    if css:
                        self.server.css_cache[path] = css
                        break
                except BaseException as ex:
                    print('EX: _get_style_sheet ' + path + ' ' +
                          str(tries) + ' ' + str(ex))
                    time.sleep(1)
                    tries += 1
        if css:
            msg = css.encode('utf-8')
            msglen = len(msg)
            self._set_headers('text/css', msglen,
                              None, calling_domain, False)
            self._write(msg)
            fitness_performance(getreq_start_time,
                                self.server.fitness,
                                '_GET', '_get_style_sheet',
                                self.server.debug)
            return True
        self._404()
        return True

    def _show_qrcode(self, calling_domain: str, path: str,
                     base_dir: str, domain: str,
                     onion_domain: str, i2p_domain: str,
                     port: int, getreq_start_time) -> bool:
        """Shows a QR code for an account
        """
        nickname = get_nickname_from_actor(path)
        if not nickname:
            self._404()
            return True
        if onion_domain:
            qrcode_domain = onion_domain
            port = 80
        elif i2p_domain:
            qrcode_domain = i2p_domain
            port = 80
        else:
            qrcode_domain = domain
        save_person_qrcode(base_dir, nickname, domain, qrcode_domain, port)
        qr_filename = \
            acct_dir(base_dir, nickname, domain) + '/qrcode.png'
        if os.path.isfile(qr_filename):
            if self._etag_exists(qr_filename):
                # The file has not changed
                self._304()
                return

            tries = 0
            media_binary = None
            while tries < 5:
                try:
                    with open(qr_filename, 'rb') as av_file:
                        media_binary = av_file.read()
                        break
                except OSError as ex:
                    print('EX: _show_qrcode ' + str(tries) + ' ' + str(ex))
                    time.sleep(1)
                    tries += 1
            if media_binary:
                mime_type = media_file_mime_type(qr_filename)
                self._set_headers_etag(qr_filename, mime_type,
                                       media_binary, None,
                                       self.server.domain_full,
                                       False, None)
                self._write(media_binary)
                fitness_performance(getreq_start_time,
                                    self.server.fitness,
                                    '_GET', '_show_qrcode',
                                    self.server.debug)
                return True
        self._404()
        return True

    def _search_screen_banner(self, path: str,
                              base_dir: str, domain: str,
                              getreq_start_time) -> bool:
        """Shows a banner image on the search screen
        """
        nickname = get_nickname_from_actor(path)
        if not nickname:
            self._404()
            return True
        banner_filename = \
            acct_dir(base_dir, nickname, domain) + '/search_banner.png'
        if not os.path.isfile(banner_filename):
            if os.path.isfile(base_dir + '/theme/default/search_banner.png'):
                copyfile(base_dir + '/theme/default/search_banner.png',
                         banner_filename)
        if os.path.isfile(banner_filename):
            if self._etag_exists(banner_filename):
                # The file has not changed
                self._304()
                return True

            tries = 0
            media_binary = None
            while tries < 5:
                try:
                    with open(banner_filename, 'rb') as av_file:
                        media_binary = av_file.read()
                        break
                except OSError as ex:
                    print('EX: _search_screen_banner ' +
                          str(tries) + ' ' + str(ex))
                    time.sleep(1)
                    tries += 1
            if media_binary:
                mime_type = media_file_mime_type(banner_filename)
                self._set_headers_etag(banner_filename, mime_type,
                                       media_binary, None,
                                       self.server.domain_full,
                                       False, None)
                self._write(media_binary)
                fitness_performance(getreq_start_time,
                                    self.server.fitness,
                                    '_GET', '_search_screen_banner',
                                    self.server.debug)
                return True
        self._404()
        return True

    def _column_image(self, side: str, path: str, base_dir: str, domain: str,
                      getreq_start_time) -> bool:
        """Shows an image at the top of the left/right column
        """
        nickname = get_nickname_from_actor(path)
        if not nickname:
            self._404()
            return True
        banner_filename = \
            acct_dir(base_dir, nickname, domain) + '/' + \
            side + '_col_image.png'
        if os.path.isfile(banner_filename):
            if self._etag_exists(banner_filename):
                # The file has not changed
                self._304()
                return True

            tries = 0
            media_binary = None
            while tries < 5:
                try:
                    with open(banner_filename, 'rb') as av_file:
                        media_binary = av_file.read()
                        break
                except OSError as ex:
                    print('EX: _column_image ' + str(tries) + ' ' + str(ex))
                    time.sleep(1)
                    tries += 1
            if media_binary:
                mime_type = media_file_mime_type(banner_filename)
                self._set_headers_etag(banner_filename, mime_type,
                                       media_binary, None,
                                       self.server.domain_full,
                                       False, None)
                self._write(media_binary)
                fitness_performance(getreq_start_time,
                                    self.server.fitness,
                                    '_GET', '_column_image ' + side,
                                    self.server.debug)
                return True
        self._404()
        return True

    def _show_background_image(self, path: str,
                               base_dir: str, getreq_start_time) -> bool:
        """Show a background image
        """
        image_extensions = get_image_extensions()
        for ext in image_extensions:
            for bg_im in ('follow', 'options', 'login', 'welcome'):
                # follow screen background image
                if path.endswith('/' + bg_im + '-background.' + ext):
                    bg_filename = \
                        base_dir + '/accounts/' + \
                        bg_im + '-background.' + ext
                    if os.path.isfile(bg_filename):
                        if self._etag_exists(bg_filename):
                            # The file has not changed
                            self._304()
                            return True

                        tries = 0
                        bg_binary = None
                        while tries < 5:
                            try:
                                with open(bg_filename, 'rb') as av_file:
                                    bg_binary = av_file.read()
                                    break
                            except OSError as ex:
                                print('EX: _show_background_image ' +
                                      str(tries) + ' ' + str(ex))
                                time.sleep(1)
                                tries += 1
                        if bg_binary:
                            if ext == 'jpg':
                                ext = 'jpeg'
                            self._set_headers_etag(bg_filename,
                                                   'image/' + ext,
                                                   bg_binary, None,
                                                   self.server.domain_full,
                                                   False, None)
                            self._write(bg_binary)
                            fitness_performance(getreq_start_time,
                                                self.server.fitness,
                                                '_GET',
                                                '_show_background_image',
                                                self.server.debug)
                            return True
        self._404()
        return True

    def _show_default_profile_background(self, base_dir: str, theme_name: str,
                                         getreq_start_time) -> bool:
        """If a background image is missing after searching for a handle
        then substitute this image
        """
        image_extensions = get_image_extensions()
        for ext in image_extensions:
            bg_filename = \
                base_dir + '/theme/' + theme_name + '/image.' + ext
            if os.path.isfile(bg_filename):
                if self._etag_exists(bg_filename):
                    # The file has not changed
                    self._304()
                    return True

                tries = 0
                bg_binary = None
                while tries < 5:
                    try:
                        with open(bg_filename, 'rb') as av_file:
                            bg_binary = av_file.read()
                            break
                    except OSError as ex:
                        print('EX: _show_default_profile_background ' +
                              str(tries) + ' ' + str(ex))
                        time.sleep(1)
                        tries += 1
                if bg_binary:
                    if ext == 'jpg':
                        ext = 'jpeg'
                    self._set_headers_etag(bg_filename,
                                           'image/' + ext,
                                           bg_binary, None,
                                           self.server.domain_full,
                                           False, None)
                    self._write(bg_binary)
                    fitness_performance(getreq_start_time,
                                        self.server.fitness,
                                        '_GET',
                                        '_show_default_profile_background',
                                        self.server.debug)
                    return True
                break

        self._404()
        return True

    def _show_share_image(self, path: str,
                          base_dir: str, getreq_start_time) -> bool:
        """Show a shared item image
        """
        if not is_image_file(path):
            self._404()
            return True

        media_str = path.split('/sharefiles/')[1]
        media_filename = base_dir + '/sharefiles/' + media_str
        if not os.path.isfile(media_filename):
            self._404()
            return True

        if self._etag_exists(media_filename):
            # The file has not changed
            self._304()
            return True

        media_file_type = get_image_mime_type(media_filename)
        media_binary = None
        try:
            with open(media_filename, 'rb') as av_file:
                media_binary = av_file.read()
        except OSError:
            print('EX: unable to read binary ' + media_filename)
        if media_binary:
            self._set_headers_etag(media_filename,
                                   media_file_type,
                                   media_binary, None,
                                   self.server.domain_full,
                                   False, None)
            self._write(media_binary)
        fitness_performance(getreq_start_time,
                            self.server.fitness,
                            '_GET', '_show_share_image',
                            self.server.debug)
        return True

    def _show_avatar_or_banner(self, referer_domain: str, path: str,
                               base_dir: str, domain: str,
                               getreq_start_time) -> bool:
        """Shows an avatar or banner or profile background image
        """
        if '/users/' not in path:
            if '/system/accounts/avatars/' not in path and \
               '/system/accounts/headers/' not in path and \
               '/accounts/avatars/' not in path and \
               '/accounts/headers/' not in path:
                return False
        if not is_image_file(path):
            return False
        if '/system/accounts/avatars/' in path:
            avatar_str = path.split('/system/accounts/avatars/')[1]
        elif '/accounts/avatars/' in path:
            avatar_str = path.split('/accounts/avatars/')[1]
        elif '/system/accounts/headers/' in path:
            avatar_str = path.split('/system/accounts/headers/')[1]
        elif '/accounts/headers/' in path:
            avatar_str = path.split('/accounts/headers/')[1]
        else:
            avatar_str = path.split('/users/')[1]
        if not ('/' in avatar_str and '.temp.' not in path):
            return False
        avatar_nickname = avatar_str.split('/')[0]
        avatar_file = avatar_str.split('/')[1]
        avatar_file_ext = avatar_file.split('.')[-1]
        # remove any numbers, eg. avatar123.png becomes avatar.png
        if avatar_file.startswith('avatar'):
            avatar_file = 'avatar.' + avatar_file_ext
        elif avatar_file.startswith('banner'):
            avatar_file = 'banner.' + avatar_file_ext
        elif avatar_file.startswith('search_banner'):
            avatar_file = 'search_banner.' + avatar_file_ext
        elif avatar_file.startswith('image'):
            avatar_file = 'image.' + avatar_file_ext
        elif avatar_file.startswith('left_col_image'):
            avatar_file = 'left_col_image.' + avatar_file_ext
        elif avatar_file.startswith('right_col_image'):
            avatar_file = 'right_col_image.' + avatar_file_ext
        avatar_filename = \
            acct_dir(base_dir, avatar_nickname, domain) + '/' + avatar_file
        if not os.path.isfile(avatar_filename):
            original_ext = avatar_file_ext
            original_avatar_file = avatar_file
            alt_ext = get_image_extensions()
            alt_found = False
            for alt in alt_ext:
                if alt == original_ext:
                    continue
                avatar_file = \
                    original_avatar_file.replace('.' + original_ext,
                                                 '.' + alt)
                avatar_filename = \
                    acct_dir(base_dir, avatar_nickname, domain) + \
                    '/' + avatar_file
                if os.path.isfile(avatar_filename):
                    alt_found = True
                    break
            if not alt_found:
                return False
        if self._etag_exists(avatar_filename):
            # The file has not changed
            self._304()
            return True

        avatar_tm = os.path.getmtime(avatar_filename)
        last_modified_time = datetime.datetime.fromtimestamp(avatar_tm)
        last_modified_time_str = \
            last_modified_time.strftime('%a, %d %b %Y %H:%M:%S GMT')

        media_image_type = get_image_mime_type(avatar_file)
        media_binary = None
        try:
            with open(avatar_filename, 'rb') as av_file:
                media_binary = av_file.read()
        except OSError:
            print('EX: unable to read avatar ' + avatar_filename)
        if media_binary:
            self._set_headers_etag(avatar_filename, media_image_type,
                                   media_binary, None,
                                   referer_domain, True,
                                   last_modified_time_str)
            self._write(media_binary)
        fitness_performance(getreq_start_time,
                            self.server.fitness,
                            '_GET', '_show_avatar_or_banner',
                            self.server.debug)
        return True

    def _confirm_delete_event(self, calling_domain: str, path: str,
                              base_dir: str, http_prefix: str, cookie: str,
                              translate: {}, domain_full: str,
                              onion_domain: str, i2p_domain: str,
                              getreq_start_time) -> bool:
        """Confirm whether to delete a calendar event
        """
        post_id = path.split('?eventid=')[1]
        if '?' in post_id:
            post_id = post_id.split('?')[0]
        post_time = path.split('?time=')[1]
        if '?' in post_time:
            post_time = post_time.split('?')[0]
        post_year = path.split('?year=')[1]
        if '?' in post_year:
            post_year = post_year.split('?')[0]
        post_month = path.split('?month=')[1]
        if '?' in post_month:
            post_month = post_month.split('?')[0]
        post_day = path.split('?day=')[1]
        if '?' in post_day:
            post_day = post_day.split('?')[0]
        # show the confirmation screen screen
        msg = html_calendar_delete_confirm(translate,
                                           base_dir, path,
                                           http_prefix,
                                           domain_full,
                                           post_id, post_time,
                                           post_year, post_month, post_day,
                                           calling_domain)
        if not msg:
            actor = \
                http_prefix + '://' + \
                domain_full + \
                path.split('/eventdelete')[0]
            if calling_domain.endswith('.onion') and onion_domain:
                actor = \
                    'http://' + onion_domain + \
                    path.split('/eventdelete')[0]
            elif calling_domain.endswith('.i2p') and i2p_domain:
                actor = \
                    'http://' + i2p_domain + \
                    path.split('/eventdelete')[0]
            self._redirect_headers(actor + '/calendar',
                                   cookie, calling_domain)
            fitness_performance(getreq_start_time,
                                self.server.fitness,
                                '_GET', '_confirm_delete_event',
                                self.server.debug)
            return True
        msg = msg.encode('utf-8')
        msglen = len(msg)
        self._set_headers('text/html', msglen,
                          cookie, calling_domain, False)
        self._write(msg)
        return True

    def _show_new_post(self, edit_post_params: {},
                       calling_domain: str, path: str,
                       media_instance: bool, translate: {},
                       base_dir: str, http_prefix: str,
                       in_reply_to_url: str, reply_to_list: [],
                       reply_is_chat: bool,
                       share_description: str, reply_page_number: int,
                       reply_category: str,
                       domain: str, domain_full: str,
                       getreq_start_time, cookie,
                       no_drop_down: bool, conversation_id: str,
                       curr_session) -> bool:
        """Shows the new post screen
        """
        is_new_post_endpoint = False
        if '/users/' in path and '/new' in path:
            # Various types of new post in the web interface
            new_post_endpoints = get_new_post_endpoints()
            for curr_post_type in new_post_endpoints:
                if path.endswith('/' + curr_post_type):
                    is_new_post_endpoint = True
                    break
        if is_new_post_endpoint:
            nickname = get_nickname_from_actor(path)
            if not nickname:
                self._404()
                return True
            if in_reply_to_url:
                reply_interval_hours = self.server.default_reply_interval_hrs
                if not can_reply_to(base_dir, nickname, domain,
                                    in_reply_to_url, reply_interval_hours):
                    print('Reply outside of time window ' + in_reply_to_url +
                          str(reply_interval_hours) + ' hours')
                    self._403()
                    return True
                if self.server.debug:
                    print('Reply is within time interval: ' +
                          str(reply_interval_hours) + ' hours')

            access_keys = self.server.access_keys
            if self.server.key_shortcuts.get(nickname):
                access_keys = self.server.key_shortcuts[nickname]

            custom_submit_text = get_config_param(base_dir, 'customSubmitText')

            post_json_object = None
            if in_reply_to_url:
                reply_post_filename = \
                    locate_post(base_dir, nickname, domain, in_reply_to_url)
                if reply_post_filename:
                    post_json_object = load_json(reply_post_filename)

            bold_reading = False
            if self.server.bold_reading.get(nickname):
                bold_reading = True

            languages_understood = \
                get_understood_languages(base_dir,
                                         self.server.http_prefix,
                                         nickname,
                                         self.server.domain_full,
                                         self.server.person_cache)

            msg = \
                html_new_post(edit_post_params, media_instance,
                              translate,
                              base_dir,
                              http_prefix,
                              path, in_reply_to_url,
                              reply_to_list,
                              share_description, None,
                              reply_page_number,
                              reply_category,
                              nickname, domain,
                              domain_full,
                              self.server.default_timeline,
                              self.server.newswire,
                              self.server.theme_name,
                              no_drop_down, access_keys,
                              custom_submit_text,
                              conversation_id,
                              self.server.recent_posts_cache,
                              self.server.max_recent_posts,
                              curr_session,
                              self.server.cached_webfingers,
                              self.server.person_cache,
                              self.server.port,
                              post_json_object,
                              self.server.project_version,
                              self.server.yt_replace_domain,
                              self.server.twitter_replacement_domain,
                              self.server.show_published_date_only,
                              self.server.peertube_instances,
                              self.server.allow_local_network_access,
                              self.server.system_language,
                              languages_understood,
                              self.server.max_like_count,
                              self.server.signing_priv_key_pem,
                              self.server.cw_lists,
                              self.server.lists_enabled,
                              self.server.default_timeline,
                              reply_is_chat,
                              bold_reading,
                              self.server.dogwhistles,
                              self.server.min_images_for_accounts)
            if not msg:
                print('Error replying to ' + in_reply_to_url)
                self._404()
                return True
            msg = msg.encode('utf-8')
            msglen = len(msg)
            self._set_headers('text/html', msglen,
                              cookie, calling_domain, False)
            self._write(msg)
            fitness_performance(getreq_start_time,
                                self.server.fitness,
                                '_GET', '_show_new_post',
                                self.server.debug)
            return True
        return False

    def _show_known_crawlers(self, calling_domain: str, path: str,
                             base_dir: str, known_crawlers: {}) -> bool:
        """Show a list of known web crawlers
        """
        if '/users/' not in path:
            return False
        if not path.endswith('/crawlers'):
            return False
        nickname = get_nickname_from_actor(path)
        if not nickname:
            return False
        if not is_moderator(base_dir, nickname):
            return False
        crawlers_list = []
        curr_time = int(time.time())
        recent_crawlers = 60 * 60 * 24 * 30
        for ua_str, item in known_crawlers.items():
            if item['lastseen'] - curr_time < recent_crawlers:
                hits_str = str(item['hits']).zfill(8)
                crawlers_list.append(hits_str + ' ' + ua_str)
        crawlers_list.sort(reverse=True)
        msg = ''
        for line_str in crawlers_list:
            msg += line_str + '\n'
        msg = msg.encode('utf-8')
        msglen = len(msg)
        self._set_headers('text/plain; charset=utf-8', msglen,
                          None, calling_domain, True)
        self._write(msg)
        return True

    def _edit_profile(self, calling_domain: str, path: str,
                      translate: {}, base_dir: str,
                      http_prefix: str, domain: str, port: int,
                      cookie: str) -> bool:
        """Show the edit profile screen
        """
        if '/users/' in path and path.endswith('/editprofile'):
            peertube_instances = self.server.peertube_instances
            nickname = get_nickname_from_actor(path)

            access_keys = self.server.access_keys
            if '/users/' in path:
                if self.server.key_shortcuts.get(nickname):
                    access_keys = self.server.key_shortcuts[nickname]

            default_reply_interval_hrs = self.server.default_reply_interval_hrs
            msg = html_edit_profile(self.server, translate,
                                    base_dir, path, domain, port,
                                    self.server.default_timeline,
                                    self.server.theme_name,
                                    peertube_instances,
                                    self.server.text_mode_banner,
                                    self.server.user_agents_blocked,
                                    self.server.crawlers_allowed,
                                    access_keys,
                                    default_reply_interval_hrs,
                                    self.server.cw_lists,
                                    self.server.lists_enabled,
                                    self.server.system_language,
                                    self.server.min_images_for_accounts)
            if msg:
                msg = msg.encode('utf-8')
                msglen = len(msg)
                self._set_headers('text/html', msglen,
                                  cookie, calling_domain, False)
                self._write(msg)
            else:
                self._404()
            return True
        return False

    def _edit_links(self, calling_domain: str, path: str,
                    translate: {}, base_dir: str,
                    http_prefix: str, domain: str, port: int,
                    cookie: str, theme: str) -> bool:
        """Show the links from the left column
        """
        if '/users/' in path and path.endswith('/editlinks'):
            nickname = path.split('/users/')[1]
            if '/' in nickname:
                nickname = nickname.split('/')[0]

            access_keys = self.server.access_keys
            if self.server.key_shortcuts.get(nickname):
                access_keys = self.server.key_shortcuts[nickname]

            msg = html_edit_links(translate,
                                  base_dir,
                                  path, domain,
                                  port,
                                  http_prefix,
                                  self.server.default_timeline,
                                  theme, access_keys)
            if msg:
                msg = msg.encode('utf-8')
                msglen = len(msg)
                self._set_headers('text/html', msglen,
                                  cookie, calling_domain, False)
                self._write(msg)
            else:
                self._404()
            return True
        return False

    def _edit_newswire(self, calling_domain: str, path: str,
                       translate: {}, base_dir: str,
                       http_prefix: str, domain: str, port: int,
                       cookie: str) -> bool:
        """Show the newswire from the right column
        """
        if '/users/' in path and path.endswith('/editnewswire'):
            nickname = path.split('/users/')[1]
            if '/' in nickname:
                nickname = nickname.split('/')[0]

            access_keys = self.server.access_keys
            if self.server.key_shortcuts.get(nickname):
                access_keys = self.server.key_shortcuts[nickname]

            msg = html_edit_newswire(translate,
                                     base_dir,
                                     path, domain,
                                     port,
                                     http_prefix,
                                     self.server.default_timeline,
                                     self.server.theme_name,
                                     access_keys,
                                     self.server.dogwhistles)
            if msg:
                msg = msg.encode('utf-8')
                msglen = len(msg)
                self._set_headers('text/html', msglen,
                                  cookie, calling_domain, False)
                self._write(msg)
            else:
                self._404()
            return True
        return False

    def _edit_news_post2(self, calling_domain: str, path: str,
                         translate: {}, base_dir: str,
                         http_prefix: str, domain: str, port: int,
                         domain_full: str,
                         cookie: str) -> bool:
        """Show the edit screen for a news post
        """
        if '/users/' in path and '/editnewspost=' in path:
            post_actor = 'news'
            if '?actor=' in path:
                post_actor = path.split('?actor=')[1]
                if '?' in post_actor:
                    post_actor = post_actor.split('?')[0]
            post_id = path.split('/editnewspost=')[1]
            if '?' in post_id:
                post_id = post_id.split('?')[0]
            post_url = \
                local_actor_url(http_prefix, post_actor, domain_full) + \
                '/statuses/' + post_id
            path = path.split('/editnewspost=')[0]
            msg = html_edit_news_post(translate, base_dir,
                                      path, domain, port,
                                      http_prefix,
                                      post_url,
                                      self.server.system_language)
            if msg:
                msg = msg.encode('utf-8')
                msglen = len(msg)
                self._set_headers('text/html', msglen,
                                  cookie, calling_domain, False)
                self._write(msg)
            else:
                self._404()
            return True
        return False

    def _get_following_json(self, base_dir: str, path: str,
                            calling_domain: str, referer_domain: str,
                            http_prefix: str,
                            domain: str, port: int,
                            following_items_per_page: int,
                            debug: bool, list_name: str = 'following') -> None:
        """Returns json collection for following.txt
        """
        following_json = \
            get_following_feed(base_dir, domain, port, path, http_prefix,
                               True, following_items_per_page, list_name)
        if not following_json:
            if debug:
                print(list_name + ' json feed not found for ' + path)
            self._404()
            return
        msg_str = json.dumps(following_json,
                             ensure_ascii=False)
        msg_str = self._convert_domains(calling_domain,
                                        referer_domain,
                                        msg_str)
        msg = msg_str.encode('utf-8')
        msglen = len(msg)
        accept_str = self.headers['Accept']
        protocol_str = \
            get_json_content_from_accept(accept_str)
        self._set_headers(protocol_str, msglen,
                          None, calling_domain, False)
        self._write(msg)

    def _send_block(self, http_prefix: str,
                    blocker_nickname: str, blocker_domain_full: str,
                    blocking_nickname: str, blocking_domain_full: str,
                    curr_session, proxy_type: str) -> bool:
        if blocker_domain_full == blocking_domain_full:
            if blocker_nickname == blocking_nickname:
                # don't block self
                return False
        block_actor = \
            local_actor_url(http_prefix, blocker_nickname, blocker_domain_full)
        to_url = 'https://www.w3.org/ns/activitystreams#Public'
        cc_url = block_actor + '/followers'

        blocked_url = \
            http_prefix + '://' + blocking_domain_full + \
            '/@' + blocking_nickname
        block_json = {
            "@context": "https://www.w3.org/ns/activitystreams",
            'type': 'Block',
            'actor': block_actor,
            'object': blocked_url,
            'to': [to_url],
            'cc': [cc_url]
        }
        self._post_to_outbox(block_json, self.server.project_version,
                             blocker_nickname,
                             curr_session, proxy_type)
        return True

    def _get_referer_domain(self, ua_str: str) -> str:
        """Returns the referer domain
        Which domain is the GET request coming from?
        """
        referer_domain = None
        if self.headers.get('referer'):
            referer_domain = \
                user_agent_domain(self.headers['referer'], self.server.debug)
        elif self.headers.get('Referer'):
            referer_domain = \
                user_agent_domain(self.headers['Referer'], self.server.debug)
        elif self.headers.get('Signature'):
            if 'keyId="' in self.headers['Signature']:
                referer_domain = self.headers['Signature'].split('keyId="')[1]
                if '/' in referer_domain:
                    referer_domain = referer_domain.split('/')[0]
                elif '#' in referer_domain:
                    referer_domain = referer_domain.split('#')[0]
                elif '"' in referer_domain:
                    referer_domain = referer_domain.split('"')[0]
        elif ua_str:
            referer_domain = user_agent_domain(ua_str, self.server.debug)
        return referer_domain

    def _get_user_agent(self) -> str:
        """Returns the user agent string from the headers
        """
        ua_str = None
        if self.headers.get('User-Agent'):
            ua_str = self.headers['User-Agent']
        elif self.headers.get('user-agent'):
            ua_str = self.headers['user-agent']
        elif self.headers.get('User-agent'):
            ua_str = self.headers['User-agent']
        return ua_str

    def _permitted_crawler_path(self, path: str) -> bool:
        """Is the given path permitted to be crawled by a search engine?
        this should only allow through basic information, such as nodeinfo
        """
        if path == '/' or path == '/about' or path == '/login' or \
           path.startswith('/blog/'):
            return True
        return False

    def do_GET(self):
        calling_domain = self.server.domain_full

        if self.headers.get('Host'):
            calling_domain = decoded_host(self.headers['Host'])
            if self.server.onion_domain:
                if calling_domain not in (self.server.domain,
                                          self.server.domain_full,
                                          self.server.onion_domain):
                    print('GET domain blocked: ' + calling_domain)
                    self._400()
                    return
            elif self.server.i2p_domain:
                if calling_domain not in (self.server.domain,
                                          self.server.domain_full,
                                          self.server.i2p_domain):
                    print('GET domain blocked: ' + calling_domain)
                    self._400()
                    return
            else:
                if calling_domain not in (self.server.domain,
                                          self.server.domain_full):
                    print('GET domain blocked: ' + calling_domain)
                    self._400()
                    return

        ua_str = self._get_user_agent()

        if not self._permitted_crawler_path(self.path):
            block, self.server.blocked_cache_last_updated = \
                blocked_user_agent(calling_domain, ua_str,
                                   self.server.news_instance,
                                   self.server.debug,
                                   self.server.user_agents_blocked,
                                   self.server.blocked_cache_last_updated,
                                   self.server.base_dir,
                                   self.server.blocked_cache,
                                   self.server.blocked_cache_update_secs,
                                   self.server.crawlers_allowed,
                                   self.server.known_bots)
            if block:
                self._400()
                return

        referer_domain = self._get_referer_domain(ua_str)

        curr_session, proxy_type = \
            get_session_for_domains(self.server,
                                    calling_domain, referer_domain)

        getreq_start_time = time.time()

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'start', self.server.debug)

        if self._show_vcard(self.server.base_dir,
                            self.path, calling_domain, referer_domain,
                            self.server.domain):
            return

        # getting the public key for an account
        acct_pub_key_json = \
            self._get_account_pub_key(self.path, self.server.person_cache,
                                      self.server.base_dir,
                                      self.server.http_prefix,
                                      self.server.domain,
                                      self.server.onion_domain,
                                      self.server.i2p_domain,
                                      calling_domain)
        if acct_pub_key_json:
            msg_str = json.dumps(acct_pub_key_json, ensure_ascii=False)
            msg = msg_str.encode('utf-8')
            msglen = len(msg)
            accept_str = self.headers['Accept']
            protocol_str = \
                get_json_content_from_accept(accept_str)
            self._set_headers(protocol_str, msglen,
                              None, calling_domain, False)
            self._write(msg)
            return

        # Since fediverse crawlers are quite active,
        # make returning info to them high priority
        # get nodeinfo endpoint
        if self._nodeinfo(ua_str, calling_domain, referer_domain,
                          self.server.http_prefix, 5, self.server.debug):
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_nodeinfo[calling_domain]',
                            self.server.debug)

        if self._security_txt(ua_str, calling_domain, referer_domain,
                              self.server.http_prefix, 5, self.server.debug):
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_security_txt[calling_domain]',
                            self.server.debug)

        if self.path == '/logout':
            if not self.server.news_instance:
                msg = \
                    html_login(self.server.translate,
                               self.server.base_dir,
                               self.server.http_prefix,
                               self.server.domain_full,
                               self.server.system_language,
                               False, ua_str).encode('utf-8')
                msglen = len(msg)
                self._logout_headers('text/html', msglen, calling_domain)
                self._write(msg)
            else:
                if calling_domain.endswith('.onion') and \
                   self.server.onion_domain:
                    self._logout_redirect('http://' +
                                          self.server.onion_domain +
                                          '/users/news', None,
                                          calling_domain)
                elif (calling_domain.endswith('.i2p') and
                      self.server.i2p_domain):
                    self._logout_redirect('http://' +
                                          self.server.i2p_domain +
                                          '/users/news', None,
                                          calling_domain)
                else:
                    self._logout_redirect(self.server.http_prefix +
                                          '://' +
                                          self.server.domain_full +
                                          '/users/news',
                                          None, calling_domain)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', 'logout',
                                self.server.debug)
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'show logout',
                            self.server.debug)

        # replace https://domain/@nick with https://domain/users/nick
        if self.path.startswith('/@'):
            self.path = self.path.replace('/@', '/users/')
            # replace https://domain/@nick/statusnumber
            # with https://domain/users/nick/statuses/statusnumber
            nickname = self.path.split('/users/')[1]
            if '/' in nickname:
                status_number_str = nickname.split('/')[1]
                if status_number_str.isdigit():
                    nickname = nickname.split('/')[0]
                    self.path = \
                        self.path.replace('/users/' + nickname + '/',
                                          '/users/' + nickname + '/statuses/')

        # instance actor
        if self.path in ('/actor', '/users/instance.actor', '/users/actor',
                         '/Actor', '/users/Actor'):
            self.path = '/users/inbox'
            if self._show_instance_actor(calling_domain, referer_domain,
                                         self.path,
                                         self.server.base_dir,
                                         self.server.http_prefix,
                                         self.server.domain,
                                         self.server.domain_full,
                                         self.server.onion_domain,
                                         self.server.i2p_domain,
                                         getreq_start_time,
                                         None, self.server.debug,
                                         self.server.enable_shared_inbox):
                return
            else:
                self._404()
                return

        # turn off dropdowns on new post screen
        no_drop_down = False
        if self.path.endswith('?nodropdown'):
            no_drop_down = True
            self.path = self.path.replace('?nodropdown', '')

        # redirect music to #nowplaying list
        if self.path == '/music' or self.path == '/NowPlaying':
            self.path = '/tags/NowPlaying'

        if self.server.debug:
            print('DEBUG: GET from ' + self.server.base_dir +
                  ' path: ' + self.path + ' busy: ' +
                  str(self.server.getreq_busy))

        if self.server.debug:
            print(str(self.headers))

        cookie = None
        if self.headers.get('Cookie'):
            cookie = self.headers['Cookie']

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'get cookie',
                            self.server.debug)

        if '/manifest.json' in self.path:
            if self._has_accept(calling_domain):
                if not self._request_http():
                    self._progressive_web_app_manifest(self.server.base_dir,
                                                       calling_domain,
                                                       referer_domain,
                                                       getreq_start_time)
                    return
                else:
                    self.path = '/'

        if '/browserconfig.xml' in self.path:
            if self._has_accept(calling_domain):
                self._browser_config(calling_domain, referer_domain,
                                     getreq_start_time)
                return

        # default newswire favicon, for links to sites which
        # have no favicon
        if not self.path.startswith('/favicons/'):
            if 'newswire_favicon.ico' in self.path:
                self._get_favicon(calling_domain, self.server.base_dir,
                                  self.server.debug,
                                  'newswire_favicon.ico')
                return

            # favicon image
            if 'favicon.ico' in self.path:
                self._get_favicon(calling_domain, self.server.base_dir,
                                  self.server.debug,
                                  'favicon.ico')
                return

        # check authorization
        authorized = self._is_authorized()
        if self.server.debug:
            if authorized:
                print('GET Authorization granted ' + self.path)
            else:
                print('GET Not authorized ' + self.path + ' ' +
                      str(self.headers))

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'isAuthorized',
                            self.server.debug)

        if authorized and self.path.endswith('/bots.txt'):
            known_bots_str = ''
            for bot_name in self.server.known_bots:
                known_bots_str += bot_name + '\n'
            msg = known_bots_str.encode('utf-8')
            msglen = len(msg)
            self._set_headers('text/plain; charset=utf-8',
                              msglen, None, calling_domain, True)
            self._write(msg)
            if self.server.debug:
                print('Sent known bots: ' +
                      self.server.path + ' ' + calling_domain)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', 'get_known_bots',
                                self.server.debug)
            return

        # shared items catalog for this instance
        # this is only accessible to instance members or to
        # other instances which present an authorization token
        if self.path.startswith('/catalog') or \
           (self.path.startswith('/users/') and '/catalog' in self.path):
            catalog_authorized = authorized
            if not catalog_authorized:
                if self.server.debug:
                    print('Catalog access is not authorized. ' +
                          'Checking Authorization header')
                # Check the authorization token
                if self.headers.get('Origin') and \
                   self.headers.get('Authorization'):
                    permitted_domains = \
                        self.server.shared_items_federated_domains
                    shared_item_tokens = \
                        self.server.shared_item_federation_tokens
                    if authorize_shared_items(permitted_domains,
                                              self.server.base_dir,
                                              self.headers['Origin'],
                                              calling_domain,
                                              self.headers['Authorization'],
                                              self.server.debug,
                                              shared_item_tokens):
                        catalog_authorized = True
                    elif self.server.debug:
                        print('Authorization token refused for ' +
                              'shared items federation')
                elif self.server.debug:
                    print('No Authorization header is available for ' +
                          'shared items federation')
            # show shared items catalog for federation
            if self._has_accept(calling_domain) and catalog_authorized:
                catalog_type = 'json'
                if self.path.endswith('.csv') or self._request_csv():
                    catalog_type = 'csv'
                elif self.path.endswith('.json') or not self._request_http():
                    catalog_type = 'json'
                if self.server.debug:
                    print('Preparing DFC catalog in format ' + catalog_type)

                if catalog_type == 'json':
                    # catalog as a json
                    if not self.path.startswith('/users/'):
                        if self.server.debug:
                            print('Catalog for the instance')
                        catalog_json = \
                            shares_catalog_endpoint(self.server.base_dir,
                                                    self.server.http_prefix,
                                                    self.server.domain_full,
                                                    self.path, 'shares')
                    else:
                        domain_full = self.server.domain_full
                        http_prefix = self.server.http_prefix
                        nickname = self.path.split('/users/')[1]
                        if '/' in nickname:
                            nickname = nickname.split('/')[0]
                        if self.server.debug:
                            print('Catalog for account: ' + nickname)
                        base_dir = self.server.base_dir
                        catalog_json = \
                            shares_catalog_account_endpoint(base_dir,
                                                            http_prefix,
                                                            nickname,
                                                            self.server.domain,
                                                            domain_full,
                                                            self.path,
                                                            self.server.debug,
                                                            'shares')
                    msg_str = json.dumps(catalog_json,
                                         ensure_ascii=False)
                    msg_str = self._convert_domains(calling_domain,
                                                    referer_domain,
                                                    msg_str)
                    msg = msg_str.encode('utf-8')
                    msglen = len(msg)
                    accept_str = self.headers['Accept']
                    protocol_str = \
                        get_json_content_from_accept(accept_str)
                    self._set_headers(protocol_str, msglen,
                                      None, calling_domain, False)
                    self._write(msg)
                    return
                elif catalog_type == 'csv':
                    # catalog as a CSV file for import into a spreadsheet
                    msg = \
                        shares_catalog_csv_endpoint(self.server.base_dir,
                                                    self.server.http_prefix,
                                                    self.server.domain_full,
                                                    self.path,
                                                    'shares').encode('utf-8')
                    msglen = len(msg)
                    self._set_headers('text/csv',
                                      msglen, None, calling_domain, False)
                    self._write(msg)
                    return
                self._404()
                return
            self._400()
            return

        # wanted items catalog for this instance
        # this is only accessible to instance members or to
        # other instances which present an authorization token
        if self.path.startswith('/wantedItems') or \
           (self.path.startswith('/users/') and '/wantedItems' in self.path):
            catalog_authorized = authorized
            if not catalog_authorized:
                if self.server.debug:
                    print('Wanted catalog access is not authorized. ' +
                          'Checking Authorization header')
                # Check the authorization token
                if self.headers.get('Origin') and \
                   self.headers.get('Authorization'):
                    permitted_domains = \
                        self.server.shared_items_federated_domains
                    shared_item_tokens = \
                        self.server.shared_item_federation_tokens
                    if authorize_shared_items(permitted_domains,
                                              self.server.base_dir,
                                              self.headers['Origin'],
                                              calling_domain,
                                              self.headers['Authorization'],
                                              self.server.debug,
                                              shared_item_tokens):
                        catalog_authorized = True
                    elif self.server.debug:
                        print('Authorization token refused for ' +
                              'wanted items federation')
                elif self.server.debug:
                    print('No Authorization header is available for ' +
                          'wanted items federation')
            # show wanted items catalog for federation
            if self._has_accept(calling_domain) and catalog_authorized:
                catalog_type = 'json'
                if self.path.endswith('.csv') or self._request_csv():
                    catalog_type = 'csv'
                elif self.path.endswith('.json') or not self._request_http():
                    catalog_type = 'json'
                if self.server.debug:
                    print('Preparing DFC wanted catalog in format ' +
                          catalog_type)

                if catalog_type == 'json':
                    # catalog as a json
                    if not self.path.startswith('/users/'):
                        if self.server.debug:
                            print('Wanted catalog for the instance')
                        catalog_json = \
                            shares_catalog_endpoint(self.server.base_dir,
                                                    self.server.http_prefix,
                                                    self.server.domain_full,
                                                    self.path, 'wanted')
                    else:
                        domain_full = self.server.domain_full
                        http_prefix = self.server.http_prefix
                        nickname = self.path.split('/users/')[1]
                        if '/' in nickname:
                            nickname = nickname.split('/')[0]
                        if self.server.debug:
                            print('Wanted catalog for account: ' + nickname)
                        base_dir = self.server.base_dir
                        catalog_json = \
                            shares_catalog_account_endpoint(base_dir,
                                                            http_prefix,
                                                            nickname,
                                                            self.server.domain,
                                                            domain_full,
                                                            self.path,
                                                            self.server.debug,
                                                            'wanted')
                    msg_str = json.dumps(catalog_json,
                                         ensure_ascii=False)
                    msg_str = self._convert_domains(calling_domain,
                                                    referer_domain,
                                                    msg_str)
                    msg = msg_str.encode('utf-8')
                    msglen = len(msg)
                    accept_str = self.headers['Accept']
                    protocol_str = \
                        get_json_content_from_accept(accept_str)
                    self._set_headers(protocol_str, msglen,
                                      None, calling_domain, False)
                    self._write(msg)
                    return
                elif catalog_type == 'csv':
                    # catalog as a CSV file for import into a spreadsheet
                    msg = \
                        shares_catalog_csv_endpoint(self.server.base_dir,
                                                    self.server.http_prefix,
                                                    self.server.domain_full,
                                                    self.path,
                                                    'wanted').encode('utf-8')
                    msglen = len(msg)
                    self._set_headers('text/csv',
                                      msglen, None, calling_domain, False)
                    self._write(msg)
                    return
                self._404()
                return
            self._400()
            return

        # minimal mastodon api
        if self._masto_api(self.path, calling_domain, ua_str,
                           authorized,
                           self.server.http_prefix,
                           self.server.base_dir,
                           self.authorized_nickname,
                           self.server.domain,
                           self.server.domain_full,
                           self.server.onion_domain,
                           self.server.i2p_domain,
                           self.server.translate,
                           self.server.registration,
                           self.server.system_language,
                           self.server.project_version,
                           self.server.custom_emoji,
                           self.server.show_node_info_accounts,
                           referer_domain,
                           self.server.debug,
                           self.server.known_crawlers):
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_masto_api[calling_domain]',
                            self.server.debug)

        curr_session = \
            self._establish_session("GET", curr_session,
                                    proxy_type)
        if not curr_session:
            self._404()
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', 'session fail',
                                self.server.debug)
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'create session',
                            self.server.debug)

        # is this a html/ssml/icalendar request?
        html_getreq = False
        csv_getreq = False
        ssml_getreq = False
        icalendar_getreq = False
        if self._has_accept(calling_domain):
            if self._request_http():
                html_getreq = True
            elif self._request_csv():
                csv_getreq = True
            elif self._request_ssml():
                ssml_getreq = True
            elif self._request_icalendar():
                icalendar_getreq = True
        else:
            if self.headers.get('Connection'):
                # https://developer.mozilla.org/en-US/
                # docs/Web/HTTP/Protocol_upgrade_mechanism
                if self.headers.get('Upgrade'):
                    print('HTTP Connection request: ' +
                          self.headers['Upgrade'])
                else:
                    print('HTTP Connection request: ' +
                          self.headers['Connection'])
                self._200()
            else:
                print('WARN: No Accept header ' + str(self.headers))
                self._400()
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'hasAccept',
                            self.server.debug)

        # cached favicon images
        # Note that this comes before the busy flag to avoid conflicts
        if self.path.startswith('/favicons/'):
            if self.server.domain_full in self.path:
                # favicon for this instance
                self._get_favicon(calling_domain, self.server.base_dir,
                                  self.server.debug,
                                  'favicon.ico')
                return
            self._show_cached_favicon(referer_domain, self.path,
                                      self.server.base_dir,
                                      getreq_start_time)
            return

        # get css
        # Note that this comes before the busy flag to avoid conflicts
        if self.path.endswith('.css'):
            if self._get_style_sheet(self.server.base_dir,
                                     calling_domain, self.path,
                                     getreq_start_time):
                return

        if authorized and '/exports/' in self.path:
            self._get_exported_theme(self.path,
                                     self.server.base_dir,
                                     self.server.domain_full)
            return

        # get fonts
        if '/fonts/' in self.path:
            self._get_fonts(calling_domain, self.path,
                            self.server.base_dir, self.server.debug,
                            getreq_start_time)
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'fonts',
                            self.server.debug)

        if self.path in ('/sharedInbox', '/users/inbox', '/actor/inbox',
                         '/users/' + self.server.domain):
            # if shared inbox is not enabled
            if not self.server.enable_shared_inbox:
                self._503()
                return

            self.path = '/inbox'

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'sharedInbox enabled',
                            self.server.debug)

        if self.path == '/categories.xml':
            self._get_hashtag_categories_feed(calling_domain, self.path,
                                              self.server.base_dir,
                                              proxy_type,
                                              getreq_start_time,
                                              self.server.debug,
                                              curr_session)
            return

        if self.path == '/newswire.xml':
            self._get_newswire_feed(calling_domain, self.path,
                                    proxy_type,
                                    getreq_start_time,
                                    self.server.debug,
                                    curr_session)
            return

        # RSS 2.0
        if self.path.startswith('/blog/') and \
           self.path.endswith('/rss.xml'):
            if not self.path == '/blog/rss.xml':
                self._get_rss2feed(calling_domain, self.path,
                                   self.server.base_dir,
                                   self.server.http_prefix,
                                   self.server.domain,
                                   self.server.port,
                                   proxy_type,
                                   getreq_start_time,
                                   self.server.debug,
                                   curr_session)
            else:
                self._get_rss2site(calling_domain, self.path,
                                   self.server.base_dir,
                                   self.server.http_prefix,
                                   self.server.domain_full,
                                   self.server.port,
                                   proxy_type,
                                   self.server.translate,
                                   getreq_start_time,
                                   self.server.debug,
                                   curr_session)
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'rss2 done',
                            self.server.debug)

        # RSS 3.0
        if self.path.startswith('/blog/') and \
           self.path.endswith('/rss.txt'):
            self._get_rss3feed(calling_domain, self.path,
                               self.server.base_dir,
                               self.server.http_prefix,
                               self.server.domain,
                               self.server.port,
                               proxy_type,
                               getreq_start_time,
                               self.server.debug,
                               self.server.system_language,
                               curr_session)
            return

        users_in_path = False
        if '/users/' in self.path:
            users_in_path = True

        if authorized and not html_getreq and users_in_path:
            if '/following?page=' in self.path:
                self._get_following_json(self.server.base_dir,
                                         self.path,
                                         calling_domain, referer_domain,
                                         self.server.http_prefix,
                                         self.server.domain,
                                         self.server.port,
                                         self.server.followingItemsPerPage,
                                         self.server.debug, 'following')
                return
            if '/followers?page=' in self.path:
                self._get_following_json(self.server.base_dir,
                                         self.path,
                                         calling_domain, referer_domain,
                                         self.server.http_prefix,
                                         self.server.domain,
                                         self.server.port,
                                         self.server.followingItemsPerPage,
                                         self.server.debug, 'followers')
                return
            if '/followrequests?page=' in self.path:
                self._get_following_json(self.server.base_dir,
                                         self.path,
                                         calling_domain, referer_domain,
                                         self.server.http_prefix,
                                         self.server.domain,
                                         self.server.port,
                                         self.server.followingItemsPerPage,
                                         self.server.debug,
                                         'followrequests')
                return

        # authorized endpoint used for TTS of posts
        # arriving in your inbox
        if authorized and users_in_path and \
           self.path.endswith('/speaker'):
            if 'application/ssml' not in self.headers['Accept']:
                # json endpoint
                self._get_speaker(calling_domain, referer_domain,
                                  self.path,
                                  self.server.base_dir,
                                  self.server.domain)
            else:
                xml_str = \
                    get_ssml_box(self.server.base_dir,
                                 self.path, self.server.domain,
                                 self.server.system_language,
                                 self.server.instanceTitle,
                                 'inbox')
                if xml_str:
                    msg = xml_str.encode('utf-8')
                    msglen = len(msg)
                    self._set_headers('application/xrd+xml', msglen,
                                      None, calling_domain, False)
                    self._write(msg)
            return

        # show a podcast episode
        if authorized and users_in_path and html_getreq and \
           '?podepisode=' in self.path:
            nickname = self.path.split('/users/')[1]
            if '/' in nickname:
                nickname = nickname.split('/')[0]
            episode_timestamp = self.path.split('?podepisode=')[1].strip()
            episode_timestamp = episode_timestamp.replace('__', ' ')
            episode_timestamp = episode_timestamp.replace('aa', ':')
            if self.server.newswire.get(episode_timestamp):
                pod_episode = self.server.newswire[episode_timestamp]
                html_str = \
                    html_podcast_episode(self.server.translate,
                                         self.server.base_dir,
                                         nickname,
                                         self.server.domain,
                                         pod_episode,
                                         self.server.theme_name,
                                         self.server.default_timeline,
                                         self.server.text_mode_banner,
                                         self.server.access_keys,
                                         self.server.session,
                                         self.server.session_onion,
                                         self.server.session_i2p,
                                         self.server.http_prefix,
                                         self.server.debug)
                if html_str:
                    msg = html_str.encode('utf-8')
                    msglen = len(msg)
                    self._set_headers('text/html', msglen,
                                      None, calling_domain, False)
                    self._write(msg)
                    return

        # redirect to the welcome screen
        if html_getreq and authorized and users_in_path and \
           '/welcome' not in self.path:
            nickname = self.path.split('/users/')[1]
            if '/' in nickname:
                nickname = nickname.split('/')[0]
            if '?' in nickname:
                nickname = nickname.split('?')[0]
            if nickname == self.authorized_nickname and \
               self.path != '/users/' + nickname:
                if not is_welcome_screen_complete(self.server.base_dir,
                                                  nickname,
                                                  self.server.domain):
                    self._redirect_headers('/users/' + nickname + '/welcome',
                                           cookie, calling_domain)
                    return

        if not html_getreq and \
           users_in_path and self.path.endswith('/pinned'):
            nickname = self.path.split('/users/')[1]
            if '/' in nickname:
                nickname = nickname.split('/')[0]
            pinned_post_json = \
                get_pinned_post_as_json(self.server.base_dir,
                                        self.server.http_prefix,
                                        nickname, self.server.domain,
                                        self.server.domain_full,
                                        self.server.system_language)
            message_json = {}
            if pinned_post_json:
                post_id = remove_id_ending(pinned_post_json['id'])
                message_json = \
                    outbox_message_create_wrap(self.server.http_prefix,
                                               nickname,
                                               self.server.domain,
                                               self.server.port,
                                               pinned_post_json)
                message_json['id'] = post_id + '/activity'
                message_json['object']['id'] = post_id
                message_json['object']['url'] = replace_users_with_at(post_id)
                message_json['object']['atomUri'] = post_id
            msg_str = json.dumps(message_json,
                                 ensure_ascii=False)
            msg_str = self._convert_domains(calling_domain,
                                            referer_domain,
                                            msg_str)
            msg = msg_str.encode('utf-8')
            msglen = len(msg)
            accept_str = self.headers['Accept']
            protocol_str = \
                get_json_content_from_accept(accept_str)
            self._set_headers(protocol_str, msglen,
                              None, calling_domain, False)
            self._write(msg)
            return

        if not html_getreq and \
           users_in_path and self.path.endswith('/collections/featured'):
            nickname = self.path.split('/users/')[1]
            if '/' in nickname:
                nickname = nickname.split('/')[0]
            # return the featured posts collection
            self._get_featured_collection(calling_domain, referer_domain,
                                          self.server.base_dir,
                                          self.server.http_prefix,
                                          nickname, self.server.domain,
                                          self.server.domain_full,
                                          self.server.system_language)
            return

        if not html_getreq and \
           users_in_path and self.path.endswith('/collections/featuredTags'):
            self._get_featured_tags_collection(calling_domain, referer_domain,
                                               self.path,
                                               self.server.http_prefix,
                                               self.server.domain_full)
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', '_get_featured_tags_collection done',
                            self.server.debug)

        # show a performance graph
        if authorized and '/performance?graph=' in self.path:
            graph = self.path.split('?graph=')[1]
            if html_getreq and not graph.endswith('.json'):
                if graph == 'post':
                    graph = '_POST'
                elif graph == 'inbox':
                    graph = 'INBOX'
                elif graph == 'get':
                    graph = '_GET'
                msg = \
                    html_watch_points_graph(self.server.base_dir,
                                            self.server.fitness,
                                            graph, 16).encode('utf-8')
                msglen = len(msg)
                self._set_headers('text/html', msglen,
                                  cookie, calling_domain, False)
                self._write(msg)
                fitness_performance(getreq_start_time, self.server.fitness,
                                    '_GET', 'graph',
                                    self.server.debug)
                return
            graph = graph.replace('.json', '')
            if graph == 'post':
                graph = '_POST'
            elif graph == 'inbox':
                graph = 'INBOX'
            elif graph == 'get':
                graph = '_GET'
            watch_points_json = \
                sorted_watch_points(self.server.fitness, graph)
            msg_str = json.dumps(watch_points_json,
                                 ensure_ascii=False)
            msg_str = self._convert_domains(calling_domain,
                                            referer_domain,
                                            msg_str)
            msg = msg_str.encode('utf-8')
            msglen = len(msg)
            accept_str = self.headers['Accept']
            protocol_str = \
                get_json_content_from_accept(accept_str)
            self._set_headers(protocol_str, msglen,
                              None, calling_domain, False)
            self._write(msg)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', 'graph json',
                                self.server.debug)
            return

        # show the main blog page
        if html_getreq and \
           self.path in ('/blog', '/blog/', '/blogs', '/blogs/'):
            if '/rss.xml' not in self.path:
                curr_session = \
                    self._establish_session("show the main blog page",
                                            curr_session,
                                            proxy_type)
                if not curr_session:
                    self._404()
                    return
                msg = html_blog_view(authorized,
                                     curr_session,
                                     self.server.base_dir,
                                     self.server.http_prefix,
                                     self.server.translate,
                                     self.server.domain,
                                     self.server.port,
                                     MAX_POSTS_IN_BLOGS_FEED,
                                     self.server.peertube_instances,
                                     self.server.system_language,
                                     self.server.person_cache,
                                     self.server.debug)
                if msg is not None:
                    msg = msg.encode('utf-8')
                    msglen = len(msg)
                    self._set_headers('text/html', msglen,
                                      cookie, calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time, self.server.fitness,
                                        '_GET', 'blog view',
                                        self.server.debug)
                    return
                self._404()
                return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'blog view done',
                            self.server.debug)

        # show a particular page of blog entries
        # for a particular account
        if html_getreq and self.path.startswith('/blog/'):
            if '/rss.xml' not in self.path:
                if self._show_blog_page(authorized,
                                        calling_domain, self.path,
                                        self.server.base_dir,
                                        self.server.http_prefix,
                                        self.server.domain,
                                        self.server.port,
                                        getreq_start_time,
                                        proxy_type,
                                        cookie, self.server.translate,
                                        self.server.debug,
                                        curr_session):
                    return

        # list of registered devices for e2ee
        # see https://github.com/tootsuite/mastodon/pull/13820
        if authorized and users_in_path:
            if self.path.endswith('/collections/devices'):
                nickname = self.path.split('/users/')
                if '/' in nickname:
                    nickname = nickname.split('/')[0]
                dev_json = e2e_edevices_collection(self.server.base_dir,
                                                   nickname,
                                                   self.server.domain,
                                                   self.server.domain_full,
                                                   self.server.http_prefix)
                msg_str = json.dumps(dev_json, ensure_ascii=False)
                msg_str = self._convert_domains(calling_domain,
                                                referer_domain,
                                                msg_str)
                msg = msg_str.encode('utf-8')
                msglen = len(msg)
                accept_str = self.headers['Accept']
                protocol_str = \
                    get_json_content_from_accept(accept_str)
                self._set_headers(protocol_str, msglen,
                                  None, calling_domain, False)
                self._write(msg)
                fitness_performance(getreq_start_time, self.server.fitness,
                                    '_GET', 'registered devices',
                                    self.server.debug)
                return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'registered devices done',
                            self.server.debug)

        if html_getreq and users_in_path:
            # show the person options screen with view/follow/block/report
            if '?options=' in self.path:
                self._show_person_options(calling_domain, self.path,
                                          self.server.base_dir,
                                          self.server.http_prefix,
                                          self.server.domain,
                                          self.server.domain_full,
                                          getreq_start_time,
                                          self.server.onion_domain,
                                          self.server.i2p_domain,
                                          cookie, self.server.debug,
                                          authorized,
                                          curr_session)
                return

            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', 'person options done',
                                self.server.debug)
            # show blog post
            blog_filename, nickname = \
                path_contains_blog_link(self.server.base_dir,
                                        self.server.http_prefix,
                                        self.server.domain,
                                        self.server.domain_full,
                                        self.path)
            if blog_filename and nickname:
                post_json_object = load_json(blog_filename)
                if is_blog_post(post_json_object):
                    msg = html_blog_post(curr_session,
                                         authorized,
                                         self.server.base_dir,
                                         self.server.http_prefix,
                                         self.server.translate,
                                         nickname, self.server.domain,
                                         self.server.domain_full,
                                         post_json_object,
                                         self.server.peertube_instances,
                                         self.server.system_language,
                                         self.server.person_cache,
                                         self.server.debug,
                                         self.server.content_license_url)
                    if msg is not None:
                        msg = msg.encode('utf-8')
                        msglen = len(msg)
                        self._set_headers('text/html', msglen,
                                          cookie, calling_domain, False)
                        self._write(msg)
                        fitness_performance(getreq_start_time,
                                            self.server.fitness,
                                            '_GET', 'blog post 2',
                                            self.server.debug)
                        return
                    self._404()
                    return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'blog post 2 done',
                            self.server.debug)

        # after selecting a shared item from the left column then show it
        if html_getreq and \
           '?showshare=' in self.path and '/users/' in self.path:
            item_id = self.path.split('?showshare=')[1]
            if '?' in item_id:
                item_id = item_id.split('?')[0]
            category = ''
            if '?category=' in self.path:
                category = self.path.split('?category=')[1]
            if '?' in category:
                category = category.split('?')[0]
            users_path = self.path.split('?showshare=')[0]
            nickname = users_path.replace('/users/', '')
            item_id = urllib.parse.unquote_plus(item_id.strip())
            msg = \
                html_show_share(self.server.base_dir,
                                self.server.domain, nickname,
                                self.server.http_prefix,
                                self.server.domain_full,
                                item_id, self.server.translate,
                                self.server.shared_items_federated_domains,
                                self.server.default_timeline,
                                self.server.theme_name, 'shares', category)
            if not msg:
                if calling_domain.endswith('.onion') and \
                   self.server.onion_domain:
                    actor = 'http://' + self.server.onion_domain + users_path
                elif (calling_domain.endswith('.i2p') and
                      self.server.i2p_domain):
                    actor = 'http://' + self.server.i2p_domain + users_path
                self._redirect_headers(actor + '/tlshares',
                                       cookie, calling_domain)
                return
            msg = msg.encode('utf-8')
            msglen = len(msg)
            self._set_headers('text/html', msglen,
                              cookie, calling_domain, False)
            self._write(msg)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', 'html_show_share',
                                self.server.debug)
            return

        # after selecting a wanted item from the left column then show it
        if html_getreq and \
           '?showwanted=' in self.path and '/users/' in self.path:
            item_id = self.path.split('?showwanted=')[1]
            if ';' in item_id:
                item_id = item_id.split(';')[0]
            category = self.path.split('?category=')[1]
            if ';' in category:
                category = category.split(';')[0]
            users_path = self.path.split('?showwanted=')[0]
            nickname = users_path.replace('/users/', '')
            item_id = urllib.parse.unquote_plus(item_id.strip())
            msg = \
                html_show_share(self.server.base_dir,
                                self.server.domain, nickname,
                                self.server.http_prefix,
                                self.server.domain_full,
                                item_id, self.server.translate,
                                self.server.shared_items_federated_domains,
                                self.server.default_timeline,
                                self.server.theme_name, 'wanted', category)
            if not msg:
                if calling_domain.endswith('.onion') and \
                   self.server.onion_domain:
                    actor = 'http://' + self.server.onion_domain + users_path
                elif (calling_domain.endswith('.i2p') and
                      self.server.i2p_domain):
                    actor = 'http://' + self.server.i2p_domain + users_path
                self._redirect_headers(actor + '/tlwanted',
                                       cookie, calling_domain)
                return
            msg = msg.encode('utf-8')
            msglen = len(msg)
            self._set_headers('text/html', msglen,
                              cookie, calling_domain, False)
            self._write(msg)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', 'htmlShowWanted',
                                self.server.debug)
            return

        # remove a shared item
        if html_getreq and '?rmshare=' in self.path:
            item_id = self.path.split('?rmshare=')[1]
            item_id = urllib.parse.unquote_plus(item_id.strip())
            users_path = self.path.split('?rmshare=')[0]
            actor = \
                self.server.http_prefix + '://' + \
                self.server.domain_full + users_path
            msg = html_confirm_remove_shared_item(self.server.translate,
                                                  self.server.base_dir,
                                                  actor, item_id,
                                                  calling_domain, 'shares')
            if not msg:
                if calling_domain.endswith('.onion') and \
                   self.server.onion_domain:
                    actor = 'http://' + self.server.onion_domain + users_path
                elif (calling_domain.endswith('.i2p') and
                      self.server.i2p_domain):
                    actor = 'http://' + self.server.i2p_domain + users_path
                self._redirect_headers(actor + '/tlshares',
                                       cookie, calling_domain)
                return
            msg = msg.encode('utf-8')
            msglen = len(msg)
            self._set_headers('text/html', msglen,
                              cookie, calling_domain, False)
            self._write(msg)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', 'remove shared item',
                                self.server.debug)
            return

        # remove a wanted item
        if html_getreq and '?rmwanted=' in self.path:
            item_id = self.path.split('?rmwanted=')[1]
            item_id = urllib.parse.unquote_plus(item_id.strip())
            users_path = self.path.split('?rmwanted=')[0]
            actor = \
                self.server.http_prefix + '://' + \
                self.server.domain_full + users_path
            msg = html_confirm_remove_shared_item(self.server.translate,
                                                  self.server.base_dir,
                                                  actor, item_id,
                                                  calling_domain, 'wanted')
            if not msg:
                if calling_domain.endswith('.onion') and \
                   self.server.onion_domain:
                    actor = 'http://' + self.server.onion_domain + users_path
                elif (calling_domain.endswith('.i2p') and
                      self.server.i2p_domain):
                    actor = 'http://' + self.server.i2p_domain + users_path
                self._redirect_headers(actor + '/tlwanted',
                                       cookie, calling_domain)
                return
            msg = msg.encode('utf-8')
            msglen = len(msg)
            self._set_headers('text/html', msglen,
                              cookie, calling_domain, False)
            self._write(msg)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', 'remove shared item',
                                self.server.debug)
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'remove shared item done',
                            self.server.debug)

        if self.path.startswith('/terms'):
            if calling_domain.endswith('.onion') and \
               self.server.onion_domain:
                msg = html_terms_of_service(self.server.base_dir, 'http',
                                            self.server.onion_domain)
            elif (calling_domain.endswith('.i2p') and
                  self.server.i2p_domain):
                msg = html_terms_of_service(self.server.base_dir, 'http',
                                            self.server.i2p_domain)
            else:
                msg = html_terms_of_service(self.server.base_dir,
                                            self.server.http_prefix,
                                            self.server.domain_full)
            msg = msg.encode('utf-8')
            msglen = len(msg)
            self._login_headers('text/html', msglen, calling_domain)
            self._write(msg)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', 'terms of service shown',
                                self.server.debug)
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'terms of service done',
                            self.server.debug)

        # show a list of who you are following
        if (authorized and users_in_path and
            (self.path.endswith('/followingaccounts') or
             self.path.endswith('/followingaccounts.csv'))):
            nickname = get_nickname_from_actor(self.path)
            if not nickname:
                self._404()
                return
            following_filename = \
                acct_dir(self.server.base_dir,
                         nickname, self.server.domain) + '/following.txt'
            if not os.path.isfile(following_filename):
                self._404()
                return
            if self.path.endswith('/followingaccounts.csv'):
                html_getreq = False
                csv_getreq = True
            if html_getreq:
                msg = html_following_list(self.server.base_dir,
                                          following_filename)
                msglen = len(msg)
                self._login_headers('text/html', msglen, calling_domain)
                self._write(msg.encode('utf-8'))
            elif csv_getreq:
                msg = csv_following_list(following_filename,
                                         self.server.base_dir,
                                         nickname,
                                         self.server.domain)
                msglen = len(msg)
                self._login_headers('text/csv', msglen, calling_domain)
                self._write(msg.encode('utf-8'))
            else:
                self._404()
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', 'following accounts shown',
                                self.server.debug)
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'following accounts done',
                            self.server.debug)

        # show a list of who are your followers
        if authorized and users_in_path and \
           self.path.endswith('/followersaccounts'):
            nickname = get_nickname_from_actor(self.path)
            if not nickname:
                self._404()
                return
            followers_filename = \
                acct_dir(self.server.base_dir,
                         nickname, self.server.domain) + '/followers.txt'
            if not os.path.isfile(followers_filename):
                self._404()
                return
            if html_getreq:
                msg = html_following_list(self.server.base_dir,
                                          followers_filename)
                msglen = len(msg)
                self._login_headers('text/html', msglen, calling_domain)
                self._write(msg.encode('utf-8'))
            elif csv_getreq:
                msg = csv_following_list(followers_filename,
                                         self.server.base_dir,
                                         nickname,
                                         self.server.domain)
                msglen = len(msg)
                self._login_headers('text/csv', msglen, calling_domain)
                self._write(msg.encode('utf-8'))
            else:
                self._404()
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', 'followers accounts shown',
                                self.server.debug)
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'followers accounts done',
                            self.server.debug)

        if self.path.endswith('/about'):
            if calling_domain.endswith('.onion'):
                msg = \
                    html_about(self.server.base_dir, 'http',
                               self.server.onion_domain,
                               None, self.server.translate,
                               self.server.system_language)
            elif calling_domain.endswith('.i2p'):
                msg = \
                    html_about(self.server.base_dir, 'http',
                               self.server.i2p_domain,
                               None, self.server.translate,
                               self.server.system_language)
            else:
                msg = \
                    html_about(self.server.base_dir,
                               self.server.http_prefix,
                               self.server.domain_full,
                               self.server.onion_domain,
                               self.server.translate,
                               self.server.system_language)
            msg = msg.encode('utf-8')
            msglen = len(msg)
            self._login_headers('text/html', msglen, calling_domain)
            self._write(msg)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', 'show about screen',
                                self.server.debug)
            return

        if self.path in ('/specification', '/protocol', '/activitypub'):
            if calling_domain.endswith('.onion'):
                msg = \
                    html_specification(self.server.base_dir, 'http',
                                       self.server.onion_domain,
                                       None, self.server.translate,
                                       self.server.system_language)
            elif calling_domain.endswith('.i2p'):
                msg = \
                    html_specification(self.server.base_dir, 'http',
                                       self.server.i2p_domain,
                                       None, self.server.translate,
                                       self.server.system_language)
            else:
                msg = \
                    html_specification(self.server.base_dir,
                                       self.server.http_prefix,
                                       self.server.domain_full,
                                       self.server.onion_domain,
                                       self.server.translate,
                                       self.server.system_language)
            msg = msg.encode('utf-8')
            msglen = len(msg)
            self._login_headers('text/html', msglen, calling_domain)
            self._write(msg)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', 'show specification screen',
                                self.server.debug)
            return

        if self.path in ('/manual', '/usermanual', '/userguide'):
            if calling_domain.endswith('.onion'):
                msg = \
                    html_manual(self.server.base_dir, 'http',
                                self.server.onion_domain,
                                None, self.server.translate,
                                self.server.system_language)
            elif calling_domain.endswith('.i2p'):
                msg = \
                    html_manual(self.server.base_dir, 'http',
                                self.server.i2p_domain,
                                None, self.server.translate,
                                self.server.system_language)
            else:
                msg = \
                    html_manual(self.server.base_dir,
                                self.server.http_prefix,
                                self.server.domain_full,
                                self.server.onion_domain,
                                self.server.translate,
                                self.server.system_language)
            msg = msg.encode('utf-8')
            msglen = len(msg)
            self._login_headers('text/html', msglen, calling_domain)
            self._write(msg)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', 'show user manual screen',
                                self.server.debug)
            return

        if html_getreq and users_in_path and authorized and \
           self.path.endswith('/accesskeys'):
            nickname = self.path.split('/users/')[1]
            if '/' in nickname:
                nickname = nickname.split('/')[0]

            access_keys = self.server.access_keys
            if self.server.key_shortcuts.get(nickname):
                access_keys = \
                    self.server.key_shortcuts[nickname]

            msg = \
                html_access_keys(self.server.base_dir,
                                 nickname, self.server.domain,
                                 self.server.translate,
                                 access_keys,
                                 self.server.access_keys,
                                 self.server.default_timeline,
                                 self.server.theme_name)
            msg = msg.encode('utf-8')
            msglen = len(msg)
            self._login_headers('text/html', msglen, calling_domain)
            self._write(msg)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', 'show accesskeys screen',
                                self.server.debug)
            return

        if html_getreq and users_in_path and authorized and \
           self.path.endswith('/themedesigner'):
            nickname = self.path.split('/users/')[1]
            if '/' in nickname:
                nickname = nickname.split('/')[0]

            if not is_artist(self.server.base_dir, nickname):
                self._403()
                return

            msg = \
                html_theme_designer(self.server.base_dir,
                                    nickname, self.server.domain,
                                    self.server.translate,
                                    self.server.default_timeline,
                                    self.server.theme_name,
                                    self.server.access_keys)
            msg = msg.encode('utf-8')
            msglen = len(msg)
            self._login_headers('text/html', msglen, calling_domain)
            self._write(msg)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', 'show theme designer screen',
                                self.server.debug)
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'show about screen done',
                            self.server.debug)

        # the initial welcome screen after first logging in
        if html_getreq and authorized and \
           '/users/' in self.path and self.path.endswith('/welcome'):
            nickname = self.path.split('/users/')[1]
            if '/' in nickname:
                nickname = nickname.split('/')[0]
            if not is_welcome_screen_complete(self.server.base_dir,
                                              nickname,
                                              self.server.domain):
                msg = \
                    html_welcome_screen(self.server.base_dir, nickname,
                                        self.server.system_language,
                                        self.server.translate,
                                        self.server.theme_name)
                msg = msg.encode('utf-8')
                msglen = len(msg)
                self._login_headers('text/html', msglen, calling_domain)
                self._write(msg)
                fitness_performance(getreq_start_time, self.server.fitness,
                                    '_GET', 'show welcome screen',
                                    self.server.debug)
                return
            self.path = self.path.replace('/welcome', '')

        # the welcome screen which allows you to set an avatar image
        if html_getreq and authorized and \
           '/users/' in self.path and self.path.endswith('/welcome_profile'):
            nickname = self.path.split('/users/')[1]
            if '/' in nickname:
                nickname = nickname.split('/')[0]
            if not is_welcome_screen_complete(self.server.base_dir,
                                              nickname,
                                              self.server.domain):
                msg = \
                    html_welcome_profile(self.server.base_dir, nickname,
                                         self.server.domain,
                                         self.server.http_prefix,
                                         self.server.domain_full,
                                         self.server.system_language,
                                         self.server.translate,
                                         self.server.theme_name)
                msg = msg.encode('utf-8')
                msglen = len(msg)
                self._login_headers('text/html', msglen, calling_domain)
                self._write(msg)
                fitness_performance(getreq_start_time, self.server.fitness,
                                    '_GET', 'show welcome profile screen',
                                    self.server.debug)
                return
            self.path = self.path.replace('/welcome_profile', '')

        # the final welcome screen
        if html_getreq and authorized and \
           '/users/' in self.path and self.path.endswith('/welcome_final'):
            nickname = self.path.split('/users/')[1]
            if '/' in nickname:
                nickname = nickname.split('/')[0]
            if not is_welcome_screen_complete(self.server.base_dir,
                                              nickname,
                                              self.server.domain):
                msg = \
                    html_welcome_final(self.server.base_dir, nickname,
                                       self.server.system_language,
                                       self.server.translate,
                                       self.server.theme_name)
                msg = msg.encode('utf-8')
                msglen = len(msg)
                self._login_headers('text/html', msglen, calling_domain)
                self._write(msg)
                fitness_performance(getreq_start_time, self.server.fitness,
                                    '_GET', 'show welcome final screen',
                                    self.server.debug)
                return
            self.path = self.path.replace('/welcome_final', '')

        # if not authorized then show the login screen
        if html_getreq and self.path != '/login' and \
           not is_image_file(self.path) and \
           self.path != '/' and \
           self.path != '/users/news/linksmobile' and \
           self.path != '/users/news/newswiremobile':
            if self._redirect_to_login_screen(calling_domain, self.path,
                                              self.server.http_prefix,
                                              self.server.domain_full,
                                              self.server.onion_domain,
                                              self.server.i2p_domain,
                                              getreq_start_time,
                                              authorized, self.server.debug):
                return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'show login screen done',
                            self.server.debug)

        # manifest images used to create a home screen icon
        # when selecting "add to home screen" in browsers
        # which support progressive web apps
        if self.path in ('/logo72.png', '/logo96.png', '/logo128.png',
                         '/logo144.png', '/logo150.png', '/logo192.png',
                         '/logo256.png', '/logo512.png',
                         '/apple-touch-icon.png'):
            media_filename = \
                self.server.base_dir + '/img' + self.path
            if os.path.isfile(media_filename):
                if self._etag_exists(media_filename):
                    # The file has not changed
                    self._304()
                    return

                tries = 0
                media_binary = None
                while tries < 5:
                    try:
                        with open(media_filename, 'rb') as av_file:
                            media_binary = av_file.read()
                            break
                    except OSError as ex:
                        print('EX: manifest logo ' +
                              str(tries) + ' ' + str(ex))
                        time.sleep(1)
                        tries += 1
                if media_binary:
                    mime_type = media_file_mime_type(media_filename)
                    self._set_headers_etag(media_filename, mime_type,
                                           media_binary, cookie,
                                           self.server.domain_full,
                                           False, None)
                    self._write(media_binary)
                    fitness_performance(getreq_start_time, self.server.fitness,
                                        '_GET', 'manifest logo shown',
                                        self.server.debug)
                    return
            self._404()
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'manifest logo done',
                            self.server.debug)

        # manifest images used to show example screenshots
        # for use by app stores
        if self.path == '/screenshot1.jpg' or \
           self.path == '/screenshot2.jpg':
            screen_filename = \
                self.server.base_dir + '/img' + self.path
            if os.path.isfile(screen_filename):
                if self._etag_exists(screen_filename):
                    # The file has not changed
                    self._304()
                    return

                tries = 0
                media_binary = None
                while tries < 5:
                    try:
                        with open(screen_filename, 'rb') as av_file:
                            media_binary = av_file.read()
                            break
                    except OSError as ex:
                        print('EX: manifest screenshot ' +
                              str(tries) + ' ' + str(ex))
                        time.sleep(1)
                        tries += 1
                if media_binary:
                    mime_type = media_file_mime_type(screen_filename)
                    self._set_headers_etag(screen_filename, mime_type,
                                           media_binary, cookie,
                                           self.server.domain_full,
                                           False, None)
                    self._write(media_binary)
                    fitness_performance(getreq_start_time, self.server.fitness,
                                        '_GET', 'show screenshot',
                                        self.server.debug)
                    return
            self._404()
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'show screenshot done',
                            self.server.debug)

        # image on login screen or qrcode
        if (is_image_file(self.path) and
            (self.path.startswith('/login.') or
             self.path.startswith('/qrcode.png'))):
            icon_filename = \
                self.server.base_dir + '/accounts' + self.path
            if os.path.isfile(icon_filename):
                if self._etag_exists(icon_filename):
                    # The file has not changed
                    self._304()
                    return

                tries = 0
                media_binary = None
                while tries < 5:
                    try:
                        with open(icon_filename, 'rb') as av_file:
                            media_binary = av_file.read()
                            break
                    except OSError as ex:
                        print('EX: login screen image ' +
                              str(tries) + ' ' + str(ex))
                        time.sleep(1)
                        tries += 1
                if media_binary:
                    mime_type_str = media_file_mime_type(icon_filename)
                    self._set_headers_etag(icon_filename,
                                           mime_type_str,
                                           media_binary, cookie,
                                           self.server.domain_full,
                                           False, None)
                    self._write(media_binary)
                    fitness_performance(getreq_start_time, self.server.fitness,
                                        '_GET', 'login screen logo',
                                        self.server.debug)
                    return
            self._404()
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'login screen logo done',
                            self.server.debug)

        # QR code for account handle
        if users_in_path and \
           self.path.endswith('/qrcode.png'):
            if self._show_qrcode(calling_domain, self.path,
                                 self.server.base_dir,
                                 self.server.domain,
                                 self.server.onion_domain,
                                 self.server.i2p_domain,
                                 self.server.port,
                                 getreq_start_time):
                return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'account qrcode done',
                            self.server.debug)

        # search screen banner image
        if users_in_path:
            if self.path.endswith('/search_banner.png'):
                if self._search_screen_banner(self.path,
                                              self.server.base_dir,
                                              self.server.domain,
                                              getreq_start_time):
                    return

            if self.path.endswith('/left_col_image.png'):
                if self._column_image('left', self.path,
                                      self.server.base_dir,
                                      self.server.domain,
                                      getreq_start_time):
                    return

            if self.path.endswith('/right_col_image.png'):
                if self._column_image('right', self.path,
                                      self.server.base_dir,
                                      self.server.domain,
                                      getreq_start_time):
                    return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'search screen banner done',
                            self.server.debug)

        if self.path.startswith('/defaultprofilebackground'):
            self._show_default_profile_background(self.server.base_dir,
                                                  self.server.theme_name,
                                                  getreq_start_time)
            return

        # show a background image on the login or person options page
        if '-background.' in self.path:
            if self._show_background_image(self.path,
                                           self.server.base_dir,
                                           getreq_start_time):
                return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'background shown done',
                            self.server.debug)

        # emoji images
        if '/emoji/' in self.path:
            self._show_emoji(self.path, self.server.base_dir,
                             getreq_start_time)
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'show emoji done',
                            self.server.debug)

        # show media
        # Note that this comes before the busy flag to avoid conflicts
        # replace mastoson-style media path
        if '/system/media_attachments/files/' in self.path:
            self.path = self.path.replace('/system/media_attachments/files/',
                                          '/media/')
        if '/media/' in self.path:
            self._show_media(self.path, self.server.base_dir,
                             getreq_start_time)
            return

        if '/ontologies/' in self.path or \
           '/data/' in self.path:
            if not has_users_path(self.path):
                self._get_ontology(calling_domain,
                                   self.path, self.server.base_dir,
                                   getreq_start_time)
                return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'show media done',
                            self.server.debug)

        # show shared item images
        # Note that this comes before the busy flag to avoid conflicts
        if '/sharefiles/' in self.path:
            if self._show_share_image(self.path, self.server.base_dir,
                                      getreq_start_time):
                return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'share image done',
                            self.server.debug)

        # icon images
        # Note that this comes before the busy flag to avoid conflicts
        if self.path.startswith('/icons/'):
            self._show_icon(self.path, self.server.base_dir,
                            getreq_start_time)
            return

        # show images within https://instancedomain/activitypub
        if self.path.startswith('/activitypub-tutorial-'):
            if self.path.endswith('.png'):
                self._show_specification_image(self.path,
                                               self.server.base_dir,
                                               getreq_start_time)
                return

        # show images within https://instancedomain/manual
        if self.path.startswith('/manual-'):
            if is_image_file(self.path):
                self._show_manual_image(self.path,
                                        self.server.base_dir,
                                        getreq_start_time)
                return

        # help screen images
        # Note that this comes before the busy flag to avoid conflicts
        if self.path.startswith('/helpimages/'):
            self._show_help_screen_image(self.path,
                                         self.server.base_dir,
                                         getreq_start_time)
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'help screen image done',
                            self.server.debug)

        # cached avatar images
        # Note that this comes before the busy flag to avoid conflicts
        if self.path.startswith('/avatars/'):
            self._show_cached_avatar(referer_domain, self.path,
                                     self.server.base_dir,
                                     getreq_start_time)
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'cached avatar done',
                            self.server.debug)

        # show avatar or background image
        # Note that this comes before the busy flag to avoid conflicts
        if self._show_avatar_or_banner(referer_domain, self.path,
                                       self.server.base_dir,
                                       self.server.domain,
                                       getreq_start_time):
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'avatar or banner shown done',
                            self.server.debug)

        # This busy state helps to avoid flooding
        # Resources which are expected to be called from a web page
        # should be above this
        curr_time_getreq = int(time.time() * 1000)
        if self.server.getreq_busy:
            if curr_time_getreq - self.server.last_getreq < 500:
                if self.server.debug:
                    print('DEBUG: GET Busy')
                self.send_response(429)
                self.end_headers()
                return
        self.server.getreq_busy = True
        self.server.last_getreq = curr_time_getreq

        # returns after this point should set getreq_busy to False

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'GET busy time',
                            self.server.debug)

        if not permitted_dir(self.path):
            if self.server.debug:
                print('DEBUG: GET Not permitted')
            self._404()
            self.server.getreq_busy = False
            return

        # get webfinger endpoint for a person
        if self._webfinger(calling_domain, referer_domain):
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', 'webfinger called',
                                self.server.debug)
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'permitted directory',
                            self.server.debug)

        # show the login screen
        if (self.path.startswith('/login') or
            (self.path == '/' and
             not authorized and
             not self.server.news_instance)):
            # request basic auth
            msg = html_login(self.server.translate,
                             self.server.base_dir,
                             self.server.http_prefix,
                             self.server.domain_full,
                             self.server.system_language,
                             True, ua_str).encode('utf-8')
            msglen = len(msg)
            self._login_headers('text/html', msglen, calling_domain)
            self._write(msg)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', 'login shown',
                                self.server.debug)
            self.server.getreq_busy = False
            return

        # show the news front page
        if self.path == '/' and \
           not authorized and \
           self.server.news_instance:
            if calling_domain.endswith('.onion') and \
               self.server.onion_domain:
                self._logout_redirect('http://' +
                                      self.server.onion_domain +
                                      '/users/news', None,
                                      calling_domain)
            elif (calling_domain.endswith('.i2p') and
                  self.server.i2p_domain):
                self._logout_redirect('http://' +
                                      self.server.i2p_domain +
                                      '/users/news', None,
                                      calling_domain)
            else:
                self._logout_redirect(self.server.http_prefix +
                                      '://' +
                                      self.server.domain_full +
                                      '/users/news',
                                      None, calling_domain)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', 'news front page shown',
                                self.server.debug)
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'login shown done',
                            self.server.debug)

        # the newswire screen on mobile
        if html_getreq and self.path.startswith('/users/') and \
           self.path.endswith('/newswiremobile'):
            if (authorized or
                (not authorized and
                 self.path.startswith('/users/news/') and
                 self.server.news_instance)):
                nickname = get_nickname_from_actor(self.path)
                if not nickname:
                    self._404()
                    self.server.getreq_busy = False
                    return
                timeline_path = \
                    '/users/' + nickname + '/' + self.server.default_timeline
                show_publish_as_icon = self.server.show_publish_as_icon
                rss_icon_at_top = self.server.rss_icon_at_top
                icons_as_buttons = self.server.icons_as_buttons
                default_timeline = self.server.default_timeline
                access_keys = self.server.access_keys
                if self.server.key_shortcuts.get(nickname):
                    access_keys = self.server.key_shortcuts[nickname]
                msg = \
                    html_newswire_mobile(self.server.base_dir,
                                         nickname,
                                         self.server.domain,
                                         self.server.domain_full,
                                         self.server.http_prefix,
                                         self.server.translate,
                                         self.server.newswire,
                                         self.server.positive_voting,
                                         timeline_path,
                                         show_publish_as_icon,
                                         authorized,
                                         rss_icon_at_top,
                                         icons_as_buttons,
                                         default_timeline,
                                         self.server.theme_name,
                                         access_keys).encode('utf-8')
                msglen = len(msg)
                self._set_headers('text/html', msglen,
                                  cookie, calling_domain, False)
                self._write(msg)
                self.server.getreq_busy = False
                return

        if html_getreq and self.path.startswith('/users/') and \
           self.path.endswith('/linksmobile'):
            if (authorized or
                (not authorized and
                 self.path.startswith('/users/news/') and
                 self.server.news_instance)):
                nickname = get_nickname_from_actor(self.path)
                if not nickname:
                    self._404()
                    self.server.getreq_busy = False
                    return
                access_keys = self.server.access_keys
                if self.server.key_shortcuts.get(nickname):
                    access_keys = self.server.key_shortcuts[nickname]
                timeline_path = \
                    '/users/' + nickname + '/' + self.server.default_timeline
                icons_as_buttons = self.server.icons_as_buttons
                default_timeline = self.server.default_timeline
                shared_items_domains = \
                    self.server.shared_items_federated_domains
                msg = \
                    html_links_mobile(self.server.base_dir, nickname,
                                      self.server.domain_full,
                                      self.server.http_prefix,
                                      self.server.translate,
                                      timeline_path,
                                      authorized,
                                      self.server.rss_icon_at_top,
                                      icons_as_buttons,
                                      default_timeline,
                                      self.server.theme_name,
                                      access_keys,
                                      shared_items_domains).encode('utf-8')
                msglen = len(msg)
                self._set_headers('text/html', msglen, cookie, calling_domain,
                                  False)
                self._write(msg)
                self.server.getreq_busy = False
                return

        # hashtag search
        if self.path.startswith('/tags/') or \
           (authorized and '/tags/' in self.path):
            if self.path.startswith('/tags/rss2/'):
                self._hashtag_search_rss2(calling_domain,
                                          self.path, cookie,
                                          self.server.base_dir,
                                          self.server.http_prefix,
                                          self.server.domain,
                                          self.server.domain_full,
                                          self.server.port,
                                          self.server.onion_domain,
                                          self.server.i2p_domain,
                                          getreq_start_time,
                                          curr_session)
                self.server.getreq_busy = False
                return
            self._hashtag_search(calling_domain,
                                 self.path, cookie,
                                 self.server.base_dir,
                                 self.server.http_prefix,
                                 self.server.domain,
                                 self.server.domain_full,
                                 self.server.port,
                                 self.server.onion_domain,
                                 self.server.i2p_domain,
                                 getreq_start_time,
                                 curr_session)
            self.server.getreq_busy = False
            return

        # hashtag map kml
        if self.path.startswith('/tagmaps/') or \
           (authorized and '/tagmaps/' in self.path):
            map_str = \
                map_format_from_tagmaps_path(self.server.base_dir, self.path,
                                             self.server.map_format,
                                             self.server.domain)
            if map_str:
                msg = map_str.encode('utf-8')
                msglen = len(msg)
                if self.server.map_format == 'gpx':
                    header_type = \
                        'application/gpx+xml; charset=utf-8'
                else:
                    header_type = \
                        'application/vnd.google-earth.kml+xml; charset=utf-8'
                self._set_headers(header_type, msglen,
                                  None, calling_domain, True)
                self._write(msg)
                self.server.getreq_busy = False
                return
            self._404()
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'hashtag search done',
                            self.server.debug)

        # show or hide buttons in the web interface
        if html_getreq and users_in_path and \
           self.path.endswith('/minimal') and \
           authorized:
            nickname = self.path.split('/users/')[1]
            if '/' in nickname:
                nickname = nickname.split('/')[0]
                not_min = not is_minimal(self.server.base_dir,
                                         self.server.domain, nickname)
                set_minimal(self.server.base_dir,
                            self.server.domain, nickname, not_min)
                if not (self.server.media_instance or
                        self.server.blogs_instance):
                    self.path = '/users/' + nickname + '/inbox'
                else:
                    if self.server.blogs_instance:
                        self.path = '/users/' + nickname + '/tlblogs'
                    elif self.server.media_instance:
                        self.path = '/users/' + nickname + '/tlmedia'
                    else:
                        self.path = '/users/' + nickname + '/tlfeatures'

        # search for a fediverse address, shared item or emoji
        # from the web interface by selecting search icon
        if html_getreq and users_in_path:
            if self.path.endswith('/search') or \
               '/search?' in self.path:
                if '?' in self.path:
                    self.path = self.path.split('?')[0]

                nickname = self.path.split('/users/')[1]
                if '/' in nickname:
                    nickname = nickname.split('/')[0]

                access_keys = self.server.access_keys
                if self.server.key_shortcuts.get(nickname):
                    access_keys = self.server.key_shortcuts[nickname]

                # show the search screen
                msg = html_search(self.server.translate,
                                  self.server.base_dir, self.path,
                                  self.server.domain,
                                  self.server.default_timeline,
                                  self.server.theme_name,
                                  self.server.text_mode_banner,
                                  access_keys)
                if msg:
                    msg = msg.encode('utf-8')
                    msglen = len(msg)
                    self._set_headers('text/html', msglen, cookie,
                                      calling_domain, False)
                    self._write(msg)
                    fitness_performance(getreq_start_time, self.server.fitness,
                                        '_GET', 'search screen shown',
                                        self.server.debug)
                self.server.getreq_busy = False
                return

        # show a hashtag category from the search screen
        if html_getreq and '/category/' in self.path:
            msg = html_search_hashtag_category(self.server.translate,
                                               self.server.base_dir, self.path,
                                               self.server.domain,
                                               self.server.theme_name)
            if msg:
                msg = msg.encode('utf-8')
                msglen = len(msg)
                self._set_headers('text/html', msglen, cookie, calling_domain,
                                  False)
                self._write(msg)
            fitness_performance(getreq_start_time, self.server.fitness,
                                '_GET', 'hashtag category screen shown',
                                self.server.debug)
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'search screen shown done',
                            self.server.debug)

        # Show the html calendar for a user
        if html_getreq and users_in_path:
            if '/calendar' in self.path:
                nickname = self.path.split('/users/')[1]
                if '/' in nickname:
                    nickname = nickname.split('/')[0]

                access_keys = self.server.access_keys
                if self.server.key_shortcuts.get(nickname):
                    access_keys = self.server.key_shortcuts[nickname]

                # show the calendar screen
                msg = html_calendar(self.server.person_cache,
                                    self.server.translate,
                                    self.server.base_dir, self.path,
                                    self.server.http_prefix,
                                    self.server.domain_full,
                                    self.server.text_mode_banner,
                                    access_keys,
                                    False, self.server.system_language,
                                    self.server.default_timeline,
                                    self.server.theme_name)
                if msg:
                    msg = msg.encode('utf-8')
                    msglen = len(msg)
                    if 'ical=true' in self.path:
                        self._set_headers('text/calendar',
                                          msglen, cookie, calling_domain,
                                          False)
                    else:
                        self._set_headers('text/html',
                                          msglen, cookie, calling_domain,
                                          False)
                    self._write(msg)
                    fitness_performance(getreq_start_time, self.server.fitness,
                                        '_GET', 'calendar shown',
                                        self.server.debug)
                else:
                    self._404()
                self.server.getreq_busy = False
                return

        # Show the icalendar for a user
        if icalendar_getreq and users_in_path:
            if '/calendar' in self.path:
                nickname = self.path.split('/users/')[1]
                if '/' in nickname:
                    nickname = nickname.split('/')[0]

                access_keys = self.server.access_keys
                if self.server.key_shortcuts.get(nickname):
                    access_keys = self.server.key_shortcuts[nickname]

                # show the calendar screen
                msg = html_calendar(self.server.person_cache,
                                    self.server.translate,
                                    self.server.base_dir, self.path,
                                    self.server.http_prefix,
                                    self.server.domain_full,
                                    self.server.text_mode_banner,
                                    access_keys,
                                    True,
                                    self.server.system_language,
                                    self.server.default_timeline,
                                    self.server.theme_name)
                if msg:
                    msg = msg.encode('utf-8')
                    msglen = len(msg)
                    self._set_headers('text/calendar',
                                      msglen, cookie, calling_domain,
                                      False)
                    self._write(msg)
                else:
                    self._404()
                fitness_performance(getreq_start_time, self.server.fitness,
                                    '_GET', 'icalendar shown',
                                    self.server.debug)
                self.server.getreq_busy = False
                return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'calendar shown done',
                            self.server.debug)

        # Show confirmation for deleting a calendar event
        if html_getreq and users_in_path:
            if '/eventdelete' in self.path and \
               '?time=' in self.path and \
               '?eventid=' in self.path:
                if self._confirm_delete_event(calling_domain, self.path,
                                              self.server.base_dir,
                                              self.server.http_prefix,
                                              cookie,
                                              self.server.translate,
                                              self.server.domain_full,
                                              self.server.onion_domain,
                                              self.server.i2p_domain,
                                              getreq_start_time):
                    self.server.getreq_busy = False
                    return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'calendar delete shown done',
                            self.server.debug)

        # search for emoji by name
        if html_getreq and users_in_path:
            if self.path.endswith('/searchemoji'):
                # show the search screen
                msg = \
                    html_search_emoji_text_entry(self.server.translate,
                                                 self.server.base_dir,
                                                 self.path).encode('utf-8')
                msglen = len(msg)
                self._set_headers('text/html', msglen,
                                  cookie, calling_domain, False)
                self._write(msg)
                fitness_performance(getreq_start_time, self.server.fitness,
                                    '_GET', 'emoji search shown',
                                    self.server.debug)
                self.server.getreq_busy = False
                return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'emoji search shown done',
                            self.server.debug)

        repeat_private = False
        if html_getreq and '?repeatprivate=' in self.path:
            repeat_private = True
            self.path = self.path.replace('?repeatprivate=', '?repeat=')
        # announce/repeat button was pressed
        if authorized and html_getreq and '?repeat=' in self.path:
            self._announce_button(calling_domain, self.path,
                                  self.server.base_dir,
                                  cookie, proxy_type,
                                  self.server.http_prefix,
                                  self.server.domain,
                                  self.server.domain_full,
                                  self.server.port,
                                  self.server.onion_domain,
                                  self.server.i2p_domain,
                                  getreq_start_time,
                                  repeat_private,
                                  self.server.debug,
                                  curr_session)
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'show announce done',
                            self.server.debug)

        if authorized and html_getreq and '?unrepeatprivate=' in self.path:
            self.path = self.path.replace('?unrepeatprivate=', '?unrepeat=')

        # undo an announce/repeat from the web interface
        if authorized and html_getreq and '?unrepeat=' in self.path:
            self._announce_button_undo(calling_domain, self.path,
                                       self.server.base_dir,
                                       cookie, proxy_type,
                                       self.server.http_prefix,
                                       self.server.domain,
                                       self.server.domain_full,
                                       self.server.onion_domain,
                                       self.server.i2p_domain,
                                       getreq_start_time,
                                       self.server.debug,
                                       self.server.recent_posts_cache,
                                       curr_session)
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'unannounce done',
                            self.server.debug)

        # send a newswire moderation vote from the web interface
        if authorized and '/newswirevote=' in self.path and \
           self.path.startswith('/users/'):
            self._newswire_vote(calling_domain, self.path,
                                cookie,
                                self.server.base_dir,
                                self.server.http_prefix,
                                self.server.domain_full,
                                self.server.onion_domain,
                                self.server.i2p_domain,
                                getreq_start_time,
                                self.server.newswire)
            self.server.getreq_busy = False
            return

        # send a newswire moderation unvote from the web interface
        if authorized and '/newswireunvote=' in self.path and \
           self.path.startswith('/users/'):
            self._newswire_unvote(calling_domain, self.path,
                                  cookie,
                                  self.server.base_dir,
                                  self.server.http_prefix,
                                  self.server.domain_full,
                                  self.server.onion_domain,
                                  self.server.i2p_domain,
                                  getreq_start_time,
                                  self.server.debug,
                                  self.server.newswire)
            self.server.getreq_busy = False
            return

        # send a follow request approval from the web interface
        if authorized and '/followapprove=' in self.path and \
           self.path.startswith('/users/'):
            self._follow_approve_button(calling_domain, self.path,
                                        cookie,
                                        self.server.base_dir,
                                        self.server.http_prefix,
                                        self.server.domain,
                                        self.server.domain_full,
                                        self.server.port,
                                        self.server.onion_domain,
                                        self.server.i2p_domain,
                                        getreq_start_time,
                                        proxy_type,
                                        self.server.debug,
                                        curr_session)
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'follow approve done',
                            self.server.debug)

        # deny a follow request from the web interface
        if authorized and '/followdeny=' in self.path and \
           self.path.startswith('/users/'):
            self._follow_deny_button(calling_domain, self.path,
                                     cookie,
                                     self.server.base_dir,
                                     self.server.http_prefix,
                                     self.server.domain,
                                     self.server.domain_full,
                                     self.server.port,
                                     self.server.onion_domain,
                                     self.server.i2p_domain,
                                     getreq_start_time,
                                     self.server.debug)
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'follow deny done',
                            self.server.debug)

        # like from the web interface icon
        if authorized and html_getreq and '?like=' in self.path:
            self._like_button(calling_domain, self.path,
                              self.server.base_dir,
                              self.server.http_prefix,
                              self.server.domain,
                              self.server.domain_full,
                              self.server.onion_domain,
                              self.server.i2p_domain,
                              getreq_start_time,
                              proxy_type,
                              cookie,
                              self.server.debug,
                              curr_session)
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'like button done',
                            self.server.debug)

        # undo a like from the web interface icon
        if authorized and html_getreq and '?unlike=' in self.path:
            self._undo_like_button(calling_domain, self.path,
                                   self.server.base_dir,
                                   self.server.http_prefix,
                                   self.server.domain,
                                   self.server.domain_full,
                                   self.server.onion_domain,
                                   self.server.i2p_domain,
                                   getreq_start_time,
                                   proxy_type,
                                   cookie, self.server.debug,
                                   curr_session)
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'unlike button done',
                            self.server.debug)

        # emoji reaction from the web interface icon
        if authorized and html_getreq and \
           '?react=' in self.path and \
           '?actor=' in self.path:
            self._reaction_button(calling_domain, self.path,
                                  self.server.base_dir,
                                  self.server.http_prefix,
                                  self.server.domain,
                                  self.server.domain_full,
                                  self.server.onion_domain,
                                  self.server.i2p_domain,
                                  getreq_start_time,
                                  proxy_type,
                                  cookie,
                                  self.server.debug,
                                  curr_session)
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'emoji reaction button done',
                            self.server.debug)

        # undo an emoji reaction from the web interface icon
        if authorized and html_getreq and \
           '?unreact=' in self.path and \
           '?actor=' in self.path:
            self._undo_reaction_button(calling_domain, self.path,
                                       self.server.base_dir,
                                       self.server.http_prefix,
                                       self.server.domain,
                                       self.server.domain_full,
                                       self.server.onion_domain,
                                       self.server.i2p_domain,
                                       getreq_start_time,
                                       proxy_type,
                                       cookie, self.server.debug,
                                       curr_session)
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'unreaction button done',
                            self.server.debug)

        # bookmark from the web interface icon
        if authorized and html_getreq and '?bookmark=' in self.path:
            self._bookmark_button(calling_domain, self.path,
                                  self.server.base_dir,
                                  self.server.http_prefix,
                                  self.server.domain,
                                  self.server.domain_full,
                                  self.server.port,
                                  self.server.onion_domain,
                                  self.server.i2p_domain,
                                  getreq_start_time,
                                  proxy_type,
                                  cookie, self.server.debug,
                                  curr_session)
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'bookmark shown done',
                            self.server.debug)

        # emoji recation from the web interface bottom icon
        if authorized and html_getreq and '?selreact=' in self.path:
            self._reaction_picker(calling_domain, self.path,
                                  self.server.base_dir,
                                  self.server.http_prefix,
                                  self.server.domain,
                                  self.server.port,
                                  getreq_start_time,
                                  cookie, self.server.debug,
                                  curr_session)
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'bookmark shown done',
                            self.server.debug)

        # undo a bookmark from the web interface icon
        if authorized and html_getreq and '?unbookmark=' in self.path:
            self._undo_bookmark_button(calling_domain, self.path,
                                       self.server.base_dir,
                                       self.server.http_prefix,
                                       self.server.domain,
                                       self.server.domain_full,
                                       self.server.port,
                                       self.server.onion_domain,
                                       self.server.i2p_domain,
                                       getreq_start_time,
                                       proxy_type, cookie,
                                       self.server.debug,
                                       curr_session)
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'unbookmark shown done',
                            self.server.debug)

        # delete button is pressed on a post
        if authorized and html_getreq and '?delete=' in self.path:
            self._delete_button(calling_domain, self.path,
                                self.server.base_dir,
                                self.server.http_prefix,
                                self.server.domain_full,
                                self.server.onion_domain,
                                self.server.i2p_domain,
                                getreq_start_time,
                                proxy_type, cookie,
                                self.server.debug,
                                curr_session)
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'delete shown done',
                            self.server.debug)

        # The mute button is pressed
        if authorized and html_getreq and '?mute=' in self.path:
            self._mute_button(calling_domain, self.path,
                              self.server.base_dir,
                              self.server.http_prefix,
                              self.server.domain,
                              self.server.domain_full,
                              self.server.port,
                              self.server.onion_domain,
                              self.server.i2p_domain,
                              getreq_start_time,
                              cookie, self.server.debug,
                              curr_session)
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'post muted done',
                            self.server.debug)

        # unmute a post from the web interface icon
        if authorized and html_getreq and '?unmute=' in self.path:
            self._undo_mute_button(calling_domain, self.path,
                                   self.server.base_dir,
                                   self.server.http_prefix,
                                   self.server.domain,
                                   self.server.domain_full,
                                   self.server.port,
                                   self.server.onion_domain,
                                   self.server.i2p_domain,
                                   getreq_start_time,
                                   cookie, self.server.debug,
                                   curr_session)
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'unmute activated done',
                            self.server.debug)

        # reply from the web interface icon
        in_reply_to_url = None
#        replyWithDM = False
        reply_to_list = []
        reply_page_number = 1
        reply_category = ''
        share_description = None
        conversation_id = None
#        replytoActor = None
        if html_getreq:
            if '?conversationId=' in self.path:
                conversation_id = self.path.split('?conversationId=')[1]
                if '?' in conversation_id:
                    conversation_id = conversation_id.split('?')[0]
            # public reply
            if '?replyto=' in self.path:
                in_reply_to_url = self.path.split('?replyto=')[1]
                if '?' in in_reply_to_url:
                    mentions_list = in_reply_to_url.split('?')
                    for ment in mentions_list:
                        if ment.startswith('mention='):
                            reply_handle = ment.replace('mention=', '')
                            if reply_handle not in reply_to_list:
                                reply_to_list.append(reply_handle)
                        if ment.startswith('page='):
                            reply_page_str = ment.replace('page=', '')
                            if len(reply_page_str) > 5:
                                reply_page_str = "1"
                            if reply_page_str.isdigit():
                                reply_page_number = int(reply_page_str)
#                        if m.startswith('actor='):
#                            replytoActor = m.replace('actor=', '')
                    in_reply_to_url = mentions_list[0]
                self.path = self.path.split('?replyto=')[0] + '/newpost'
                if self.server.debug:
                    print('DEBUG: replyto path ' + self.path)

            # unlisted reply
            if '?replyunlisted=' in self.path:
                in_reply_to_url = self.path.split('?replyunlisted=')[1]
                if '?' in in_reply_to_url:
                    mentions_list = in_reply_to_url.split('?')
                    for ment in mentions_list:
                        if ment.startswith('mention='):
                            reply_handle = ment.replace('mention=', '')
                            if reply_handle not in reply_to_list:
                                reply_to_list.append(reply_handle)
                        if ment.startswith('page='):
                            reply_page_str = ment.replace('page=', '')
                            if len(reply_page_str) > 5:
                                reply_page_str = "1"
                            if reply_page_str.isdigit():
                                reply_page_number = int(reply_page_str)
                    in_reply_to_url = mentions_list[0]
                self.path = \
                    self.path.split('?replyunlisted=')[0] + '/newunlisted'
                if self.server.debug:
                    print('DEBUG: replyunlisted path ' + self.path)

            # reply to followers
            if '?replyfollowers=' in self.path:
                in_reply_to_url = self.path.split('?replyfollowers=')[1]
                if '?' in in_reply_to_url:
                    mentions_list = in_reply_to_url.split('?')
                    for ment in mentions_list:
                        if ment.startswith('mention='):
                            reply_handle = ment.replace('mention=', '')
                            ment2 = ment.replace('mention=', '')
                            if ment2 not in reply_to_list:
                                reply_to_list.append(reply_handle)
                        if ment.startswith('page='):
                            reply_page_str = ment.replace('page=', '')
                            if len(reply_page_str) > 5:
                                reply_page_str = "1"
                            if reply_page_str.isdigit():
                                reply_page_number = int(reply_page_str)
#                        if m.startswith('actor='):
#                            replytoActor = m.replace('actor=', '')
                    in_reply_to_url = mentions_list[0]
                self.path = self.path.split('?replyfollowers=')[0] + \
                    '/newfollowers'
                if self.server.debug:
                    print('DEBUG: replyfollowers path ' + self.path)

            # replying as a direct message,
            # for moderation posts or the dm timeline
            reply_is_chat = False
            if '?replydm=' in self.path or '?replychat=' in self.path:
                reply_type = 'replydm'
                if '?replychat=' in self.path:
                    reply_type = 'replychat'
                    reply_is_chat = True
                in_reply_to_url = self.path.split('?' + reply_type + '=')[1]
                in_reply_to_url = urllib.parse.unquote_plus(in_reply_to_url)
                if '?' in in_reply_to_url:
                    # multiple parameters
                    mentions_list = in_reply_to_url.split('?')
                    for ment in mentions_list:
                        if ment.startswith('mention='):
                            reply_handle = ment.replace('mention=', '')
                            in_reply_to_url = reply_handle
                            if reply_handle not in reply_to_list:
                                reply_to_list.append(reply_handle)
                        elif ment.startswith('page='):
                            reply_page_str = ment.replace('page=', '')
                            if len(reply_page_str) > 5:
                                reply_page_str = "1"
                            if reply_page_str.isdigit():
                                reply_page_number = int(reply_page_str)
                        elif ment.startswith('category='):
                            reply_category = ment.replace('category=', '')
                        elif ment.startswith('sharedesc:'):
                            # get the title for the shared item
                            share_description = \
                                ment.replace('sharedesc:', '').strip()
                            share_description = \
                                share_description.replace('_', ' ')
                    in_reply_to_url = mentions_list[0]
                else:
                    # single parameter
                    if in_reply_to_url.startswith('mention='):
                        reply_handle = in_reply_to_url.replace('mention=', '')
                        in_reply_to_url = reply_handle
                        if reply_handle not in reply_to_list:
                            reply_to_list.append(reply_handle)
                    elif in_reply_to_url.startswith('sharedesc:'):
                        # get the title for the shared item
                        share_description = \
                            in_reply_to_url.replace('sharedesc:', '').strip()
                        share_description = \
                            share_description.replace('_', ' ')

                self.path = \
                    self.path.split('?' + reply_type + '=')[0] + '/newdm'
                if self.server.debug:
                    print('DEBUG: ' + reply_type + ' path ' + self.path)

            # Edit a blog post
            if authorized and \
               '/users/' in self.path and \
               '?editblogpost=' in self.path and \
               ';actor=' in self.path:
                message_id = self.path.split('?editblogpost=')[1]
                if ';' in message_id:
                    message_id = message_id.split(';')[0]
                actor = self.path.split(';actor=')[1]
                if ';' in actor:
                    actor = actor.split(';')[0]
                nickname = get_nickname_from_actor(self.path.split('?')[0])
                if not nickname:
                    self._404()
                    self.server.getreq_busy = False
                    return
                if nickname == actor:
                    post_url = \
                        local_actor_url(self.server.http_prefix, nickname,
                                        self.server.domain_full) + \
                        '/statuses/' + message_id
                    msg = html_edit_blog(self.server.media_instance,
                                         self.server.translate,
                                         self.server.base_dir,
                                         self.path, reply_page_number,
                                         nickname, self.server.domain,
                                         post_url,
                                         self.server.system_language)
                    if msg:
                        msg = msg.encode('utf-8')
                        msglen = len(msg)
                        self._set_headers('text/html', msglen,
                                          cookie, calling_domain, False)
                        self._write(msg)
                        self.server.getreq_busy = False
                        return

            # Edit a post
            edit_post_params = {}
            if authorized and \
               '/users/' in self.path and \
               '/postedit?scope=' in self.path and \
               ';postid=' in self.path and \
               ';actor=' in self.path:
                post_scope = self.path.split('?scope=')[1]
                if ';' in post_scope:
                    post_scope = post_scope.split(';')[0]
                edit_post_params['scope'] = post_scope
                message_id = self.path.split(';postid=')[1]
                if ';' in message_id:
                    message_id = message_id.split(';')[0]
                if ';replyTo=' in self.path:
                    reply_to = self.path.split(';replyTo=')[1]
                    if ';' in reply_to:
                        reply_to = message_id.split(';')[0]
                    edit_post_params['replyTo'] = reply_to
                actor = self.path.split(';actor=')[1]
                if ';' in actor:
                    actor = actor.split(';')[0]
                edit_post_params['actor'] = actor
                nickname = get_nickname_from_actor(self.path.split('?')[0])
                edit_post_params['nickname'] = nickname
                if not nickname:
                    self._404()
                    self.server.getreq_busy = False
                    return
                if nickname != actor:
                    self._404()
                    self.server.getreq_busy = False
                    return
                post_url = \
                    local_actor_url(self.server.http_prefix, nickname,
                                    self.server.domain_full) + \
                    '/statuses/' + message_id
                edit_post_params['post_url'] = post_url
                # use the new post functions, but using edit_post_params
                new_post_scope = post_scope
                if post_scope == 'public':
                    new_post_scope = 'post'
                self.path = '/users/' + nickname + '/new' + new_post_scope

            # list of known crawlers accessing nodeinfo or masto API
            if self._show_known_crawlers(calling_domain, self.path,
                                         self.server.base_dir,
                                         self.server.known_crawlers):
                self.server.getreq_busy = False
                return

            # edit profile in web interface
            if self._edit_profile(calling_domain, self.path,
                                  self.server.translate,
                                  self.server.base_dir,
                                  self.server.http_prefix,
                                  self.server.domain,
                                  self.server.port,
                                  cookie):
                self.server.getreq_busy = False
                return

            # edit links from the left column of the timeline in web interface
            if self._edit_links(calling_domain, self.path,
                                self.server.translate,
                                self.server.base_dir,
                                self.server.http_prefix,
                                self.server.domain,
                                self.server.port,
                                cookie,
                                self.server.theme_name):
                self.server.getreq_busy = False
                return

            # edit newswire from the right column of the timeline
            if self._edit_newswire(calling_domain, self.path,
                                   self.server.translate,
                                   self.server.base_dir,
                                   self.server.http_prefix,
                                   self.server.domain,
                                   self.server.port,
                                   cookie):
                self.server.getreq_busy = False
                return

            # edit news post
            if self._edit_news_post2(calling_domain, self.path,
                                     self.server.translate,
                                     self.server.base_dir,
                                     self.server.http_prefix,
                                     self.server.domain,
                                     self.server.port,
                                     self.server.domain_full,
                                     cookie):
                self.server.getreq_busy = False
                return

            if self._show_new_post(edit_post_params,
                                   calling_domain, self.path,
                                   self.server.media_instance,
                                   self.server.translate,
                                   self.server.base_dir,
                                   self.server.http_prefix,
                                   in_reply_to_url, reply_to_list,
                                   reply_is_chat,
                                   share_description, reply_page_number,
                                   reply_category,
                                   self.server.domain,
                                   self.server.domain_full,
                                   getreq_start_time,
                                   cookie, no_drop_down, conversation_id,
                                   curr_session):
                self.server.getreq_busy = False
                return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'new post done',
                            self.server.debug)

        # get an individual post from the path /@nickname/statusnumber
        if self._show_individual_at_post(ssml_getreq, authorized,
                                         calling_domain, referer_domain,
                                         self.path,
                                         self.server.base_dir,
                                         self.server.http_prefix,
                                         self.server.domain,
                                         self.server.domain_full,
                                         self.server.port,
                                         self.server.onion_domain,
                                         self.server.i2p_domain,
                                         getreq_start_time,
                                         proxy_type,
                                         cookie, self.server.debug,
                                         curr_session):
            self.server.getreq_busy = False
            return

        # show the likers of a post
        if self._show_likers_of_post(authorized,
                                     calling_domain, self.path,
                                     self.server.base_dir,
                                     self.server.http_prefix,
                                     self.server.domain,
                                     self.server.port,
                                     getreq_start_time,
                                     cookie, self.server.debug,
                                     curr_session):
            self.server.getreq_busy = False
            return

        # show the announcers/repeaters of a post
        if self._show_announcers_of_post(authorized,
                                         calling_domain, self.path,
                                         self.server.base_dir,
                                         self.server.http_prefix,
                                         self.server.domain,
                                         self.server.port,
                                         getreq_start_time,
                                         cookie, self.server.debug,
                                         curr_session):
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'individual post done',
                            self.server.debug)

        # get replies to a post /users/nickname/statuses/number/replies
        if self.path.endswith('/replies') or '/replies?page=' in self.path:
            if self._show_replies_to_post(authorized,
                                          calling_domain, referer_domain,
                                          self.path,
                                          self.server.base_dir,
                                          self.server.http_prefix,
                                          self.server.domain,
                                          self.server.domain_full,
                                          self.server.port,
                                          getreq_start_time,
                                          proxy_type, cookie,
                                          self.server.debug,
                                          curr_session):
                self.server.getreq_busy = False
                return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'post replies done',
                            self.server.debug)

        # roles on profile screen
        if self.path.endswith('/roles') and users_in_path:
            if self._show_roles(calling_domain, referer_domain,
                                self.path,
                                self.server.base_dir,
                                self.server.http_prefix,
                                self.server.domain,
                                getreq_start_time,
                                proxy_type,
                                cookie, self.server.debug,
                                curr_session):
                self.server.getreq_busy = False
                return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'show roles done',
                            self.server.debug)

        # show skills on the profile page
        if self.path.endswith('/skills') and users_in_path:
            if self._show_skills(calling_domain, referer_domain,
                                 self.path,
                                 self.server.base_dir,
                                 self.server.http_prefix,
                                 self.server.domain,
                                 getreq_start_time,
                                 proxy_type,
                                 cookie, self.server.debug,
                                 curr_session):
                self.server.getreq_busy = False
                return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'show skills done',
                            self.server.debug)

        if '?notifypost=' in self.path and users_in_path and authorized:
            if self._show_notify_post(authorized,
                                      calling_domain, referer_domain,
                                      self.path,
                                      self.server.base_dir,
                                      self.server.http_prefix,
                                      self.server.domain,
                                      self.server.port,
                                      getreq_start_time,
                                      proxy_type,
                                      cookie, self.server.debug,
                                      curr_session):
                self.server.getreq_busy = False
                return

        # get an individual post from the path
        # /users/nickname/statuses/number
        if '/statuses/' in self.path and users_in_path:
            if self._show_individual_post(ssml_getreq, authorized,
                                          calling_domain, referer_domain,
                                          self.path,
                                          self.server.base_dir,
                                          self.server.http_prefix,
                                          self.server.domain,
                                          self.server.domain_full,
                                          self.server.port,
                                          getreq_start_time,
                                          proxy_type,
                                          cookie, self.server.debug,
                                          curr_session):
                self.server.getreq_busy = False
                return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'show status done',
                            self.server.debug)

        # get the inbox timeline for a given person
        if self.path.endswith('/inbox') or '/inbox?page=' in self.path:
            if self._show_inbox(authorized,
                                calling_domain, referer_domain,
                                self.path,
                                self.server.base_dir,
                                self.server.http_prefix,
                                self.server.domain,
                                self.server.port,
                                getreq_start_time,
                                cookie, self.server.debug,
                                self.server.recent_posts_cache,
                                curr_session,
                                self.server.default_timeline,
                                self.server.max_recent_posts,
                                self.server.translate,
                                self.server.cached_webfingers,
                                self.server.person_cache,
                                self.server.allow_deletion,
                                self.server.project_version,
                                self.server.yt_replace_domain,
                                self.server.twitter_replacement_domain,
                                ua_str):
                self.server.getreq_busy = False
                return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'show inbox done',
                            self.server.debug)

        # get the direct messages timeline for a given person
        if self.path.endswith('/dm') or '/dm?page=' in self.path:
            if self._show_dms(authorized,
                              calling_domain, referer_domain,
                              self.path,
                              self.server.base_dir,
                              self.server.http_prefix,
                              self.server.domain,
                              self.server.port,
                              getreq_start_time,
                              cookie, self.server.debug,
                              curr_session, ua_str):
                self.server.getreq_busy = False
                return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'show dms done',
                            self.server.debug)

        # get the replies timeline for a given person
        if self.path.endswith('/tlreplies') or '/tlreplies?page=' in self.path:
            if self._show_replies(authorized,
                                  calling_domain, referer_domain,
                                  self.path,
                                  self.server.base_dir,
                                  self.server.http_prefix,
                                  self.server.domain,
                                  self.server.port,
                                  getreq_start_time,
                                  cookie, self.server.debug,
                                  curr_session, ua_str):
                self.server.getreq_busy = False
                return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'show replies 2 done',
                            self.server.debug)

        # get the media timeline for a given person
        if self.path.endswith('/tlmedia') or '/tlmedia?page=' in self.path:
            if self._show_media_timeline(authorized,
                                         calling_domain, referer_domain,
                                         self.path,
                                         self.server.base_dir,
                                         self.server.http_prefix,
                                         self.server.domain,
                                         self.server.port,
                                         getreq_start_time,
                                         cookie, self.server.debug,
                                         curr_session, ua_str):
                self.server.getreq_busy = False
                return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'show media 2 done',
                            self.server.debug)

        # get the blogs for a given person
        if self.path.endswith('/tlblogs') or '/tlblogs?page=' in self.path:
            if self._show_blogs_timeline(authorized,
                                         calling_domain, referer_domain,
                                         self.path,
                                         self.server.base_dir,
                                         self.server.http_prefix,
                                         self.server.domain,
                                         self.server.port,
                                         getreq_start_time,
                                         cookie, self.server.debug,
                                         curr_session, ua_str):
                self.server.getreq_busy = False
                return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'show blogs 2 done',
                            self.server.debug)

        # get the news for a given person
        if self.path.endswith('/tlnews') or '/tlnews?page=' in self.path:
            if self._show_news_timeline(authorized,
                                        calling_domain, referer_domain,
                                        self.path,
                                        self.server.base_dir,
                                        self.server.http_prefix,
                                        self.server.domain,
                                        self.server.port,
                                        getreq_start_time,
                                        cookie, self.server.debug,
                                        curr_session, ua_str):
                self.server.getreq_busy = False
                return

        # get features (local blogs) for a given person
        if self.path.endswith('/tlfeatures') or \
           '/tlfeatures?page=' in self.path:
            if self._show_features_timeline(authorized,
                                            calling_domain, referer_domain,
                                            self.path,
                                            self.server.base_dir,
                                            self.server.http_prefix,
                                            self.server.domain,
                                            self.server.port,
                                            getreq_start_time,
                                            cookie, self.server.debug,
                                            curr_session, ua_str):
                self.server.getreq_busy = False
                return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'show news 2 done',
                            self.server.debug)

        # get the shared items timeline for a given person
        if self.path.endswith('/tlshares') or '/tlshares?page=' in self.path:
            if self._show_shares_timeline(authorized,
                                          calling_domain, self.path,
                                          self.server.base_dir,
                                          self.server.http_prefix,
                                          self.server.domain,
                                          self.server.port,
                                          getreq_start_time,
                                          cookie, self.server.debug,
                                          curr_session, ua_str):
                self.server.getreq_busy = False
                return

        # get the wanted items timeline for a given person
        if self.path.endswith('/tlwanted') or '/tlwanted?page=' in self.path:
            if self._show_wanted_timeline(authorized,
                                          calling_domain, self.path,
                                          self.server.base_dir,
                                          self.server.http_prefix,
                                          self.server.domain,
                                          self.server.port,
                                          getreq_start_time,
                                          cookie, self.server.debug,
                                          curr_session, ua_str):
                self.server.getreq_busy = False
                return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'show shares 2 done',
                            self.server.debug)

        # block a domain from html_account_info
        if authorized and users_in_path and \
           '/accountinfo?blockdomain=' in self.path and \
           '?handle=' in self.path:
            nickname = self.path.split('/users/')[1]
            if '/' in nickname:
                nickname = nickname.split('/')[0]
            if not is_moderator(self.server.base_dir, nickname):
                self._400()
                self.server.getreq_busy = False
                return
            block_domain = self.path.split('/accountinfo?blockdomain=')[1]
            search_handle = block_domain.split('?handle=')[1]
            search_handle = urllib.parse.unquote_plus(search_handle)
            block_domain = block_domain.split('?handle=')[0]
            block_domain = urllib.parse.unquote_plus(block_domain.strip())
            if '?' in block_domain:
                block_domain = block_domain.split('?')[0]
            add_global_block(self.server.base_dir, '*', block_domain)
            msg = \
                html_account_info(self.server.translate,
                                  self.server.base_dir,
                                  self.server.http_prefix,
                                  nickname,
                                  self.server.domain,
                                  self.server.port,
                                  search_handle,
                                  self.server.debug,
                                  self.server.system_language,
                                  self.server.signing_priv_key_pem)
            if msg:
                msg = msg.encode('utf-8')
                msglen = len(msg)
                self._login_headers('text/html',
                                    msglen, calling_domain)
                self._write(msg)
            self.server.getreq_busy = False
            return

        # unblock a domain from html_account_info
        if authorized and users_in_path and \
           '/accountinfo?unblockdomain=' in self.path and \
           '?handle=' in self.path:
            nickname = self.path.split('/users/')[1]
            if '/' in nickname:
                nickname = nickname.split('/')[0]
            if not is_moderator(self.server.base_dir, nickname):
                self._400()
                self.server.getreq_busy = False
                return
            block_domain = self.path.split('/accountinfo?unblockdomain=')[1]
            search_handle = block_domain.split('?handle=')[1]
            search_handle = urllib.parse.unquote_plus(search_handle)
            block_domain = block_domain.split('?handle=')[0]
            block_domain = urllib.parse.unquote_plus(block_domain.strip())
            remove_global_block(self.server.base_dir, '*', block_domain)
            msg = \
                html_account_info(self.server.translate,
                                  self.server.base_dir,
                                  self.server.http_prefix,
                                  nickname,
                                  self.server.domain,
                                  self.server.port,
                                  search_handle,
                                  self.server.debug,
                                  self.server.system_language,
                                  self.server.signing_priv_key_pem)
            if msg:
                msg = msg.encode('utf-8')
                msglen = len(msg)
                self._login_headers('text/html',
                                    msglen, calling_domain)
                self._write(msg)
            self.server.getreq_busy = False
            return

        # get the bookmarks timeline for a given person
        if self.path.endswith('/tlbookmarks') or \
           '/tlbookmarks?page=' in self.path or \
           self.path.endswith('/bookmarks') or \
           '/bookmarks?page=' in self.path:
            if self._show_bookmarks_timeline(authorized,
                                             calling_domain, referer_domain,
                                             self.path,
                                             self.server.base_dir,
                                             self.server.http_prefix,
                                             self.server.domain,
                                             self.server.port,
                                             getreq_start_time,
                                             cookie, self.server.debug,
                                             curr_session, ua_str):
                self.server.getreq_busy = False
                return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'show bookmarks 2 done',
                            self.server.debug)

        # outbox timeline
        if self.path.endswith('/outbox') or \
           '/outbox?page=' in self.path:
            if self._show_outbox_timeline(authorized,
                                          calling_domain, referer_domain,
                                          self.path,
                                          self.server.base_dir,
                                          self.server.http_prefix,
                                          self.server.domain,
                                          self.server.port,
                                          getreq_start_time,
                                          cookie, self.server.debug,
                                          curr_session, ua_str,
                                          proxy_type):
                self.server.getreq_busy = False
                return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'show outbox done',
                            self.server.debug)

        # get the moderation feed for a moderator
        if self.path.endswith('/moderation') or \
           '/moderation?' in self.path:
            if self._show_mod_timeline(authorized,
                                       calling_domain, referer_domain,
                                       self.path,
                                       self.server.base_dir,
                                       self.server.http_prefix,
                                       self.server.domain,
                                       self.server.port,
                                       getreq_start_time,
                                       cookie, self.server.debug,
                                       curr_session, ua_str):
                self.server.getreq_busy = False
                return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'show moderation done',
                            self.server.debug)

        if self._show_shares_feed(authorized,
                                  calling_domain, referer_domain,
                                  self.path,
                                  self.server.base_dir,
                                  self.server.http_prefix,
                                  self.server.domain,
                                  self.server.port,
                                  getreq_start_time,
                                  proxy_type,
                                  cookie, self.server.debug, 'shares',
                                  curr_session):
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'show profile 2 done',
                            self.server.debug)

        if self._show_following_feed(authorized,
                                     calling_domain, referer_domain,
                                     self.path,
                                     self.server.base_dir,
                                     self.server.http_prefix,
                                     self.server.domain,
                                     self.server.port,
                                     getreq_start_time,
                                     proxy_type,
                                     cookie, self.server.debug,
                                     curr_session):
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'show profile 3 done',
                            self.server.debug)

        if self._show_followers_feed(authorized,
                                     calling_domain, referer_domain,
                                     self.path,
                                     self.server.base_dir,
                                     self.server.http_prefix,
                                     self.server.domain,
                                     self.server.port,
                                     getreq_start_time,
                                     proxy_type,
                                     cookie, self.server.debug,
                                     curr_session):
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'show profile 4 done',
                            self.server.debug)

        # look up a person
        if self._show_person_profile(authorized,
                                     calling_domain, referer_domain,
                                     self.path,
                                     self.server.base_dir,
                                     self.server.http_prefix,
                                     self.server.domain,
                                     self.server.onion_domain,
                                     self.server.i2p_domain,
                                     getreq_start_time,
                                     proxy_type,
                                     cookie, self.server.debug,
                                     curr_session):
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'show profile posts done',
                            self.server.debug)

        # check that a json file was requested
        if not self.path.endswith('.json'):
            if self.server.debug:
                print('DEBUG: GET Not json: ' + self.path +
                      ' ' + self.server.base_dir)
            self._404()
            self.server.getreq_busy = False
            return

        if not self._secure_mode(curr_session,
                                 proxy_type):
            if self.server.debug:
                print('WARN: Unauthorized GET')
            self._404()
            self.server.getreq_busy = False
            return

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'authorized fetch',
                            self.server.debug)

        # check that the file exists
        filename = self.server.base_dir + self.path
        if os.path.isfile(filename):
            content = None
            try:
                with open(filename, 'r', encoding='utf-8') as rfile:
                    content = rfile.read()
            except OSError:
                print('EX: unable to read file ' + filename)
            if content:
                content_json = json.loads(content)
                msg_str = json.dumps(content_json, ensure_ascii=False)
                msg_str = self._convert_domains(calling_domain,
                                                referer_domain,
                                                msg_str)
                msg = msg_str.encode('utf-8')
                msglen = len(msg)
                accept_str = self.headers['Accept']
                protocol_str = \
                    get_json_content_from_accept(accept_str)
                self._set_headers(protocol_str, msglen,
                                  None, calling_domain, False)
                self._write(msg)
                fitness_performance(getreq_start_time, self.server.fitness,
                                    '_GET', 'arbitrary json',
                                    self.server.debug)
        else:
            if self.server.debug:
                print('DEBUG: GET Unknown file')
            self._404()
        self.server.getreq_busy = False

        fitness_performance(getreq_start_time, self.server.fitness,
                            '_GET', 'end benchmarks',
                            self.server.debug)

    def _dav_handler(self, endpoint_type: str, debug: bool):
        calling_domain = self.server.domain_full
        if not self._has_accept(calling_domain):
            self._400()
            return
        accept_str = self.headers['Accept']
        if 'application/xml' not in accept_str:
            if debug:
                print(endpoint_type.upper() + ' is not of xml type')
            self._400()
            return
        if not self.headers.get('Content-length'):
            print(endpoint_type.upper() + ' has no content-length')
            self._400()
            return

        # check that the content length string is not too long
        if isinstance(self.headers['Content-length'], str):
            max_content_size = len(str(self.server.maxMessageLength))
            if len(self.headers['Content-length']) > max_content_size:
                self._400()
                return

        length = int(self.headers['Content-length'])
        if length > self.server.max_post_length:
            print(endpoint_type.upper() +
                  ' request size too large ' + self.path)
            self._400()
            return
        if not self.path.startswith('/calendars/'):
            print(endpoint_type.upper() + ' without /calendars ' + self.path)
            self._404()
            return
        if debug:
            print(endpoint_type.upper() + ' checking authorization')
        if not self._is_authorized():
            print(endpoint_type.upper() + ' not authorized')
            self._403()
            return
        nickname = self.path.split('/calendars/')[1]
        if '/' in nickname:
            nickname = nickname.split('/')[0]
        if not nickname:
            print(endpoint_type.upper() + ' no nickname ' + self.path)
            self._400()
            return
        if not os.path.isdir(self.server.base_dir + '/accounts/' +
                             nickname + '@' + self.server.domain):
            print(endpoint_type.upper() +
                  ' for non-existent account ' + self.path)
            self._404()
            return
        propfind_bytes = None
        try:
            propfind_bytes = self.rfile.read(length)
        except SocketError as ex:
            if ex.errno == errno.ECONNRESET:
                print('EX: ' + endpoint_type.upper() +
                      ' connection reset by peer')
            else:
                print('EX: ' + endpoint_type.upper() + ' socket error')
            self._400()
            return
        except ValueError as ex:
            print('EX: ' + endpoint_type.upper() +
                  ' rfile.read failed, ' + str(ex))
            self._400()
            return
        if not propfind_bytes:
            self._404()
            return
        depth = 0
        if self.headers.get('Depth'):
            depth = self.headers['Depth']
        propfind_xml = propfind_bytes.decode('utf-8')
        response_str = None
        if endpoint_type == 'propfind':
            response_str = \
                dav_propfind_response(nickname, propfind_xml)
        elif endpoint_type == 'put':
            response_str = \
                dav_put_response(self.server.base_dir,
                                 nickname, self.server.domain,
                                 propfind_xml,
                                 self.server.http_prefix,
                                 self.server.system_language,
                                 self.server.recent_dav_etags)
        elif endpoint_type == 'report':
            curr_etag = None
            if self.headers.get('ETag'):
                curr_etag = self.headers['ETag']
            elif self.headers.get('Etag'):
                curr_etag = self.headers['Etag']
            response_str = \
                dav_report_response(self.server.base_dir,
                                    nickname, self.server.domain,
                                    propfind_xml,
                                    self.server.person_cache,
                                    self.server.http_prefix,
                                    curr_etag,
                                    self.server.recent_dav_etags,
                                    self.server.domain_full,
                                    self.server.system_language)
        elif endpoint_type == 'delete':
            response_str = \
                dav_delete_response(self.server.base_dir,
                                    nickname, self.server.domain,
                                    depth, self.path,
                                    self.server.http_prefix,
                                    self.server.debug,
                                    self.server.recent_posts_cache)
        if not response_str:
            self._404()
            return
        if response_str == 'Not modified':
            if endpoint_type == 'put':
                self._200()
                return
            self._304()
            return
        if response_str.startswith('ETag:') and endpoint_type == 'put':
            response_etag = response_str.split('ETag:', 1)[1]
            self._201(response_etag)
        elif response_str != 'Ok':
            message_xml = response_str.encode('utf-8')
            message_xml_len = len(message_xml)
            self._set_headers('application/xml; charset=utf-8',
                              message_xml_len,
                              None, calling_domain, False)
            self._write(message_xml)
            if 'multistatus' in response_str:
                return self._207()
        self._200()

    def do_PROPFIND(self):
        self._dav_handler('propfind', self.server.debug)

    def do_PUT(self):
        self._dav_handler('put', self.server.debug)

    def do_REPORT(self):
        self._dav_handler('report', self.server.debug)

    def do_DELETE(self):
        self._dav_handler('delete', self.server.debug)

    def do_HEAD(self):
        calling_domain = self.server.domain_full
        if self.headers.get('Host'):
            calling_domain = decoded_host(self.headers['Host'])
            if self.server.onion_domain:
                if calling_domain not in (self.server.domain,
                                          self.server.domain_full,
                                          self.server.onion_domain):
                    print('HEAD domain blocked: ' + calling_domain)
                    self._400()
                    return
            else:
                if calling_domain not in (self.server.domain,
                                          self.server.domain_full):
                    print('HEAD domain blocked: ' + calling_domain)
                    self._400()
                    return

        check_path = self.path
        etag = None
        file_length = -1
        last_modified_time_str = None

        if '/media/' in self.path or \
           '/accounts/avatars/' in self.path or \
           '/accounts/headers/' in self.path:
            if is_image_file(self.path) or \
               path_is_video(self.path) or \
               path_is_audio(self.path):
                if '/media/' in self.path:
                    media_str = self.path.split('/media/')[1]
                    media_filename = \
                        self.server.base_dir + '/media/' + media_str
                elif '/accounts/avatars/' in self.path:
                    avatar_file = self.path.split('/accounts/avatars/')[1]
                    if '/' not in avatar_file:
                        self._404()
                        return
                    nickname = avatar_file.split('/')[0]
                    avatar_file = avatar_file.split('/')[1]
                    avatar_file_ext = avatar_file.split('.')[-1]
                    # remove any numbers, eg. avatar123.png becomes avatar.png
                    if avatar_file.startswith('avatar'):
                        avatar_file = 'avatar.' + avatar_file_ext
                    media_filename = \
                        self.server.base_dir + '/accounts/' + \
                        nickname + '@' + self.server.domain + '/' + \
                        avatar_file
                else:
                    banner_file = self.path.split('/accounts/headers/')[1]
                    if '/' not in banner_file:
                        self._404()
                        return
                    nickname = banner_file.split('/')[0]
                    banner_file = banner_file.split('/')[1]
                    banner_file_ext = banner_file.split('.')[-1]
                    # remove any numbers, eg. banner123.png becomes banner.png
                    if banner_file.startswith('banner'):
                        banner_file = 'banner.' + banner_file_ext
                    media_filename = \
                        self.server.base_dir + '/accounts/' + \
                        nickname + '@' + self.server.domain + '/' + \
                        banner_file

                if os.path.isfile(media_filename):
                    check_path = media_filename
                    file_length = os.path.getsize(media_filename)
                    media_tm = os.path.getmtime(media_filename)
                    last_modified_time = \
                        datetime.datetime.fromtimestamp(media_tm)
                    time_format_str = '%a, %d %b %Y %H:%M:%S GMT'
                    last_modified_time_str = \
                        last_modified_time.strftime(time_format_str)
                    media_tag_filename = media_filename + '.etag'
                    if os.path.isfile(media_tag_filename):
                        try:
                            with open(media_tag_filename, 'r',
                                      encoding='utf-8') as efile:
                                etag = efile.read()
                        except OSError:
                            print('EX: do_HEAD unable to read ' +
                                  media_tag_filename)
                    else:
                        media_binary = None
                        try:
                            with open(media_filename, 'rb') as av_file:
                                media_binary = av_file.read()
                        except OSError:
                            print('EX: unable to read media binary ' +
                                  media_filename)
                        if media_binary:
                            etag = md5(media_binary).hexdigest()  # nosec
                            try:
                                with open(media_tag_filename, 'w+',
                                          encoding='utf-8') as efile:
                                    efile.write(etag)
                            except OSError:
                                print('EX: do_HEAD unable to write ' +
                                      media_tag_filename)
                else:
                    self._404()
                    return

        media_file_type = media_file_mime_type(check_path)
        self._set_headers_head(media_file_type, file_length,
                               etag, calling_domain, False,
                               last_modified_time_str)

    def _receive_new_post_process(self, post_type: str, path: str, headers: {},
                                  length: int, post_bytes, boundary: str,
                                  calling_domain: str, cookie: str,
                                  content_license_url: str,
                                  curr_session, proxy_type: str) -> int:
        # Note: this needs to happen synchronously
        # 0=this is not a new post
        # 1=new post success
        # -1=new post failed
        # 2=new post canceled
        if self.server.debug:
            print('DEBUG: receiving POST')

        if ' boundary=' in headers['Content-Type']:
            if self.server.debug:
                print('DEBUG: receiving POST headers ' +
                      headers['Content-Type'] +
                      ' path ' + path)
            nickname = None
            nickname_str = path.split('/users/')[1]
            if '?' in nickname_str:
                nickname_str = nickname_str.split('?')[0]
            if '/' in nickname_str:
                nickname = nickname_str.split('/')[0]
            else:
                nickname = nickname_str
            if self.server.debug:
                print('DEBUG: POST nickname ' + str(nickname))
            if not nickname:
                print('WARN: no nickname found when receiving ' + post_type +
                      ' path ' + path)
                return -1

            # get the message id of an edited post
            edited_postid = None
            print('DEBUG: edited_postid path ' + path)
            if '?editid=' in path:
                edited_postid = path.split('?editid=')[1]
                if '?' in edited_postid:
                    edited_postid = edited_postid.split('?')[0]
                print('DEBUG: edited_postid ' + edited_postid)

            # get the published date of an edited post
            edited_published = None
            if '?editpub=' in path:
                edited_published = path.split('?editpub=')[1]
                if '?' in edited_published:
                    edited_published = \
                        edited_published.split('?')[0]
                print('DEBUG: edited_published ' +
                      edited_published)

            length = int(headers['Content-Length'])
            if length > self.server.max_post_length:
                print('POST size too large')
                return -1

            boundary = headers['Content-Type'].split('boundary=')[1]
            if ';' in boundary:
                boundary = boundary.split(';')[0]

            # Note: we don't use cgi here because it's due to be deprecated
            # in Python 3.8/3.10
            # Instead we use the multipart mime parser from the email module
            if self.server.debug:
                print('DEBUG: extracting media from POST')
            media_bytes, post_bytes = \
                extract_media_in_form_post(post_bytes, boundary, 'attachpic')
            if self.server.debug:
                if media_bytes:
                    print('DEBUG: media was found. ' +
                          str(len(media_bytes)) + ' bytes')
                else:
                    print('DEBUG: no media was found in POST')

            # Note: a .temp extension is used here so that at no time is
            # an image with metadata publicly exposed, even for a few mS
            filename_base = \
                acct_dir(self.server.base_dir,
                         nickname, self.server.domain) + '/upload.temp'

            filename, attachment_media_type = \
                save_media_in_form_post(media_bytes, self.server.debug,
                                        filename_base)
            if self.server.debug:
                if filename:
                    print('DEBUG: POST media filename is ' + filename)
                else:
                    print('DEBUG: no media filename in POST')

            if filename:
                if is_image_file(filename):
                    post_image_filename = filename.replace('.temp', '')
                    print('Removing metadata from ' + post_image_filename)
                    city = get_spoofed_city(self.server.city,
                                            self.server.base_dir,
                                            nickname, self.server.domain)
                    if self.server.low_bandwidth:
                        convert_image_to_low_bandwidth(filename)
                    process_meta_data(self.server.base_dir,
                                      nickname, self.server.domain,
                                      filename, post_image_filename, city,
                                      content_license_url)
                    if os.path.isfile(post_image_filename):
                        print('POST media saved to ' + post_image_filename)
                    else:
                        print('ERROR: POST media could not be saved to ' +
                              post_image_filename)
                else:
                    if os.path.isfile(filename):
                        new_filename = filename.replace('.temp', '')
                        os.rename(filename, new_filename)
                        filename = new_filename

            fields = \
                extract_text_fields_in_post(post_bytes, boundary,
                                            self.server.debug)
            if self.server.debug:
                if fields:
                    print('DEBUG: text field extracted from POST ' +
                          str(fields))
                else:
                    print('WARN: no text fields could be extracted from POST')

            # was the citations button pressed on the newblog screen?
            citations_button_press = False
            if post_type == 'newblog' and fields.get('submitCitations'):
                if fields['submitCitations'] == \
                   self.server.translate['Citations']:
                    citations_button_press = True

            if not citations_button_press:
                # process the received text fields from the POST
                if not fields.get('message') and \
                   not fields.get('imageDescription') and \
                   not fields.get('pinToProfile'):
                    print('WARN: no message, image description or pin')
                    return -1
                submit_text1 = self.server.translate['Publish']
                submit_text2 = self.server.translate['Send']
                submit_text3 = submit_text2
                custom_submit_text = \
                    get_config_param(self.server.base_dir, 'customSubmitText')
                if custom_submit_text:
                    submit_text3 = custom_submit_text
                if fields.get('submitPost'):
                    if fields['submitPost'] != submit_text1 and \
                       fields['submitPost'] != submit_text2 and \
                       fields['submitPost'] != submit_text3:
                        print('WARN: no submit field ' + fields['submitPost'])
                        return -1
                else:
                    print('WARN: no submitPost')
                    return 2

            if not fields.get('imageDescription'):
                fields['imageDescription'] = None
            if not fields.get('subject'):
                fields['subject'] = None
            if not fields.get('replyTo'):
                fields['replyTo'] = None

            if not fields.get('schedulePost'):
                fields['schedulePost'] = False
            else:
                fields['schedulePost'] = True
            print('DEBUG: shedulePost ' + str(fields['schedulePost']))

            if not fields.get('eventDate'):
                fields['eventDate'] = None
            if not fields.get('eventTime'):
                fields['eventTime'] = None
            if not fields.get('eventEndTime'):
                fields['eventEndTime'] = None
            if not fields.get('location'):
                fields['location'] = None
            if not fields.get('languagesDropdown'):
                fields['languagesDropdown'] = self.server.system_language

            if not citations_button_press:
                # Store a file which contains the time in seconds
                # since epoch when an attempt to post something was made.
                # This is then used for active monthly users counts
                last_used_filename = \
                    acct_dir(self.server.base_dir,
                             nickname, self.server.domain) + '/.lastUsed'
                try:
                    with open(last_used_filename, 'w+',
                              encoding='utf-8') as lastfile:
                        lastfile.write(str(int(time.time())))
                except OSError:
                    print('EX: _receive_new_post_process unable to write ' +
                          last_used_filename)

            mentions_str = ''
            if fields.get('mentions'):
                mentions_str = fields['mentions'].strip() + ' '
            if not fields.get('commentsEnabled'):
                comments_enabled = False
            else:
                comments_enabled = True

            if post_type == 'newpost':
                if not fields.get('pinToProfile'):
                    pin_to_profile = False
                else:
                    pin_to_profile = True
                    # is the post message empty?
                    if not fields['message']:
                        # remove the pinned content from profile screen
                        undo_pinned_post(self.server.base_dir,
                                         nickname, self.server.domain)
                        return 1

                city = get_spoofed_city(self.server.city,
                                        self.server.base_dir,
                                        nickname, self.server.domain)

                conversation_id = None
                if fields.get('conversationId'):
                    conversation_id = fields['conversationId']

                languages_understood = \
                    get_understood_languages(self.server.base_dir,
                                             self.server.http_prefix,
                                             nickname,
                                             self.server.domain_full,
                                             self.server.person_cache)

                message_json = \
                    create_public_post(self.server.base_dir,
                                       nickname,
                                       self.server.domain,
                                       self.server.port,
                                       self.server.http_prefix,
                                       mentions_str + fields['message'],
                                       False, False, comments_enabled,
                                       filename, attachment_media_type,
                                       fields['imageDescription'],
                                       city,
                                       fields['replyTo'], fields['replyTo'],
                                       fields['subject'],
                                       fields['schedulePost'],
                                       fields['eventDate'],
                                       fields['eventTime'],
                                       fields['eventEndTime'],
                                       fields['location'], False,
                                       fields['languagesDropdown'],
                                       conversation_id,
                                       self.server.low_bandwidth,
                                       self.server.content_license_url,
                                       languages_understood,
                                       self.server.translate)
                if message_json:
                    if edited_postid:
                        message_json['id'] = \
                            edited_postid + '/activity'
                        message_json['object']['id'] = \
                            edited_postid
                        message_json['object']['url'] = \
                            edited_postid
                        message_json['type'] = 'Update'
                        print('DEBUG: sending edited public post ' +
                              str(message_json))
                    if fields['schedulePost']:
                        return 1
                    if pin_to_profile:
                        sys_language = self.server.system_language
                        content_str = \
                            get_base_content_from_post(message_json,
                                                       sys_language)
                        pin_post(self.server.base_dir,
                                 nickname, self.server.domain, content_str)
                        return 1
                    if self._post_to_outbox(message_json,
                                            self.server.project_version,
                                            nickname,
                                            curr_session, proxy_type):
                        populate_replies(self.server.base_dir,
                                         self.server.http_prefix,
                                         self.server.domain_full,
                                         message_json,
                                         self.server.max_replies,
                                         self.server.debug)
                        return 1
                    return -1
            elif post_type == 'newblog':
                # citations button on newblog screen
                if citations_button_press:
                    message_json = \
                        html_citations(self.server.base_dir,
                                       nickname,
                                       self.server.domain,
                                       self.server.http_prefix,
                                       self.server.default_timeline,
                                       self.server.translate,
                                       self.server.newswire,
                                       fields['subject'],
                                       fields['message'],
                                       filename, attachment_media_type,
                                       fields['imageDescription'],
                                       self.server.theme_name)
                    if message_json:
                        message_json = message_json.encode('utf-8')
                        message_json_len = len(message_json)
                        self._set_headers('text/html',
                                          message_json_len,
                                          cookie, calling_domain, False)
                        self._write(message_json)
                        return 1
                    else:
                        return -1
                if not fields['subject']:
                    print('WARN: blog posts must have a title')
                    return -1
                if not fields['message']:
                    print('WARN: blog posts must have content')
                    return -1
                # submit button on newblog screen
                save_to_file = False
                client_to_server = False
                city = None
                conversation_id = None
                if fields.get('conversationId'):
                    conversation_id = fields['conversationId']
                languages_understood = \
                    get_understood_languages(self.server.base_dir,
                                             self.server.http_prefix,
                                             nickname,
                                             self.server.domain_full,
                                             self.server.person_cache)
                message_json = \
                    create_blog_post(self.server.base_dir, nickname,
                                     self.server.domain, self.server.port,
                                     self.server.http_prefix,
                                     fields['message'],
                                     save_to_file,
                                     client_to_server, comments_enabled,
                                     filename, attachment_media_type,
                                     fields['imageDescription'],
                                     city,
                                     fields['replyTo'], fields['replyTo'],
                                     fields['subject'],
                                     fields['schedulePost'],
                                     fields['eventDate'],
                                     fields['eventTime'],
                                     fields['eventEndTime'],
                                     fields['location'],
                                     fields['languagesDropdown'],
                                     conversation_id,
                                     self.server.low_bandwidth,
                                     self.server.content_license_url,
                                     languages_understood,
                                     self.server.translate)
                if message_json:
                    if fields['schedulePost']:
                        return 1
                    if self._post_to_outbox(message_json,
                                            self.server.project_version,
                                            nickname,
                                            curr_session, proxy_type):
                        refresh_newswire(self.server.base_dir)
                        populate_replies(self.server.base_dir,
                                         self.server.http_prefix,
                                         self.server.domain_full,
                                         message_json,
                                         self.server.max_replies,
                                         self.server.debug)
                        return 1
                    return -1
            elif post_type == 'editblogpost':
                print('Edited blog post received')
                post_filename = \
                    locate_post(self.server.base_dir,
                                nickname, self.server.domain,
                                fields['postUrl'])
                if os.path.isfile(post_filename):
                    post_json_object = load_json(post_filename)
                    if post_json_object:
                        cached_filename = \
                            acct_dir(self.server.base_dir,
                                     nickname, self.server.domain) + \
                            '/postcache/' + \
                            fields['postUrl'].replace('/', '#') + '.html'
                        if os.path.isfile(cached_filename):
                            print('Edited blog post, removing cached html')
                            try:
                                os.remove(cached_filename)
                            except OSError:
                                print('EX: _receive_new_post_process ' +
                                      'unable to delete ' + cached_filename)
                        # remove from memory cache
                        remove_post_from_cache(post_json_object,
                                               self.server.recent_posts_cache)
                        # change the blog post title
                        post_json_object['object']['summary'] = \
                            fields['subject']
                        # format message
                        tags = []
                        hashtags_dict = {}
                        mentioned_recipients = []
                        fields['message'] = \
                            add_html_tags(self.server.base_dir,
                                          self.server.http_prefix,
                                          nickname, self.server.domain,
                                          fields['message'],
                                          mentioned_recipients,
                                          hashtags_dict,
                                          self.server.translate,
                                          True)
                        # replace emoji with unicode
                        tags = []
                        for _, tag in hashtags_dict.items():
                            tags.append(tag)
                        # get list of tags
                        fields['message'] = \
                            replace_emoji_from_tags(curr_session,
                                                    self.server.base_dir,
                                                    fields['message'],
                                                    tags, 'content',
                                                    self.server.debug,
                                                    True)

                        post_json_object['object']['content'] = \
                            fields['message']
                        content_map = post_json_object['object']['contentMap']
                        content_map[self.server.system_language] = \
                            fields['message']

                        img_description = ''
                        if fields.get('imageDescription'):
                            img_description = fields['imageDescription']

                        if filename:
                            city = get_spoofed_city(self.server.city,
                                                    self.server.base_dir,
                                                    nickname,
                                                    self.server.domain)
                            post_json_object['object'] = \
                                attach_media(self.server.base_dir,
                                             self.server.http_prefix,
                                             nickname,
                                             self.server.domain,
                                             self.server.port,
                                             post_json_object['object'],
                                             filename,
                                             attachment_media_type,
                                             img_description,
                                             city,
                                             self.server.low_bandwidth,
                                             self.server.content_license_url)

                        replace_you_tube(post_json_object,
                                         self.server.yt_replace_domain,
                                         self.server.system_language)
                        replace_twitter(post_json_object,
                                        self.server.twitter_replacement_domain,
                                        self.server.system_language)
                        save_json(post_json_object, post_filename)
                        # also save to the news actor
                        if nickname != 'news':
                            post_filename = \
                                post_filename.replace('#users#' +
                                                      nickname + '#',
                                                      '#users#news#')
                            save_json(post_json_object, post_filename)
                        print('Edited blog post, resaved ' + post_filename)
                        return 1
                    else:
                        print('Edited blog post, unable to load json for ' +
                              post_filename)
                else:
                    print('Edited blog post not found ' +
                          str(fields['postUrl']))
                return -1
            elif post_type == 'newunlisted':
                city = get_spoofed_city(self.server.city,
                                        self.server.base_dir,
                                        nickname,
                                        self.server.domain)
                save_to_file = False
                client_to_server = False

                conversation_id = None
                if fields.get('conversationId'):
                    conversation_id = fields['conversationId']

                languages_understood = \
                    get_understood_languages(self.server.base_dir,
                                             self.server.http_prefix,
                                             nickname,
                                             self.server.domain_full,
                                             self.server.person_cache)

                message_json = \
                    create_unlisted_post(self.server.base_dir,
                                         nickname,
                                         self.server.domain, self.server.port,
                                         self.server.http_prefix,
                                         mentions_str + fields['message'],
                                         save_to_file,
                                         client_to_server, comments_enabled,
                                         filename, attachment_media_type,
                                         fields['imageDescription'],
                                         city,
                                         fields['replyTo'],
                                         fields['replyTo'],
                                         fields['subject'],
                                         fields['schedulePost'],
                                         fields['eventDate'],
                                         fields['eventTime'],
                                         fields['eventEndTime'],
                                         fields['location'],
                                         fields['languagesDropdown'],
                                         conversation_id,
                                         self.server.low_bandwidth,
                                         self.server.content_license_url,
                                         languages_understood,
                                         self.server.translate)
                if message_json:
                    if edited_postid:
                        message_json['id'] = \
                            edited_postid + '/activity'
                        message_json['object']['id'] = \
                            edited_postid
                        message_json['object']['url'] = \
                            edited_postid
                        message_json['type'] = 'Update'
                        print('DEBUG: sending edited unlisted post ' +
                              str(message_json))

                    if fields['schedulePost']:
                        return 1
                    if self._post_to_outbox(message_json,
                                            self.server.project_version,
                                            nickname,
                                            curr_session, proxy_type):
                        populate_replies(self.server.base_dir,
                                         self.server.http_prefix,
                                         self.server.domain,
                                         message_json,
                                         self.server.max_replies,
                                         self.server.debug)
                        return 1
                    return -1
            elif post_type == 'newfollowers':
                city = get_spoofed_city(self.server.city,
                                        self.server.base_dir,
                                        nickname,
                                        self.server.domain)
                save_to_file = False
                client_to_server = False

                conversation_id = None
                if fields.get('conversationId'):
                    conversation_id = fields['conversationId']

                mentions_message = mentions_str + fields['message']
                languages_understood = \
                    get_understood_languages(self.server.base_dir,
                                             self.server.http_prefix,
                                             nickname,
                                             self.server.domain_full,
                                             self.server.person_cache)
                message_json = \
                    create_followers_only_post(self.server.base_dir,
                                               nickname,
                                               self.server.domain,
                                               self.server.port,
                                               self.server.http_prefix,
                                               mentions_message,
                                               save_to_file,
                                               client_to_server,
                                               comments_enabled,
                                               filename, attachment_media_type,
                                               fields['imageDescription'],
                                               city,
                                               fields['replyTo'],
                                               fields['replyTo'],
                                               fields['subject'],
                                               fields['schedulePost'],
                                               fields['eventDate'],
                                               fields['eventTime'],
                                               fields['eventEndTime'],
                                               fields['location'],
                                               fields['languagesDropdown'],
                                               conversation_id,
                                               self.server.low_bandwidth,
                                               self.server.content_license_url,
                                               languages_understood,
                                               self.server.translate)
                if message_json:
                    if edited_postid:
                        message_json['id'] = \
                            edited_postid + '/activity'
                        message_json['object']['id'] = \
                            edited_postid
                        message_json['object']['url'] = \
                            edited_postid
                        message_json['type'] = 'Update'
                        print('DEBUG: sending edited followers post ' +
                              str(message_json))

                    if fields['schedulePost']:
                        return 1
                    if self._post_to_outbox(message_json,
                                            self.server.project_version,
                                            nickname,
                                            curr_session, proxy_type):
                        populate_replies(self.server.base_dir,
                                         self.server.http_prefix,
                                         self.server.domain,
                                         message_json,
                                         self.server.max_replies,
                                         self.server.debug)
                        return 1
                    return -1
            elif post_type == 'newdm':
                message_json = None
                print('A DM was posted')
                if '@' in mentions_str:
                    city = get_spoofed_city(self.server.city,
                                            self.server.base_dir,
                                            nickname,
                                            self.server.domain)
                    save_to_file = False
                    client_to_server = False

                    conversation_id = None
                    if fields.get('conversationId'):
                        conversation_id = fields['conversationId']
                    content_license_url = self.server.content_license_url

                    languages_understood = \
                        get_understood_languages(self.server.base_dir,
                                                 self.server.http_prefix,
                                                 nickname,
                                                 self.server.domain_full,
                                                 self.server.person_cache)

                    reply_is_chat = False
                    if fields.get('replychatmsg'):
                        reply_is_chat = fields['replychatmsg']

                    message_json = \
                        create_direct_message_post(self.server.base_dir,
                                                   nickname,
                                                   self.server.domain,
                                                   self.server.port,
                                                   self.server.http_prefix,
                                                   mentions_str +
                                                   fields['message'],
                                                   save_to_file,
                                                   client_to_server,
                                                   comments_enabled,
                                                   filename,
                                                   attachment_media_type,
                                                   fields['imageDescription'],
                                                   city,
                                                   fields['replyTo'],
                                                   fields['replyTo'],
                                                   fields['subject'],
                                                   True,
                                                   fields['schedulePost'],
                                                   fields['eventDate'],
                                                   fields['eventTime'],
                                                   fields['eventEndTime'],
                                                   fields['location'],
                                                   fields['languagesDropdown'],
                                                   conversation_id,
                                                   self.server.low_bandwidth,
                                                   content_license_url,
                                                   languages_understood,
                                                   reply_is_chat,
                                                   self.server.translate)
                if message_json:
                    print('DEBUG: posting DM edited_postid ' +
                          str(edited_postid))
                    if edited_postid:
                        edited_updated = \
                            message_json['object']['published']
                        if edited_published:
                            message_json['published'] = \
                                edited_published
                            message_json['object']['published'] = \
                                edited_published
                        message_json['id'] = \
                            edited_postid + '/activity'
                        message_json['object']['id'] = \
                            edited_postid
                        message_json['object']['url'] = \
                            edited_postid
                        message_json['updated'] = \
                            edited_updated
                        message_json['object']['updated'] = \
                            edited_updated
                        message_json['type'] = 'Update'
                        print('DEBUG: sending edited dm post ' +
                              str(message_json))

                    if fields['schedulePost']:
                        return 1
                    print('Sending new DM to ' +
                          str(message_json['object']['to']))
                    if self._post_to_outbox(message_json,
                                            self.server.project_version,
                                            nickname,
                                            curr_session, proxy_type):
                        populate_replies(self.server.base_dir,
                                         self.server.http_prefix,
                                         self.server.domain,
                                         message_json,
                                         self.server.max_replies,
                                         self.server.debug)
                        return 1
                    return -1
            elif post_type == 'newreminder':
                message_json = None
                handle = nickname + '@' + self.server.domain_full
                print('A reminder was posted for ' + handle)
                if '@' + handle not in mentions_str:
                    mentions_str = '@' + handle + ' ' + mentions_str
                city = get_spoofed_city(self.server.city,
                                        self.server.base_dir,
                                        nickname,
                                        self.server.domain)
                save_to_file = False
                client_to_server = False
                comments_enabled = False
                conversation_id = None
                mentions_message = mentions_str + fields['message']
                languages_understood = \
                    get_understood_languages(self.server.base_dir,
                                             self.server.http_prefix,
                                             nickname,
                                             self.server.domain_full,
                                             self.server.person_cache)
                message_json = \
                    create_direct_message_post(self.server.base_dir,
                                               nickname,
                                               self.server.domain,
                                               self.server.port,
                                               self.server.http_prefix,
                                               mentions_message,
                                               save_to_file,
                                               client_to_server,
                                               comments_enabled,
                                               filename, attachment_media_type,
                                               fields['imageDescription'],
                                               city,
                                               None, None,
                                               fields['subject'],
                                               True, fields['schedulePost'],
                                               fields['eventDate'],
                                               fields['eventTime'],
                                               fields['eventEndTime'],
                                               fields['location'],
                                               fields['languagesDropdown'],
                                               conversation_id,
                                               self.server.low_bandwidth,
                                               self.server.content_license_url,
                                               languages_understood,
                                               False, self.server.translate)
                if message_json:
                    if fields['schedulePost']:
                        return 1
                    print('DEBUG: new reminder to ' +
                          str(message_json['object']['to']))
                    if self._post_to_outbox(message_json,
                                            self.server.project_version,
                                            nickname,
                                            curr_session, proxy_type):
                        return 1
                    return -1
            elif post_type == 'newreport':
                if attachment_media_type:
                    if attachment_media_type != 'image':
                        return -1
                # So as to be sure that this only goes to moderators
                # and not accounts being reported we disable any
                # included fediverse addresses by replacing '@' with '-at-'
                fields['message'] = fields['message'].replace('@', '-at-')
                city = get_spoofed_city(self.server.city,
                                        self.server.base_dir,
                                        nickname,
                                        self.server.domain)
                languages_understood = \
                    get_understood_languages(self.server.base_dir,
                                             self.server.http_prefix,
                                             nickname,
                                             self.server.domain_full,
                                             self.server.person_cache)
                message_json = \
                    create_report_post(self.server.base_dir,
                                       nickname,
                                       self.server.domain, self.server.port,
                                       self.server.http_prefix,
                                       mentions_str + fields['message'],
                                       False, False, True,
                                       filename, attachment_media_type,
                                       fields['imageDescription'],
                                       city,
                                       self.server.debug, fields['subject'],
                                       fields['languagesDropdown'],
                                       self.server.low_bandwidth,
                                       self.server.content_license_url,
                                       languages_understood,
                                       self.server.translate)
                if message_json:
                    if self._post_to_outbox(message_json,
                                            self.server.project_version,
                                            nickname,
                                            curr_session, proxy_type):
                        return 1
                    return -1
            elif post_type == 'newquestion':
                if not fields.get('duration'):
                    return -1
                if not fields.get('message'):
                    return -1
#                questionStr = fields['message']
                q_options = []
                for question_ctr in range(8):
                    if fields.get('questionOption' + str(question_ctr)):
                        q_options.append(fields['questionOption' +
                                                str(question_ctr)])
                if not q_options:
                    return -1
                city = get_spoofed_city(self.server.city,
                                        self.server.base_dir,
                                        nickname,
                                        self.server.domain)
                if isinstance(fields['duration'], str):
                    if len(fields['duration']) > 5:
                        return -1
                int_duration_days = int(fields['duration'])
                languages_understood = \
                    get_understood_languages(self.server.base_dir,
                                             self.server.http_prefix,
                                             nickname,
                                             self.server.domain_full,
                                             self.server.person_cache)
                message_json = \
                    create_question_post(self.server.base_dir,
                                         nickname,
                                         self.server.domain,
                                         self.server.port,
                                         self.server.http_prefix,
                                         fields['message'], q_options,
                                         False, False,
                                         comments_enabled,
                                         filename, attachment_media_type,
                                         fields['imageDescription'],
                                         city,
                                         fields['subject'],
                                         int_duration_days,
                                         fields['languagesDropdown'],
                                         self.server.low_bandwidth,
                                         self.server.content_license_url,
                                         languages_understood,
                                         self.server.translate)
                if message_json:
                    if self.server.debug:
                        print('DEBUG: new Question')
                    if self._post_to_outbox(message_json,
                                            self.server.project_version,
                                            nickname,
                                            curr_session, proxy_type):
                        return 1
                return -1
            elif post_type in ('newshare', 'newwanted'):
                if not fields.get('itemQty'):
                    print(post_type + ' no itemQty')
                    return -1
                if not fields.get('itemType'):
                    print(post_type + ' no itemType')
                    return -1
                if 'itemPrice' not in fields:
                    print(post_type + ' no itemPrice')
                    return -1
                if 'itemCurrency' not in fields:
                    print(post_type + ' no itemCurrency')
                    return -1
                if not fields.get('category'):
                    print(post_type + ' no category')
                    return -1
                if not fields.get('duration'):
                    print(post_type + ' no duratio')
                    return -1
                if attachment_media_type:
                    if attachment_media_type != 'image':
                        print('Attached media is not an image')
                        return -1
                duration_str = fields['duration']
                if duration_str:
                    if ' ' not in duration_str:
                        duration_str = duration_str + ' days'
                city = get_spoofed_city(self.server.city,
                                        self.server.base_dir,
                                        nickname,
                                        self.server.domain)
                item_qty = 1
                if fields['itemQty']:
                    if is_float(fields['itemQty']):
                        item_qty = float(fields['itemQty'])
                item_price = "0.00"
                item_currency = "EUR"
                if fields['itemPrice']:
                    item_price, item_currency = \
                        get_price_from_string(fields['itemPrice'])
                if fields['itemCurrency']:
                    item_currency = fields['itemCurrency']
                if post_type == 'newshare':
                    print('Adding shared item')
                    shares_file_type = 'shares'
                else:
                    print('Adding wanted item')
                    shares_file_type = 'wanted'
                add_share(self.server.base_dir,
                          self.server.http_prefix,
                          nickname,
                          self.server.domain, self.server.port,
                          fields['subject'],
                          fields['message'],
                          filename,
                          item_qty, fields['itemType'],
                          fields['category'],
                          fields['location'],
                          duration_str,
                          self.server.debug,
                          city, item_price, item_currency,
                          fields['languagesDropdown'],
                          self.server.translate, shares_file_type,
                          self.server.low_bandwidth,
                          self.server.content_license_url)
                if filename:
                    if os.path.isfile(filename):
                        try:
                            os.remove(filename)
                        except OSError:
                            print('EX: _receive_new_post_process ' +
                                  'unable to delete ' + filename)
                self.post_to_nickname = nickname
                return 1
        return -1

    def _receive_new_post(self, post_type: str, path: str,
                          calling_domain: str, cookie: str,
                          content_license_url: str,
                          curr_session, proxy_type: str) -> int:
        """A new post has been created
        This creates a thread to send the new post
        """
        page_number = 1
        original_path = path

        if '/users/' not in path:
            print('Not receiving new post for ' + path +
                  ' because /users/ not in path')
            return None

        if '?' + post_type + '?' not in path:
            print('Not receiving new post for ' + path +
                  ' because ?' + post_type + '? not in path')
            return None

        print('New post begins: ' + post_type + ' ' + path)

        if '?page=' in path:
            page_number_str = path.split('?page=')[1]
            if '?' in page_number_str:
                page_number_str = page_number_str.split('?')[0]
            if '#' in page_number_str:
                page_number_str = page_number_str.split('#')[0]
            if len(page_number_str) > 5:
                page_number_str = "1"
            if page_number_str.isdigit():
                page_number = int(page_number_str)
                path = path.split('?page=')[0]

        # get the username who posted
        new_post_thread_name = None
        if '/users/' in path:
            new_post_thread_name = path.split('/users/')[1]
            if '/' in new_post_thread_name:
                new_post_thread_name = new_post_thread_name.split('/')[0]
        if not new_post_thread_name:
            new_post_thread_name = '*'

        if self.server.new_post_thread.get(new_post_thread_name):
            print('Waiting for previous new post thread to end')
            wait_ctr = 0
            np_thread = self.server.new_post_thread[new_post_thread_name]
            while np_thread.is_alive() and wait_ctr < 8:
                time.sleep(1)
                wait_ctr += 1
            if wait_ctr >= 8:
                print('Killing previous new post thread for ' +
                      new_post_thread_name)
                np_thread.kill()

        # make a copy of self.headers
        headers = copy.deepcopy(self.headers)
        headers_without_cookie = copy.deepcopy(headers)
        if 'cookie' in headers_without_cookie:
            del headers_without_cookie['cookie']
        if 'Cookie' in headers_without_cookie:
            del headers_without_cookie['Cookie']
        print('New post headers: ' + str(headers_without_cookie))

        length = int(headers['Content-Length'])
        if length > self.server.max_post_length:
            print('POST size too large')
            return None

        if not headers.get('Content-Type'):
            if headers.get('Content-type'):
                headers['Content-Type'] = headers['Content-type']
            elif headers.get('content-type'):
                headers['Content-Type'] = headers['content-type']
        if headers.get('Content-Type'):
            if ' boundary=' in headers['Content-Type']:
                boundary = headers['Content-Type'].split('boundary=')[1]
                if ';' in boundary:
                    boundary = boundary.split(';')[0]

                try:
                    post_bytes = self.rfile.read(length)
                except SocketError as ex:
                    if ex.errno == errno.ECONNRESET:
                        print('WARN: POST post_bytes ' +
                              'connection reset by peer')
                    else:
                        print('WARN: POST post_bytes socket error')
                    return None
                except ValueError as ex:
                    print('EX: POST post_bytes rfile.read failed, ' +
                          str(ex))
                    return None

                # second length check from the bytes received
                # since Content-Length could be untruthful
                length = len(post_bytes)
                if length > self.server.max_post_length:
                    print('POST size too large')
                    return None

                # Note sending new posts needs to be synchronous,
                # otherwise any attachments can get mangled if
                # other events happen during their decoding
                print('Creating new post from: ' + new_post_thread_name)
                self._receive_new_post_process(post_type,
                                               original_path,
                                               headers, length,
                                               post_bytes, boundary,
                                               calling_domain, cookie,
                                               content_license_url,
                                               curr_session, proxy_type)
        return page_number

    def _crypto_ap_iread_handle(self):
        """Reads handle
        """
        message_bytes = None
        max_device_id_length = 2048
        length = int(self.headers['Content-length'])
        if length >= max_device_id_length:
            print('WARN: handle post to crypto API is too long ' +
                  str(length) + ' bytes')
            return {}
        try:
            message_bytes = self.rfile.read(length)
        except SocketError as ex:
            if ex.errno == errno.ECONNRESET:
                print('WARN: handle POST message_bytes ' +
                      'connection reset by peer')
            else:
                print('WARN: handle POST message_bytes socket error')
            return {}
        except ValueError as ex:
            print('EX: handle POST message_bytes rfile.read failed ' +
                  str(ex))
            return {}

        len_message = len(message_bytes)
        if len_message > 2048:
            print('WARN: handle post to crypto API is too long ' +
                  str(len_message) + ' bytes')
            return {}

        handle = message_bytes.decode("utf-8")
        if not handle:
            return None
        if '@' not in handle:
            return None
        if '[' in handle:
            return json.loads(message_bytes)
        if handle.startswith('@'):
            handle = handle[1:]
        if '@' not in handle:
            return None
        return handle.strip()

    def _crypto_ap_iread_json(self) -> {}:
        """Obtains json from POST to the crypto API
        """
        message_bytes = None
        max_crypto_message_length = 10240
        length = int(self.headers['Content-length'])
        if length >= max_crypto_message_length:
            print('WARN: post to crypto API is too long ' +
                  str(length) + ' bytes')
            return {}
        try:
            message_bytes = self.rfile.read(length)
        except SocketError as ex:
            if ex.errno == errno.ECONNRESET:
                print('WARN: POST message_bytes ' +
                      'connection reset by peer')
            else:
                print('WARN: POST message_bytes socket error')
            return {}
        except ValueError as ex:
            print('EX: POST message_bytes rfile.read failed, ' + str(ex))
            return {}

        len_message = len(message_bytes)
        if len_message > 10240:
            print('WARN: post to crypto API is too long ' +
                  str(len_message) + ' bytes')
            return {}

        return json.loads(message_bytes)

    def _crypto_api_query(self, calling_domain: str,
                          referer_domain: str) -> bool:
        handle = self._crypto_ap_iread_handle()
        if not handle:
            return False
        if isinstance(handle, str):
            person_dir = self.server.base_dir + '/accounts/' + handle
            if not os.path.isdir(person_dir + '/devices'):
                return False
            devices_list = []
            for _, _, files in os.walk(person_dir + '/devices'):
                for fname in files:
                    device_filename = \
                        os.path.join(person_dir + '/devices', fname)
                    if not os.path.isfile(device_filename):
                        continue
                    content_json = load_json(device_filename)
                    if content_json:
                        devices_list.append(content_json)
                break
            # return the list of devices for this handle
            msg_str = \
                json.dumps(devices_list, ensure_ascii=False)
            msg_str = self._convert_domains(calling_domain,
                                            referer_domain,
                                            msg_str)
            msg = msg_str.encode('utf-8')
            msglen = len(msg)
            accept_str = self.headers['Accept']
            protocol_str = \
                get_json_content_from_accept(accept_str)
            self._set_headers(protocol_str, msglen,
                              None, calling_domain, False)
            self._write(msg)
            return True
        return False

    def _crypto_api(self, path: str, authorized: bool,
                    calling_domain: str, referer_domain: str) -> None:
        """POST or GET with the crypto API
        """
        if authorized and path.startswith('/api/v1/crypto/keys/upload'):
            # register a device to an authorized account
            if not self.authorized_nickname:
                self._400()
                return
            device_keys = self._crypto_ap_iread_json()
            if not device_keys:
                self._400()
                return
            if isinstance(device_keys, dict):
                if not e2e_evalid_device(device_keys):
                    self._400()
                    return
                fingerprint_key = \
                    device_keys['fingerprint_key']['publicKeyBase64']
                e2e_eadd_device(self.server.base_dir,
                                self.authorized_nickname,
                                self.server.domain,
                                device_keys['deviceId'],
                                device_keys['name'],
                                device_keys['claim'],
                                fingerprint_key,
                                device_keys['identityKey']['publicKeyBase64'],
                                device_keys['fingerprint_key']['type'],
                                device_keys['identityKey']['type'])
                self._200()
                return
            self._400()
        elif path.startswith('/api/v1/crypto/keys/query'):
            # given a handle (nickname@domain) return a list of the devices
            # registered to that handle
            if not self._crypto_api_query(calling_domain, referer_domain):
                self._400()
        elif path.startswith('/api/v1/crypto/keys/claim'):
            # TODO
            self._200()
        elif authorized and path.startswith('/api/v1/crypto/delivery'):
            # TODO
            self._200()
        elif (authorized and
              path.startswith('/api/v1/crypto/encrypted_messages/clear')):
            # TODO
            self._200()
        elif path.startswith('/api/v1/crypto/encrypted_messages'):
            # TODO
            self._200()
        else:
            self._400()

    def do_POST(self):
        proxy_type = self.server.proxy_type
        postreq_start_time = time.time()

        if self.server.debug:
            print('DEBUG: POST to ' + self.server.base_dir +
                  ' path: ' + self.path + ' busy: ' +
                  str(self.server.postreq_busy))

        calling_domain = self.server.domain_full
        if self.headers.get('Host'):
            calling_domain = decoded_host(self.headers['Host'])
            if self.server.onion_domain:
                if calling_domain not in (self.server.domain,
                                          self.server.domain_full,
                                          self.server.onion_domain):
                    print('POST domain blocked: ' + calling_domain)
                    self._400()
                    return
            elif self.server.i2p_domain:
                if calling_domain not in (self.server.domain,
                                          self.server.domain_full,
                                          self.server.i2p_domain):
                    print('POST domain blocked: ' + calling_domain)
                    self._400()
                    return
            else:
                if calling_domain not in (self.server.domain,
                                          self.server.domain_full):
                    print('POST domain blocked: ' + calling_domain)
                    self._400()
                    return

        curr_time_postreq = int(time.time() * 1000)
        if self.server.postreq_busy:
            if curr_time_postreq - self.server.last_postreq < 500:
                self.send_response(429)
                self.end_headers()
                return
        self.server.postreq_busy = True
        self.server.last_postreq = curr_time_postreq

        ua_str = self._get_user_agent()

        block, self.server.blocked_cache_last_updated = \
            blocked_user_agent(calling_domain, ua_str,
                               self.server.news_instance,
                               self.server.debug,
                               self.server.user_agents_blocked,
                               self.server.blocked_cache_last_updated,
                               self.server.base_dir,
                               self.server.blocked_cache,
                               self.server.blocked_cache_update_secs,
                               self.server.crawlers_allowed,
                               self.server.known_bots)
        if block:
            self._400()
            self.server.postreq_busy = False
            return

        if not self.headers.get('Content-type'):
            print('Content-type header missing')
            self._400()
            self.server.postreq_busy = False
            return

        curr_session, proxy_type = \
            get_session_for_domain(self.server, calling_domain)

        curr_session = \
            self._establish_session("POST", curr_session,
                                    proxy_type)
        if not curr_session:
            fitness_performance(postreq_start_time, self.server.fitness,
                                '_POST', 'create_session',
                                self.server.debug)
            self._404()
            self.server.postreq_busy = False
            return

        # returns after this point should set postreq_busy to False

        # remove any trailing slashes from the path
        if not self.path.endswith('confirm'):
            self.path = self.path.replace('/outbox/', '/outbox')
            self.path = self.path.replace('/tlblogs/', '/tlblogs')
            self.path = self.path.replace('/inbox/', '/inbox')
            self.path = self.path.replace('/shares/', '/shares')
            self.path = self.path.replace('/wanted/', '/wanted')
            self.path = self.path.replace('/sharedInbox/', '/sharedInbox')

        if self.path == '/inbox':
            if not self.server.enable_shared_inbox:
                self._503()
                self.server.postreq_busy = False
                return

        cookie = None
        if self.headers.get('Cookie'):
            cookie = self.headers['Cookie']

        # check authorization
        authorized = self._is_authorized()
        if not authorized and self.server.debug:
            print('POST Not authorized')
            print(str(self.headers))

        if self.path.startswith('/api/v1/crypto/'):
            self._crypto_api(self.path, authorized,
                             calling_domain, calling_domain)
            self.server.postreq_busy = False
            return

        # if this is a POST to the outbox then check authentication
        self.outbox_authenticated = False
        self.post_to_nickname = None

        fitness_performance(postreq_start_time, self.server.fitness,
                            '_POST', 'start',
                            self.server.debug)

        # POST to login screen, containing credentials
        if self.path.startswith('/login'):
            self._post_login_screen(calling_domain, cookie,
                                    self.server.base_dir,
                                    self.server.http_prefix,
                                    self.server.domain,
                                    self.server.domain_full,
                                    self.server.port,
                                    self.server.onion_domain,
                                    self.server.i2p_domain,
                                    ua_str, self.server.debug)
            self.server.postreq_busy = False
            return

        fitness_performance(postreq_start_time, self.server.fitness,
                            '_POST', '_login_screen',
                            self.server.debug)

        if authorized and self.path.endswith('/sethashtagcategory'):
            self._set_hashtag_category(calling_domain, cookie,
                                       self.path,
                                       self.server.base_dir,
                                       self.server.domain,
                                       self.server.debug,
                                       self.server.system_language)
            self.server.postreq_busy = False
            return

        # update of profile/avatar from web interface,
        # after selecting Edit button then Submit
        if authorized and self.path.endswith('/profiledata'):
            self._profile_edit(calling_domain, cookie, self.path,
                               self.server.base_dir, self.server.http_prefix,
                               self.server.domain,
                               self.server.domain_full,
                               self.server.onion_domain,
                               self.server.i2p_domain, self.server.debug,
                               self.server.allow_local_network_access,
                               self.server.system_language,
                               self.server.content_license_url,
                               curr_session,
                               proxy_type)
            self.server.postreq_busy = False
            return

        if authorized and self.path.endswith('/linksdata'):
            self._links_update(calling_domain, cookie, self.path,
                               self.server.base_dir, self.server.debug,
                               self.server.default_timeline,
                               self.server.allow_local_network_access)
            self.server.postreq_busy = False
            return

        if authorized and self.path.endswith('/newswiredata'):
            self._newswire_update(calling_domain, cookie,
                                  self.path,
                                  self.server.base_dir,
                                  self.server.domain, self.server.debug,
                                  self.server.default_timeline)
            self.server.postreq_busy = False
            return

        if authorized and self.path.endswith('/citationsdata'):
            self._citations_update(calling_domain, cookie,
                                   self.path,
                                   self.server.base_dir,
                                   self.server.domain,
                                   self.server.debug,
                                   self.server.newswire)
            self.server.postreq_busy = False
            return

        if authorized and self.path.endswith('/newseditdata'):
            self._news_post_edit(calling_domain, cookie, self.path,
                                 self.server.base_dir,
                                 self.server.domain,
                                 self.server.debug)
            self.server.postreq_busy = False
            return

        fitness_performance(postreq_start_time, self.server.fitness,
                            '_POST', '_news_post_edit',
                            self.server.debug)

        users_in_path = False
        if '/users/' in self.path:
            users_in_path = True

        # moderator action buttons
        if authorized and users_in_path and \
           self.path.endswith('/moderationaction'):
            self._moderator_actions(self.path, calling_domain, cookie,
                                    self.server.base_dir,
                                    self.server.http_prefix,
                                    self.server.domain,
                                    self.server.port,
                                    self.server.debug)
            self.server.postreq_busy = False
            return

        fitness_performance(postreq_start_time, self.server.fitness,
                            '_POST', '_moderator_actions',
                            self.server.debug)

        search_for_emoji = False
        if self.path.endswith('/searchhandleemoji'):
            search_for_emoji = True
            self.path = self.path.replace('/searchhandleemoji',
                                          '/searchhandle')
            if self.server.debug:
                print('DEBUG: searching for emoji')
                print('authorized: ' + str(authorized))

        fitness_performance(postreq_start_time, self.server.fitness,
                            '_POST', 'searchhandleemoji',
                            self.server.debug)

        # a search was made
        if ((authorized or search_for_emoji) and
            (self.path.endswith('/searchhandle') or
             '/searchhandle?page=' in self.path)):
            self._receive_search_query(calling_domain, cookie,
                                       authorized, self.path,
                                       self.server.base_dir,
                                       self.server.http_prefix,
                                       self.server.domain,
                                       self.server.domain_full,
                                       self.server.port,
                                       search_for_emoji,
                                       self.server.onion_domain,
                                       self.server.i2p_domain,
                                       postreq_start_time,
                                       self.server.debug,
                                       curr_session,
                                       proxy_type)
            self.server.postreq_busy = False
            return

        fitness_performance(postreq_start_time, self.server.fitness,
                            '_POST', '_receive_search_query',
                            self.server.debug)

        if not authorized:
            if self.path.endswith('/rmpost'):
                print('ERROR: attempt to remove post was not authorized. ' +
                      self.path)
                self._400()
                self.server.postreq_busy = False
                return
        else:
            # a vote/question/poll is posted
            if self.path.endswith('/question') or \
               '/question?page=' in self.path:
                self._receive_vote(calling_domain, cookie,
                                   self.path,
                                   self.server.http_prefix,
                                   self.server.domain_full,
                                   self.server.onion_domain,
                                   self.server.i2p_domain,
                                   curr_session,
                                   proxy_type)
                self.server.postreq_busy = False
                return

            # removes a shared item
            if self.path.endswith('/rmshare'):
                self._remove_share(calling_domain, cookie,
                                   authorized, self.path,
                                   self.server.base_dir,
                                   self.server.http_prefix,
                                   self.server.domain_full,
                                   self.server.onion_domain,
                                   self.server.i2p_domain)
                self.server.postreq_busy = False
                return

            # removes a wanted item
            if self.path.endswith('/rmwanted'):
                self._remove_wanted(calling_domain, cookie,
                                    authorized, self.path,
                                    self.server.base_dir,
                                    self.server.http_prefix,
                                    self.server.domain_full,
                                    self.server.onion_domain,
                                    self.server.i2p_domain)
                self.server.postreq_busy = False
                return

            fitness_performance(postreq_start_time, self.server.fitness,
                                '_POST', '_remove_wanted',
                                self.server.debug)

            # removes a post
            if self.path.endswith('/rmpost'):
                if '/users/' not in self.path:
                    print('ERROR: attempt to remove post ' +
                          'was not authorized. ' + self.path)
                    self._400()
                    self.server.postreq_busy = False
                    return
            if self.path.endswith('/rmpost'):
                self._receive_remove_post(calling_domain, cookie,
                                          self.path,
                                          self.server.base_dir,
                                          self.server.http_prefix,
                                          self.server.domain,
                                          self.server.domain_full,
                                          self.server.onion_domain,
                                          self.server.i2p_domain,
                                          curr_session, proxy_type)
                self.server.postreq_busy = False
                return

            fitness_performance(postreq_start_time, self.server.fitness,
                                '_POST', '_remove_post',
                                self.server.debug)

            # decision to follow in the web interface is confirmed
            if self.path.endswith('/followconfirm'):
                self._follow_confirm(calling_domain, cookie,
                                     self.path,
                                     self.server.base_dir,
                                     self.server.http_prefix,
                                     self.server.domain,
                                     self.server.domain_full,
                                     self.server.port,
                                     self.server.onion_domain,
                                     self.server.i2p_domain,
                                     self.server.debug,
                                     curr_session,
                                     proxy_type)
                self.server.postreq_busy = False
                return

            fitness_performance(postreq_start_time, self.server.fitness,
                                '_POST', '_follow_confirm',
                                self.server.debug)

            # decision to unfollow in the web interface is confirmed
            if self.path.endswith('/unfollowconfirm'):
                self._unfollow_confirm(calling_domain, cookie,
                                       self.path,
                                       self.server.base_dir,
                                       self.server.http_prefix,
                                       self.server.domain,
                                       self.server.domain_full,
                                       self.server.port,
                                       self.server.onion_domain,
                                       self.server.i2p_domain,
                                       self.server.debug,
                                       curr_session, proxy_type)
                self.server.postreq_busy = False
                return

            fitness_performance(postreq_start_time, self.server.fitness,
                                '_POST', '_unfollow_confirm',
                                self.server.debug)

            # decision to unblock in the web interface is confirmed
            if self.path.endswith('/unblockconfirm'):
                self._unblock_confirm(calling_domain, cookie,
                                      self.path,
                                      self.server.base_dir,
                                      self.server.http_prefix,
                                      self.server.domain,
                                      self.server.domain_full,
                                      self.server.port,
                                      self.server.onion_domain,
                                      self.server.i2p_domain,
                                      self.server.debug)
                self.server.postreq_busy = False
                return

            fitness_performance(postreq_start_time, self.server.fitness,
                                '_POST', '_unblock_confirm',
                                self.server.debug)

            # decision to block in the web interface is confirmed
            if self.path.endswith('/blockconfirm'):
                self._block_confirm(calling_domain, cookie,
                                    self.path,
                                    self.server.base_dir,
                                    self.server.http_prefix,
                                    self.server.domain,
                                    self.server.domain_full,
                                    self.server.port,
                                    self.server.onion_domain,
                                    self.server.i2p_domain,
                                    self.server.debug,
                                    curr_session,
                                    proxy_type)
                self.server.postreq_busy = False
                return

            fitness_performance(postreq_start_time, self.server.fitness,
                                '_POST', '_block_confirm',
                                self.server.debug)

            # an option was chosen from person options screen
            # view/follow/block/report
            if self.path.endswith('/personoptions'):
                self._person_options(self.path,
                                     calling_domain, cookie,
                                     self.server.base_dir,
                                     self.server.http_prefix,
                                     self.server.domain,
                                     self.server.domain_full,
                                     self.server.port,
                                     self.server.onion_domain,
                                     self.server.i2p_domain,
                                     self.server.debug,
                                     curr_session)
                self.server.postreq_busy = False
                return

            # Change the key shortcuts
            if users_in_path and \
               self.path.endswith('/changeAccessKeys'):
                nickname = self.path.split('/users/')[1]
                if '/' in nickname:
                    nickname = nickname.split('/')[0]

                if not self.server.key_shortcuts.get(nickname):
                    access_keys = self.server.access_keys
                    self.server.key_shortcuts[nickname] = access_keys.copy()
                access_keys = self.server.key_shortcuts[nickname]

                self._key_shortcuts(calling_domain, cookie,
                                    self.server.base_dir,
                                    self.server.http_prefix,
                                    nickname,
                                    self.server.domain,
                                    self.server.domain_full,
                                    self.server.onion_domain,
                                    self.server.i2p_domain,
                                    access_keys,
                                    self.server.default_timeline)
                self.server.postreq_busy = False
                return

            # theme designer submit/cancel button
            if users_in_path and \
               self.path.endswith('/changeThemeSettings'):
                nickname = self.path.split('/users/')[1]
                if '/' in nickname:
                    nickname = nickname.split('/')[0]

                if not self.server.key_shortcuts.get(nickname):
                    access_keys = self.server.access_keys
                    self.server.key_shortcuts[nickname] = access_keys.copy()
                access_keys = self.server.key_shortcuts[nickname]
                allow_local_network_access = \
                    self.server.allow_local_network_access

                self._theme_designer_edit(calling_domain, cookie,
                                          self.server.base_dir,
                                          self.server.http_prefix,
                                          nickname,
                                          self.server.domain,
                                          self.server.domain_full,
                                          self.server.onion_domain,
                                          self.server.i2p_domain,
                                          self.server.default_timeline,
                                          self.server.theme_name,
                                          allow_local_network_access,
                                          self.server.system_language,
                                          self.server.dyslexic_font)
                self.server.postreq_busy = False
                return

        # update the shared item federation token for the calling domain
        # if it is within the permitted federation
        if self.headers.get('Origin') and \
           self.headers.get('SharesCatalog'):
            if self.server.debug:
                print('SharesCatalog header: ' + self.headers['SharesCatalog'])
            if not self.server.shared_items_federated_domains:
                si_domains_str = \
                    get_config_param(self.server.base_dir,
                                     'sharedItemsFederatedDomains')
                if si_domains_str:
                    if self.server.debug:
                        print('Loading shared items federated domains list')
                    si_domains_list = si_domains_str.split(',')
                    domains_list = self.server.shared_items_federated_domains
                    for si_domain in si_domains_list:
                        domains_list.append(si_domain.strip())
            origin_domain = self.headers.get('Origin')
            if origin_domain != self.server.domain_full and \
               origin_domain != self.server.onion_domain and \
               origin_domain != self.server.i2p_domain and \
               origin_domain in self.server.shared_items_federated_domains:
                if self.server.debug:
                    print('DEBUG: ' +
                          'POST updating shared item federation ' +
                          'token for ' + origin_domain + ' to ' +
                          self.server.domain_full)
                shared_item_tokens = self.server.shared_item_federation_tokens
                shares_token = self.headers['SharesCatalog']
                self.server.shared_item_federation_tokens = \
                    update_shared_item_federation_token(self.server.base_dir,
                                                        origin_domain,
                                                        shares_token,
                                                        self.server.debug,
                                                        shared_item_tokens)
            elif self.server.debug:
                fed_domains = self.server.shared_items_federated_domains
                if origin_domain not in fed_domains:
                    print('origin_domain is not in federated domains list ' +
                          origin_domain)
                else:
                    print('origin_domain is not a different instance. ' +
                          origin_domain + ' ' + self.server.domain_full + ' ' +
                          str(fed_domains))

        fitness_performance(postreq_start_time, self.server.fitness,
                            '_POST', 'SharesCatalog',
                            self.server.debug)

        # receive different types of post created by html_new_post
        new_post_endpoints = get_new_post_endpoints()
        for curr_post_type in new_post_endpoints:
            if not authorized:
                if self.server.debug:
                    print('POST was not authorized')
                break

            post_redirect = self.server.default_timeline
            if curr_post_type == 'newshare':
                post_redirect = 'tlshares'
            elif curr_post_type == 'newwanted':
                post_redirect = 'tlwanted'

            page_number = \
                self._receive_new_post(curr_post_type, self.path,
                                       calling_domain, cookie,
                                       self.server.content_license_url,
                                       curr_session, proxy_type)
            if page_number:
                print(curr_post_type + ' post received')
                nickname = self.path.split('/users/')[1]
                if '?' in nickname:
                    nickname = nickname.split('?')[0]
                if '/' in nickname:
                    nickname = nickname.split('/')[0]

                if calling_domain.endswith('.onion') and \
                   self.server.onion_domain:
                    actor_path_str = \
                        local_actor_url('http', nickname,
                                        self.server.onion_domain) + \
                        '/' + post_redirect + \
                        '?page=' + str(page_number)
                    self._redirect_headers(actor_path_str, cookie,
                                           calling_domain)
                elif (calling_domain.endswith('.i2p') and
                      self.server.i2p_domain):
                    actor_path_str = \
                        local_actor_url('http', nickname,
                                        self.server.i2p_domain) + \
                        '/' + post_redirect + \
                        '?page=' + str(page_number)
                    self._redirect_headers(actor_path_str, cookie,
                                           calling_domain)
                else:
                    actor_path_str = \
                        local_actor_url(self.server.http_prefix, nickname,
                                        self.server.domain_full) + \
                        '/' + post_redirect + '?page=' + str(page_number)
                    self._redirect_headers(actor_path_str, cookie,
                                           calling_domain)
                self.server.postreq_busy = False
                return

        fitness_performance(postreq_start_time, self.server.fitness,
                            '_POST', 'receive post',
                            self.server.debug)

        if self.path.endswith('/outbox') or \
           self.path.endswith('/wanted') or \
           self.path.endswith('/shares'):
            if users_in_path:
                if authorized:
                    self.outbox_authenticated = True
                    path_users_section = self.path.split('/users/')[1]
                    self.post_to_nickname = path_users_section.split('/')[0]
            if not self.outbox_authenticated:
                self.send_response(405)
                self.end_headers()
                self.server.postreq_busy = False
                return

        fitness_performance(postreq_start_time, self.server.fitness,
                            '_POST', 'authorized',
                            self.server.debug)

        # check that the post is to an expected path
        if not (self.path.endswith('/outbox') or
                self.path.endswith('/inbox') or
                self.path.endswith('/wanted') or
                self.path.endswith('/shares') or
                self.path.endswith('/moderationaction') or
                self.path == '/sharedInbox'):
            print('Attempt to POST to invalid path ' + self.path)
            self._400()
            self.server.postreq_busy = False
            return

        fitness_performance(postreq_start_time, self.server.fitness,
                            '_POST', 'check path',
                            self.server.debug)

        is_media_content = False
        if self.headers['Content-type'].startswith('image/') or \
           self.headers['Content-type'].startswith('video/') or \
           self.headers['Content-type'].startswith('audio/'):
            is_media_content = True

        # check that the content length string is not too long
        if isinstance(self.headers['Content-length'], str):
            if not is_media_content:
                max_content_size = len(str(self.server.maxMessageLength))
            else:
                max_content_size = len(str(self.server.maxMediaSize))
            if len(self.headers['Content-length']) > max_content_size:
                self._400()
                self.server.postreq_busy = False
                return

        # read the message and convert it into a python dictionary
        length = int(self.headers['Content-length'])
        if self.server.debug:
            print('DEBUG: content-length: ' + str(length))
        if not is_media_content:
            if length > self.server.maxMessageLength:
                print('Maximum message length exceeded ' + str(length))
                self._400()
                self.server.postreq_busy = False
                return
        else:
            if length > self.server.maxMediaSize:
                print('Maximum media size exceeded ' + str(length))
                self._400()
                self.server.postreq_busy = False
                return

        # receive images to the outbox
        if self.headers['Content-type'].startswith('image/') and \
           users_in_path:
            self._receive_image(length, self.path,
                                self.server.base_dir,
                                self.server.domain,
                                self.server.debug)
            self.server.postreq_busy = False
            return

        # refuse to receive non-json content
        content_type_str = self.headers['Content-type']
        if not content_type_str.startswith('application/json') and \
           not content_type_str.startswith('application/activity+json') and \
           not content_type_str.startswith('application/ld+json'):
            print("POST is not json: " + self.headers['Content-type'])
            if self.server.debug:
                print(str(self.headers))
                length = int(self.headers['Content-length'])
                if length < self.server.max_post_length:
                    try:
                        unknown_post = self.rfile.read(length).decode('utf-8')
                    except SocketError as ex:
                        if ex.errno == errno.ECONNRESET:
                            print('EX: POST unknown_post ' +
                                  'connection reset by peer')
                        else:
                            print('EX: POST unknown_post socket error')
                        self.send_response(400)
                        self.end_headers()
                        self.server.postreq_busy = False
                        return
                    except ValueError as ex:
                        print('EX: POST unknown_post rfile.read failed, ' +
                              str(ex))
                        self.send_response(400)
                        self.end_headers()
                        self.server.postreq_busy = False
                        return
                    print(str(unknown_post))
            self._400()
            self.server.postreq_busy = False
            return

        if self.server.debug:
            print('DEBUG: Reading message')

        fitness_performance(postreq_start_time, self.server.fitness,
                            '_POST', 'check content type',
                            self.server.debug)

        # check content length before reading bytes
        if self.path in ('/sharedInbox', '/inbox'):
            length = 0
            if self.headers.get('Content-length'):
                length = int(self.headers['Content-length'])
            elif self.headers.get('Content-Length'):
                length = int(self.headers['Content-Length'])
            elif self.headers.get('content-length'):
                length = int(self.headers['content-length'])
            if length > 10240:
                print('WARN: post to shared inbox is too long ' +
                      str(length) + ' bytes')
                self._400()
                self.server.postreq_busy = False
                return

        try:
            message_bytes = self.rfile.read(length)
        except SocketError as ex:
            if ex.errno == errno.ECONNRESET:
                print('WARN: POST message_bytes ' +
                      'connection reset by peer')
            else:
                print('WARN: POST message_bytes socket error')
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return
        except ValueError as ex:
            print('EX: POST message_bytes rfile.read failed, ' + str(ex))
            self.send_response(400)
            self.end_headers()
            self.server.postreq_busy = False
            return

        # check content length after reading bytes
        if self.path in ('/sharedInbox', '/inbox'):
            len_message = len(message_bytes)
            if len_message > 10240:
                print('WARN: post to shared inbox is too long ' +
                      str(len_message) + ' bytes')
                self._400()
                self.server.postreq_busy = False
                return

        if contains_invalid_chars(message_bytes.decode("utf-8")):
            self._400()
            self.server.postreq_busy = False
            return

        # convert the raw bytes to json
        message_json = json.loads(message_bytes)

        fitness_performance(postreq_start_time, self.server.fitness,
                            '_POST', 'load json',
                            self.server.debug)

        # https://www.w3.org/TR/activitypub/#object-without-create
        if self.outbox_authenticated:
            if self._post_to_outbox(message_json,
                                    self.server.project_version, None,
                                    curr_session, proxy_type):
                if message_json.get('id'):
                    locn_str = remove_id_ending(message_json['id'])
                    self.headers['Location'] = locn_str
                self.send_response(201)
                self.end_headers()
                self.server.postreq_busy = False
                return
            else:
                if self.server.debug:
                    print('Failed to post to outbox')
                self.send_response(403)
                self.end_headers()
                self.server.postreq_busy = False
                return

        fitness_performance(postreq_start_time, self.server.fitness,
                            '_POST', '_post_to_outbox',
                            self.server.debug)

        # check the necessary properties are available
        if self.server.debug:
            print('DEBUG: Check message has params')

        if not message_json:
            self.send_response(403)
            self.end_headers()
            self.server.postreq_busy = False
            return

        if self.path.endswith('/inbox') or \
           self.path == '/sharedInbox':
            if not inbox_message_has_params(message_json):
                if self.server.debug:
                    print("DEBUG: inbox message doesn't have the " +
                          "required parameters")
                self.send_response(403)
                self.end_headers()
                self.server.postreq_busy = False
                return

        fitness_performance(postreq_start_time, self.server.fitness,
                            '_POST', 'inbox_message_has_params',
                            self.server.debug)

        header_signature = self._getheader_signature_input()

        if header_signature:
            if 'keyId=' not in header_signature:
                if self.server.debug:
                    print('DEBUG: POST to inbox has no keyId in ' +
                          'header signature parameter')
                self.send_response(403)
                self.end_headers()
                self.server.postreq_busy = False
                return

        fitness_performance(postreq_start_time, self.server.fitness,
                            '_POST', 'keyId check',
                            self.server.debug)

        if not self.server.unit_test:
            if not inbox_permitted_message(self.server.domain,
                                           message_json,
                                           self.server.federation_list):
                if self.server.debug:
                    # https://www.youtube.com/watch?v=K3PrSj9XEu4
                    print('DEBUG: Ah Ah Ah')
                self.send_response(403)
                self.end_headers()
                self.server.postreq_busy = False
                return

        fitness_performance(postreq_start_time, self.server.fitness,
                            '_POST', 'inbox_permitted_message',
                            self.server.debug)

        if self.server.debug:
            print('INBOX: POST saving to inbox queue')
        if users_in_path:
            path_users_section = self.path.split('/users/')[1]
            if '/' not in path_users_section:
                if self.server.debug:
                    print('INBOX: This is not a users endpoint')
            else:
                self.post_to_nickname = path_users_section.split('/')[0]
                if self.post_to_nickname:
                    queue_status = \
                        self._update_inbox_queue(self.post_to_nickname,
                                                 message_json, message_bytes,
                                                 self.server.debug)
                    if queue_status in range(0, 4):
                        self.server.postreq_busy = False
                        return
                    if self.server.debug:
                        print('INBOX: _update_inbox_queue exited ' +
                              'without doing anything')
                else:
                    if self.server.debug:
                        print('INBOX: self.post_to_nickname is None')
            self.send_response(403)
            self.end_headers()
            self.server.postreq_busy = False
            return
        if self.path in ('/sharedInbox', '/inbox'):
            if self.server.debug:
                print('INBOX: POST to shared inbox')
            queue_status = \
                self._update_inbox_queue('inbox', message_json,
                                         message_bytes,
                                         self.server.debug)
            if queue_status in range(0, 4):
                self.server.postreq_busy = False
                return
        self._200()
        self.server.postreq_busy = False


class PubServerUnitTest(PubServer):
    protocol_version = 'HTTP/1.0'


class EpicyonServer(ThreadingHTTPServer):
    def handle_error(self, request, client_address):
        # surpress connection reset errors
        cls, e_ret = sys.exc_info()[:2]
        if cls is ConnectionResetError:
            if e_ret.errno != errno.ECONNRESET:
                print('ERROR: (EpicyonServer) ' + str(cls) + ", " + str(e_ret))
        elif cls is BrokenPipeError:
            pass
        else:
            print('ERROR: (EpicyonServer) ' + str(cls) + ", " + str(e_ret))
            return HTTPServer.handle_error(self, request, client_address)


def run_posts_queue(base_dir: str, send_threads: [], debug: bool,
                    timeout_mins: int) -> None:
    """Manages the threads used to send posts
    """
    while True:
        time.sleep(1)
        remove_dormant_threads(base_dir, send_threads, debug, timeout_mins)


def run_shares_expire(version_number: str, base_dir: str) -> None:
    """Expires shares as needed
    """
    while True:
        time.sleep(120)
        expire_shares(base_dir)


def run_posts_watchdog(project_version: str, httpd) -> None:
    """This tries to keep the posts thread running even if it dies
    """
    print('THREAD: Starting posts queue watchdog')
    posts_queue_original = httpd.thrPostsQueue.clone(run_posts_queue)
    begin_thread(httpd.thrPostsQueue, 'run_posts_watchdog')
    while True:
        time.sleep(20)
        if httpd.thrPostsQueue.is_alive():
            continue
        httpd.thrPostsQueue.kill()
        print('THREAD: restarting posts queue')
        httpd.thrPostsQueue = posts_queue_original.clone(run_posts_queue)
        begin_thread(httpd.thrPostsQueue, 'run_posts_watchdog 2')
        print('Restarting posts queue...')


def run_shares_expire_watchdog(project_version: str, httpd) -> None:
    """This tries to keep the shares expiry thread running even if it dies
    """
    print('THREAD: Starting shares expiry watchdog')
    shares_expire_original = httpd.thrSharesExpire.clone(run_shares_expire)
    begin_thread(httpd.thrSharesExpire, 'run_shares_expire_watchdog')
    while True:
        time.sleep(20)
        if httpd.thrSharesExpire.is_alive():
            continue
        httpd.thrSharesExpire.kill()
        print('THREAD: restarting shares watchdog')
        httpd.thrSharesExpire = shares_expire_original.clone(run_shares_expire)
        begin_thread(httpd.thrSharesExpire, 'run_shares_expire_watchdog 2')
        print('Restarting shares expiry...')


def load_tokens(base_dir: str, tokens_dict: {}, tokens_lookup: {}) -> None:
    """Loads shared items access tokens for each account
    """
    for _, dirs, _ in os.walk(base_dir + '/accounts'):
        for handle in dirs:
            if '@' in handle:
                token_filename = base_dir + '/accounts/' + handle + '/.token'
                if not os.path.isfile(token_filename):
                    continue
                nickname = handle.split('@')[0]
                token = None
                try:
                    with open(token_filename, 'r',
                              encoding='utf-8') as fp_tok:
                        token = fp_tok.read()
                except OSError as ex:
                    print('WARN: Unable to read token for ' +
                          nickname + ' ' + str(ex))
                if not token:
                    continue
                tokens_dict[nickname] = token
                tokens_lookup[token] = nickname
        break


def run_daemon(map_format: str,
               clacks: str,
               preferred_podcast_formats: [],
               check_actor_timeout: int,
               crawlers_allowed: [],
               dyslexic_font: bool,
               content_license_url: str,
               lists_enabled: str,
               default_reply_interval_hrs: int,
               low_bandwidth: bool,
               max_like_count: int,
               shared_items_federated_domains: [],
               user_agents_blocked: [],
               log_login_failures: bool,
               city: str,
               show_node_info_accounts: bool,
               show_node_info_version: bool,
               broch_mode: bool,
               verify_all_signatures: bool,
               send_threads_timeout_mins: int,
               dormant_months: int,
               max_newswire_posts: int,
               allow_local_network_access: bool,
               max_feed_item_size_kb: int,
               publish_button_at_top: bool,
               rss_icon_at_top: bool,
               icons_as_buttons: bool,
               full_width_tl_button_header: bool,
               show_publish_as_icon: bool,
               max_followers: int,
               max_news_posts: int,
               max_mirrored_articles: int,
               max_newswire_feed_size_kb: int,
               max_newswire_posts_per_source: int,
               show_published_date_only: bool,
               voting_time_mins: int,
               positive_voting: bool,
               newswire_votes_threshold: int,
               news_instance: bool,
               blogs_instance: bool,
               media_instance: bool,
               max_recent_posts: int,
               enable_shared_inbox: bool, registration: bool,
               language: str, project_version: str,
               instance_id: str, client_to_server: bool,
               base_dir: str, domain: str,
               onion_domain: str, i2p_domain: str,
               yt_replace_domain: str,
               twitter_replacement_domain: str,
               port: int = 80, proxy_port: int = 80,
               http_prefix: str = 'https',
               fed_list: [] = [],
               max_mentions: int = 10, max_emoji: int = 10,
               secure_mode: bool = False,
               proxy_type: str = None, max_replies: int = 64,
               domain_max_posts_per_day: int = 8640,
               account_max_posts_per_day: int = 864,
               allow_deletion: bool = False,
               debug: bool = False, unit_test: bool = False,
               instance_only_skills_search: bool = False,
               send_threads: [] = [],
               manual_follower_approval: bool = True) -> None:
    if len(domain) == 0:
        domain = 'localhost'
    if '.' not in domain:
        if domain != 'localhost':
            print('Invalid domain: ' + domain)
            return

    if unit_test:
        server_address = (domain, proxy_port)
        pub_handler = partial(PubServerUnitTest)
    else:
        server_address = ('', proxy_port)
        pub_handler = partial(PubServer)

    if not os.path.isdir(base_dir + '/accounts'):
        print('Creating accounts directory')
        os.mkdir(base_dir + '/accounts')

    try:
        httpd = EpicyonServer(server_address, pub_handler)
    except SocketError as ex:
        if ex.errno == errno.ECONNREFUSED:
            print('EX: HTTP server address is already in use. ' +
                  str(server_address))
            return False

        print('EX: HTTP server failed to start. ' + str(ex))
        print('server_address: ' + str(server_address))
        return False

    # scan the theme directory for any svg files containing scripts
    assert not scan_themes_for_scripts(base_dir)

    # which accounts should minimize all attached images by default
    httpd.min_images_for_accounts = load_min_images_for_accounts(base_dir)

    # caches css files
    httpd.css_cache = {}

    httpd.clacks = get_config_param(base_dir, 'clacks')
    if not httpd.clacks:
        if clacks:
            httpd.clacks = clacks
        else:
            httpd.clacks = 'GNU Natalie Nguyen'

    # load a list of dogwhistle words
    dogwhistles_filename = base_dir + '/accounts/dogwhistles.txt'
    if not os.path.isfile(dogwhistles_filename):
        dogwhistles_filename = base_dir + '/default_dogwhistles.txt'
    httpd.dogwhistles = load_dogwhistles(dogwhistles_filename)

    # list of preferred podcast formats
    # eg ['audio/opus', 'audio/mp3', 'audio/speex']
    httpd.preferred_podcast_formats = preferred_podcast_formats

    # for each account, whether bold reading is enabled
    httpd.bold_reading = load_bold_reading(base_dir)

    httpd.account_timezone = load_account_timezones(base_dir)

    httpd.post_to_nickname = None

    httpd.nodeinfo_is_active = False
    httpd.security_txt_is_active = False
    httpd.vcard_is_active = False
    httpd.masto_api_is_active = False

    # use kml or gpx format for hashtag maps
    httpd.map_format = map_format.lower()

    httpd.dyslexic_font = dyslexic_font

    # license for content of the instance
    if not content_license_url:
        content_license_url = 'https://creativecommons.org/licenses/by/4.0'
    httpd.content_license_url = content_license_url

    # fitness metrics
    fitness_filename = base_dir + '/accounts/fitness.json'
    httpd.fitness = {}
    if os.path.isfile(fitness_filename):
        fitness = load_json(fitness_filename)
        if fitness is not None:
            httpd.fitness = fitness

    # initialize authorized fetch key
    httpd.signing_priv_key_pem = None

    httpd.show_node_info_accounts = show_node_info_accounts
    httpd.show_node_info_version = show_node_info_version

    # ASCII/ANSI text banner used in shell browsers, such as Lynx
    httpd.text_mode_banner = get_text_mode_banner(base_dir)

    # key shortcuts SHIFT + ALT + [key]
    httpd.access_keys = {
        'Page up': ',',
        'Page down': '.',
        'submitButton': 'y',
        'followButton': 'f',
        'blockButton': 'b',
        'infoButton': 'i',
        'snoozeButton': 's',
        'reportButton': '[',
        'viewButton': 'v',
        'unblockButton': 'u',
        'enterPetname': 'p',
        'enterNotes': 'n',
        'menuTimeline': 't',
        'menuEdit': 'e',
        'menuThemeDesigner': 'z',
        'menuProfile': 'p',
        'menuInbox': 'i',
        'menuSearch': '/',
        'menuNewPost': 'n',
        'menuNewBlog': '0',
        'menuCalendar': 'c',
        'menuDM': 'd',
        'menuReplies': 'r',
        'menuOutbox': 's',
        'menuBookmarks': 'q',
        'menuShares': 'h',
        'menuWanted': 'w',
        'menuBlogs': 'b',
        'menuNewswire': '#',
        'menuLinks': 'l',
        'menuMedia': 'm',
        'menuModeration': 'o',
        'menuFollowing': 'f',
        'menuFollowers': 'g',
        'menuRoles': 'o',
        'menuSkills': 'a',
        'menuLogout': 'x',
        'menuKeys': 'k',
        'Public': 'p',
        'Reminder': 'r'
    }

    # timeout used when getting rss feeds
    httpd.rss_timeout_sec = 20

    # timeout used when checking for actor changes when clicking an avatar
    # and entering person options screen
    if check_actor_timeout < 2:
        check_actor_timeout = 2
    httpd.check_actor_timeout = check_actor_timeout

    # how many hours after a post was published can a reply be made
    default_reply_interval_hrs = 9999999
    httpd.default_reply_interval_hrs = default_reply_interval_hrs

    # recent caldav etags for each account
    httpd.recent_dav_etags = {}

    httpd.key_shortcuts = {}
    load_access_keys_for_accounts(base_dir, httpd.key_shortcuts,
                                  httpd.access_keys)

    # wheither to use low bandwidth images
    httpd.low_bandwidth = low_bandwidth

    # list of blocked user agent types within the User-Agent header
    httpd.user_agents_blocked = user_agents_blocked

    # list of crawler bots permitted within the User-Agent header
    httpd.crawlers_allowed = crawlers_allowed

    # list of web crawlers known to the system
    httpd.known_bots = load_known_web_bots(base_dir)

    httpd.unit_test = unit_test
    httpd.allow_local_network_access = allow_local_network_access
    if unit_test:
        # unit tests are run on the local network with LAN addresses
        httpd.allow_local_network_access = True
    httpd.yt_replace_domain = yt_replace_domain
    httpd.twitter_replacement_domain = twitter_replacement_domain

    # newswire storing rss feeds
    httpd.newswire = {}

    # maximum number of posts to appear in the newswire on the right column
    httpd.max_newswire_posts = max_newswire_posts

    # whether to require that all incoming posts have valid jsonld signatures
    httpd.verify_all_signatures = verify_all_signatures

    # This counter is used to update the list of blocked domains in memory.
    # It helps to avoid touching the disk and so improves flooding resistance
    httpd.blocklistUpdateCtr = 0
    httpd.blocklistUpdateInterval = 100
    httpd.domainBlocklist = get_domain_blocklist(base_dir)

    httpd.manual_follower_approval = manual_follower_approval
    if domain.endswith('.onion'):
        onion_domain = domain
    elif domain.endswith('.i2p'):
        i2p_domain = domain
    httpd.onion_domain = onion_domain
    httpd.i2p_domain = i2p_domain
    httpd.media_instance = media_instance
    httpd.blogs_instance = blogs_instance

    # load translations dictionary
    httpd.translate = {}
    httpd.system_language = 'en'
    if not unit_test:
        httpd.translate, httpd.system_language = \
            load_translations_from_file(base_dir, language)
        if not httpd.system_language:
            print('ERROR: no system language loaded')
            sys.exit()
        print('System language: ' + httpd.system_language)
        if not httpd.translate:
            print('ERROR: no translations were loaded')
            sys.exit()

    # spoofed city for gps location misdirection
    httpd.city = city

    # For moderated newswire feeds this is the amount of time allowed
    # for voting after the post arrives
    httpd.voting_time_mins = voting_time_mins
    # on the newswire, whether moderators vote positively for items
    # or against them (veto)
    httpd.positive_voting = positive_voting
    # number of votes needed to remove a newswire item from the news timeline
    # or if positive voting is anabled to add the item to the news timeline
    httpd.newswire_votes_threshold = newswire_votes_threshold
    # maximum overall size of an rss/atom feed read by the newswire daemon
    # If the feed is too large then this is probably a DoS attempt
    httpd.max_newswire_feed_size_kb = max_newswire_feed_size_kb

    # For each newswire source (account or rss feed)
    # this is the maximum number of posts to show for each.
    # This avoids one or two sources from dominating the news,
    # and also prevents big feeds from slowing down page load times
    httpd.max_newswire_posts_per_source = max_newswire_posts_per_source

    # Show only the date at the bottom of posts, and not the time
    httpd.show_published_date_only = show_published_date_only

    # maximum number of news articles to mirror
    httpd.max_mirrored_articles = max_mirrored_articles

    # maximum number of posts in the news timeline/outbox
    httpd.max_news_posts = max_news_posts

    # The maximum number of tags per post which can be
    # attached to RSS feeds pulled in via the newswire
    httpd.maxTags = 32

    # maximum number of followers per account
    httpd.max_followers = max_followers

    # whether to show an icon for publish on the
    # newswire, or a 'Publish' button
    httpd.show_publish_as_icon = show_publish_as_icon

    # Whether to show the timeline header containing inbox, outbox
    # calendar, etc as the full width of the screen or not
    httpd.full_width_tl_button_header = full_width_tl_button_header

    # whether to show icons in the header (eg calendar) as buttons
    httpd.icons_as_buttons = icons_as_buttons

    # whether to show the RSS icon at the top or the bottom of the timeline
    httpd.rss_icon_at_top = rss_icon_at_top

    # Whether to show the newswire publish button at the top,
    # above the header image
    httpd.publish_button_at_top = publish_button_at_top

    # maximum size of individual RSS feed items, in K
    httpd.max_feed_item_size_kb = max_feed_item_size_kb

    # maximum size of a hashtag category, in K
    httpd.maxCategoriesFeedItemSizeKb = 1024

    # how many months does a followed account need to be unseen
    # for it to be considered dormant?
    httpd.dormant_months = dormant_months

    # maximum number of likes to display on a post
    httpd.max_like_count = max_like_count
    if httpd.max_like_count < 0:
        httpd.max_like_count = 0
    elif httpd.max_like_count > 16:
        httpd.max_like_count = 16

    httpd.followingItemsPerPage = 12
    if registration == 'open':
        httpd.registration = True
    else:
        httpd.registration = False
    httpd.enable_shared_inbox = enable_shared_inbox
    httpd.outboxThread = {}
    httpd.outbox_thread_index = {}
    httpd.new_post_thread = {}
    httpd.project_version = project_version
    httpd.secure_mode = secure_mode
    # max POST size of 30M
    httpd.max_post_length = 1024 * 1024 * 30
    httpd.maxMediaSize = httpd.max_post_length
    # Maximum text length is 64K - enough for a blog post
    httpd.maxMessageLength = 64000
    # Maximum overall number of posts per box
    httpd.maxPostsInBox = 32000
    httpd.domain = domain
    httpd.port = port
    httpd.domain_full = get_full_domain(domain, port)
    if onion_domain:
        save_domain_qrcode(base_dir, 'http', onion_domain)
    elif i2p_domain:
        save_domain_qrcode(base_dir, 'http', i2p_domain)
    else:
        save_domain_qrcode(base_dir, http_prefix, httpd.domain_full)
    clear_person_qrcodes(base_dir)
    httpd.http_prefix = http_prefix
    httpd.debug = debug
    httpd.federation_list = fed_list.copy()
    httpd.shared_items_federated_domains = \
        shared_items_federated_domains.copy()
    httpd.base_dir = base_dir
    httpd.instance_id = instance_id
    httpd.person_cache = {}
    httpd.cached_webfingers = {}
    httpd.favicons_cache = {}
    httpd.proxy_type = proxy_type
    httpd.session = None
    httpd.session_onion = None
    httpd.session_i2p = None
    httpd.last_getreq = 0
    httpd.last_postreq = 0
    httpd.getreq_busy = False
    httpd.postreq_busy = False
    httpd.received_message = False
    httpd.inbox_queue = []
    httpd.send_threads = send_threads
    httpd.postLog = []
    httpd.max_queue_length = 64
    httpd.allow_deletion = allow_deletion
    httpd.last_login_time = 0
    httpd.last_login_failure = 0
    httpd.login_failure_count = {}
    httpd.log_login_failures = log_login_failures
    httpd.max_replies = max_replies
    httpd.tokens = {}
    httpd.tokens_lookup = {}
    load_tokens(base_dir, httpd.tokens, httpd.tokens_lookup)
    httpd.instance_only_skills_search = instance_only_skills_search
    # contains threads used to send posts to followers
    httpd.followers_threads = []

    # create a cache of blocked domains in memory.
    # This limits the amount of slow disk reads which need to be done
    httpd.blocked_cache = []
    httpd.blocked_cache_last_updated = 0
    httpd.blocked_cache_update_secs = 120
    httpd.blocked_cache_last_updated = \
        update_blocked_cache(base_dir, httpd.blocked_cache,
                             httpd.blocked_cache_last_updated,
                             httpd.blocked_cache_update_secs)

    # get the list of custom emoji, for use by the mastodon api
    httpd.custom_emoji = \
        metadata_custom_emoji(base_dir, http_prefix, httpd.domain_full)

    # whether to enable broch mode, which locks down the instance
    set_broch_mode(base_dir, httpd.domain_full, broch_mode)

    if not os.path.isdir(base_dir + '/accounts/inbox@' + domain):
        print('Creating shared inbox: inbox@' + domain)
        create_shared_inbox(base_dir, 'inbox', domain, port, http_prefix)

    if not os.path.isdir(base_dir + '/accounts/news@' + domain):
        print('Creating news inbox: news@' + domain)
        create_news_inbox(base_dir, domain, port, http_prefix)
        set_config_param(base_dir, "listsEnabled", "Murdoch press")

    # dict of known web crawlers accessing nodeinfo or the masto API
    # and how many times they have been seen
    httpd.known_crawlers = {}
    known_crawlers_filename = base_dir + '/accounts/knownCrawlers.json'
    if os.path.isfile(known_crawlers_filename):
        httpd.known_crawlers = load_json(known_crawlers_filename)
    # when was the last crawler seen?
    httpd.last_known_crawler = 0

    if lists_enabled:
        httpd.lists_enabled = lists_enabled
    else:
        httpd.lists_enabled = get_config_param(base_dir, "listsEnabled")
    httpd.cw_lists = load_cw_lists(base_dir, True)

    # set the avatar for the news account
    httpd.theme_name = get_config_param(base_dir, 'theme')
    if not httpd.theme_name:
        httpd.theme_name = 'default'
    if is_news_theme_name(base_dir, httpd.theme_name):
        news_instance = True

    httpd.news_instance = news_instance
    httpd.default_timeline = 'inbox'
    if media_instance:
        httpd.default_timeline = 'tlmedia'
    if blogs_instance:
        httpd.default_timeline = 'tlblogs'
    if news_instance:
        httpd.default_timeline = 'tlfeatures'

    set_news_avatar(base_dir,
                    httpd.theme_name,
                    http_prefix,
                    domain,
                    httpd.domain_full)

    if not os.path.isdir(base_dir + '/cache'):
        os.mkdir(base_dir + '/cache')
    if not os.path.isdir(base_dir + '/cache/actors'):
        print('Creating actors cache')
        os.mkdir(base_dir + '/cache/actors')
    if not os.path.isdir(base_dir + '/cache/announce'):
        print('Creating announce cache')
        os.mkdir(base_dir + '/cache/announce')
    if not os.path.isdir(base_dir + '/cache/avatars'):
        print('Creating avatars cache')
        os.mkdir(base_dir + '/cache/avatars')

    archive_dir = base_dir + '/archive'
    if not os.path.isdir(archive_dir):
        print('Creating archive')
        os.mkdir(archive_dir)

    if not os.path.isdir(base_dir + '/sharefiles'):
        print('Creating shared item files directory')
        os.mkdir(base_dir + '/sharefiles')

    print('THREAD: Creating fitness thread')
    httpd.thrFitness = \
        thread_with_trace(target=fitness_thread,
                          args=(base_dir, httpd.fitness), daemon=True)
    begin_thread(httpd.thrFitness, 'run_daemon thrFitness')

    httpd.recent_posts_cache = {}

    print('THREAD: Creating cache expiry thread')
    httpd.thrCache = \
        thread_with_trace(target=expire_cache,
                          args=(base_dir, httpd.person_cache,
                                httpd.http_prefix,
                                archive_dir,
                                httpd.recent_posts_cache,
                                httpd.maxPostsInBox), daemon=True)
    begin_thread(httpd.thrCache, 'run_daemon thrCache')

    # number of mins after which sending posts or updates will expire
    httpd.send_threads_timeout_mins = send_threads_timeout_mins

    print('THREAD: Creating posts queue')
    httpd.thrPostsQueue = \
        thread_with_trace(target=run_posts_queue,
                          args=(base_dir, httpd.send_threads, debug,
                                httpd.send_threads_timeout_mins), daemon=True)
    if not unit_test:
        print('THREAD: run_posts_watchdog')
        httpd.thrPostsWatchdog = \
            thread_with_trace(target=run_posts_watchdog,
                              args=(project_version, httpd), daemon=True)
        begin_thread(httpd.thrPostsWatchdog, 'run_daemon thrPostWatchdog')
    else:
        begin_thread(httpd.thrPostsQueue, 'run_daemon thrPostWatchdog 2')

    print('THREAD: Creating expire thread for shared items')
    httpd.thrSharesExpire = \
        thread_with_trace(target=run_shares_expire,
                          args=(project_version, base_dir), daemon=True)
    if not unit_test:
        print('THREAD: run_shares_expire_watchdog')
        httpd.thrSharesExpireWatchdog = \
            thread_with_trace(target=run_shares_expire_watchdog,
                              args=(project_version, httpd), daemon=True)
        begin_thread(httpd.thrSharesExpireWatchdog,
                     'run_daemon thrSharesExpireWatchdog')
    else:
        begin_thread(httpd.thrSharesExpire,
                     'run_daemon thrSharesExpireWatchdog 2')

    httpd.max_recent_posts = max_recent_posts
    httpd.iconsCache = {}
    httpd.fontsCache = {}

    # create tokens used for shared item federation
    fed_domains = httpd.shared_items_federated_domains
    httpd.shared_item_federation_tokens = \
        generate_shared_item_federation_tokens(fed_domains,
                                               base_dir)
    si_federation_tokens = httpd.shared_item_federation_tokens
    httpd.shared_item_federation_tokens = \
        create_shared_item_federation_token(base_dir, httpd.domain_full, False,
                                            si_federation_tokens)

    # load peertube instances from file into a list
    httpd.peertube_instances = []
    load_peertube_instances(base_dir, httpd.peertube_instances)

    create_initial_last_seen(base_dir, http_prefix)

    print('THREAD: Creating inbox queue')
    httpd.thrInboxQueue = \
        thread_with_trace(target=run_inbox_queue,
                          args=(httpd, httpd.recent_posts_cache,
                                httpd.max_recent_posts,
                                project_version,
                                base_dir, http_prefix, httpd.send_threads,
                                httpd.postLog, httpd.cached_webfingers,
                                httpd.person_cache, httpd.inbox_queue,
                                domain, onion_domain, i2p_domain,
                                port, proxy_type,
                                httpd.federation_list,
                                max_replies,
                                domain_max_posts_per_day,
                                account_max_posts_per_day,
                                allow_deletion, debug,
                                max_mentions, max_emoji,
                                httpd.translate, unit_test,
                                httpd.yt_replace_domain,
                                httpd.twitter_replacement_domain,
                                httpd.show_published_date_only,
                                httpd.max_followers,
                                httpd.allow_local_network_access,
                                httpd.peertube_instances,
                                verify_all_signatures,
                                httpd.theme_name,
                                httpd.system_language,
                                httpd.max_like_count,
                                httpd.signing_priv_key_pem,
                                httpd.default_reply_interval_hrs,
                                httpd.cw_lists), daemon=True)

    print('THREAD: Creating scheduled post thread')
    httpd.thrPostSchedule = \
        thread_with_trace(target=run_post_schedule,
                          args=(base_dir, httpd, 20), daemon=True)

    print('THREAD: Creating newswire thread')
    httpd.thrNewswireDaemon = \
        thread_with_trace(target=run_newswire_daemon,
                          args=(base_dir, httpd,
                                http_prefix, domain, port,
                                httpd.translate), daemon=True)

    print('THREAD: Creating federated shares thread')
    httpd.thrFederatedSharesDaemon = \
        thread_with_trace(target=run_federated_shares_daemon,
                          args=(base_dir, httpd,
                                http_prefix, httpd.domain_full,
                                proxy_type, debug,
                                httpd.system_language), daemon=True)

    # flags used when restarting the inbox queue
    httpd.restart_inbox_queue_in_progress = False
    httpd.restart_inbox_queue = False

    update_hashtag_categories(base_dir)

    print('Adding hashtag categories for language ' + httpd.system_language)
    load_hashtag_categories(base_dir, httpd.system_language)

    # signing key used for authorized fetch
    # this is the instance actor private key
    httpd.signing_priv_key_pem = get_instance_actor_key(base_dir, domain)

    # threads used for checking for actor changes when clicking on
    # avatar icon / person options
    httpd.thrCheckActor = {}

    if not unit_test:
        print('THREAD: Creating import following watchdog')
        httpd.thrImportFollowing = \
            thread_with_trace(target=run_import_following_watchdog,
                              args=(project_version, httpd), daemon=True)
        begin_thread(httpd.thrImportFollowing,
                     'run_daemon thrImportFollowing')

        print('THREAD: Creating inbox queue watchdog')
        httpd.thrWatchdog = \
            thread_with_trace(target=run_inbox_queue_watchdog,
                              args=(project_version, httpd), daemon=True)
        begin_thread(httpd.thrWatchdog, 'run_daemon thrWatchdog')

        print('THREAD: Creating scheduled post watchdog')
        httpd.thrWatchdogSchedule = \
            thread_with_trace(target=run_post_schedule_watchdog,
                              args=(project_version, httpd), daemon=True)
        begin_thread(httpd.thrWatchdogSchedule,
                     'run_daemon thrWatchdogSchedule')

        print('THREAD: Creating newswire watchdog')
        httpd.thrNewswireWatchdog = \
            thread_with_trace(target=run_newswire_watchdog,
                              args=(project_version, httpd), daemon=True)
        begin_thread(httpd.thrNewswireWatchdog,
                     'run_daemon thrNewswireWatchdog')

        print('THREAD: Creating federated shares watchdog')
        httpd.thrFederatedSharesWatchdog = \
            thread_with_trace(target=run_federated_shares_watchdog,
                              args=(project_version, httpd), daemon=True)
        begin_thread(httpd.thrFederatedSharesWatchdog,
                     'run_daemon thrFederatedSharesWatchdog')
    else:
        print('Starting inbox queue')
        begin_thread(httpd.thrInboxQueue, 'run_daemon start inbox')
        print('Starting scheduled posts daemon')
        begin_thread(httpd.thrPostSchedule,
                     'run_daemon start scheduled posts')
        print('Starting federated shares daemon')
        begin_thread(httpd.thrFederatedSharesDaemon,
                     'run_daemon start federated shares')

    if client_to_server:
        print('Running ActivityPub client on ' +
              domain + ' port ' + str(proxy_port))
    else:
        print('Running ActivityPub server on ' +
              domain + ' port ' + str(proxy_port))
    httpd.serve_forever()