__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 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 = \ '' + str(http_code) + '' \ '' \ '
' + str(http_code) + '
' \ '

' + http_description + '

' \ '
' + long_description + '
' \ '' 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) 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 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, 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).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 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, 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).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') 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) 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) 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 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) 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) # 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 = \ '\n' + \ '\n' + \ ' \n' + \ ' \n' + \ ' \n' + \ ' #eeeeee\n' + \ ' \n' + \ ' \n' + \ '' 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') 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 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) 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 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) 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 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) 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 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) 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 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) 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) 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 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) 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 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) 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) 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 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) 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 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) 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) 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) 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) 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, '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) 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) 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) 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) 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) 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) 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) 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 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) 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) 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) 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) 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) 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 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) 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, 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 msg = html_new_post(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, 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).encode('utf-8') if not msg: print('Error replying to ' + in_reply_to_url) self._404() return True 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) 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 # 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(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 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_text = self.server.translate['Publish'] custom_submit_text = \ get_config_param(self.server.base_dir, 'customSubmitText') if custom_submit_text: submit_text = custom_submit_text if fields.get('submitPost'): if fields['submitPost'] != submit_text: 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 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 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 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: 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 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, 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) # 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()