__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 import os 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 wellknown_protocol_handler 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 keys import get_instance_actor_key from posts import get_max_profile_posts from posts import set_max_profile_posts 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 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 inbox import receive_edit_to_post 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 remove_follower 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 add_account_blocks 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_default_path 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_conversation import html_conversation_view 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 html_hashtag_search_remote from webapp_search import rss_hashtag_search from webapp_search import hashtag_search_json 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_reply_language from languages import load_default_post_languages from languages import set_default_post_language 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 is_public_post_from_url from utils import license_link_from_name from utils import acct_handle_dir from utils import load_reverse_timeline from utils import save_reverse_timeline from utils import load_min_images_for_accounts from utils import set_minimize_all_images from utils import get_json_content_from_accept from utils import remove_eol from utils import text_in_file from utils import is_onion_request from utils import is_i2p_request from utils import get_account_timezone from utils import set_account_timezone from utils import load_account_timezones from utils import local_network_host from utils import undo_reaction_collection_entry from utils import get_new_post_endpoints from utils import has_actor from utils import set_reply_interval_hours from utils import can_reply_to from utils import is_dm from utils import replace_users_with_at from utils import local_actor_url from utils import is_float from utils import valid_password from utils import get_base_content_from_post from utils import acct_dir from utils import get_image_extension_from_mime_type from utils import get_image_mime_type from utils import has_object_dict from utils import user_agent_domain from utils import is_local_network_address from utils import permitted_dir from utils import is_account_dir from utils import get_occupation_skills from utils import get_occupation_name from utils import set_occupation_name from utils import load_translations_from_file from utils import load_bold_reading from utils import get_local_network_addresses from utils import decoded_host from utils import is_public_post from utils import get_locked_account from utils import has_users_path from utils import get_full_domain from utils import remove_html from utils import is_editor from utils import is_artist from utils import get_image_extensions from utils import media_file_mime_type from utils import get_css from utils import first_paragraph_from_string from utils import clear_from_post_caches from utils import contains_invalid_chars from utils import is_system_account from utils import set_config_param from utils import get_config_param from utils import remove_id_ending from utils import undo_likes_collection_entry from utils import delete_post from utils import is_blog_post from utils import remove_avatar_from_cache from utils import locate_post from utils import get_cached_post_filename from utils import remove_post_from_cache from utils import get_nickname_from_actor from utils import get_domain_from_actor from utils import get_status_number from utils import url_permitted from utils import load_json from utils import save_json from utils import is_suspended from utils import dangerous_markup from utils import refresh_newswire from utils import is_image_file from utils import has_group_type from manualapprove import manual_deny_follow_request_thread from manualapprove import manual_approve_follow_request_thread from announce import create_announce from content import add_name_emojis_to_tags 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 from relationships import get_moved_feed from relationships import get_inactive_feed from relationships import update_moved_actors # 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 _update_edited_post(self, base_dir: str, nickname: str, domain: str, message_json: {}, edited_published: str, edited_postid: str, recent_posts_cache: {}, box_name: str, max_mentions: int, max_emoji: int, allow_local_network_access: bool, debug: bool, system_language: str, http_prefix: str, domain_full: str, person_cache: {}, signing_priv_key_pem: str, max_recent_posts: int, translate: {}, session, cached_webfingers: {}, port: int, allow_deletion: bool, yt_replace_domain: str, twitter_replacement_domain: str, show_published_date_only: bool, peertube_instances: [], theme_name: str, max_like_count: int, cw_lists: {}, dogwhistles: {}, min_images_for_accounts: [], max_hashtags: int) -> None: """When an edited post is created this assigns a published and updated date to it, and uses the previous id """ edited_updated = \ message_json['object']['published'] if edited_published: message_json['published'] = \ edited_published message_json['object']['published'] = \ edited_published message_json['id'] = \ edited_postid + '/activity' message_json['object']['id'] = \ edited_postid message_json['object']['url'] = \ edited_postid message_json['updated'] = \ edited_updated message_json['object']['updated'] = \ edited_updated message_json['type'] = 'Update' message_json2 = message_json.copy() receive_edit_to_post(recent_posts_cache, message_json2, base_dir, nickname, domain, max_mentions, max_emoji, allow_local_network_access, debug, system_language, http_prefix, domain_full, person_cache, signing_priv_key_pem, max_recent_posts, translate, session, cached_webfingers, port, allow_deletion, yt_replace_domain, twitter_replacement_domain, show_published_date_only, peertube_instances, theme_name, max_like_count, cw_lists, dogwhistles, min_images_for_accounts, max_hashtags) # update the index id_str = edited_postid.split('/')[-1] index_filename = \ acct_dir(base_dir, nickname, domain) + '/' + box_name + '.index' if not text_in_file(id_str, index_filename): try: with open(index_filename, 'r+', encoding='utf-8') as fp_index: content = fp_index.read() if id_str + '\n' not in content: fp_index.seek(0, 0) fp_index.write(id_str + '\n' + content) except OSError as ex: print('WARN: Failed to write index after edit ' + index_filename + ' ' + str(ex)) 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.send_header('accept-ranges', 'bytes') 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, code: int = 303) -> 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(code) if code != 303: print('Redirect headers: ' + str(code)) 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, cookie: 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 # protocol handler. See https://fedi-to.github.io/protocol-handler.html if self.path.startswith('/.well-known/protocol-handler'): if calling_domain.endswith('.onion'): protocol_url = \ wellknown_protocol_handler(self.path, self.server.base_dir, 'http', self.server.onion_domain) elif calling_domain.endswith('.i2p'): protocol_url = \ wellknown_protocol_handler(self.path, self.server.base_dir, 'http', self.server.i2p_domain) else: protocol_url = \ wellknown_protocol_handler(self.path, self.server.base_dir, self.server.http_prefix, self.server.domain_full) if protocol_url: self._redirect_headers(protocol_url, cookie, calling_domain, 308) else: self._404() return True # nodeinfo if self.path.startswith('/.well-known/nodeinfo') or \ self.path.startswith('/.well-known/x-nodeinfo'): if calling_domain.endswith('.onion') and \ self.server.onion_domain: wf_result = \ webfinger_node_info('http', self.server.onion_domain) elif (calling_domain.endswith('.i2p') and self.server.i2p_domain): wf_result = \ webfinger_node_info('http', self.server.i2p_domain) else: wf_result = \ webfinger_node_info(self.server.http_prefix, self.server.domain_full) if wf_result: msg_str = json.dumps(wf_result) msg_str = self._convert_domains(calling_domain, referer_domain, msg_str) msg = msg_str.encode('utf-8') msglen = len(msg) if self._has_accept(calling_domain): accept_str = self.headers.get('Accept') protocol_str = \ get_json_content_from_accept(accept_str) self._set_headers(protocol_str, msglen, None, calling_domain, True) else: self._set_headers('application/ld+json', msglen, None, calling_domain, True) self._write(msg) return True self._404() return True if self.server.debug: print('DEBUG: WEBFINGER lookup ' + self.path + ' ' + str(self.server.base_dir)) wf_result = \ webfinger_lookup(self.path, self.server.base_dir, self.server.domain, self.server.onion_domain, self.server.i2p_domain, self.server.port, self.server.debug) if wf_result: msg_str = json.dumps(wf_result) msg_str = self._convert_domains(calling_domain, referer_domain, msg_str) msg = msg_str.encode('utf-8') msglen = len(msg) self._set_headers('application/jrd+json', msglen, None, calling_domain, True) self._write(msg) else: if self.server.debug: print('DEBUG: WEBFINGER lookup 404 ' + self.path) self._404() return True def _post_to_outbox(self, message_json: {}, version: str, post_to_nickname: str, curr_session, proxy_type: str) -> bool: """post is received by the outbox Client to server message post https://www.w3.org/TR/activitypub/#client-to-server-outbox-delivery """ if not curr_session: return False city = self.server.city if post_to_nickname: print('Posting to nickname ' + post_to_nickname) self.post_to_nickname = post_to_nickname city = get_spoofed_city(self.server.city, self.server.base_dir, post_to_nickname, self.server.domain) shared_items_federated_domains = \ self.server.shared_items_federated_domains shared_item_federation_tokens = \ self.server.shared_item_federation_tokens return post_message_to_outbox(curr_session, self.server.translate, message_json, self.post_to_nickname, self.server, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.domain_full, self.server.onion_domain, self.server.i2p_domain, self.server.port, self.server.recent_posts_cache, self.server.followers_threads, self.server.federation_list, self.server.send_threads, self.server.postLog, self.server.cached_webfingers, self.server.person_cache, self.server.allow_deletion, proxy_type, version, self.server.debug, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.allow_local_network_access, city, self.server.system_language, shared_items_federated_domains, shared_item_federation_tokens, self.server.low_bandwidth, self.server.signing_priv_key_pem, self.server.peertube_instances, self.server.theme_name, self.server.max_like_count, self.server.max_recent_posts, self.server.cw_lists, self.server.lists_enabled, self.server.content_license_url, self.server.dogwhistles, self.server.min_images_for_accounts) def _get_outbox_thread_index(self, nickname: str, max_outbox_threads_per_account: int) -> int: """Returns the outbox thread index for the given account This is a ring buffer used to store the thread objects which are sending out posts """ account_outbox_thread_name = nickname if not account_outbox_thread_name: account_outbox_thread_name = '*' # create the buffer for the given account if not self.server.outboxThread.get(account_outbox_thread_name): self.server.outboxThread[account_outbox_thread_name] = \ [None] * max_outbox_threads_per_account self.server.outbox_thread_index[account_outbox_thread_name] = 0 return 0 # increment the ring buffer index index = self.server.outbox_thread_index[account_outbox_thread_name] + 1 if index >= max_outbox_threads_per_account: index = 0 self.server.outbox_thread_index[account_outbox_thread_name] = index # remove any existing thread from the current index in the buffer acct = account_outbox_thread_name if self.server.outboxThread.get(acct): if len(self.server.outboxThread[acct]) > index: try: if self.server.outboxThread[acct][index].is_alive(): self.server.outboxThread[acct][index].kill() except BaseException: pass return index def _post_to_outbox_thread(self, message_json: {}, curr_session, proxy_type: str) -> bool: """Creates a thread to send a post """ account_outbox_thread_name = self.post_to_nickname if not account_outbox_thread_name: account_outbox_thread_name = '*' index = self._get_outbox_thread_index(account_outbox_thread_name, 8) print('Creating outbox thread ' + account_outbox_thread_name + '/' + str(self.server.outbox_thread_index[account_outbox_thread_name])) print('THREAD: _post_to_outbox') self.server.outboxThread[account_outbox_thread_name][index] = \ thread_with_trace(target=self._post_to_outbox, args=(message_json.copy(), self.server.project_version, None, curr_session, proxy_type), daemon=True) print('Starting outbox thread') outbox_thread = \ self.server.outboxThread[account_outbox_thread_name][index] begin_thread(outbox_thread, '_post_to_outbox_thread') return True def _update_inbox_queue(self, nickname: str, message_json: {}, message_bytes: str, debug: bool) -> int: """Update the inbox queue """ if debug: print('INBOX: checking inbox queue restart') if self.server.restart_inbox_queue_in_progress: self._503() print('INBOX: ' + 'message arrived but currently restarting inbox queue') self.server.postreq_busy = False return 2 # check that the incoming message has a fully recognized # linked data context if debug: print('INBOX: checking valid context') if not has_valid_context(message_json): print('INBOX: ' + 'message arriving at inbox queue has no valid context') self._400() self.server.postreq_busy = False return 3 # check for blocked domains so that they can be rejected early if debug: print('INBOX: checking for actor') message_domain = None if not has_actor(message_json, self.server.debug): print('INBOX: message arriving at inbox queue has no actor') self._400() self.server.postreq_busy = False return 3 # actor should be a string if debug: print('INBOX: checking that actor is string') if not isinstance(message_json['actor'], str): print('INBOX: ' + 'actor should be a string ' + str(message_json['actor'])) self._400() self.server.postreq_busy = False return 3 # check that some additional fields are strings if debug: print('INBOX: checking fields 1') string_fields = ('id', 'type', 'published') for check_field in string_fields: if not message_json.get(check_field): continue if not isinstance(message_json[check_field], str): print('INBOX: ' + 'id, type and published fields should be strings ' + check_field + ' ' + str(message_json[check_field])) self._400() self.server.postreq_busy = False return 3 # check that to/cc fields are lists if debug: print('INBOX: checking to and cc fields') list_fields = ('to', 'cc') for check_field in list_fields: if not message_json.get(check_field): continue if not isinstance(message_json[check_field], list): print('INBOX: To and Cc fields should be strings ' + check_field + ' ' + str(message_json[check_field])) self._400() self.server.postreq_busy = False return 3 if has_object_dict(message_json): if debug: print('INBOX: checking object fields') string_fields = ( 'id', 'actor', 'type', 'content', 'published', 'summary', 'url', 'attributedTo' ) for check_field in string_fields: if not message_json['object'].get(check_field): continue if not isinstance(message_json['object'][check_field], str): print('INBOX: ' + check_field + ' should be a string ' + str(message_json['object'][check_field])) self._400() self.server.postreq_busy = False return 3 # check that some fields are lists if debug: print('INBOX: checking object to and cc fields') list_fields = ('to', 'cc', 'attachment') for check_field in list_fields: if not message_json['object'].get(check_field): continue if not isinstance(message_json['object'][check_field], list): print('INBOX: ' + check_field + ' should be a list ' + str(message_json['object'][check_field])) self._400() self.server.postreq_busy = False return 3 # check that the content does not contain impossibly long urls if message_json['object'].get('content'): content_str = message_json['object']['content'] if not valid_url_lengths(content_str, 2048): print('INBOX: content contains urls which are too long ' + message_json['actor']) self._400() self.server.postreq_busy = False return 3 # check that the summary does not contain links if message_json['object'].get('summary'): if len(message_json['object']['summary']) > 1024: print('INBOX: summary is too long ' + message_json['actor'] + ' ' + message_json['object']['summary']) self._400() self.server.postreq_busy = False return 3 if '://' in message_json['object']['summary']: print('INBOX: summary should not contain links ' + message_json['actor'] + ' ' + message_json['object']['summary']) self._400() self.server.postreq_busy = False return 3 # if this is a local only post, is it really local? if 'localOnly' in message_json['object'] and \ message_json['object'].get('to') and \ message_json['object'].get('attributedTo'): if message_json['object']['localOnly'] is True: # check that the to addresses are local if isinstance(message_json['object']['to'], list): for to_actor in message_json['object']['to']: to_domain, to_port = \ get_domain_from_actor(to_actor) to_domain_full = \ get_full_domain(to_domain, to_port) if self.server.domain_full != to_domain_full: print("REJECT: inbox " + "local only post isn't local " + str(message_json)) self._400() self.server.postreq_busy = False return 3 # check that the sender is local local_actor = message_json['object']['attributedTo'] local_domain, local_port = \ get_domain_from_actor(local_actor) local_domain_full = \ get_full_domain(local_domain, local_port) if self.server.domain_full != local_domain_full: print("REJECT: inbox local only post isn't local " + str(message_json)) 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 moderation_text = moderation_text.strip() moderation_reason = None if ' ' in moderation_text: moderation_domain = moderation_text.split(' ', 1)[0] moderation_reason = moderation_text.split(' ', 1)[1] else: moderation_domain = moderation_text if moderation_domain.startswith('http') or \ moderation_domain.startswith('ipfs') or \ moderation_domain.startswith('ipns') or \ moderation_domain.startswith('hyper'): # https://domain block_domain, block_port = \ get_domain_from_actor(moderation_domain) full_block_domain = \ get_full_domain(block_domain, block_port) if '@' in moderation_domain: # nick@domain or *@domain full_block_domain = \ moderation_domain.split('@')[1] else: # assume the text is a domain name if not full_block_domain and '.' in moderation_domain: nickname = '*' full_block_domain = \ moderation_domain.strip() if full_block_domain or nickname.startswith('#'): if nickname.startswith('#') and ' ' in nickname: nickname = nickname.split(' ')[0] add_global_block(base_dir, nickname, full_block_domain, moderation_reason) if moderation_button == 'unblock': full_block_domain = None if ' ' in moderation_text: moderation_domain = moderation_text.split(' ', 1)[0] else: moderation_domain = moderation_text if moderation_domain.startswith('http') or \ moderation_domain.startswith('ipfs') or \ moderation_domain.startswith('ipns') or \ moderation_domain.startswith('hyper'): # https://domain block_domain, block_port = \ get_domain_from_actor(moderation_domain) full_block_domain = \ get_full_domain(block_domain, block_port) if '@' in moderation_domain: # nick@domain or *@domain full_block_domain = moderation_domain.split('@')[1] else: # assume the text is a domain name if not full_block_domain and '.' in moderation_domain: nickname = '*' full_block_domain = moderation_domain.strip() if full_block_domain or nickname.startswith('#'): if nickname.startswith('#') and ' ' in nickname: nickname = nickname.split(' ')[0] 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] # actor for the movedTo options_actor_moved = None if 'movedToActor=' in options_confirm_params: options_actor_moved = \ options_confirm_params.split('movedToActor=')[1] if '&' in options_actor_moved: options_actor_moved = options_actor_moved.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) 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(chooser_nickname): access_keys = self.server.key_shortcuts[chooser_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(chooser_nickname): timezone = \ self.server.account_timezone.get(chooser_nickname) profile_handle = remove_eol(options_actor).strip() # establish the session curr_proxy_type = self.server.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(chooser_nickname): bold_reading = True min_images_for_accounts = \ self.server.min_images_for_accounts profile_str = \ html_profile_after_search(recent_posts_cache, self.server.max_recent_posts, self.server.translate, base_dir, users_path, http_prefix, chooser_nickname, domain, port, profile_handle, curr_session, cached_webfingers, self.server.person_cache, self.server.debug, self.server.project_version, yt_replace_domain, twitter_replacement_domain, show_published_date_only, self.server.default_timeline, peertube_instances, allow_local_network_access, self.server.theme_name, access_keys, self.server.system_language, self.server.max_like_count, signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, timezone, self.server.onion_domain, self.server.i2p_domain, bold_reading, self.server.dogwhistles, min_images_for_accounts) if profile_str: msg = profile_str.encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) self.server.postreq_busy = False return 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, move button # See html_person_options followStr if '&submitMove=' in options_confirm_params and options_actor_moved: if debug: print('Moving ' + options_actor_moved) msg = \ html_confirm_follow(self.server.translate, base_dir, users_path, options_actor_moved, options_avatar_url).encode('utf-8') if msg: msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) self.server.postreq_busy = False return # person options screen, unfollow button # See html_person_options followStr if '&submitUnfollow=' in options_confirm_params or \ '&submitLeave=' in options_confirm_params: print('Unfollowing ' + options_actor) msg = \ html_confirm_unfollow(self.server.translate, base_dir, users_path, options_actor, options_avatar_url).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) self.server.postreq_busy = False return # person options screen, DM button # See html_person_options if '&submitDM=' in options_confirm_params: if debug: print('Sending DM to ' + options_actor) report_path = path.replace('/personoptions', '') + '/newdm' access_keys = self.server.access_keys if '/users/' in path: nickname = path.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] if self.server.key_shortcuts.get(nickname): access_keys = self.server.key_shortcuts[nickname] custom_submit_text = get_config_param(base_dir, 'customSubmitText') conversation_id = None reply_is_chat = False bold_reading = False if self.server.bold_reading.get(chooser_nickname): bold_reading = True languages_understood = \ get_understood_languages(base_dir, http_prefix, chooser_nickname, self.server.domain_full, self.server.person_cache) default_post_language = self.server.system_language if self.server.default_post_language.get(nickname): default_post_language = \ self.server.default_post_language[nickname] msg = \ html_new_post({}, False, self.server.translate, base_dir, http_prefix, report_path, None, [options_actor], None, None, page_number, '', chooser_nickname, domain, domain_full, self.server.default_timeline, self.server.newswire, self.server.theme_name, True, access_keys, custom_submit_text, conversation_id, self.server.recent_posts_cache, self.server.max_recent_posts, curr_session, self.server.cached_webfingers, self.server.person_cache, self.server.port, None, self.server.project_version, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.peertube_instances, self.server.allow_local_network_access, self.server.system_language, languages_understood, self.server.max_like_count, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, self.server.default_timeline, reply_is_chat, bold_reading, self.server.dogwhistles, self.server.min_images_for_accounts, None, None, default_post_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.postreq_busy = False return # person options screen, Info button # See html_person_options if '&submitPersonInfo=' in options_confirm_params: if is_moderator(self.server.base_dir, chooser_nickname): if debug: print('Showing info for ' + options_actor) signing_priv_key_pem = self.server.signing_priv_key_pem msg = \ html_account_info(self.server.translate, base_dir, http_prefix, chooser_nickname, domain, self.server.port, options_actor, self.server.debug, self.server.system_language, signing_priv_key_pem) if msg: msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) self.server.postreq_busy = False return self._404() return # person options screen, snooze button # See html_person_options if '&submitSnooze=' in options_confirm_params: users_path = path.split('/personoptions')[0] this_actor = http_prefix + '://' + domain_full + users_path if debug: print('Snoozing ' + options_actor + ' ' + this_actor) if '/users/' in this_actor: nickname = this_actor.split('/users/')[1] person_snooze(base_dir, nickname, domain, options_actor) if calling_domain.endswith('.onion') and onion_domain: this_actor = 'http://' + onion_domain + users_path elif (calling_domain.endswith('.i2p') and i2p_domain): this_actor = 'http://' + i2p_domain + users_path actor_path_str = \ this_actor + '/' + self.server.default_timeline + \ '?page=' + str(page_number) self._redirect_headers(actor_path_str, cookie, calling_domain) self.server.postreq_busy = False return # person options screen, unsnooze button # See html_person_options if '&submitUnSnooze=' in options_confirm_params: users_path = path.split('/personoptions')[0] this_actor = http_prefix + '://' + domain_full + users_path if debug: print('Unsnoozing ' + options_actor + ' ' + this_actor) if '/users/' in this_actor: nickname = this_actor.split('/users/')[1] person_unsnooze(base_dir, nickname, domain, options_actor) if calling_domain.endswith('.onion') and onion_domain: this_actor = 'http://' + onion_domain + users_path elif (calling_domain.endswith('.i2p') and i2p_domain): this_actor = 'http://' + i2p_domain + users_path actor_path_str = \ this_actor + '/' + self.server.default_timeline + \ '?page=' + str(page_number) self._redirect_headers(actor_path_str, cookie, calling_domain) self.server.postreq_busy = False return # person options screen, report button # See html_person_options if '&submitReport=' in options_confirm_params: if debug: print('Reporting ' + options_actor) report_path = \ path.replace('/personoptions', '') + '/newreport' access_keys = self.server.access_keys if '/users/' in path: nickname = path.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] if self.server.key_shortcuts.get(nickname): access_keys = self.server.key_shortcuts[nickname] custom_submit_text = get_config_param(base_dir, 'customSubmitText') conversation_id = None reply_is_chat = False bold_reading = False if self.server.bold_reading.get(chooser_nickname): bold_reading = True languages_understood = \ get_understood_languages(base_dir, http_prefix, chooser_nickname, self.server.domain_full, self.server.person_cache) default_post_language = self.server.system_language if self.server.default_post_language.get(nickname): default_post_language = \ self.server.default_post_language[nickname] msg = \ html_new_post({}, False, self.server.translate, base_dir, http_prefix, report_path, None, [], None, post_url, page_number, '', chooser_nickname, domain, domain_full, self.server.default_timeline, self.server.newswire, self.server.theme_name, True, access_keys, custom_submit_text, conversation_id, self.server.recent_posts_cache, self.server.max_recent_posts, curr_session, self.server.cached_webfingers, self.server.person_cache, self.server.port, None, self.server.project_version, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.peertube_instances, self.server.allow_local_network_access, self.server.system_language, languages_understood, self.server.max_like_count, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, self.server.default_timeline, reply_is_chat, bold_reading, self.server.dogwhistles, self.server.min_images_for_accounts, None, None, default_post_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.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) remove_follower(base_dir, blocker_nickname, domain, 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 _unblock_confirm(self, calling_domain: str, cookie: str, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, debug: bool) -> None: """Confirms a unblock """ users_path = path.split('/unblockconfirm')[0] origin_path_str = http_prefix + '://' + domain_full + users_path blocker_nickname = get_nickname_from_actor(origin_path_str) if not blocker_nickname: if calling_domain.endswith('.onion') and onion_domain: origin_path_str = 'http://' + onion_domain + users_path elif (calling_domain.endswith('.i2p') and i2p_domain): origin_path_str = 'http://' + i2p_domain + users_path print('WARN: unable to find nickname in ' + origin_path_str) self._redirect_headers(origin_path_str, cookie, calling_domain) self.server.postreq_busy = False return length = int(self.headers['Content-length']) try: block_confirm_params = self.rfile.read(length).decode('utf-8') except SocketError as ex: if ex.errno == errno.ECONNRESET: print('EX: POST block_confirm_params ' + 'connection was reset') else: print('EX: POST block_confirm_params socket error') self.send_response(400) self.end_headers() self.server.postreq_busy = False return except ValueError as ex: print('EX: POST block_confirm_params rfile.read failed, ' + str(ex)) self.send_response(400) self.end_headers() self.server.postreq_busy = False return if '&submitYes=' in block_confirm_params: blocking_actor = \ urllib.parse.unquote_plus(block_confirm_params) blocking_actor = blocking_actor.split('actor=')[1] if '&' in blocking_actor: blocking_actor = blocking_actor.split('&')[0] blocking_nickname = get_nickname_from_actor(blocking_actor) if not blocking_nickname: if calling_domain.endswith('.onion') and onion_domain: origin_path_str = 'http://' + onion_domain + users_path elif (calling_domain.endswith('.i2p') and i2p_domain): origin_path_str = 'http://' + i2p_domain + users_path print('WARN: unable to find nickname in ' + blocking_actor) self._redirect_headers(origin_path_str, cookie, calling_domain) self.server.postreq_busy = False return blocking_domain, blocking_port = \ get_domain_from_actor(blocking_actor) blocking_domain_full = \ get_full_domain(blocking_domain, blocking_port) if blocker_nickname == blocking_nickname and \ blocking_domain == domain and \ blocking_port == port: if debug: print('You cannot unblock yourself!') else: if debug: print(blocker_nickname + ' stops blocking ' + blocking_actor) remove_block(base_dir, blocker_nickname, domain, blocking_nickname, blocking_domain_full) if is_moderator(base_dir, blocker_nickname): remove_global_block(base_dir, blocking_nickname, blocking_domain_full) if calling_domain.endswith('.onion') and onion_domain: origin_path_str = 'http://' + onion_domain + users_path elif (calling_domain.endswith('.i2p') and i2p_domain): origin_path_str = 'http://' + i2p_domain + users_path self._redirect_headers(origin_path_str, cookie, calling_domain) self.server.postreq_busy = False def _receive_search_query(self, calling_domain: str, cookie: str, authorized: bool, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, search_for_emoji: bool, onion_domain: str, i2p_domain: str, getreq_start_time, debug: bool, curr_session, proxy_type: str) -> None: """Receive a search query """ # get the page number page_number = 1 if '/searchhandle?page=' in path: page_number_str = path.split('/searchhandle?page=')[1] if '#' in page_number_str: page_number_str = page_number_str.split('#')[0] if len(page_number_str) > 5: page_number_str = "1" if page_number_str.isdigit(): page_number = int(page_number_str) path = path.split('?page=')[0] users_path = path.replace('/searchhandle', '') actor_str = self._get_instance_url(calling_domain) + users_path length = int(self.headers['Content-length']) try: search_params = self.rfile.read(length).decode('utf-8') except SocketError as ex: if ex.errno == errno.ECONNRESET: print('EX: POST search_params connection was reset') else: print('EX: POST search_params socket error') self.send_response(400) self.end_headers() self.server.postreq_busy = False return except ValueError as ex: print('EX: POST search_params rfile.read failed, ' + str(ex)) self.send_response(400) self.end_headers() self.server.postreq_busy = False return if 'submitBack=' in search_params: # go back on search screen self._redirect_headers(actor_str + '/' + self.server.default_timeline, cookie, calling_domain) self.server.postreq_busy = False return if 'searchtext=' in search_params: search_str = search_params.split('searchtext=')[1] if '&' in search_str: search_str = search_str.split('&')[0] search_str = \ urllib.parse.unquote_plus(search_str.strip()) search_str = search_str.strip() # hashtags can be combined case if not search_str.startswith('#'): search_str = search_str.lower() print('search_str: ' + search_str) if search_for_emoji: search_str = ':' + search_str + ':' if search_str.startswith('#'): nickname = get_nickname_from_actor(actor_str) if not nickname: self.send_response(400) self.end_headers() self.server.postreq_busy = False return # hashtag search timezone = None if self.server.account_timezone.get(nickname): timezone = \ self.server.account_timezone.get(nickname) bold_reading = False if self.server.bold_reading.get(nickname): bold_reading = True hashtag_str = \ html_hashtag_search(nickname, domain, port, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, base_dir, search_str[1:], 1, MAX_POSTS_IN_HASHTAG_FEED, curr_session, self.server.cached_webfingers, self.server.person_cache, http_prefix, self.server.project_version, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.peertube_instances, self.server.allow_local_network_access, self.server.theme_name, self.server.system_language, self.server.max_like_count, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, timezone, bold_reading, self.server.dogwhistles, self.server.map_format, self.server.access_keys, 'search', self.server.min_images_for_accounts) if hashtag_str: msg = hashtag_str.encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) self.server.postreq_busy = False return elif (search_str.startswith('*') or search_str.endswith(' skill')): possible_endings = ( ' skill' ) for poss_ending in possible_endings: if search_str.endswith(poss_ending): search_str = search_str.replace(poss_ending, '') break # skill search search_str = search_str.replace('*', '').strip() nickname = get_nickname_from_actor(actor_str) skill_str = \ html_skills_search(actor_str, self.server.translate, base_dir, search_str, self.server.instance_only_skills_search, 64, nickname, domain, self.server.theme_name, self.server.access_keys) if skill_str: msg = skill_str.encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) self.server.postreq_busy = False return elif (search_str.startswith("'") or search_str.endswith(' history') or search_str.endswith(' in sent') or search_str.endswith(' in outbox') or search_str.endswith(' in outgoing') or search_str.endswith(' in sent items') or search_str.endswith(' in sent posts') or search_str.endswith(' in outgoing posts') or search_str.endswith(' in my history') or search_str.endswith(' in my outbox') or search_str.endswith(' in my posts')): possible_endings = ( ' in my posts', ' in my history', ' in my outbox', ' in sent posts', ' in outgoing posts', ' in sent items', ' in history', ' in outbox', ' in outgoing', ' in sent', ' history' ) for poss_ending in possible_endings: if search_str.endswith(poss_ending): search_str = search_str.replace(poss_ending, '') break # your post history search nickname = get_nickname_from_actor(actor_str) if not nickname: self.send_response(400) self.end_headers() self.server.postreq_busy = False return search_str = search_str.replace("'", '', 1).strip() timezone = None if self.server.account_timezone.get(nickname): timezone = \ self.server.account_timezone.get(nickname) bold_reading = False if self.server.bold_reading.get(nickname): bold_reading = True history_str = \ html_history_search(self.server.translate, base_dir, http_prefix, nickname, domain, search_str, MAX_POSTS_IN_FEED, page_number, self.server.project_version, self.server.recent_posts_cache, self.server.max_recent_posts, curr_session, self.server.cached_webfingers, self.server.person_cache, port, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.peertube_instances, self.server.allow_local_network_access, self.server.theme_name, 'outbox', self.server.system_language, self.server.max_like_count, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, timezone, bold_reading, self.server.dogwhistles, self.server.access_keys, self.server.min_images_for_accounts) if history_str: msg = history_str.encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) self.server.postreq_busy = False return elif (search_str.startswith('-') or search_str.endswith(' in my saved items') or search_str.endswith(' in my saved posts') or search_str.endswith(' in my bookmarks') or search_str.endswith(' in my saved') or search_str.endswith(' in my saves') or search_str.endswith(' in saved posts') or search_str.endswith(' in saved items') or search_str.endswith(' in bookmarks') or search_str.endswith(' in saved') or search_str.endswith(' in saves') or search_str.endswith(' bookmark')): possible_endings = ( ' in my bookmarks' ' in my saved posts' ' in my saved items' ' in my saved' ' in my saves' ' in saved posts' ' in saved items' ' in saved' ' in saves' ' in bookmarks' ' bookmark' ) for poss_ending in possible_endings: if search_str.endswith(poss_ending): search_str = search_str.replace(poss_ending, '') break # bookmark search nickname = get_nickname_from_actor(actor_str) if not nickname: self.send_response(400) self.end_headers() self.server.postreq_busy = False return search_str = search_str.replace('-', '', 1).strip() timezone = None if self.server.account_timezone.get(nickname): timezone = \ self.server.account_timezone.get(nickname) bold_reading = False if self.server.bold_reading.get(nickname): bold_reading = True bookmarks_str = \ html_history_search(self.server.translate, base_dir, http_prefix, nickname, domain, search_str, MAX_POSTS_IN_FEED, page_number, self.server.project_version, self.server.recent_posts_cache, self.server.max_recent_posts, curr_session, self.server.cached_webfingers, self.server.person_cache, port, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.peertube_instances, self.server.allow_local_network_access, self.server.theme_name, 'bookmarks', self.server.system_language, self.server.max_like_count, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, timezone, bold_reading, self.server.dogwhistles, self.server.access_keys, self.server.min_images_for_accounts) if bookmarks_str: msg = bookmarks_str.encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) self.server.postreq_busy = False return elif ('@' in search_str or ('://' in search_str and has_users_path(search_str))): remote_only = False if search_str.endswith(';remote'): search_str = search_str.replace(';remote', '') remote_only = True 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 not remote_only and \ is_following_actor(base_dir, nickname, domain, search_str): # get the actor if not has_users_path(search_str): search_nickname = get_nickname_from_actor(search_str) if not search_nickname: self.send_response(400) self.end_headers() self.server.postreq_busy = False return search_domain, search_port = \ get_domain_from_actor(search_str) search_domain_full = \ get_full_domain(search_domain, search_port) actor = \ local_actor_url(http_prefix, search_nickname, search_domain_full) else: actor = search_str # establish the session curr_proxy_type = proxy_type if '.onion/' in actor: curr_proxy_type = 'tor' curr_session = self.server.session_onion elif '.i2p/' in actor: curr_proxy_type = 'i2p' curr_session = self.server.session_i2p curr_session = \ self._establish_session("handle search", curr_session, curr_proxy_type) if not curr_session: self.server.postreq_busy = False return # get the avatar url for the actor avatar_url = \ get_avatar_image_url(curr_session, base_dir, http_prefix, actor, self.server.person_cache, None, True, self.server.signing_priv_key_pem) profile_path_str += \ '?options=' + actor + ';1;' + avatar_url self._show_person_options(calling_domain, profile_path_str, base_dir, http_prefix, domain, domain_full, getreq_start_time, onion_domain, i2p_domain, cookie, debug, authorized, curr_session) return else: show_published_date_only = \ self.server.show_published_date_only allow_local_network_access = \ self.server.allow_local_network_access access_keys = self.server.access_keys if self.server.key_shortcuts.get(nickname): access_keys = self.server.key_shortcuts[nickname] signing_priv_key_pem = \ self.server.signing_priv_key_pem twitter_replacement_domain = \ self.server.twitter_replacement_domain peertube_instances = \ self.server.peertube_instances yt_replace_domain = \ self.server.yt_replace_domain cached_webfingers = \ self.server.cached_webfingers recent_posts_cache = \ self.server.recent_posts_cache timezone = None if self.server.account_timezone.get(nickname): timezone = \ self.server.account_timezone.get(nickname) profile_handle = remove_eol(search_str).strip() # establish the session curr_proxy_type = proxy_type if '.onion/' in profile_handle or \ profile_handle.endswith('.onion'): curr_proxy_type = 'tor' curr_session = self.server.session_onion elif ('.i2p/' in profile_handle or profile_handle.endswith('.i2p')): curr_proxy_type = 'i2p' curr_session = self.server.session_i2p curr_session = \ self._establish_session("handle search", curr_session, curr_proxy_type) if not curr_session: self.server.postreq_busy = False return bold_reading = False if self.server.bold_reading.get(nickname): bold_reading = True min_images_for_accounts = \ self.server.min_images_for_accounts profile_str = \ html_profile_after_search(recent_posts_cache, self.server.max_recent_posts, self.server.translate, base_dir, profile_path_str, http_prefix, nickname, domain, port, profile_handle, curr_session, cached_webfingers, self.server.person_cache, self.server.debug, self.server.project_version, yt_replace_domain, twitter_replacement_domain, show_published_date_only, self.server.default_timeline, peertube_instances, allow_local_network_access, self.server.theme_name, access_keys, self.server.system_language, self.server.max_like_count, signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, timezone, self.server.onion_domain, self.server.i2p_domain, bold_reading, self.server.dogwhistles, min_images_for_accounts) if profile_str: msg = profile_str.encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) self.server.postreq_busy = False return actor_str = \ self._get_instance_url(calling_domain) + users_path self._redirect_headers(actor_str + '/search', cookie, calling_domain) self.server.postreq_busy = False return elif (search_str.startswith(':') or search_str.endswith(' emoji')): # eg. "cat emoji" if search_str.endswith(' emoji'): search_str = \ search_str.replace(' emoji', '') # emoji search nickname = get_nickname_from_actor(actor_str) emoji_str = \ html_search_emoji(self.server.translate, base_dir, search_str, nickname, domain, self.server.theme_name, self.server.access_keys) if emoji_str: msg = emoji_str.encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) self.server.postreq_busy = False return elif search_str.startswith('.'): # wanted items search shared_items_federated_domains = \ self.server.shared_items_federated_domains nickname = get_nickname_from_actor(actor_str) wanted_items_str = \ html_search_shared_items(self.server.translate, base_dir, search_str[1:], page_number, MAX_POSTS_IN_FEED, http_prefix, domain_full, actor_str, calling_domain, shared_items_federated_domains, 'wanted', nickname, domain, self.server.theme_name, self.server.access_keys) if wanted_items_str: msg = wanted_items_str.encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) self.server.postreq_busy = False return else: # shared items search shared_items_federated_domains = \ self.server.shared_items_federated_domains nickname = get_nickname_from_actor(actor_str) shared_items_str = \ html_search_shared_items(self.server.translate, base_dir, search_str, page_number, MAX_POSTS_IN_FEED, http_prefix, domain_full, actor_str, calling_domain, shared_items_federated_domains, 'shares', nickname, domain, self.server.theme_name, self.server.access_keys) if shared_items_str: msg = shared_items_str.encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) self.server.postreq_busy = False return actor_str = self._get_instance_url(calling_domain) + users_path self._redirect_headers(actor_str + '/' + self.server.default_timeline, cookie, calling_domain) self.server.postreq_busy = False def _receive_vote(self, calling_domain: str, cookie: str, path: str, http_prefix: str, domain_full: str, onion_domain: str, i2p_domain: str, curr_session, proxy_type: str) -> None: """Receive a vote via POST """ first_post_id = '' if '?firstpost=' in path: first_post_id = path.split('?firstpost=')[1] path = path.split('?firstpost=')[0] if ';firstpost=' in path: first_post_id = path.split(';firstpost=')[1] path = path.split(';firstpost=')[0] if first_post_id: if '?' in first_post_id: first_post_id = first_post_id.split('?')[0] if ';' in first_post_id: first_post_id = first_post_id.split(';')[0] first_post_id = first_post_id.replace('/', '--') first_post_id = ';firstpost=' + first_post_id.replace('#', '--') last_post_id = '' if '?lastpost=' in path: last_post_id = path.split('?lastpost=')[1] path = path.split('?lastpost=')[0] if ';lastpost=' in path: last_post_id = path.split(';lastpost=')[1] path = path.split(';lastpost=')[0] if last_post_id: if '?' in last_post_id: last_post_id = last_post_id.split('?')[0] if ';' in last_post_id: last_post_id = last_post_id.split(';')[0] last_post_id = last_post_id.replace('/', '--') last_post_id = ';lastpost=' + last_post_id.replace('#', '--') 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) # 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) + first_post_id + last_post_id 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) if 'messageId=' in remove_post_confirm_params: remove_message_id = \ remove_post_confirm_params.split('messageId=')[1] elif 'eventid=' in remove_post_confirm_params: remove_message_id = \ remove_post_confirm_params.split('eventid=')[1] else: self.send_response(400) self.end_headers() self.server.postreq_busy = False return 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', '') # change registrations open status registrations_open = False if self.server.registration or \ get_config_param(base_dir, "registration") == 'open': registrations_open = True if fields.get('regOpen'): if fields['regOpen'] != registrations_open: registrations_open = fields['regOpen'] set_config_param(base_dir, 'registration', 'open') remaining = \ get_config_param(base_dir, 'registrationsRemaining') if not remaining: set_config_param(base_dir, 'registrationsRemaining', 10) self.server.registration = True else: if registrations_open: set_config_param(base_dir, 'registration', 'closed') self.server.registration = False # change registrations remaining reg_str = "registrationsRemaining" remaining = get_config_param(base_dir, reg_str) if fields.get('regRemaining'): if fields['regRemaining'] != remaining: remaining = int(fields['regRemaining']) if remaining < 0: remaining = 0 elif remaining > 10: remaining = 10 set_config_param(base_dir, reg_str, remaining) # 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 content license if fields.get('contentLicenseUrl'): if fields['contentLicenseUrl'] != \ self.server.content_license_url: license_str = fields['contentLicenseUrl'] if '://' not in license_str: license_str = \ license_link_from_name(license_str) set_config_param(base_dir, 'contentLicenseUrl', license_str) self.server.content_license_url = \ license_str else: license_str = \ 'https://creativecommons.org/' + \ 'licenses/by-nc/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 # set maximum preview posts on profile screen max_profile_posts = \ get_max_profile_posts(base_dir, nickname, domain, 20) if fields.get('maxRecentProfilePosts'): if fields['maxRecentProfilePosts'] != \ str(max_profile_posts): max_profile_posts = \ fields['maxRecentProfilePosts'] set_max_profile_posts(base_dir, nickname, domain, max_profile_posts) else: set_max_profile_posts(base_dir, nickname, domain, 20) # 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 actor_json['tag'] = [] 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: for _, tag in actor_tags.items(): actor_json['tag'].append(tag) actor_changed = True else: if check_name_and_bio: redirect_path = 'previewAvatar' else: if check_name_and_bio: redirect_path = 'previewAvatar' admin_nickname = \ get_config_param(base_dir, 'admin') if admin_nickname: # whether to require jsonld signatures # on all incoming posts if path.startswith('/users/' + admin_nickname + '/'): show_node_info_accounts = False if fields.get('showNodeInfoAccounts'): if fields['showNodeInfoAccounts'] == 'on': show_node_info_accounts = True self.server.show_node_info_accounts = \ show_node_info_accounts set_config_param(base_dir, "showNodeInfoAccounts", show_node_info_accounts) show_node_info_version = False if fields.get('showNodeInfoVersion'): if fields['showNodeInfoVersion'] == 'on': show_node_info_version = True self.server.show_node_info_version = \ show_node_info_version set_config_param(base_dir, "showNodeInfoVersion", show_node_info_version) verify_all_signatures = False if fields.get('verifyallsignatures'): if fields['verifyallsignatures'] == 'on': verify_all_signatures = True self.server.verify_all_signatures = \ verify_all_signatures set_config_param(base_dir, "verifyAllSignatures", verify_all_signatures) broch_mode = False if fields.get('brochMode'): if fields['brochMode'] == 'on': broch_mode = True curr_broch_mode = \ get_config_param(base_dir, "brochMode") if broch_mode != curr_broch_mode: set_broch_mode(self.server.base_dir, self.server.domain_full, broch_mode) set_config_param(base_dir, 'brochMode', broch_mode) # shared item federation domains si_domain_updated = False fed_domains_variable = \ "sharedItemsFederatedDomains" fed_domains_str = \ get_config_param(base_dir, fed_domains_variable) if not fed_domains_str: fed_domains_str = '' shared_items_form_str = '' if fields.get('shareDomainList'): shared_it_list = \ fed_domains_str.split(',') for shared_federated_domain in shared_it_list: shared_items_form_str += \ shared_federated_domain.strip() + '\n' share_domain_list = fields['shareDomainList'] if share_domain_list != \ shared_items_form_str: shared_items_form_str2 = \ share_domain_list.replace('\n', ',') shared_items_field = \ "sharedItemsFederatedDomains" set_config_param(base_dir, shared_items_field, shared_items_form_str2) si_domain_updated = True else: if fed_domains_str: shared_items_field = \ "sharedItemsFederatedDomains" set_config_param(base_dir, shared_items_field, '') si_domain_updated = True if si_domain_updated: si_domains = shared_items_form_str.split('\n') si_tokens = \ self.server.shared_item_federation_tokens self.server.shared_items_federated_domains = \ si_domains domain_full = self.server.domain_full base_dir = \ self.server.base_dir self.server.shared_item_federation_tokens = \ merge_shared_item_tokens(base_dir, domain_full, si_domains, si_tokens) # change moderators list set_roles_from_list(base_dir, domain, admin_nickname, 'moderators', 'moderator', fields, path, 'moderators.txt') # change site editors list set_roles_from_list(base_dir, domain, admin_nickname, 'editors', 'editor', fields, path, 'editors.txt') # change site devops list set_roles_from_list(base_dir, domain, admin_nickname, 'devopslist', 'devops', fields, path, 'devops.txt') # change site counselors list set_roles_from_list(base_dir, domain, admin_nickname, 'counselors', 'counselor', fields, path, 'counselors.txt') # change site artists list set_roles_from_list(base_dir, domain, admin_nickname, 'artists', 'artist', fields, path, 'artists.txt') # remove scheduled posts if fields.get('removeScheduledPosts'): if fields['removeScheduledPosts'] == 'on': remove_scheduled_posts(base_dir, nickname, domain) # approve followers if on_final_welcome_screen: # Default setting created via the welcome screen actor_json['manuallyApprovesFollowers'] = True actor_changed = True else: approve_followers = False if fields.get('approveFollowers'): if fields['approveFollowers'] == 'on': approve_followers = True if approve_followers != \ actor_json['manuallyApprovesFollowers']: actor_json['manuallyApprovesFollowers'] = \ approve_followers actor_changed = True # reject spam actors reject_spam_actors = False if fields.get('rejectSpamActors'): if fields['rejectSpamActors'] == 'on': reject_spam_actors = True curr_reject_spam_actors = False actor_spam_filter_filename = \ acct_dir(base_dir, nickname, domain) + \ '/.reject_spam_actors' if os.path.isfile(actor_spam_filter_filename): curr_reject_spam_actors = True if reject_spam_actors != curr_reject_spam_actors: if reject_spam_actors: try: with open(actor_spam_filter_filename, 'w+', encoding='utf-8') as fp_spam: fp_spam.write('\n') except OSError: print('EX: unable to write reject spam actors') else: try: os.remove(actor_spam_filter_filename) except OSError: print('EX: ' + 'unable to remove reject spam actors') # keep DMs during post expiry expire_keep_dms = False if fields.get('expiryKeepDMs'): if fields['expiryKeepDMs'] == 'on': expire_keep_dms = True curr_keep_dms = \ get_post_expiry_keep_dms(base_dir, nickname, domain) if curr_keep_dms != expire_keep_dms: set_post_expiry_keep_dms(base_dir, nickname, domain, expire_keep_dms) actor_changed = True # remove a custom font if fields.get('removeCustomFont'): if (fields['removeCustomFont'] == 'on' and (is_artist(base_dir, nickname) or path.startswith('/users/' + admin_nickname + '/'))): font_ext = ('woff', 'woff2', 'otf', 'ttf') for ext in font_ext: if os.path.isfile(base_dir + '/fonts/custom.' + ext): try: os.remove(base_dir + '/fonts/custom.' + ext) except OSError: print('EX: _profile_edit ' + 'unable to delete ' + base_dir + '/fonts/custom.' + ext) if os.path.isfile(base_dir + '/fonts/custom.' + ext + '.etag'): try: os.remove(base_dir + '/fonts/custom.' + ext + '.etag') except OSError: print('EX: _profile_edit ' + 'unable to delete ' + base_dir + '/fonts/custom.' + ext + '.etag') curr_theme = get_theme(base_dir) if curr_theme: self.server.theme_name = curr_theme allow_local_network_access = \ self.server.allow_local_network_access set_theme(base_dir, curr_theme, domain, allow_local_network_access, system_language, self.server.dyslexic_font, False) self.server.text_mode_banner = \ get_text_mode_banner(base_dir) self.server.iconsCache = {} self.server.fontsCache = {} self.server.show_publish_as_icon = \ get_config_param(base_dir, 'showPublishAsIcon') self.server.full_width_tl_button_header = \ get_config_param(base_dir, 'fullWidthTimeline' + 'ButtonHeader') self.server.icons_as_buttons = \ get_config_param(base_dir, 'iconsAsButtons') self.server.rss_icon_at_top = \ get_config_param(base_dir, 'rssIconAtTop') self.server.publish_button_at_top = \ get_config_param(base_dir, 'publishButtonAtTop') # only receive DMs from accounts you follow follow_dms_filename = \ acct_dir(base_dir, nickname, domain) + '/.followDMs' if on_final_welcome_screen: # initial default setting created via # the welcome screen try: with open(follow_dms_filename, 'w+', encoding='utf-8') as ffile: ffile.write('\n') except OSError: print('EX: unable to write follow DMs ' + follow_dms_filename) actor_changed = True else: follow_dms_active = False if fields.get('followDMs'): if fields['followDMs'] == 'on': follow_dms_active = True try: with open(follow_dms_filename, 'w+', encoding='utf-8') as ffile: ffile.write('\n') except OSError: print('EX: unable to write follow DMs 2 ' + follow_dms_filename) if not follow_dms_active: if os.path.isfile(follow_dms_filename): try: os.remove(follow_dms_filename) except OSError: print('EX: _profile_edit ' + 'unable to delete ' + follow_dms_filename) # remove Twitter retweets remove_twitter_filename = \ acct_dir(base_dir, nickname, domain) + \ '/.removeTwitter' remove_twitter_active = False if fields.get('removeTwitter'): if fields['removeTwitter'] == 'on': remove_twitter_active = True try: with open(remove_twitter_filename, 'w+', encoding='utf-8') as rfile: rfile.write('\n') except OSError: print('EX: unable to write remove twitter ' + remove_twitter_filename) if not remove_twitter_active: if os.path.isfile(remove_twitter_filename): try: os.remove(remove_twitter_filename) except OSError: print('EX: _profile_edit ' + 'unable to delete ' + remove_twitter_filename) # hide Like button hide_like_button_file = \ acct_dir(base_dir, nickname, domain) + \ '/.hideLikeButton' notify_likes_filename = \ acct_dir(base_dir, nickname, domain) + \ '/.notifyLikes' hide_like_button_active = False if fields.get('hideLikeButton'): if fields['hideLikeButton'] == 'on': hide_like_button_active = True try: with open(hide_like_button_file, 'w+', encoding='utf-8') as rfil: rfil.write('\n') except OSError: print('EX: unable to write hide like ' + hide_like_button_file) # remove notify likes selection if os.path.isfile(notify_likes_filename): try: os.remove(notify_likes_filename) except OSError: print('EX: _profile_edit ' + 'unable to delete ' + notify_likes_filename) if not hide_like_button_active: if os.path.isfile(hide_like_button_file): try: os.remove(hide_like_button_file) except OSError: print('EX: _profile_edit ' + 'unable to delete ' + hide_like_button_file) # Minimize all images from edit profile screen minimize_all_images = False if fields.get('minimizeAllImages'): if fields['minimizeAllImages'] == 'on': minimize_all_images = True min_img_acct = self.server.min_images_for_accounts set_minimize_all_images(base_dir, nickname, domain, True, min_img_acct) print('min_images_for_accounts: ' + str(min_img_acct)) if not minimize_all_images: min_img_acct = self.server.min_images_for_accounts set_minimize_all_images(base_dir, nickname, domain, False, min_img_acct) print('min_images_for_accounts: ' + str(min_img_acct)) # hide Reaction button hide_reaction_button_file = \ acct_dir(base_dir, nickname, domain) + \ '/.hideReactionButton' notify_reactions_filename = \ acct_dir(base_dir, nickname, domain) + \ '/.notifyReactions' hide_reaction_button_active = False if fields.get('hideReactionButton'): if fields['hideReactionButton'] == 'on': hide_reaction_button_active = True try: with open(hide_reaction_button_file, 'w+', encoding='utf-8') as rfile: rfile.write('\n') except OSError: print('EX: unable to write hide reaction ' + hide_reaction_button_file) # remove notify Reaction selection if os.path.isfile(notify_reactions_filename): try: os.remove(notify_reactions_filename) except OSError: print('EX: _profile_edit ' + 'unable to delete ' + notify_reactions_filename) if not hide_reaction_button_active: if os.path.isfile(hide_reaction_button_file): try: os.remove(hide_reaction_button_file) except OSError: print('EX: _profile_edit ' + 'unable to delete ' + hide_reaction_button_file) # bold reading checkbox bold_reading_filename = \ acct_dir(base_dir, nickname, domain) + \ '/.boldReading' bold_reading = False if fields.get('boldReading'): if fields['boldReading'] == 'on': bold_reading = True self.server.bold_reading[nickname] = True try: with open(bold_reading_filename, 'w+', encoding='utf-8') as rfile: rfile.write('\n') except OSError: print('EX: unable to write bold reading ' + bold_reading_filename) if not bold_reading: if self.server.bold_reading.get(nickname): del self.server.bold_reading[nickname] if os.path.isfile(bold_reading_filename): try: os.remove(bold_reading_filename) except OSError: print('EX: _profile_edit ' + 'unable to delete ' + bold_reading_filename) # reverse timelines checkbox reverse = False if fields.get('reverseTimelines'): if fields['reverseTimelines'] == 'on': reverse = True if nickname not in self.server.reverse_sequence: self.server.reverse_sequence.append(nickname) save_reverse_timeline(base_dir, self.server.reverse_sequence) if not reverse: if nickname in self.server.reverse_sequence: self.server.reverse_sequence.remove(nickname) save_reverse_timeline(base_dir, self.server.reverse_sequence) # 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 if fields.get('blocked'): add_account_blocks(base_dir, nickname, domain, fields['blocked']) else: add_account_blocks(base_dir, nickname, domain, '') # 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: add_name_emojis_to_tags(base_dir, http_prefix, domain, self.server.port, actor_json) # 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 } ], "protocol_handlers": [ { "protocol": "web+ap", "url": "?target=%s" }, { "protocol": "web+epicyon", "url": "?target=%s" } ] } 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, self.server.blocked_cache) if msg: msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_person_options', debug) else: self._404() return if '/users/news/' in path: self._redirect_headers(origin_path_str + '/tlfeatures', cookie, calling_domain) return if calling_domain.endswith('.onion') and onion_domain: origin_path_str_absolute = \ 'http://' + onion_domain + origin_path_str elif calling_domain.endswith('.i2p') and i2p_domain: origin_path_str_absolute = \ 'http://' + i2p_domain + origin_path_str else: origin_path_str_absolute = \ http_prefix + '://' + domain_full + origin_path_str self._redirect_headers(origin_path_str_absolute, cookie, calling_domain) def _show_media(self, path: str, base_dir: str, getreq_start_time) -> None: """Returns a media file """ if is_image_file(path) or \ path_is_video(path) or \ path_is_audio(path): media_str = path.split('/media/')[1] media_filename = base_dir + '/media/' + media_str if os.path.isfile(media_filename): if self._etag_exists(media_filename): # The file has not changed self._304() return media_file_type = media_file_mime_type(media_filename) media_tm = os.path.getmtime(media_filename) last_modified_time = datetime.datetime.fromtimestamp(media_tm) last_modified_time_str = \ last_modified_time.strftime('%a, %d %b %Y %H:%M:%S GMT') media_binary = None try: with open(media_filename, 'rb') as av_file: media_binary = av_file.read() except OSError: print('EX: unable to read media binary ' + media_filename) if media_binary: self._set_headers_etag(media_filename, media_file_type, media_binary, None, None, True, last_modified_time_str) self._write(media_binary) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_media', self.server.debug) return self._404() def _get_ontology(self, calling_domain: str, path: str, base_dir: str, getreq_start_time) -> None: """Returns an ontology file """ if '.owl' in path or '.rdf' in path or '.json' in path: if '/ontologies/' in path: ontology_str = path.split('/ontologies/')[1].replace('#', '') else: ontology_str = path.split('/data/')[1].replace('#', '') ontology_filename = None ontology_file_type = 'application/rdf+xml' if ontology_str.startswith('DFC_'): ontology_filename = base_dir + '/ontology/DFC/' + ontology_str else: ontology_str = ontology_str.replace('/data/', '') ontology_filename = base_dir + '/ontology/' + ontology_str if ontology_str.endswith('.json'): ontology_file_type = 'application/ld+json' if os.path.isfile(ontology_filename): ontology_file = None try: with open(ontology_filename, 'r', encoding='utf-8') as fp_ont: ontology_file = fp_ont.read() except OSError: print('EX: unable to read ontology ' + ontology_filename) if ontology_file: ontology_file = \ ontology_file.replace('static.datafoodconsortium.org', calling_domain) if not calling_domain.endswith('.i2p') and \ not calling_domain.endswith('.onion'): ontology_file = \ ontology_file.replace('http://' + calling_domain, 'https://' + calling_domain) msg = ontology_file.encode('utf-8') msglen = len(msg) self._set_headers(ontology_file_type, msglen, None, calling_domain, False) self._write(msg) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_get_ontology', self.server.debug) return self._404() def _show_emoji(self, path: str, base_dir: str, getreq_start_time) -> None: """Returns an emoji image """ if is_image_file(path): emoji_str = path.split('/emoji/')[1] emoji_filename = base_dir + '/emoji/' + emoji_str if not os.path.isfile(emoji_filename): emoji_filename = base_dir + '/emojicustom/' + emoji_str if os.path.isfile(emoji_filename): if self._etag_exists(emoji_filename): # The file has not changed self._304() return media_image_type = get_image_mime_type(emoji_filename) media_binary = None try: with open(emoji_filename, 'rb') as av_file: media_binary = av_file.read() except OSError: print('EX: unable to read emoji image ' + emoji_filename) if media_binary: self._set_headers_etag(emoji_filename, media_image_type, media_binary, None, self.server.domain_full, False, None) self._write(media_binary) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_emoji', self.server.debug) return self._404() def _show_icon(self, path: str, base_dir: str, getreq_start_time) -> None: """Shows an icon """ if not path.endswith('.png'): self._404() return media_str = path.split('/icons/')[1] if '/' not in media_str: if not self.server.theme_name: theme = 'default' else: theme = self.server.theme_name icon_filename = media_str else: theme = media_str.split('/')[0] icon_filename = media_str.split('/')[1] media_filename = \ base_dir + '/theme/' + theme + '/icons/' + icon_filename if self._etag_exists(media_filename): # The file has not changed self._304() return if self.server.iconsCache.get(media_str): media_binary = self.server.iconsCache[media_str] mime_type_str = media_file_mime_type(media_filename) self._set_headers_etag(media_filename, mime_type_str, media_binary, None, self.server.domain_full, False, None) self._write(media_binary) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_icon', self.server.debug) return if os.path.isfile(media_filename): media_binary = None try: with open(media_filename, 'rb') as av_file: media_binary = av_file.read() except OSError: print('EX: unable to read icon image ' + media_filename) if media_binary: mime_type = media_file_mime_type(media_filename) self._set_headers_etag(media_filename, mime_type, media_binary, None, self.server.domain_full, False, None) self._write(media_binary) self.server.iconsCache[media_str] = media_binary fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_icon', self.server.debug) return self._404() def _show_specification_image(self, path: str, base_dir: str, getreq_start_time) -> None: """Shows an image within the ActivityPub specification document """ image_filename = path.split('/', 1)[1] if '/' in image_filename: self._404() return media_filename = \ base_dir + '/specification/' + image_filename if self._etag_exists(media_filename): # The file has not changed self._304() return if self.server.iconsCache.get(media_filename): media_binary = self.server.iconsCache[media_filename] mime_type_str = media_file_mime_type(media_filename) self._set_headers_etag(media_filename, mime_type_str, media_binary, None, self.server.domain_full, False, None) self._write(media_binary) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_specification_image', self.server.debug) return if os.path.isfile(media_filename): media_binary = None try: with open(media_filename, 'rb') as av_file: media_binary = av_file.read() except OSError: print('EX: unable to read specification image ' + media_filename) if media_binary: mime_type = media_file_mime_type(media_filename) self._set_headers_etag(media_filename, mime_type, media_binary, None, self.server.domain_full, False, None) self._write(media_binary) self.server.iconsCache[media_filename] = media_binary fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_specification_image', self.server.debug) return self._404() def _show_manual_image(self, path: str, base_dir: str, getreq_start_time) -> None: """Shows an image within the manual """ image_filename = path.split('/', 1)[1] if '/' in image_filename: self._404() return media_filename = \ base_dir + '/manual/' + image_filename if self._etag_exists(media_filename): # The file has not changed self._304() return if self.server.iconsCache.get(media_filename): media_binary = self.server.iconsCache[media_filename] mime_type_str = media_file_mime_type(media_filename) self._set_headers_etag(media_filename, mime_type_str, media_binary, None, self.server.domain_full, False, None) self._write(media_binary) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_manual_image', self.server.debug) return if os.path.isfile(media_filename): media_binary = None try: with open(media_filename, 'rb') as av_file: media_binary = av_file.read() except OSError: print('EX: unable to read manual image ' + media_filename) if media_binary: mime_type = media_file_mime_type(media_filename) self._set_headers_etag(media_filename, mime_type, media_binary, None, self.server.domain_full, False, None) self._write(media_binary) self.server.iconsCache[media_filename] = media_binary fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_manual_image', self.server.debug) return self._404() def _show_help_screen_image(self, path: str, base_dir: str, getreq_start_time) -> None: """Shows a help screen image """ if not is_image_file(path): return media_str = path.split('/helpimages/')[1] if '/' not in media_str: if not self.server.theme_name: theme = 'default' else: theme = self.server.theme_name icon_filename = media_str else: theme = media_str.split('/')[0] icon_filename = media_str.split('/')[1] media_filename = \ base_dir + '/theme/' + theme + '/helpimages/' + icon_filename # if there is no theme-specific help image then use the default one if not os.path.isfile(media_filename): media_filename = \ base_dir + '/theme/default/helpimages/' + icon_filename if self._etag_exists(media_filename): # The file has not changed self._304() return if os.path.isfile(media_filename): media_binary = None try: with open(media_filename, 'rb') as av_file: media_binary = av_file.read() except OSError: print('EX: unable to read help image ' + media_filename) if media_binary: mime_type = media_file_mime_type(media_filename) self._set_headers_etag(media_filename, mime_type, media_binary, None, self.server.domain_full, False, None) self._write(media_binary) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_help_screen_image', self.server.debug) return self._404() def _show_cached_favicon(self, referer_domain: str, path: str, base_dir: str, getreq_start_time) -> None: """Shows a favicon image obtained from the cache """ fav_file = path.replace('/favicons/', '') fav_filename = base_dir + urllib.parse.unquote_plus(path) print('showCachedFavicon: ' + fav_filename) if self.server.favicons_cache.get(fav_file): media_binary = self.server.favicons_cache[fav_file] mime_type = media_file_mime_type(fav_filename) self._set_headers_etag(fav_filename, mime_type, media_binary, None, referer_domain, False, None) self._write(media_binary) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_cached_favicon2', self.server.debug) return if not os.path.isfile(fav_filename): self._404() return if self._etag_exists(fav_filename): # The file has not changed self._304() return media_binary = None try: with open(fav_filename, 'rb') as av_file: media_binary = av_file.read() except OSError: print('EX: unable to read cached favicon ' + fav_filename) if media_binary: mime_type = media_file_mime_type(fav_filename) self._set_headers_etag(fav_filename, mime_type, media_binary, None, referer_domain, False, None) self._write(media_binary) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_cached_favicon', self.server.debug) self.server.favicons_cache[fav_file] = media_binary return self._404() def _show_cached_avatar(self, referer_domain: str, path: str, base_dir: str, getreq_start_time) -> None: """Shows an avatar image obtained from the cache """ media_filename = base_dir + '/cache' + path if os.path.isfile(media_filename): if self._etag_exists(media_filename): # The file has not changed self._304() return media_binary = None try: with open(media_filename, 'rb') as av_file: media_binary = av_file.read() except OSError: print('EX: unable to read cached avatar ' + media_filename) if media_binary: mime_type = media_file_mime_type(media_filename) self._set_headers_etag(media_filename, mime_type, media_binary, None, referer_domain, False, None) self._write(media_binary) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_cached_avatar', self.server.debug) return self._404() def _hashtag_search(self, calling_domain: str, path: str, cookie: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, getreq_start_time, curr_session) -> None: """Return the result of a hashtag search """ page_number = 1 if '?page=' in path: page_number_str = path.split('?page=')[1] if '#' in page_number_str: page_number_str = page_number_str.split('#')[0] if len(page_number_str) > 5: page_number_str = "1" if page_number_str.isdigit(): page_number = int(page_number_str) hashtag = path.split('/tags/')[1] if '?page=' in hashtag: hashtag = hashtag.split('?page=')[0] hashtag = urllib.parse.unquote_plus(hashtag) if is_blocked_hashtag(base_dir, hashtag): print('BLOCK: hashtag #' + hashtag) msg = html_hashtag_blocked(base_dir, self.server.translate).encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) return nickname = None if '/users/' in path: nickname = path.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] if '?' in nickname: nickname = nickname.split('?')[0] timezone = None if self.server.account_timezone.get(nickname): timezone = \ self.server.account_timezone.get(nickname) bold_reading = False if self.server.bold_reading.get(nickname): bold_reading = True hashtag_str = \ html_hashtag_search(nickname, domain, port, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, base_dir, hashtag, page_number, MAX_POSTS_IN_HASHTAG_FEED, curr_session, self.server.cached_webfingers, self.server.person_cache, http_prefix, self.server.project_version, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.peertube_instances, self.server.allow_local_network_access, self.server.theme_name, self.server.system_language, self.server.max_like_count, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, timezone, bold_reading, self.server.dogwhistles, self.server.map_format, self.server.access_keys, 'search', self.server.min_images_for_accounts) if hashtag_str: msg = hashtag_str.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) else: origin_path_str = path.split('/tags/')[0] origin_path_str_absolute = \ http_prefix + '://' + domain_full + origin_path_str if calling_domain.endswith('.onion') and onion_domain: origin_path_str_absolute = \ 'http://' + onion_domain + origin_path_str elif (calling_domain.endswith('.i2p') and onion_domain): origin_path_str_absolute = \ 'http://' + i2p_domain + origin_path_str self._redirect_headers(origin_path_str_absolute + '/search', cookie, calling_domain) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_hashtag_search', self.server.debug) def _hashtag_search_rss2(self, calling_domain: str, path: str, cookie: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, getreq_start_time, curr_session) -> None: """Return an RSS 2 feed for a hashtag """ hashtag = path.split('/tags/rss2/')[1] if is_blocked_hashtag(base_dir, hashtag): self._400() return nickname = None if '/users/' in path: actor = \ http_prefix + '://' + domain_full + path nickname = \ get_nickname_from_actor(actor) hashtag_str = \ rss_hashtag_search(nickname, domain, port, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, base_dir, hashtag, MAX_POSTS_IN_FEED, curr_session, self.server.cached_webfingers, self.server.person_cache, http_prefix, self.server.project_version, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.system_language) if hashtag_str: msg = hashtag_str.encode('utf-8') msglen = len(msg) self._set_headers('text/xml', msglen, cookie, calling_domain, False) self._write(msg) else: origin_path_str = path.split('/tags/rss2/')[0] origin_path_str_absolute = \ http_prefix + '://' + domain_full + origin_path_str if calling_domain.endswith('.onion') and onion_domain: origin_path_str_absolute = \ 'http://' + onion_domain + origin_path_str elif (calling_domain.endswith('.i2p') and onion_domain): origin_path_str_absolute = \ 'http://' + i2p_domain + origin_path_str self._redirect_headers(origin_path_str_absolute + '/search', cookie, calling_domain) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_hashtag_search_rss2', self.server.debug) def _hashtag_search_json(self, calling_domain: str, referer_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) -> None: """Return a json collection for a hashtag """ page_number = 1 if '?page=' in path: page_number_str = path.split('?page=')[1] if page_number_str.isdigit(): page_number = int(page_number_str) path = path.split('?page=')[0] hashtag = path.split('/tags/')[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_json = \ hashtag_search_json(nickname, domain, port, base_dir, hashtag, page_number, MAX_POSTS_IN_FEED, http_prefix) if hashtag_json: msg_str = json.dumps(hashtag_json) msg_str = self._convert_domains(calling_domain, referer_domain, msg_str) msg = msg_str.encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, True) 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, cookie, calling_domain) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_hashtag_search_json', 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] first_post_id = '' if '?firstpost=' in path: first_post_id = path.split('?firstpost=')[1] if '?' in first_post_id: first_post_id = first_post_id.split('?')[0] first_post_id = first_post_id.replace('/', '--') first_post_id = ';firstpost=' + first_post_id.replace('#', '--') 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 '#' 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_id = None announce_json = \ create_announce(curr_session, base_dir, self.server.federation_list, self.post_to_nickname, domain, port, announce_to_str, None, http_prefix, repeat_url, False, False, self.server.send_threads, self.server.postLog, self.server.person_cache, self.server.cached_webfingers, debug, self.server.project_version, self.server.signing_priv_key_pem, self.server.domain, onion_domain, i2p_domain) announce_filename = None if announce_json: # save the announce straight to the outbox # This is because the subsequent send is within a separate thread # but the html still needs to be generated before this call ends announce_id = remove_id_ending(announce_json['id']) announce_filename = \ save_post_to_box(base_dir, http_prefix, announce_id, self.post_to_nickname, domain_full, announce_json, 'outbox') # clear the icon from the cache so that it gets updated if self.server.iconsCache.get('repeat.png'): del self.server.iconsCache['repeat.png'] # send out the announce within a separate thread self._post_to_outbox(announce_json, self.server.project_version, self.post_to_nickname, curr_session, proxy_type) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_announce_button postToOutboxThread', self.server.debug) # generate the html for the announce if announce_json and announce_filename: if debug: print('Generating html post for announce') cached_post_filename = \ get_cached_post_filename(base_dir, self.post_to_nickname, domain, announce_json) if debug: print('Announced post json: ' + str(announce_json)) print('Announced post nickname: ' + self.post_to_nickname + ' ' + domain) print('Announced post cache: ' + str(cached_post_filename)) show_individual_post_icons = True manually_approve_followers = \ follower_approval_active(base_dir, self.post_to_nickname, domain) show_repeats = not is_dm(announce_json) timezone = None if self.server.account_timezone.get(self.post_to_nickname): timezone = \ self.server.account_timezone.get(self.post_to_nickname) mitm = False if os.path.isfile(announce_filename.replace('.json', '') + '.mitm'): mitm = True bold_reading = False if self.server.bold_reading.get(self.post_to_nickname): bold_reading = True minimize_all_images = False if self.post_to_nickname in self.server.min_images_for_accounts: minimize_all_images = True individual_post_as_html(self.server.signing_priv_key_pem, False, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, page_number, base_dir, curr_session, self.server.cached_webfingers, self.server.person_cache, self.post_to_nickname, domain, self.server.port, announce_json, None, True, self.server.allow_deletion, http_prefix, self.server.project_version, timeline_str, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.peertube_instances, self.server.allow_local_network_access, self.server.theme_name, self.server.system_language, self.server.max_like_count, show_repeats, show_individual_post_icons, manually_approve_followers, False, True, False, self.server.cw_lists, self.server.lists_enabled, timezone, mitm, bold_reading, self.server.dogwhistles, minimize_all_images, None) actor_absolute = self._get_instance_url(calling_domain) + actor actor_path_str = \ actor_absolute + '/' + timeline_str + '?page=' + \ str(page_number) + first_post_id + 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] first_post_id = '' if '?firstpost=' in path: first_post_id = path.split('?firstpost=')[1] if '?' in first_post_id: first_post_id = first_post_id.split('?')[0] first_post_id = first_post_id.replace('/', '--') first_post_id = ';firstpost=' + first_post_id.replace('#', '--') 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 '#' 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) + first_post_id + 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] first_post_id = '' if '?firstpost=' in path: first_post_id = path.split('?firstpost=')[1] if '?' in first_post_id: first_post_id = first_post_id.split('?')[0] first_post_id = first_post_id.replace('/', '--') first_post_id = ';firstpost=' + first_post_id.replace('#', '--') 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 '#' in page_number_str: page_number_str = page_number_str.split('#')[0] if len(page_number_str) > 5: page_number_str = "1" if page_number_str.isdigit(): page_number = int(page_number_str) timeline_str = 'inbox' if '?tl=' in path: timeline_str = path.split('?tl=')[1] if '?' in timeline_str: timeline_str = timeline_str.split('?')[0] self.post_to_nickname = get_nickname_from_actor(actor) if not self.post_to_nickname: print('WARN: unable to find nickname in ' + actor) actor_absolute = self._get_instance_url(calling_domain) + actor actor_path_str = \ actor_absolute + '/' + timeline_str + \ '?page=' + str(page_number) + timeline_bookmark self._redirect_headers(actor_path_str, cookie, calling_domain) return if onion_domain: if '.onion/' in actor: curr_session = self.server.session_onion proxy_type = 'tor' if i2p_domain: if '.onion/' in actor: curr_session = self.server.session_i2p proxy_type = 'i2p' curr_session = \ self._establish_session("likeButton", curr_session, proxy_type) if not curr_session: self._404() return like_actor = \ local_actor_url(http_prefix, self.post_to_nickname, domain_full) actor_liked = path.split('?actor=')[1] if '?' in actor_liked: actor_liked = actor_liked.split('?')[0] # if this is an announce then send the like to the original post orig_actor, orig_post_url, orig_filename = \ get_original_post_from_announce_url(like_url, base_dir, self.post_to_nickname, domain) like_url2 = like_url liked_post_filename = orig_filename if orig_actor and orig_post_url: actor_liked = orig_actor like_url2 = orig_post_url liked_post_filename = None like_json = { "@context": "https://www.w3.org/ns/activitystreams", 'type': 'Like', 'actor': like_actor, 'to': [actor_liked], 'object': like_url2 } # send out the like to followers self._post_to_outbox(like_json, self.server.project_version, None, curr_session, proxy_type) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_like_button postToOutbox', self.server.debug) print('Locating liked post ' + like_url) # directly like the post file if not liked_post_filename: liked_post_filename = \ locate_post(base_dir, self.post_to_nickname, domain, like_url) if liked_post_filename: recent_posts_cache = self.server.recent_posts_cache liked_post_json = load_json(liked_post_filename, 0, 1) if orig_filename and orig_post_url: update_likes_collection(recent_posts_cache, base_dir, liked_post_filename, like_url, like_actor, self.post_to_nickname, domain, debug, liked_post_json) like_url = orig_post_url liked_post_filename = orig_filename if debug: print('Updating likes for ' + liked_post_filename) update_likes_collection(recent_posts_cache, base_dir, liked_post_filename, like_url, like_actor, self.post_to_nickname, domain, debug, None) if debug: print('Regenerating html post for changed likes collection') # clear the icon from the cache so that it gets updated if liked_post_json: cached_post_filename = \ get_cached_post_filename(base_dir, self.post_to_nickname, domain, liked_post_json) if debug: print('Liked post json: ' + str(liked_post_json)) print('Liked post nickname: ' + self.post_to_nickname + ' ' + domain) print('Liked post cache: ' + str(cached_post_filename)) show_individual_post_icons = True manually_approve_followers = \ follower_approval_active(base_dir, self.post_to_nickname, domain) show_repeats = not is_dm(liked_post_json) timezone = None if self.server.account_timezone.get(self.post_to_nickname): timezone = \ self.server.account_timezone.get(self.post_to_nickname) mitm = False if os.path.isfile(liked_post_filename.replace('.json', '') + '.mitm'): mitm = True bold_reading = False if self.server.bold_reading.get(self.post_to_nickname): bold_reading = True minimize_all_images = False if self.post_to_nickname in \ self.server.min_images_for_accounts: minimize_all_images = True individual_post_as_html(self.server.signing_priv_key_pem, False, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, page_number, base_dir, curr_session, self.server.cached_webfingers, self.server.person_cache, self.post_to_nickname, domain, self.server.port, liked_post_json, None, True, self.server.allow_deletion, http_prefix, self.server.project_version, timeline_str, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.peertube_instances, self.server.allow_local_network_access, self.server.theme_name, self.server.system_language, self.server.max_like_count, show_repeats, show_individual_post_icons, manually_approve_followers, False, True, False, self.server.cw_lists, self.server.lists_enabled, timezone, mitm, bold_reading, self.server.dogwhistles, minimize_all_images, None) 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) + first_post_id + \ 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] first_post_id = '' if '?firstpost=' in path: first_post_id = path.split('?firstpost=')[1] if '?' in first_post_id: first_post_id = first_post_id.split('?')[0] first_post_id = first_post_id.replace('/', '--') first_post_id = ';firstpost=' + first_post_id.replace('#', '--') 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 '#' in page_number_str: page_number_str = page_number_str.split('#')[0] if len(page_number_str) > 5: page_number_str = "1" if page_number_str.isdigit(): page_number = int(page_number_str) timeline_str = 'inbox' if '?tl=' in path: timeline_str = path.split('?tl=')[1] if '?' in timeline_str: timeline_str = timeline_str.split('?')[0] actor = path.split('?unlike=')[0] self.post_to_nickname = get_nickname_from_actor(actor) if not self.post_to_nickname: print('WARN: unable to find nickname in ' + actor) actor_absolute = self._get_instance_url(calling_domain) + actor actor_path_str = \ actor_absolute + '/' + timeline_str + \ '?page=' + str(page_number) self._redirect_headers(actor_path_str, cookie, calling_domain) return if onion_domain: if '.onion/' in actor: curr_session = self.server.session_onion proxy_type = 'tor' if i2p_domain: if '.onion/' in actor: curr_session = self.server.session_i2p proxy_type = 'i2p' curr_session = \ self._establish_session("undoLikeButton", curr_session, proxy_type) if not curr_session: self._404() return undo_actor = \ local_actor_url(http_prefix, self.post_to_nickname, domain_full) actor_liked = path.split('?actor=')[1] if '?' in actor_liked: actor_liked = actor_liked.split('?')[0] # if this is an announce then send the like to the original post orig_actor, orig_post_url, orig_filename = \ get_original_post_from_announce_url(like_url, base_dir, self.post_to_nickname, domain) like_url2 = like_url liked_post_filename = orig_filename if orig_actor and orig_post_url: actor_liked = orig_actor like_url2 = orig_post_url liked_post_filename = None undo_like_json = { "@context": "https://www.w3.org/ns/activitystreams", 'type': 'Undo', 'actor': undo_actor, 'to': [actor_liked], 'object': { 'type': 'Like', 'actor': undo_actor, 'to': [actor_liked], 'object': like_url2 } } # send out the undo like to followers self._post_to_outbox(undo_like_json, self.server.project_version, None, curr_session, proxy_type) # directly undo the like within the post file if not liked_post_filename: liked_post_filename = locate_post(base_dir, self.post_to_nickname, domain, like_url) if liked_post_filename: recent_posts_cache = self.server.recent_posts_cache liked_post_json = load_json(liked_post_filename, 0, 1) if orig_filename and orig_post_url: undo_likes_collection_entry(recent_posts_cache, base_dir, liked_post_filename, like_url, undo_actor, domain, debug, liked_post_json) like_url = orig_post_url liked_post_filename = orig_filename if debug: print('Removing likes for ' + liked_post_filename) undo_likes_collection_entry(recent_posts_cache, base_dir, liked_post_filename, like_url, undo_actor, domain, debug, None) if debug: print('Regenerating html post for changed likes collection') if liked_post_json: show_individual_post_icons = True manually_approve_followers = \ follower_approval_active(base_dir, self.post_to_nickname, domain) show_repeats = not is_dm(liked_post_json) timezone = None if self.server.account_timezone.get(self.post_to_nickname): timezone = \ self.server.account_timezone.get(self.post_to_nickname) mitm = False if os.path.isfile(liked_post_filename.replace('.json', '') + '.mitm'): mitm = True bold_reading = False if self.server.bold_reading.get(self.post_to_nickname): bold_reading = True minimize_all_images = False if self.post_to_nickname in \ self.server.min_images_for_accounts: minimize_all_images = True individual_post_as_html(self.server.signing_priv_key_pem, False, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, page_number, base_dir, curr_session, self.server.cached_webfingers, self.server.person_cache, self.post_to_nickname, domain, self.server.port, liked_post_json, None, True, self.server.allow_deletion, http_prefix, self.server.project_version, timeline_str, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.peertube_instances, self.server.allow_local_network_access, self.server.theme_name, self.server.system_language, self.server.max_like_count, show_repeats, show_individual_post_icons, manually_approve_followers, False, True, False, self.server.cw_lists, self.server.lists_enabled, timezone, mitm, bold_reading, self.server.dogwhistles, minimize_all_images, None) 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) + first_post_id + \ 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] first_post_id = '' if '?firstpost=' in path: first_post_id = path.split('?firstpost=')[1] if '?' in first_post_id: first_post_id = first_post_id.split('?')[0] first_post_id = first_post_id.replace('/', '--') first_post_id = ';firstpost=' + first_post_id.replace('#', '--') 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 '#' in page_number_str: page_number_str = page_number_str.split('#')[0] if len(page_number_str) > 5: page_number_str = "1" if page_number_str.isdigit(): page_number = int(page_number_str) timeline_str = 'inbox' if '?tl=' in path: timeline_str = path.split('?tl=')[1] if '?' in timeline_str: timeline_str = timeline_str.split('?')[0] emoji_content_encoded = None if '?emojreact=' in path: emoji_content_encoded = path.split('?emojreact=')[1] if '?' in emoji_content_encoded: emoji_content_encoded = emoji_content_encoded.split('?')[0] if not emoji_content_encoded: print('WARN: no emoji reaction ' + actor) actor_absolute = self._get_instance_url(calling_domain) + actor actor_path_str = \ actor_absolute + '/' + timeline_str + \ '?page=' + str(page_number) + timeline_bookmark self._redirect_headers(actor_path_str, cookie, calling_domain) return emoji_content = urllib.parse.unquote_plus(emoji_content_encoded) self.post_to_nickname = get_nickname_from_actor(actor) if not self.post_to_nickname: print('WARN: unable to find nickname in ' + actor) actor_absolute = self._get_instance_url(calling_domain) + actor actor_path_str = \ actor_absolute + '/' + timeline_str + \ '?page=' + str(page_number) + timeline_bookmark self._redirect_headers(actor_path_str, cookie, calling_domain) return if onion_domain: if '.onion/' in actor: curr_session = self.server.session_onion proxy_type = 'tor' if i2p_domain: if '.onion/' in actor: curr_session = self.server.session_i2p proxy_type = 'i2p' curr_session = \ self._establish_session("reactionButton", curr_session, proxy_type) if not curr_session: self._404() return reaction_actor = \ local_actor_url(http_prefix, self.post_to_nickname, domain_full) actor_reaction = path.split('?actor=')[1] if '?' in actor_reaction: actor_reaction = actor_reaction.split('?')[0] # if this is an announce then send the emoji reaction # to the original post orig_actor, orig_post_url, orig_filename = \ get_original_post_from_announce_url(reaction_url, base_dir, self.post_to_nickname, domain) reaction_url2 = reaction_url reaction_post_filename = orig_filename if orig_actor and orig_post_url: actor_reaction = orig_actor reaction_url2 = orig_post_url reaction_post_filename = None reaction_json = { "@context": "https://www.w3.org/ns/activitystreams", 'type': 'EmojiReact', 'actor': reaction_actor, 'to': [actor_reaction], 'object': reaction_url2, 'content': emoji_content } # send out the emoji reaction to followers self._post_to_outbox(reaction_json, self.server.project_version, None, curr_session, proxy_type) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_reaction_button postToOutbox', self.server.debug) print('Locating emoji reaction post ' + reaction_url) # directly emoji reaction the post file if not reaction_post_filename: reaction_post_filename = \ locate_post(base_dir, self.post_to_nickname, domain, reaction_url) if reaction_post_filename: recent_posts_cache = self.server.recent_posts_cache reaction_post_json = load_json(reaction_post_filename, 0, 1) if orig_filename and orig_post_url: update_reaction_collection(recent_posts_cache, base_dir, reaction_post_filename, reaction_url, reaction_actor, self.post_to_nickname, domain, debug, reaction_post_json, emoji_content) reaction_url = orig_post_url reaction_post_filename = orig_filename if debug: print('Updating emoji reaction for ' + reaction_post_filename) update_reaction_collection(recent_posts_cache, base_dir, reaction_post_filename, reaction_url, reaction_actor, self.post_to_nickname, domain, debug, None, emoji_content) if debug: print('Regenerating html post for changed ' + 'emoji reaction collection') # clear the icon from the cache so that it gets updated if reaction_post_json: cached_post_filename = \ get_cached_post_filename(base_dir, self.post_to_nickname, domain, reaction_post_json) if debug: print('Reaction post json: ' + str(reaction_post_json)) print('Reaction post nickname: ' + self.post_to_nickname + ' ' + domain) print('Reaction post cache: ' + str(cached_post_filename)) show_individual_post_icons = True manually_approve_followers = \ follower_approval_active(base_dir, self.post_to_nickname, domain) show_repeats = not is_dm(reaction_post_json) timezone = None if self.server.account_timezone.get(self.post_to_nickname): timezone = \ self.server.account_timezone.get(self.post_to_nickname) mitm = False if os.path.isfile(reaction_post_filename.replace('.json', '') + '.mitm'): mitm = True bold_reading = False if self.server.bold_reading.get(self.post_to_nickname): bold_reading = True minimize_all_images = False if self.post_to_nickname in \ self.server.min_images_for_accounts: minimize_all_images = True individual_post_as_html(self.server.signing_priv_key_pem, False, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, page_number, base_dir, curr_session, self.server.cached_webfingers, self.server.person_cache, self.post_to_nickname, domain, self.server.port, reaction_post_json, None, True, self.server.allow_deletion, http_prefix, self.server.project_version, timeline_str, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.peertube_instances, self.server.allow_local_network_access, self.server.theme_name, self.server.system_language, self.server.max_like_count, show_repeats, show_individual_post_icons, manually_approve_followers, False, True, False, self.server.cw_lists, self.server.lists_enabled, timezone, mitm, bold_reading, self.server.dogwhistles, minimize_all_images, None) 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) + first_post_id + \ 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] first_post_id = '' if '?firstpost=' in path: first_post_id = path.split('?firstpost=')[1] if '?' in first_post_id: first_post_id = first_post_id.split('?')[0] first_post_id = first_post_id.replace('/', '--') first_post_id = ';firstpost=' + first_post_id.replace('#', '--') 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 '#' in page_number_str: page_number_str = page_number_str.split('#')[0] if len(page_number_str) > 5: page_number_str = "1" if page_number_str.isdigit(): page_number = int(page_number_str) timeline_str = 'inbox' if '?tl=' in path: timeline_str = path.split('?tl=')[1] if '?' in timeline_str: timeline_str = timeline_str.split('?')[0] actor = path.split('?unreact=')[0] self.post_to_nickname = get_nickname_from_actor(actor) if not self.post_to_nickname: print('WARN: unable to find nickname in ' + actor) actor_absolute = self._get_instance_url(calling_domain) + actor actor_path_str = \ actor_absolute + '/' + timeline_str + \ '?page=' + str(page_number) self._redirect_headers(actor_path_str, cookie, calling_domain) return emoji_content_encoded = None if '?emojreact=' in path: emoji_content_encoded = path.split('?emojreact=')[1] if '?' in emoji_content_encoded: emoji_content_encoded = emoji_content_encoded.split('?')[0] if not emoji_content_encoded: print('WARN: no emoji reaction ' + actor) actor_absolute = self._get_instance_url(calling_domain) + actor actor_path_str = \ actor_absolute + '/' + timeline_str + \ '?page=' + str(page_number) + timeline_bookmark self._redirect_headers(actor_path_str, cookie, calling_domain) return emoji_content = urllib.parse.unquote_plus(emoji_content_encoded) if onion_domain: if '.onion/' in actor: curr_session = self.server.session_onion proxy_type = 'tor' if i2p_domain: if '.onion/' in actor: curr_session = self.server.session_i2p proxy_type = 'i2p' curr_session = \ self._establish_session("undoReactionButton", curr_session, proxy_type) if not curr_session: self._404() return undo_actor = \ local_actor_url(http_prefix, self.post_to_nickname, domain_full) actor_reaction = path.split('?actor=')[1] if '?' in actor_reaction: actor_reaction = actor_reaction.split('?')[0] # if this is an announce then send the emoji reaction # to the original post orig_actor, orig_post_url, orig_filename = \ get_original_post_from_announce_url(reaction_url, base_dir, self.post_to_nickname, domain) reaction_url2 = reaction_url reaction_post_filename = orig_filename if orig_actor and orig_post_url: actor_reaction = orig_actor reaction_url2 = orig_post_url reaction_post_filename = None undo_reaction_json = { "@context": "https://www.w3.org/ns/activitystreams", 'type': 'Undo', 'actor': undo_actor, 'to': [actor_reaction], 'object': { 'type': 'EmojiReact', 'actor': undo_actor, 'to': [actor_reaction], 'object': reaction_url2 } } # send out the undo emoji reaction to followers self._post_to_outbox(undo_reaction_json, self.server.project_version, None, curr_session, proxy_type) # directly undo the emoji reaction within the post file if not reaction_post_filename: reaction_post_filename = \ locate_post(base_dir, self.post_to_nickname, domain, reaction_url) if reaction_post_filename: recent_posts_cache = self.server.recent_posts_cache reaction_post_json = load_json(reaction_post_filename, 0, 1) if orig_filename and orig_post_url: undo_reaction_collection_entry(recent_posts_cache, base_dir, reaction_post_filename, reaction_url, undo_actor, domain, debug, reaction_post_json, emoji_content) reaction_url = orig_post_url reaction_post_filename = orig_filename if debug: print('Removing emoji reaction for ' + reaction_post_filename) undo_reaction_collection_entry(recent_posts_cache, base_dir, reaction_post_filename, reaction_url, undo_actor, domain, debug, reaction_post_json, emoji_content) if debug: print('Regenerating html post for changed ' + 'emoji reaction collection') if reaction_post_json: show_individual_post_icons = True manually_approve_followers = \ follower_approval_active(base_dir, self.post_to_nickname, domain) show_repeats = not is_dm(reaction_post_json) timezone = None if self.server.account_timezone.get(self.post_to_nickname): timezone = \ self.server.account_timezone.get(self.post_to_nickname) mitm = False if os.path.isfile(reaction_post_filename.replace('.json', '') + '.mitm'): mitm = True bold_reading = False if self.server.bold_reading.get(self.post_to_nickname): bold_reading = True minimize_all_images = False if self.post_to_nickname in \ self.server.min_images_for_accounts: minimize_all_images = True individual_post_as_html(self.server.signing_priv_key_pem, False, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, page_number, base_dir, curr_session, self.server.cached_webfingers, self.server.person_cache, self.post_to_nickname, domain, self.server.port, reaction_post_json, None, True, self.server.allow_deletion, http_prefix, self.server.project_version, timeline_str, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.peertube_instances, self.server.allow_local_network_access, self.server.theme_name, self.server.system_language, self.server.max_like_count, show_repeats, show_individual_post_icons, manually_approve_followers, False, True, False, self.server.cw_lists, self.server.lists_enabled, timezone, mitm, bold_reading, self.server.dogwhistles, minimize_all_images, None) 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) + first_post_id + \ timeline_bookmark fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_undo_reaction_button', self.server.debug) self._redirect_headers(actor_path_str, cookie, calling_domain) def _reaction_picker(self, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, port: int, getreq_start_time, cookie: str, debug: str, curr_session) -> None: """Press the emoji reaction picker icon at the bottom of the post """ page_number = 1 reaction_url = path.split('?selreact=')[1] if '?' in reaction_url: reaction_url = reaction_url.split('?')[0] timeline_bookmark = '' if '?bm=' in path: timeline_bookmark = path.split('?bm=')[1] if '?' in timeline_bookmark: timeline_bookmark = timeline_bookmark.split('?')[0] timeline_bookmark = '#' + timeline_bookmark actor = path.split('?selreact=')[0] if '?page=' in path: page_number_str = path.split('?page=')[1] if '?' in page_number_str: page_number_str = page_number_str.split('?')[0] if '#' in page_number_str: page_number_str = page_number_str.split('#')[0] if len(page_number_str) > 5: page_number_str = "1" if page_number_str.isdigit(): page_number = int(page_number_str) timeline_str = 'inbox' if '?tl=' in path: timeline_str = path.split('?tl=')[1] if '?' in timeline_str: timeline_str = timeline_str.split('?')[0] self.post_to_nickname = get_nickname_from_actor(actor) if not self.post_to_nickname: print('WARN: unable to find nickname in ' + actor) actor_absolute = self._get_instance_url(calling_domain) + actor actor_path_str = \ actor_absolute + '/' + timeline_str + \ '?page=' + str(page_number) + timeline_bookmark self._redirect_headers(actor_path_str, cookie, calling_domain) return post_json_object = None reaction_post_filename = \ locate_post(base_dir, self.post_to_nickname, domain, reaction_url) if reaction_post_filename: post_json_object = load_json(reaction_post_filename) if not reaction_post_filename or not post_json_object: print('WARN: unable to locate reaction post ' + reaction_url) actor_absolute = self._get_instance_url(calling_domain) + actor actor_path_str = \ actor_absolute + '/' + timeline_str + \ '?page=' + str(page_number) + timeline_bookmark self._redirect_headers(actor_path_str, cookie, calling_domain) return timezone = None if self.server.account_timezone.get(self.post_to_nickname): timezone = \ self.server.account_timezone.get(self.post_to_nickname) bold_reading = False if self.server.bold_reading.get(self.post_to_nickname): bold_reading = True msg = \ html_emoji_reaction_picker(self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, base_dir, curr_session, self.server.cached_webfingers, self.server.person_cache, self.post_to_nickname, domain, port, post_json_object, http_prefix, self.server.project_version, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.peertube_instances, self.server.allow_local_network_access, self.server.theme_name, self.server.system_language, self.server.max_like_count, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, timeline_str, page_number, timezone, bold_reading, self.server.dogwhistles, self.server.min_images_for_accounts) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_reaction_picker', debug) def _bookmark_button(self, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, getreq_start_time, proxy_type: str, cookie: str, debug: str, curr_session) -> None: """Bookmark button was pressed """ page_number = 1 bookmark_url = path.split('?bookmark=')[1] if '?' in bookmark_url: bookmark_url = bookmark_url.split('?')[0] first_post_id = '' if '?firstpost=' in path: first_post_id = path.split('?firstpost=')[1] if '?' in first_post_id: first_post_id = first_post_id.split('?')[0] first_post_id = first_post_id.replace('/', '--') first_post_id = ';firstpost=' + first_post_id.replace('#', '--') 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 '#' in page_number_str: page_number_str = page_number_str.split('#')[0] if len(page_number_str) > 5: page_number_str = "1" if page_number_str.isdigit(): page_number = int(page_number_str) timeline_str = 'inbox' if '?tl=' in path: timeline_str = path.split('?tl=')[1] if '?' in timeline_str: timeline_str = timeline_str.split('?')[0] self.post_to_nickname = get_nickname_from_actor(actor) if not self.post_to_nickname: print('WARN: unable to find nickname in ' + actor) actor_absolute = self._get_instance_url(calling_domain) + actor actor_path_str = \ actor_absolute + '/' + timeline_str + \ '?page=' + str(page_number) self._redirect_headers(actor_path_str, cookie, calling_domain) return if onion_domain: if '.onion/' in actor: curr_session = self.server.session_onion proxy_type = 'tor' if i2p_domain: if '.onion/' in actor: curr_session = self.server.session_i2p proxy_type = 'i2p' curr_session = \ self._establish_session("bookmarkButton", curr_session, proxy_type) if not curr_session: self._404() return bookmark_actor = \ local_actor_url(http_prefix, self.post_to_nickname, domain_full) cc_list = [] bookmark_post(self.server.recent_posts_cache, base_dir, self.server.federation_list, self.post_to_nickname, domain, port, cc_list, http_prefix, bookmark_url, bookmark_actor, debug) # clear the icon from the cache so that it gets updated if self.server.iconsCache.get('bookmark.png'): del self.server.iconsCache['bookmark.png'] bookmark_filename = \ locate_post(base_dir, self.post_to_nickname, domain, bookmark_url) if bookmark_filename: print('Regenerating html post for changed bookmark') bookmark_post_json = load_json(bookmark_filename, 0, 1) if bookmark_post_json: cached_post_filename = \ get_cached_post_filename(base_dir, self.post_to_nickname, domain, bookmark_post_json) print('Bookmarked post json: ' + str(bookmark_post_json)) print('Bookmarked post nickname: ' + self.post_to_nickname + ' ' + domain) print('Bookmarked post cache: ' + str(cached_post_filename)) show_individual_post_icons = True manually_approve_followers = \ follower_approval_active(base_dir, self.post_to_nickname, domain) show_repeats = not is_dm(bookmark_post_json) timezone = None if self.server.account_timezone.get(self.post_to_nickname): timezone = \ self.server.account_timezone.get(self.post_to_nickname) mitm = False if os.path.isfile(bookmark_filename.replace('.json', '') + '.mitm'): mitm = True bold_reading = False if self.server.bold_reading.get(self.post_to_nickname): bold_reading = True minimize_all_images = False if self.post_to_nickname in \ self.server.min_images_for_accounts: minimize_all_images = True individual_post_as_html(self.server.signing_priv_key_pem, False, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, page_number, base_dir, curr_session, self.server.cached_webfingers, self.server.person_cache, self.post_to_nickname, domain, self.server.port, bookmark_post_json, None, True, self.server.allow_deletion, http_prefix, self.server.project_version, timeline_str, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.peertube_instances, self.server.allow_local_network_access, self.server.theme_name, self.server.system_language, self.server.max_like_count, show_repeats, show_individual_post_icons, manually_approve_followers, False, True, False, self.server.cw_lists, self.server.lists_enabled, timezone, mitm, bold_reading, self.server.dogwhistles, minimize_all_images, None) 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) + first_post_id + \ 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] first_post_id = '' if '?firstpost=' in path: first_post_id = path.split('?firstpost=')[1] if '?' in first_post_id: first_post_id = first_post_id.split('?')[0] first_post_id = first_post_id.replace('/', '--') first_post_id = ';firstpost=' + first_post_id.replace('#', '--') 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 '#' in page_number_str: page_number_str = page_number_str.split('#')[0] if len(page_number_str) > 5: page_number_str = "1" if page_number_str.isdigit(): page_number = int(page_number_str) timeline_str = 'inbox' if '?tl=' in path: timeline_str = path.split('?tl=')[1] if '?' in timeline_str: timeline_str = timeline_str.split('?')[0] actor = path.split('?unbookmark=')[0] self.post_to_nickname = get_nickname_from_actor(actor) if not self.post_to_nickname: print('WARN: unable to find nickname in ' + actor) actor_absolute = self._get_instance_url(calling_domain) + actor actor_path_str = \ actor_absolute + '/' + timeline_str + \ '?page=' + str(page_number) self._redirect_headers(actor_path_str, cookie, calling_domain) return if onion_domain: if '.onion/' in actor: curr_session = self.server.session_onion proxy_type = 'tor' if i2p_domain: if '.onion/' in actor: curr_session = self.server.session_i2p proxy_type = 'i2p' curr_session = \ self._establish_session("undo_bookmarkButton", curr_session, proxy_type) if not curr_session: self._404() return undo_actor = \ local_actor_url(http_prefix, self.post_to_nickname, domain_full) cc_list = [] undo_bookmark_post(self.server.recent_posts_cache, base_dir, self.server.federation_list, self.post_to_nickname, domain, port, cc_list, http_prefix, bookmark_url, undo_actor, debug) # clear the icon from the cache so that it gets updated if self.server.iconsCache.get('bookmark_inactive.png'): del self.server.iconsCache['bookmark_inactive.png'] # self._post_to_outbox(undo_bookmark_json, # self.server.project_version, None, # curr_session, proxy_type) bookmark_filename = \ locate_post(base_dir, self.post_to_nickname, domain, bookmark_url) if bookmark_filename: print('Regenerating html post for changed unbookmark') bookmark_post_json = load_json(bookmark_filename, 0, 1) if bookmark_post_json: cached_post_filename = \ get_cached_post_filename(base_dir, self.post_to_nickname, domain, bookmark_post_json) print('Unbookmarked post json: ' + str(bookmark_post_json)) print('Unbookmarked post nickname: ' + self.post_to_nickname + ' ' + domain) print('Unbookmarked post cache: ' + str(cached_post_filename)) show_individual_post_icons = True manually_approve_followers = \ follower_approval_active(base_dir, self.post_to_nickname, domain) show_repeats = not is_dm(bookmark_post_json) timezone = None if self.server.account_timezone.get(self.post_to_nickname): timezone = \ self.server.account_timezone.get(self.post_to_nickname) mitm = False if os.path.isfile(bookmark_filename.replace('.json', '') + '.mitm'): mitm = True bold_reading = False if self.server.bold_reading.get(self.post_to_nickname): bold_reading = True minimize_all_images = False if self.post_to_nickname in \ self.server.min_images_for_accounts: minimize_all_images = True individual_post_as_html(self.server.signing_priv_key_pem, False, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, page_number, base_dir, curr_session, self.server.cached_webfingers, self.server.person_cache, self.post_to_nickname, domain, self.server.port, bookmark_post_json, None, True, self.server.allow_deletion, http_prefix, self.server.project_version, timeline_str, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.peertube_instances, self.server.allow_local_network_access, self.server.theme_name, self.server.system_language, self.server.max_like_count, show_repeats, show_individual_post_icons, manually_approve_followers, False, True, False, self.server.cw_lists, self.server.lists_enabled, timezone, mitm, bold_reading, self.server.dogwhistles, minimize_all_images, None) 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) + first_post_id + \ timeline_bookmark fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_undo_bookmark_button', self.server.debug) self._redirect_headers(actor_path_str, cookie, calling_domain) def _delete_button(self, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain_full: str, onion_domain: str, i2p_domain: str, getreq_start_time, proxy_type: str, cookie: str, debug: str, curr_session) -> None: """Delete button is pressed on a post """ if not cookie: print('ERROR: no cookie given when deleting ' + path) self._400() return page_number = 1 if '?page=' in path: page_number_str = path.split('?page=')[1] if '?' in page_number_str: page_number_str = page_number_str.split('?')[0] if '#' in page_number_str: page_number_str = page_number_str.split('#')[0] if len(page_number_str) > 5: page_number_str = "1" if page_number_str.isdigit(): page_number = int(page_number_str) delete_url = path.split('?delete=')[1] if '?' in delete_url: delete_url = delete_url.split('?')[0] timeline_str = self.server.default_timeline if '?tl=' in path: timeline_str = path.split('?tl=')[1] if '?' in timeline_str: timeline_str = timeline_str.split('?')[0] users_path = path.split('?delete=')[0] actor = \ http_prefix + '://' + domain_full + users_path if self.server.allow_deletion or \ delete_url.startswith(actor): if self.server.debug: print('DEBUG: delete_url=' + delete_url) print('DEBUG: actor=' + actor) if actor not in delete_url: # You can only delete your own posts if calling_domain.endswith('.onion') and onion_domain: actor = 'http://' + onion_domain + users_path elif calling_domain.endswith('.i2p') and i2p_domain: actor = 'http://' + i2p_domain + users_path self._redirect_headers(actor + '/' + timeline_str, cookie, calling_domain) return self.post_to_nickname = get_nickname_from_actor(actor) if not self.post_to_nickname: print('WARN: unable to find nickname in ' + actor) if calling_domain.endswith('.onion') and onion_domain: actor = 'http://' + onion_domain + users_path elif calling_domain.endswith('.i2p') and i2p_domain: actor = 'http://' + i2p_domain + users_path self._redirect_headers(actor + '/' + timeline_str, cookie, calling_domain) return if onion_domain: if '.onion/' in actor: curr_session = self.server.session_onion proxy_type = 'tor' if i2p_domain: if '.onion/' in actor: curr_session = self.server.session_i2p proxy_type = 'i2p' curr_session = \ self._establish_session("deleteButton", curr_session, proxy_type) if not curr_session: self._404() return delete_str = \ html_confirm_delete(self.server, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, page_number, curr_session, base_dir, delete_url, http_prefix, self.server.project_version, self.server.cached_webfingers, self.server.person_cache, calling_domain, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.peertube_instances, self.server.allow_local_network_access, self.server.theme_name, self.server.system_language, self.server.max_like_count, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, self.server.dogwhistles, self.server.min_images_for_accounts) if delete_str: delete_str_len = len(delete_str) self._set_headers('text/html', delete_str_len, cookie, calling_domain, False) self._write(delete_str.encode('utf-8')) self.server.getreq_busy = False return if calling_domain.endswith('.onion') and onion_domain: actor = 'http://' + onion_domain + users_path elif (calling_domain.endswith('.i2p') and i2p_domain): actor = 'http://' + i2p_domain + users_path fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_delete_button', debug) self._redirect_headers(actor + '/' + timeline_str, cookie, calling_domain) def _mute_button(self, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, getreq_start_time, cookie: str, debug: str, curr_session): """Mute button is pressed """ mute_url = path.split('?mute=')[1] if '?' in mute_url: mute_url = mute_url.split('?')[0] first_post_id = '' if '?firstpost=' in path: first_post_id = path.split('?firstpost=')[1] if '?' in first_post_id: first_post_id = first_post_id.split('?')[0] first_post_id = first_post_id.replace('/', '--') first_post_id = ';firstpost=' + first_post_id.replace('#', '--') 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 '#' in page_number_str: page_number_str = page_number_str.split('#')[0] if len(page_number_str) > 5: page_number_str = "1" if page_number_str.isdigit(): page_number = int(page_number_str) actor = \ http_prefix + '://' + domain_full + path.split('?mute=')[0] nickname = get_nickname_from_actor(actor) if not nickname: self._404() return mute_post(base_dir, nickname, domain, port, http_prefix, mute_url, self.server.recent_posts_cache, debug) mute_filename = \ locate_post(base_dir, nickname, domain, mute_url) if mute_filename: print('mute_post: Regenerating html post for changed mute status') mute_post_json = load_json(mute_filename, 0, 1) if mute_post_json: cached_post_filename = \ get_cached_post_filename(base_dir, nickname, domain, mute_post_json) print('mute_post: Muted post json: ' + str(mute_post_json)) print('mute_post: Muted post nickname: ' + nickname + ' ' + domain) print('mute_post: Muted post cache: ' + str(cached_post_filename)) show_individual_post_icons = True manually_approve_followers = \ follower_approval_active(base_dir, nickname, domain) show_repeats = not is_dm(mute_post_json) show_public_only = False store_to_cache = True use_cache_only = False allow_downloads = False show_avatar_options = True avatar_url = None timezone = None if self.server.account_timezone.get(nickname): timezone = \ self.server.account_timezone.get(nickname) mitm = False if os.path.isfile(mute_filename.replace('.json', '') + '.mitm'): mitm = True bold_reading = False if self.server.bold_reading.get(nickname): bold_reading = True minimize_all_images = False if nickname in self.server.min_images_for_accounts: minimize_all_images = True individual_post_as_html(self.server.signing_priv_key_pem, allow_downloads, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, page_number, base_dir, curr_session, self.server.cached_webfingers, self.server.person_cache, nickname, domain, self.server.port, mute_post_json, avatar_url, show_avatar_options, self.server.allow_deletion, http_prefix, self.server.project_version, timeline_str, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.peertube_instances, self.server.allow_local_network_access, self.server.theme_name, self.server.system_language, self.server.max_like_count, show_repeats, show_individual_post_icons, manually_approve_followers, show_public_only, store_to_cache, use_cache_only, self.server.cw_lists, self.server.lists_enabled, timezone, mitm, bold_reading, self.server.dogwhistles, minimize_all_images, None) 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) page_number_str = str(page_number) redirect_str = \ actor + '/' + timeline_str + '?page=' + page_number_str + \ first_post_id + timeline_bookmark self._redirect_headers(redirect_str, 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] first_post_id = '' if '?firstpost=' in path: first_post_id = path.split('?firstpost=')[1] if '?' in first_post_id: first_post_id = first_post_id.split('?')[0] first_post_id = first_post_id.replace('/', '--') first_post_id = ';firstpost=' + first_post_id.replace('#', '--') 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 '#' in page_number_str: page_number_str = page_number_str.split('#')[0] if len(page_number_str) > 5: page_number_str = "1" if page_number_str.isdigit(): page_number = int(page_number_str) actor = \ http_prefix + '://' + domain_full + path.split('?unmute=')[0] nickname = get_nickname_from_actor(actor) if not nickname: self._404() return unmute_post(base_dir, nickname, domain, port, http_prefix, mute_url, self.server.recent_posts_cache, debug) mute_filename = \ locate_post(base_dir, nickname, domain, mute_url) if mute_filename: print('unmute_post: ' + 'Regenerating html post for changed unmute status') mute_post_json = load_json(mute_filename, 0, 1) if mute_post_json: cached_post_filename = \ get_cached_post_filename(base_dir, nickname, domain, mute_post_json) print('unmute_post: Unmuted post json: ' + str(mute_post_json)) print('unmute_post: Unmuted post nickname: ' + nickname + ' ' + domain) print('unmute_post: Unmuted post cache: ' + str(cached_post_filename)) show_individual_post_icons = True manually_approve_followers = \ follower_approval_active(base_dir, nickname, domain) show_repeats = not is_dm(mute_post_json) show_public_only = False store_to_cache = True use_cache_only = False allow_downloads = False show_avatar_options = True avatar_url = None timezone = None if self.server.account_timezone.get(nickname): timezone = \ self.server.account_timezone.get(nickname) mitm = False if os.path.isfile(mute_filename.replace('.json', '') + '.mitm'): mitm = True bold_reading = False if self.server.bold_reading.get(nickname): bold_reading = True minimize_all_images = False if nickname in self.server.min_images_for_accounts: minimize_all_images = True individual_post_as_html(self.server.signing_priv_key_pem, allow_downloads, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, page_number, base_dir, curr_session, self.server.cached_webfingers, self.server.person_cache, nickname, domain, self.server.port, mute_post_json, avatar_url, show_avatar_options, self.server.allow_deletion, http_prefix, self.server.project_version, timeline_str, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.peertube_instances, self.server.allow_local_network_access, self.server.theme_name, self.server.system_language, self.server.max_like_count, show_repeats, show_individual_post_icons, manually_approve_followers, show_public_only, store_to_cache, use_cache_only, self.server.cw_lists, self.server.lists_enabled, timezone, mitm, bold_reading, self.server.dogwhistles, minimize_all_images, None) 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) page_number_str = str(page_number) redirect_str = \ actor + '/' + timeline_str + '?page=' + page_number_str + \ first_post_id + timeline_bookmark self._redirect_headers(redirect_str, 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 orig_post_url = http_prefix + ':##' + domain_full + '#users#' + \ nickname + '#statuses#' + status_number post_replies_filename = \ post_dir + '/' + orig_post_url + '.replies' if not os.path.isfile(post_replies_filename): # There are no replies, # so show empty collection context_str = \ 'https://www.w3.org/ns/activitystreams' first_str = \ local_actor_url(http_prefix, nickname, domain_full) + \ '/statuses/' + status_number + '/replies?page=true' id_str = \ local_actor_url(http_prefix, nickname, domain_full) + \ '/statuses/' + status_number + '/replies' last_str = \ local_actor_url(http_prefix, nickname, domain_full) + \ '/statuses/' + status_number + '/replies?page=true' replies_json = { '@context': context_str, 'first': first_str, 'id': id_str, 'last': last_str, 'totalItems': 0, 'type': 'OrderedCollection' } if self._request_http(): curr_session = \ self._establish_session("showRepliesToPost", curr_session, proxy_type) if not curr_session: self._404() return True recent_posts_cache = self.server.recent_posts_cache max_recent_posts = self.server.max_recent_posts translate = self.server.translate cached_webfingers = self.server.cached_webfingers person_cache = self.server.person_cache project_version = self.server.project_version yt_domain = self.server.yt_replace_domain twitter_replacement_domain = \ self.server.twitter_replacement_domain peertube_instances = self.server.peertube_instances timezone = None if self.server.account_timezone.get(nickname): timezone = \ self.server.account_timezone.get(nickname) bold_reading = False if self.server.bold_reading.get(nickname): bold_reading = True msg = \ html_post_replies(recent_posts_cache, max_recent_posts, translate, base_dir, curr_session, cached_webfingers, person_cache, nickname, domain, port, replies_json, http_prefix, project_version, yt_domain, twitter_replacement_domain, self.server.show_published_date_only, peertube_instances, self.server.allow_local_network_access, self.server.theme_name, self.server.system_language, self.server.max_like_count, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, timezone, bold_reading, self.server.dogwhistles, self.server.min_images_for_accounts) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_replies_to_post', debug) else: if self._secure_mode(curr_session, proxy_type): msg_str = json.dumps(replies_json, ensure_ascii=False) msg_str = self._convert_domains(calling_domain, referer_domain, msg_str) msg = msg_str.encode('utf-8') protocol_str = \ get_json_content_from_accept(self.headers['Accept']) msglen = len(msg) self._set_headers(protocol_str, msglen, None, calling_domain, False) self._write(msg) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_replies_to_post json', debug) else: self._404() return True else: # replies exist. Itterate through the # text file containing message ids context_str = 'https://www.w3.org/ns/activitystreams' id_str = \ local_actor_url(http_prefix, nickname, domain_full) + \ '/statuses/' + status_number + '?page=true' part_of_str = \ local_actor_url(http_prefix, nickname, domain_full) + \ '/statuses/' + status_number replies_json = { '@context': context_str, 'id': id_str, 'orderedItems': [ ], 'partOf': part_of_str, 'type': 'OrderedCollectionPage' } # if the original post is public then return the replies replies_are_public = \ is_public_post_from_url(base_dir, nickname, domain, orig_post_url) if replies_are_public: authorized = True # populate the items list with replies populate_replies_json(base_dir, nickname, domain, post_replies_filename, authorized, replies_json) # send the replies json if self._request_http(): curr_session = \ self._establish_session("showRepliesToPost2", curr_session, proxy_type) if not curr_session: self._404() return True recent_posts_cache = self.server.recent_posts_cache max_recent_posts = self.server.max_recent_posts translate = self.server.translate cached_webfingers = self.server.cached_webfingers person_cache = self.server.person_cache project_version = self.server.project_version yt_domain = self.server.yt_replace_domain twitter_replacement_domain = \ self.server.twitter_replacement_domain peertube_instances = self.server.peertube_instances timezone = None if self.server.account_timezone.get(nickname): timezone = \ self.server.account_timezone.get(nickname) bold_reading = False if self.server.bold_reading.get(nickname): bold_reading = True msg = \ html_post_replies(recent_posts_cache, max_recent_posts, translate, base_dir, curr_session, cached_webfingers, person_cache, nickname, domain, port, replies_json, http_prefix, project_version, yt_domain, twitter_replacement_domain, self.server.show_published_date_only, peertube_instances, self.server.allow_local_network_access, self.server.theme_name, self.server.system_language, self.server.max_like_count, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, timezone, bold_reading, self.server.dogwhistles, self.server.min_images_for_accounts) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_replies_to_post', debug) else: if self._secure_mode(curr_session, proxy_type): msg_str = json.dumps(replies_json, ensure_ascii=False) msg_str = self._convert_domains(calling_domain, referer_domain, msg_str) msg = msg_str.encode('utf-8') protocol_str = \ get_json_content_from_accept(self.headers['Accept']) msglen = len(msg) self._set_headers(protocol_str, msglen, None, calling_domain, False) self._write(msg) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_replies_to_post json', debug) else: self._404() return True return False def _show_roles(self, calling_domain: str, referer_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, getreq_start_time, proxy_type: str, cookie: str, debug: str, curr_session) -> bool: """Show roles within profile screen """ named_status = path.split('/users/')[1] if '/' not in named_status: return False post_sections = named_status.split('/') nickname = post_sections[0] actor_filename = acct_dir(base_dir, nickname, domain) + '.json' if not os.path.isfile(actor_filename): return False actor_json = load_json(actor_filename) if not actor_json: return False if actor_json.get('hasOccupation'): if self._request_http(): get_person = \ person_lookup(domain, path.replace('/roles', ''), base_dir) if get_person: default_timeline = \ self.server.default_timeline recent_posts_cache = \ self.server.recent_posts_cache cached_webfingers = \ self.server.cached_webfingers yt_replace_domain = \ self.server.yt_replace_domain twitter_replacement_domain = \ self.server.twitter_replacement_domain icons_as_buttons = \ self.server.icons_as_buttons access_keys = self.server.access_keys if self.server.key_shortcuts.get(nickname): access_keys = self.server.key_shortcuts[nickname] roles_list = get_actor_roles_list(actor_json) city = \ get_spoofed_city(self.server.city, base_dir, nickname, domain) shared_items_federated_domains = \ self.server.shared_items_federated_domains timezone = None if self.server.account_timezone.get(nickname): timezone = \ self.server.account_timezone.get(nickname) bold_reading = False if self.server.bold_reading.get(nickname): bold_reading = True msg = \ html_profile(self.server.signing_priv_key_pem, self.server.rss_icon_at_top, icons_as_buttons, default_timeline, recent_posts_cache, self.server.max_recent_posts, self.server.translate, self.server.project_version, base_dir, http_prefix, True, get_person, 'roles', curr_session, cached_webfingers, self.server.person_cache, yt_replace_domain, twitter_replacement_domain, self.server.show_published_date_only, self.server.newswire, self.server.theme_name, self.server.dormant_months, self.server.peertube_instances, self.server.allow_local_network_access, self.server.text_mode_banner, self.server.debug, access_keys, city, self.server.system_language, self.server.max_like_count, shared_items_federated_domains, roles_list, None, None, self.server.cw_lists, self.server.lists_enabled, self.server.content_license_url, timezone, bold_reading) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_roles', debug) else: if self._secure_mode(curr_session, proxy_type): roles_list = get_actor_roles_list(actor_json) msg_str = json.dumps(roles_list, ensure_ascii=False) msg_str = self._convert_domains(calling_domain, referer_domain, msg_str) msg = msg_str.encode('utf-8') msglen = len(msg) protocol_str = \ get_json_content_from_accept(self.headers['Accept']) self._set_headers(protocol_str, msglen, None, calling_domain, False) self._write(msg) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_roles json', debug) else: self._404() return True return False def _show_skills(self, calling_domain: str, referer_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, getreq_start_time, proxy_type: str, cookie: str, debug: str, curr_session) -> bool: """Show skills on the profile screen """ named_status = path.split('/users/')[1] if '/' in named_status: post_sections = named_status.split('/') nickname = post_sections[0] actor_filename = acct_dir(base_dir, nickname, domain) + '.json' if os.path.isfile(actor_filename): actor_json = load_json(actor_filename) if actor_json: if no_of_actor_skills(actor_json) > 0: if self._request_http(): get_person = \ person_lookup(domain, path.replace('/skills', ''), base_dir) if get_person: default_timeline = \ self.server.default_timeline recent_posts_cache = \ self.server.recent_posts_cache cached_webfingers = \ self.server.cached_webfingers yt_replace_domain = \ self.server.yt_replace_domain twitter_replacement_domain = \ self.server.twitter_replacement_domain show_published_date_only = \ self.server.show_published_date_only icons_as_buttons = \ self.server.icons_as_buttons allow_local_network_access = \ self.server.allow_local_network_access access_keys = self.server.access_keys if self.server.key_shortcuts.get(nickname): access_keys = \ self.server.key_shortcuts[nickname] actor_skills_list = \ get_occupation_skills(actor_json) skills = \ get_skills_from_list(actor_skills_list) city = get_spoofed_city(self.server.city, base_dir, nickname, domain) shared_items_fed_domains = \ self.server.shared_items_federated_domains signing_priv_key_pem = \ self.server.signing_priv_key_pem content_license_url = \ self.server.content_license_url peertube_instances = \ self.server.peertube_instances timezone = None nick = nickname if self.server.account_timezone.get(nick): timezone = \ self.server.account_timezone.get(nick) bold_reading = False if self.server.bold_reading.get(nick): bold_reading = True msg = \ html_profile(signing_priv_key_pem, self.server.rss_icon_at_top, icons_as_buttons, default_timeline, recent_posts_cache, self.server.max_recent_posts, self.server.translate, self.server.project_version, base_dir, http_prefix, True, get_person, 'skills', curr_session, cached_webfingers, self.server.person_cache, yt_replace_domain, twitter_replacement_domain, show_published_date_only, self.server.newswire, self.server.theme_name, self.server.dormant_months, peertube_instances, allow_local_network_access, self.server.text_mode_banner, self.server.debug, access_keys, city, self.server.system_language, self.server.max_like_count, shared_items_fed_domains, skills, None, None, self.server.cw_lists, self.server.lists_enabled, content_license_url, timezone, bold_reading) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_skills', self.server.debug) else: if self._secure_mode(curr_session, proxy_type): actor_skills_list = \ get_occupation_skills(actor_json) skills = \ get_skills_from_list(actor_skills_list) msg_str = json.dumps(skills, ensure_ascii=False) msg_str = self._convert_domains(calling_domain, referer_domain, msg_str) msg = msg_str.encode('utf-8') msglen = len(msg) accept_str = self.headers['Accept'] protocol_str = \ get_json_content_from_accept(accept_str) self._set_headers(protocol_str, msglen, None, calling_domain, False) self._write(msg) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_skills json', debug) else: self._404() return True actor = path.replace('/skills', '') actor_absolute = self._get_instance_url(calling_domain) + actor self._redirect_headers(actor_absolute, cookie, calling_domain) return True def _show_conversation_thread(self, authorized: bool, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, port: int, debug: str, curr_session) -> bool: """get conversation thread from the date link on a post """ if not authorized: return False if not path.startswith('/users/'): return False if '?convthread=' not in path: return False post_id = path.split('?convthread=')[1].strip() post_id = post_id.replace('--', '/') nickname = path.split('/users/')[1] if '?convthread=' in nickname: nickname = nickname.split('?convthread=')[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 conv_str = \ html_conversation_view(post_id, self.server.translate, base_dir, http_prefix, nickname, domain, 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, self.server.system_language, self.server.max_like_count, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, timezone, bold_reading, self.server.dogwhistles, self.server.access_keys, self.server.min_images_for_accounts, self.server.debug) if conv_str: msg = conv_str.encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) self.server.getreq_busy = False self._redirect_headers(post_id, None, calling_domain) return True def _show_individual_at_post(self, ssml_getreq: bool, authorized: bool, calling_domain: str, referer_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, getreq_start_time, proxy_type: str, cookie: str, debug: str, curr_session) -> bool: """get an individual post from the path /@nickname/statusnumber """ if '/@' not in path: return False liked_by = None if '?likedBy=' in path: liked_by = path.split('?likedBy=')[1].strip() if '?' in liked_by: liked_by = liked_by.split('?')[0] path = path.split('?likedBy=')[0] react_by = None react_emoji = None if '?reactBy=' in path: react_by = path.split('?reactBy=')[1].strip() if ';' in react_by: react_by = react_by.split(';')[0] if ';emoj=' in path: react_emoji = path.split(';emoj=')[1].strip() if ';' in react_emoji: react_emoji = react_emoji.split(';')[0] path = path.split('?reactBy=')[0] named_status = path.split('/@')[1] if '/' not in named_status: # show actor nickname = named_status return False post_sections = named_status.split('/') if len(post_sections) != 2: return False nickname = post_sections[0] status_number = post_sections[1] if len(status_number) <= 10 or not status_number.isdigit(): return False if ssml_getreq: ssml_filename = \ acct_dir(base_dir, nickname, domain) + '/outbox/' + \ http_prefix + ':##' + domain_full + '#users#' + nickname + \ '#statuses#' + status_number + '.ssml' if not os.path.isfile(ssml_filename): ssml_filename = \ acct_dir(base_dir, nickname, domain) + '/postcache/' + \ http_prefix + ':##' + domain_full + '#users#' + \ nickname + '#statuses#' + status_number + '.ssml' if not os.path.isfile(ssml_filename): self._404() return True ssml_str = None try: with open(ssml_filename, 'r', encoding='utf-8') as fp_ssml: ssml_str = fp_ssml.read() except OSError: pass if ssml_str: msg = ssml_str.encode('utf-8') msglen = len(msg) self._set_headers('application/ssml+xml', msglen, cookie, calling_domain, False) self._write(msg) return True self._404() return True post_filename = \ acct_dir(base_dir, nickname, domain) + '/outbox/' + \ http_prefix + ':##' + domain_full + '#users#' + nickname + \ '#statuses#' + status_number + '.json' include_create_wrapper = False if post_sections[-1] == 'activity': include_create_wrapper = True result = self._show_post_from_file(post_filename, liked_by, react_by, react_emoji, authorized, calling_domain, referer_domain, base_dir, http_prefix, nickname, domain, port, getreq_start_time, proxy_type, cookie, debug, include_create_wrapper, curr_session) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_individual_at_post', self.server.debug) return result def _show_likers_of_post(self, authorized: bool, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, port: int, getreq_start_time, cookie: str, debug: str, curr_session) -> bool: """Show the likers of a post """ if not authorized: return False if '?likers=' not in path: return False if '/users/' not in path: return False nickname = path.split('/users/')[1] if '?' in nickname: nickname = nickname.split('?')[0] post_url = path.split('?likers=')[1] if '?' in post_url: post_url = post_url.split('?')[0] post_url = post_url.replace('--', '/') bold_reading = False if self.server.bold_reading.get(nickname): bold_reading = True msg = \ html_likers_of_post(base_dir, nickname, domain, port, post_url, self.server.translate, http_prefix, self.server.theme_name, self.server.access_keys, self.server.recent_posts_cache, self.server.max_recent_posts, curr_session, self.server.cached_webfingers, self.server.person_cache, self.server.project_version, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.peertube_instances, self.server.allow_local_network_access, self.server.system_language, self.server.max_like_count, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, 'inbox', self.server.default_timeline, bold_reading, self.server.dogwhistles, self.server.min_images_for_accounts) if not msg: self._404() return True msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_likers_of_post', debug) return True def _show_announcers_of_post(self, authorized: bool, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, port: int, getreq_start_time, cookie: str, debug: str, curr_session) -> bool: """Show the announcers of a post """ if not authorized: return False if '?announcers=' not in path: return False if '/users/' not in path: return False nickname = path.split('/users/')[1] if '?' in nickname: nickname = nickname.split('?')[0] post_url = path.split('?announcers=')[1] if '?' in post_url: post_url = post_url.split('?')[0] post_url = post_url.replace('--', '/') bold_reading = False if self.server.bold_reading.get(nickname): bold_reading = True # note that the likers function is reused, but with 'shares' msg = \ html_likers_of_post(base_dir, nickname, domain, port, post_url, self.server.translate, http_prefix, self.server.theme_name, self.server.access_keys, self.server.recent_posts_cache, self.server.max_recent_posts, curr_session, self.server.cached_webfingers, self.server.person_cache, self.server.project_version, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.peertube_instances, self.server.allow_local_network_access, self.server.system_language, self.server.max_like_count, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, 'inbox', self.server.default_timeline, bold_reading, self.server.dogwhistles, self.server.min_images_for_accounts, 'shares') if not msg: self._404() return True msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_announcers_of_post', debug) return True def _show_post_from_file(self, post_filename: str, liked_by: str, react_by: str, react_emoji: str, authorized: bool, calling_domain: str, referer_domain: str, base_dir: str, http_prefix: str, nickname: str, domain: str, port: int, getreq_start_time, proxy_type: str, cookie: str, debug: str, include_create_wrapper: bool, curr_session) -> bool: """Shows an individual post from its filename """ if not os.path.isfile(post_filename): self._404() self.server.getreq_busy = False return True post_json_object = load_json(post_filename) if not post_json_object: self.send_response(429) self.end_headers() self.server.getreq_busy = False return True # Only authorized viewers get to see likes on posts # Otherwize marketers could gain more social graph info if not authorized: pjo = post_json_object if not is_public_post(pjo): self._404() self.server.getreq_busy = False return True remove_post_interactions(pjo, True) if self._request_http(): timezone = None if self.server.account_timezone.get(nickname): timezone = \ self.server.account_timezone.get(nickname) mitm = False if os.path.isfile(post_filename.replace('.json', '') + '.mitm'): mitm = True bold_reading = False if self.server.bold_reading.get(nickname): bold_reading = True msg = \ html_individual_post(self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, base_dir, curr_session, self.server.cached_webfingers, self.server.person_cache, nickname, domain, port, authorized, post_json_object, http_prefix, self.server.project_version, liked_by, react_by, react_emoji, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.peertube_instances, self.server.allow_local_network_access, self.server.theme_name, self.server.system_language, self.server.max_like_count, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, timezone, mitm, bold_reading, self.server.dogwhistles, self.server.min_images_for_accounts) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_post_from_file', debug) else: if self._secure_mode(curr_session, proxy_type): if not include_create_wrapper and \ post_json_object['type'] == 'Create' and \ has_object_dict(post_json_object): unwrapped_json = post_json_object['object'] unwrapped_json['@context'] = \ get_individual_post_context() msg_str = json.dumps(unwrapped_json, ensure_ascii=False) else: msg_str = json.dumps(post_json_object, ensure_ascii=False) msg_str = self._convert_domains(calling_domain, referer_domain, msg_str) msg = msg_str.encode('utf-8') msglen = len(msg) protocol_str = \ get_json_content_from_accept(self.headers['Accept']) self._set_headers(protocol_str, msglen, None, calling_domain, False) self._write(msg) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_post_from_file json', debug) else: self._404() self.server.getreq_busy = False return True def _show_individual_post(self, ssml_getreq: bool, authorized: bool, calling_domain: str, referer_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, getreq_start_time, proxy_type: str, cookie: str, debug: str, curr_session) -> bool: """Shows an individual post """ liked_by = None if '?likedBy=' in path: liked_by = path.split('?likedBy=')[1].strip() if '?' in liked_by: liked_by = liked_by.split('?')[0] path = path.split('?likedBy=')[0] react_by = None react_emoji = None if '?reactBy=' in path: react_by = path.split('?reactBy=')[1].strip() if ';' in react_by: react_by = react_by.split(';')[0] if ';emoj=' in path: react_emoji = path.split(';emoj=')[1].strip() if ';' in react_emoji: react_emoji = react_emoji.split(';')[0] path = path.split('?reactBy=')[0] named_status = path.split('/users/')[1] if '/' not in named_status: return False post_sections = named_status.split('/') if len(post_sections) < 3: return False nickname = post_sections[0] status_number = post_sections[2] if len(status_number) <= 10 or (not status_number.isdigit()): return False if ssml_getreq: ssml_filename = \ acct_dir(base_dir, nickname, domain) + '/outbox/' + \ http_prefix + ':##' + domain_full + '#users#' + nickname + \ '#statuses#' + status_number + '.ssml' if not os.path.isfile(ssml_filename): ssml_filename = \ acct_dir(base_dir, nickname, domain) + '/postcache/' + \ http_prefix + ':##' + domain_full + '#users#' + \ nickname + '#statuses#' + status_number + '.ssml' if not os.path.isfile(ssml_filename): self._404() return True ssml_str = None try: with open(ssml_filename, 'r', encoding='utf-8') as fp_ssml: ssml_str = fp_ssml.read() except OSError: pass if ssml_str: msg = ssml_str.encode('utf-8') msglen = len(msg) self._set_headers('application/ssml+xml', msglen, cookie, calling_domain, False) self._write(msg) return True self._404() return True post_filename = \ acct_dir(base_dir, nickname, domain) + '/outbox/' + \ http_prefix + ':##' + domain_full + '#users#' + nickname + \ '#statuses#' + status_number + '.json' include_create_wrapper = False if post_sections[-1] == 'activity': include_create_wrapper = True result = self._show_post_from_file(post_filename, liked_by, react_by, react_emoji, authorized, calling_domain, referer_domain, base_dir, http_prefix, nickname, domain, port, getreq_start_time, proxy_type, cookie, debug, include_create_wrapper, curr_session) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_individual_post', self.server.debug) return result def _show_notify_post(self, authorized: bool, calling_domain: str, referer_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, port: int, getreq_start_time, proxy_type: str, cookie: str, debug: str, curr_session) -> bool: """Shows an individual post from an account which you are following and where you have the notify checkbox set on person options """ liked_by = None react_by = None react_emoji = None post_id = path.split('?notifypost=')[1].strip() post_id = post_id.replace('-', '/') path = path.split('?notifypost=')[0] nickname = path.split('/users/')[1] if '/' in nickname: return False replies = False post_filename = locate_post(base_dir, nickname, domain, post_id, replies) if not post_filename: return False include_create_wrapper = False if path.endswith('/activity'): include_create_wrapper = True result = self._show_post_from_file(post_filename, liked_by, react_by, react_emoji, authorized, calling_domain, referer_domain, base_dir, http_prefix, nickname, domain, port, getreq_start_time, proxy_type, cookie, debug, include_create_wrapper, curr_session) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_notify_post', self.server.debug) return result def _show_inbox(self, authorized: bool, calling_domain: str, referer_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, port: int, getreq_start_time, cookie: str, debug: str, recent_posts_cache: {}, curr_session, default_timeline: str, max_recent_posts: int, translate: {}, cached_webfingers: {}, person_cache: {}, allow_deletion: bool, project_version: str, yt_replace_domain: str, twitter_replacement_domain: str, ua_str: str) -> bool: """Shows the inbox timeline """ if '/users/' in path: if authorized: inbox_feed = \ person_box_json(recent_posts_cache, base_dir, domain, port, path, http_prefix, MAX_POSTS_IN_FEED, 'inbox', authorized, 0, self.server.positive_voting, self.server.voting_time_mins) if inbox_feed: if getreq_start_time: fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_inbox', self.server.debug) if self._request_http(): nickname = path.replace('/users/', '') nickname = nickname.replace('/inbox', '') page_number = 1 if '?page=' in nickname: page_number = nickname.split('?page=')[1] if ';' in page_number: page_number = page_number.split(';')[0] 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 reverse_sequence = False if nickname in self.server.reverse_sequence: reverse_sequence = True last_post_id = None if ';lastpost=' in path: last_post_id = path.split(';lastpost=')[1] if ';' in last_post_id: last_post_id = last_post_id.split(';')[0] msg = \ html_inbox(default_timeline, recent_posts_cache, max_recent_posts, translate, page_number, MAX_POSTS_IN_FEED, curr_session, base_dir, cached_webfingers, person_cache, nickname, domain, port, inbox_feed, allow_deletion, http_prefix, project_version, minimal_nick, yt_replace_domain, twitter_replacement_domain, self.server.show_published_date_only, self.server.newswire, self.server.positive_voting, self.server.show_publish_as_icon, full_width_tl_button_header, self.server.icons_as_buttons, self.server.rss_icon_at_top, self.server.publish_button_at_top, authorized, self.server.theme_name, self.server.peertube_instances, allow_local_network_access, self.server.text_mode_banner, access_keys, self.server.system_language, self.server.max_like_count, shared_items_federated_domains, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, timezone, bold_reading, self.server.dogwhistles, ua_str, self.server.min_images_for_accounts, reverse_sequence, last_post_id) 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] if ';' in page_number: page_number = page_number.split(';')[0] 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 reverse_sequence = False if nickname in self.server.reverse_sequence: reverse_sequence = True last_post_id = None if ';lastpost=' in path: last_post_id = path.split(';lastpost=')[1] if ';' in last_post_id: last_post_id = last_post_id.split(';')[0] msg = \ html_inbox_dms(self.server.default_timeline, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, page_number, MAX_POSTS_IN_FEED, curr_session, base_dir, self.server.cached_webfingers, self.server.person_cache, nickname, domain, port, inbox_dm_feed, self.server.allow_deletion, http_prefix, self.server.project_version, minimal_nick, self.server.yt_replace_domain, twitter_replacement_domain, show_published_date_only, self.server.newswire, self.server.positive_voting, self.server.show_publish_as_icon, full_width_tl_button_header, self.server.icons_as_buttons, self.server.rss_icon_at_top, self.server.publish_button_at_top, authorized, self.server.theme_name, self.server.peertube_instances, allow_local_network_access, self.server.text_mode_banner, access_keys, self.server.system_language, self.server.max_like_count, shared_items_federated_domains, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, timezone, bold_reading, self.server.dogwhistles, ua_str, self.server.min_images_for_accounts, reverse_sequence, last_post_id) 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] if ';' in page_number: page_number = page_number.split(';')[0] 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 reverse_sequence = False if nickname in self.server.reverse_sequence: reverse_sequence = True last_post_id = None if ';lastpost=' in path: last_post_id = path.split(';lastpost=')[1] if ';' in last_post_id: last_post_id = last_post_id.split(';')[0] msg = \ html_inbox_replies(self.server.default_timeline, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, page_number, MAX_POSTS_IN_FEED, curr_session, base_dir, self.server.cached_webfingers, self.server.person_cache, nickname, domain, port, inbox_replies_feed, self.server.allow_deletion, http_prefix, self.server.project_version, minimal_nick, self.server.yt_replace_domain, twitter_replacement_domain, show_published_date_only, self.server.newswire, self.server.positive_voting, self.server.show_publish_as_icon, full_width_tl_button_header, self.server.icons_as_buttons, self.server.rss_icon_at_top, self.server.publish_button_at_top, authorized, self.server.theme_name, self.server.peertube_instances, allow_local_network_access, self.server.text_mode_banner, access_keys, self.server.system_language, self.server.max_like_count, shared_items_federated_domains, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, timezone, bold_reading, self.server.dogwhistles, ua_str, self.server.min_images_for_accounts, reverse_sequence, last_post_id) 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] if ';' in page_number: page_number = page_number.split(';')[0] 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 reverse_sequence = False if nickname in self.server.reverse_sequence: reverse_sequence = True last_post_id = None if ';lastpost=' in path: last_post_id = path.split(';lastpost=')[1] if ';' in last_post_id: last_post_id = last_post_id.split(';')[0] msg = \ html_inbox_media(self.server.default_timeline, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, page_number, MAX_POSTS_IN_MEDIA_FEED, curr_session, base_dir, self.server.cached_webfingers, self.server.person_cache, nickname, domain, port, inbox_media_feed, self.server.allow_deletion, http_prefix, self.server.project_version, minimal_nick, self.server.yt_replace_domain, twitter_replacement_domain, self.server.show_published_date_only, self.server.newswire, self.server.positive_voting, self.server.show_publish_as_icon, full_width_tl_button_header, self.server.icons_as_buttons, self.server.rss_icon_at_top, self.server.publish_button_at_top, authorized, self.server.theme_name, self.server.peertube_instances, allow_local_network_access, self.server.text_mode_banner, access_keys, self.server.system_language, self.server.max_like_count, fed_domains, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, timezone, bold_reading, self.server.dogwhistles, ua_str, self.server.min_images_for_accounts, reverse_sequence, last_post_id) 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] if ';' in page_number: page_number = page_number.split(';')[0] 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 reverse_sequence = False if nickname in self.server.reverse_sequence: reverse_sequence = True last_post_id = None if ';lastpost=' in path: last_post_id = path.split(';lastpost=')[1] if ';' in last_post_id: last_post_id = last_post_id.split(';')[0] msg = \ html_inbox_blogs(self.server.default_timeline, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, page_number, MAX_POSTS_IN_BLOGS_FEED, curr_session, base_dir, self.server.cached_webfingers, self.server.person_cache, nickname, domain, port, inbox_blogs_feed, self.server.allow_deletion, http_prefix, self.server.project_version, minimal_nick, self.server.yt_replace_domain, twitter_replacement_domain, self.server.show_published_date_only, self.server.newswire, self.server.positive_voting, self.server.show_publish_as_icon, full_width_tl_button_header, self.server.icons_as_buttons, self.server.rss_icon_at_top, self.server.publish_button_at_top, authorized, self.server.theme_name, self.server.peertube_instances, allow_local_network_access, self.server.text_mode_banner, access_keys, self.server.system_language, self.server.max_like_count, fed_domains, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, timezone, bold_reading, self.server.dogwhistles, ua_str, self.server.min_images_for_accounts, reverse_sequence, last_post_id) 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] if ';' in page_number: page_number = page_number.split(';')[0] 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 reverse_sequence = False if nickname in self.server.reverse_sequence: reverse_sequence = True msg = \ html_inbox_news(self.server.default_timeline, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, page_number, MAX_POSTS_IN_NEWS_FEED, curr_session, base_dir, self.server.cached_webfingers, self.server.person_cache, nickname, domain, port, inbox_news_feed, self.server.allow_deletion, http_prefix, self.server.project_version, minimal_nick, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.newswire, moderator, editor, artist, self.server.positive_voting, self.server.show_publish_as_icon, full_width_tl_button_header, self.server.icons_as_buttons, self.server.rss_icon_at_top, self.server.publish_button_at_top, authorized, self.server.theme_name, self.server.peertube_instances, self.server.allow_local_network_access, self.server.text_mode_banner, access_keys, self.server.system_language, self.server.max_like_count, fed_domains, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, timezone, bold_reading, self.server.dogwhistles, ua_str, self.server.min_images_for_accounts, reverse_sequence) 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] if ';' in page_number: page_number = page_number.split(';')[0] nickname = nickname.split('?page=')[0] if len(page_number) > 5: page_number = "1" if page_number.isdigit(): page_number = int(page_number) else: page_number = 1 if 'page=' not in path: newswire_votes_threshold = \ self.server.newswire_votes_threshold # if no page was specified then show the first inbox_features_feed = \ person_box_json(self.server.recent_posts_cache, base_dir, domain, port, path + '?page=1', http_prefix, MAX_POSTS_IN_BLOGS_FEED, 'tlfeatures', True, newswire_votes_threshold, self.server.positive_voting, self.server.voting_time_mins) curr_nickname = path.split('/users/')[1] if '/' in curr_nickname: curr_nickname = curr_nickname.split('/')[0] full_width_tl_button_header = \ self.server.full_width_tl_button_header minimal_nick = is_minimal(base_dir, domain, nickname) access_keys = self.server.access_keys if self.server.key_shortcuts.get(nickname): access_keys = \ self.server.key_shortcuts[nickname] shared_items_federated_domains = \ self.server.shared_items_federated_domains allow_local_network_access = \ self.server.allow_local_network_access twitter_replacement_domain = \ self.server.twitter_replacement_domain show_published_date_only = \ self.server.show_published_date_only timezone = None if self.server.account_timezone.get(nickname): timezone = \ self.server.account_timezone.get(nickname) bold_reading = False if self.server.bold_reading.get(nickname): bold_reading = True min_images_for_accounts = \ self.server.min_images_for_accounts reverse_sequence = False if nickname in self.server.reverse_sequence: reverse_sequence = 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, min_images_for_accounts, reverse_sequence) 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] if ';' in page_number: page_number = page_number.split(';')[0] 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 reverse_sequence = False if nickname in self.server.reverse_sequence: reverse_sequence = True msg = \ html_shares(self.server.default_timeline, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, page_number, MAX_POSTS_IN_FEED, curr_session, base_dir, self.server.cached_webfingers, self.server.person_cache, nickname, domain, port, self.server.allow_deletion, http_prefix, self.server.project_version, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.newswire, self.server.positive_voting, self.server.show_publish_as_icon, full_width_tl_button_header, self.server.icons_as_buttons, self.server.rss_icon_at_top, self.server.publish_button_at_top, authorized, self.server.theme_name, self.server.peertube_instances, self.server.allow_local_network_access, self.server.text_mode_banner, access_keys, self.server.system_language, self.server.max_like_count, self.server.shared_items_federated_domains, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, timezone, bold_reading, self.server.dogwhistles, ua_str, self.server.min_images_for_accounts, reverse_sequence) 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] if ';' in page_number: page_number = page_number.split(';')[0] 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 reverse_sequence = False if nickname in self.server.reverse_sequence: reverse_sequence = True msg = \ html_wanted(self.server.default_timeline, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, page_number, MAX_POSTS_IN_FEED, curr_session, base_dir, self.server.cached_webfingers, self.server.person_cache, nickname, domain, port, self.server.allow_deletion, http_prefix, self.server.project_version, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.newswire, self.server.positive_voting, self.server.show_publish_as_icon, full_width_tl_button_header, self.server.icons_as_buttons, self.server.rss_icon_at_top, self.server.publish_button_at_top, authorized, self.server.theme_name, self.server.peertube_instances, self.server.allow_local_network_access, self.server.text_mode_banner, access_keys, self.server.system_language, self.server.max_like_count, self.server.shared_items_federated_domains, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, timezone, bold_reading, self.server.dogwhistles, ua_str, self.server.min_images_for_accounts, reverse_sequence) 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] if ';' in page_number: page_number = page_number.split(';')[0] 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 reverse_sequence = False if nickname in self.server.reverse_sequence: reverse_sequence = True msg = \ html_bookmarks(self.server.default_timeline, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, page_number, MAX_POSTS_IN_FEED, curr_session, base_dir, self.server.cached_webfingers, self.server.person_cache, nickname, domain, port, bookmarks_feed, self.server.allow_deletion, http_prefix, self.server.project_version, minimal_nick, self.server.yt_replace_domain, twitter_replacement_domain, show_published_date_only, self.server.newswire, self.server.positive_voting, self.server.show_publish_as_icon, full_width_tl_button_header, self.server.icons_as_buttons, self.server.rss_icon_at_top, self.server.publish_button_at_top, authorized, self.server.theme_name, self.server.peertube_instances, allow_local_network_access, self.server.text_mode_banner, access_keys, self.server.system_language, self.server.max_like_count, shared_items_federated_domains, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, timezone, bold_reading, self.server.dogwhistles, ua_str, self.server.min_images_for_accounts, reverse_sequence) 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] if ';' in page_number: page_number = page_number.split(';')[0] 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 reverse_sequence = False if nickname in self.server.reverse_sequence: reverse_sequence = True msg = \ html_outbox(self.server.default_timeline, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, page_number, MAX_POSTS_IN_FEED, curr_session, base_dir, self.server.cached_webfingers, self.server.person_cache, nickname, domain, port, outbox_feed, self.server.allow_deletion, http_prefix, self.server.project_version, minimal_nick, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.newswire, self.server.positive_voting, self.server.show_publish_as_icon, full_width_tl_button_header, self.server.icons_as_buttons, self.server.rss_icon_at_top, self.server.publish_button_at_top, authorized, self.server.theme_name, self.server.peertube_instances, self.server.allow_local_network_access, self.server.text_mode_banner, access_keys, self.server.system_language, self.server.max_like_count, self.server.shared_items_federated_domains, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, timezone, bold_reading, self.server.dogwhistles, ua_str, self.server.min_images_for_accounts, reverse_sequence) 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] if ';' in page_number: page_number = page_number.split(';')[0] nickname = nickname.split('?page=')[0] if len(page_number) > 5: page_number = "1" if page_number.isdigit(): page_number = int(page_number) else: page_number = 1 if 'page=' not in path: # if no page was specified then show the first moderation_feed = \ person_box_json(self.server.recent_posts_cache, base_dir, domain, port, path + '?page=1', http_prefix, MAX_POSTS_IN_FEED, 'moderation', True, 0, self.server.positive_voting, self.server.voting_time_mins) full_width_tl_button_header = \ self.server.full_width_tl_button_header moderation_action_str = '' access_keys = self.server.access_keys if self.server.key_shortcuts.get(nickname): access_keys = \ self.server.key_shortcuts[nickname] shared_items_federated_domains = \ self.server.shared_items_federated_domains twitter_replacement_domain = \ self.server.twitter_replacement_domain allow_local_network_access = \ self.server.allow_local_network_access show_published_date_only = \ self.server.show_published_date_only timezone = None if self.server.account_timezone.get(nickname): timezone = \ self.server.account_timezone.get(nickname) bold_reading = False if self.server.bold_reading.get(nickname): bold_reading = True min_images_for_accounts = \ self.server.min_images_for_accounts reverse_sequence = False if nickname in self.server.reverse_sequence: reverse_sequence = 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, min_images_for_accounts, reverse_sequence) 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 '#' 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 '#' 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_moved_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 moved feed """ following = \ get_moved_feed(base_dir, domain, port, path, http_prefix, authorized, FOLLOWS_PER_PAGE) 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_moved_feed(base_dir, domain, port, path, 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 '#' 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('/moved', ''), base_dir) if get_person: curr_session = \ self._establish_session("show_moved_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, 'moved', 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_moved_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_moved_feed json', debug) else: self._404() return True return False def _show_inactive_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, dormant_months: int) -> bool: """Shows the inactive accounts feed """ following = \ get_inactive_feed(base_dir, domain, port, path, http_prefix, authorized, dormant_months, FOLLOWS_PER_PAGE) 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_inactive_feed(base_dir, domain, port, path, http_prefix, authorized, dormant_months, 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 '#' 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('/inactive', ''), base_dir) if get_person: curr_session = \ self._establish_session("show_inactive_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, 'inactive', 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_inactive_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_inactive_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 '#' 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 '#' in page_number_str: page_number_str = page_number_str.split('#')[0] if len(page_number_str) > 5: page_number_str = "1" if page_number_str.isdigit(): page_number = int(page_number_str) if page_number < 1: page_number = 1 elif page_number > 10: page_number = 10 curr_session = \ self._establish_session("showBlogPage", curr_session, proxy_type) if not curr_session: self._404() self.server.getreq_busy = False return True msg = html_blog_page(authorized, curr_session, base_dir, http_prefix, translate, nickname, domain, port, MAX_POSTS_IN_BLOGS_FEED, page_number, self.server.peertube_instances, self.server.system_language, self.server.person_cache, debug) if msg is not None: msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_blog_page', debug) return True self._404() return True def _redirect_to_login_screen(self, calling_domain: str, path: str, http_prefix: str, domain_full: str, onion_domain: str, i2p_domain: str, getreq_start_time, authorized: bool, debug: bool): """Redirects to the login screen if necessary """ divert_to_login_screen = False if '/media/' not in path and \ '/ontologies/' not in path and \ '/data/' not in path and \ '/sharefiles/' not in path and \ '/statuses/' not in path and \ '/emoji/' not in path and \ '/tags/' not in path and \ '/tagmaps/' not in path and \ '/avatars/' not in path and \ '/favicons/' not in path and \ '/headers/' not in path and \ '/fonts/' not in path and \ '/icons/' not in path: divert_to_login_screen = True if path.startswith('/users/'): nick_str = path.split('/users/')[1] if '/' not in nick_str and '?' not in nick_str: divert_to_login_screen = False else: if path.endswith('/following') or \ path.endswith('/followers') or \ path.endswith('/skills') or \ path.endswith('/roles') or \ path.endswith('/wanted') or \ path.endswith('/shares'): divert_to_login_screen = False if divert_to_login_screen and not authorized: divert_path = '/login' if self.server.news_instance: # for news instances if not logged in then show the # front page divert_path = '/users/news' # if debug: print('DEBUG: divert_to_login_screen=' + str(divert_to_login_screen)) print('DEBUG: authorized=' + str(authorized)) print('DEBUG: path=' + path) if calling_domain.endswith('.onion') and onion_domain: self._redirect_headers('http://' + onion_domain + divert_path, None, calling_domain) elif calling_domain.endswith('.i2p') and i2p_domain: self._redirect_headers('http://' + i2p_domain + divert_path, None, calling_domain) else: self._redirect_headers(http_prefix + '://' + domain_full + divert_path, None, calling_domain) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_redirect_to_login_screen', debug) return True return False def _get_style_sheet(self, base_dir: str, calling_domain: str, path: str, getreq_start_time) -> bool: """Returns the content of a css file """ # get the last part of the path # eg. /my/path/file.css becomes file.css if '/' in path: path = path.split('/')[-1] path = base_dir + '/' + path css = None if self.server.css_cache.get(path): css = self.server.css_cache[path] elif os.path.isfile(path): tries = 0 while tries < 5: try: css = get_css(self.server.base_dir, path) if css: self.server.css_cache[path] = css break except BaseException as ex: print('EX: _get_style_sheet ' + path + ' ' + str(tries) + ' ' + str(ex)) time.sleep(1) tries += 1 if css: msg = css.encode('utf-8') msglen = len(msg) self._set_headers('text/css', msglen, None, calling_domain, False) self._write(msg) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_get_style_sheet', self.server.debug) return True self._404() return True def _show_qrcode(self, calling_domain: str, path: str, base_dir: str, domain: str, onion_domain: str, i2p_domain: str, port: int, getreq_start_time) -> bool: """Shows a QR code for an account """ nickname = get_nickname_from_actor(path) if not nickname: self._404() return True if onion_domain: qrcode_domain = onion_domain port = 80 elif i2p_domain: qrcode_domain = i2p_domain port = 80 else: qrcode_domain = domain save_person_qrcode(base_dir, nickname, domain, qrcode_domain, port) qr_filename = \ acct_dir(base_dir, nickname, domain) + '/qrcode.png' if os.path.isfile(qr_filename): if self._etag_exists(qr_filename): # The file has not changed self._304() return tries = 0 media_binary = None while tries < 5: try: with open(qr_filename, 'rb') as av_file: media_binary = av_file.read() break except OSError as ex: print('EX: _show_qrcode ' + str(tries) + ' ' + str(ex)) time.sleep(1) tries += 1 if media_binary: mime_type = media_file_mime_type(qr_filename) self._set_headers_etag(qr_filename, mime_type, media_binary, None, self.server.domain_full, False, None) self._write(media_binary) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_qrcode', self.server.debug) return True self._404() return True def _search_screen_banner(self, path: str, base_dir: str, domain: str, getreq_start_time) -> bool: """Shows a banner image on the search screen """ nickname = get_nickname_from_actor(path) if not nickname: self._404() return True banner_filename = \ acct_dir(base_dir, nickname, domain) + '/search_banner.png' if not os.path.isfile(banner_filename): if os.path.isfile(base_dir + '/theme/default/search_banner.png'): copyfile(base_dir + '/theme/default/search_banner.png', banner_filename) if os.path.isfile(banner_filename): if self._etag_exists(banner_filename): # The file has not changed self._304() return True tries = 0 media_binary = None while tries < 5: try: with open(banner_filename, 'rb') as av_file: media_binary = av_file.read() break except OSError as ex: print('EX: _search_screen_banner ' + str(tries) + ' ' + str(ex)) time.sleep(1) tries += 1 if media_binary: mime_type = media_file_mime_type(banner_filename) self._set_headers_etag(banner_filename, mime_type, media_binary, None, self.server.domain_full, False, None) self._write(media_binary) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_search_screen_banner', self.server.debug) return True self._404() return True def _column_image(self, side: str, path: str, base_dir: str, domain: str, getreq_start_time) -> bool: """Shows an image at the top of the left/right column """ nickname = get_nickname_from_actor(path) if not nickname: self._404() return True banner_filename = \ acct_dir(base_dir, nickname, domain) + '/' + \ side + '_col_image.png' if os.path.isfile(banner_filename): if self._etag_exists(banner_filename): # The file has not changed self._304() return True tries = 0 media_binary = None while tries < 5: try: with open(banner_filename, 'rb') as av_file: media_binary = av_file.read() break except OSError as ex: print('EX: _column_image ' + str(tries) + ' ' + str(ex)) time.sleep(1) tries += 1 if media_binary: mime_type = media_file_mime_type(banner_filename) self._set_headers_etag(banner_filename, mime_type, media_binary, None, self.server.domain_full, False, None) self._write(media_binary) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_column_image ' + side, self.server.debug) return True self._404() return True def _show_background_image(self, path: str, base_dir: str, getreq_start_time) -> bool: """Show a background image """ image_extensions = get_image_extensions() for ext in image_extensions: for bg_im in ('follow', 'options', 'login', 'welcome'): # follow screen background image if path.endswith('/' + bg_im + '-background.' + ext): bg_filename = \ base_dir + '/accounts/' + \ bg_im + '-background.' + ext if os.path.isfile(bg_filename): if self._etag_exists(bg_filename): # The file has not changed self._304() return True tries = 0 bg_binary = None while tries < 5: try: with open(bg_filename, 'rb') as av_file: bg_binary = av_file.read() break except OSError as ex: print('EX: _show_background_image ' + str(tries) + ' ' + str(ex)) time.sleep(1) tries += 1 if bg_binary: if ext == 'jpg': ext = 'jpeg' self._set_headers_etag(bg_filename, 'image/' + ext, bg_binary, None, self.server.domain_full, False, None) self._write(bg_binary) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_background_image', self.server.debug) return True self._404() return True def _show_default_profile_background(self, base_dir: str, theme_name: str, getreq_start_time) -> bool: """If a background image is missing after searching for a handle then substitute this image """ image_extensions = get_image_extensions() for ext in image_extensions: bg_filename = \ base_dir + '/theme/' + theme_name + '/image.' + ext if os.path.isfile(bg_filename): if self._etag_exists(bg_filename): # The file has not changed self._304() return True tries = 0 bg_binary = None while tries < 5: try: with open(bg_filename, 'rb') as av_file: bg_binary = av_file.read() break except OSError as ex: print('EX: _show_default_profile_background ' + str(tries) + ' ' + str(ex)) time.sleep(1) tries += 1 if bg_binary: if ext == 'jpg': ext = 'jpeg' self._set_headers_etag(bg_filename, 'image/' + ext, bg_binary, None, self.server.domain_full, False, None) self._write(bg_binary) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_default_profile_background', self.server.debug) return True break self._404() return True def _show_share_image(self, path: str, base_dir: str, getreq_start_time) -> bool: """Show a shared item image """ if not is_image_file(path): self._404() return True media_str = path.split('/sharefiles/')[1] media_filename = base_dir + '/sharefiles/' + media_str if not os.path.isfile(media_filename): self._404() return True if self._etag_exists(media_filename): # The file has not changed self._304() return True media_file_type = get_image_mime_type(media_filename) media_binary = None try: with open(media_filename, 'rb') as av_file: media_binary = av_file.read() except OSError: print('EX: unable to read binary ' + media_filename) if media_binary: self._set_headers_etag(media_filename, media_file_type, media_binary, None, self.server.domain_full, False, None) self._write(media_binary) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_share_image', self.server.debug) return True def _show_avatar_or_banner(self, referer_domain: str, path: str, base_dir: str, domain: str, getreq_start_time) -> bool: """Shows an avatar or banner or profile background image """ if '/users/' not in path: if '/system/accounts/avatars/' not in path and \ '/system/accounts/headers/' not in path and \ '/accounts/avatars/' not in path and \ '/accounts/headers/' not in path: return False if not is_image_file(path): return False if '/system/accounts/avatars/' in path: avatar_str = path.split('/system/accounts/avatars/')[1] elif '/accounts/avatars/' in path: avatar_str = path.split('/accounts/avatars/')[1] elif '/system/accounts/headers/' in path: avatar_str = path.split('/system/accounts/headers/')[1] elif '/accounts/headers/' in path: avatar_str = path.split('/accounts/headers/')[1] else: avatar_str = path.split('/users/')[1] if not ('/' in avatar_str and '.temp.' not in path): return False avatar_nickname = avatar_str.split('/')[0] avatar_file = avatar_str.split('/')[1] avatar_file_ext = avatar_file.split('.')[-1] # remove any numbers, eg. avatar123.png becomes avatar.png if avatar_file.startswith('avatar'): avatar_file = 'avatar.' + avatar_file_ext elif avatar_file.startswith('banner'): avatar_file = 'banner.' + avatar_file_ext elif avatar_file.startswith('search_banner'): avatar_file = 'search_banner.' + avatar_file_ext elif avatar_file.startswith('image'): avatar_file = 'image.' + avatar_file_ext elif avatar_file.startswith('left_col_image'): avatar_file = 'left_col_image.' + avatar_file_ext elif avatar_file.startswith('right_col_image'): avatar_file = 'right_col_image.' + avatar_file_ext avatar_filename = \ acct_dir(base_dir, avatar_nickname, domain) + '/' + avatar_file if not os.path.isfile(avatar_filename): original_ext = avatar_file_ext original_avatar_file = avatar_file alt_ext = get_image_extensions() alt_found = False for alt in alt_ext: if alt == original_ext: continue avatar_file = \ original_avatar_file.replace('.' + original_ext, '.' + alt) avatar_filename = \ acct_dir(base_dir, avatar_nickname, domain) + \ '/' + avatar_file if os.path.isfile(avatar_filename): alt_found = True break if not alt_found: return False if self._etag_exists(avatar_filename): # The file has not changed self._304() return True avatar_tm = os.path.getmtime(avatar_filename) last_modified_time = datetime.datetime.fromtimestamp(avatar_tm) last_modified_time_str = \ last_modified_time.strftime('%a, %d %b %Y %H:%M:%S GMT') media_image_type = get_image_mime_type(avatar_file) media_binary = None try: with open(avatar_filename, 'rb') as av_file: media_binary = av_file.read() except OSError: print('EX: unable to read avatar ' + avatar_filename) if media_binary: self._set_headers_etag(avatar_filename, media_image_type, media_binary, None, referer_domain, True, last_modified_time_str) self._write(media_binary) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_avatar_or_banner', self.server.debug) return True def _confirm_delete_event(self, calling_domain: str, path: str, base_dir: str, http_prefix: str, cookie: str, translate: {}, domain_full: str, onion_domain: str, i2p_domain: str, getreq_start_time) -> bool: """Confirm whether to delete a calendar event """ post_id = path.split('?eventid=')[1] if '?' in post_id: post_id = post_id.split('?')[0] post_time = path.split('?time=')[1] if '?' in post_time: post_time = post_time.split('?')[0] post_year = path.split('?year=')[1] if '?' in post_year: post_year = post_year.split('?')[0] post_month = path.split('?month=')[1] if '?' in post_month: post_month = post_month.split('?')[0] post_day = path.split('?day=')[1] if '?' in post_day: post_day = post_day.split('?')[0] # show the confirmation screen screen msg = html_calendar_delete_confirm(translate, base_dir, path, http_prefix, domain_full, post_id, post_time, post_year, post_month, post_day, calling_domain) if not msg: actor = \ http_prefix + '://' + \ domain_full + \ path.split('/eventdelete')[0] if calling_domain.endswith('.onion') and onion_domain: actor = \ 'http://' + onion_domain + \ path.split('/eventdelete')[0] elif calling_domain.endswith('.i2p') and i2p_domain: actor = \ 'http://' + i2p_domain + \ path.split('/eventdelete')[0] self._redirect_headers(actor + '/calendar', cookie, calling_domain) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_confirm_delete_event', self.server.debug) return True msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) return True def _show_new_post(self, edit_post_params: {}, calling_domain: str, path: str, media_instance: bool, translate: {}, base_dir: str, http_prefix: str, in_reply_to_url: str, reply_to_list: [], reply_is_chat: bool, share_description: str, reply_page_number: int, reply_category: str, domain: str, domain_full: str, getreq_start_time, cookie, no_drop_down: bool, conversation_id: str, curr_session) -> bool: """Shows the new post screen """ is_new_post_endpoint = False new_post_month = None new_post_year = None if '/users/' in path and '/new' in path: if '?month=' in path: month_str = path.split('?month=')[1] if ';' in month_str: month_str = month_str.split(';')[0] if month_str.isdigit(): new_post_month = int(month_str) if new_post_month and ';year=' in path: year_str = path.split(';year=')[1] if ';' in year_str: year_str = year_str.split(';')[0] if year_str.isdigit(): new_post_year = int(year_str) if new_post_year: path = path.split('?month=')[0] # 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') default_post_language = self.server.system_language if self.server.default_post_language.get(nickname): default_post_language = \ self.server.default_post_language[nickname] 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) if post_json_object: reply_language = \ get_reply_language(base_dir, post_json_object) if reply_language: default_post_language = reply_language bold_reading = False if self.server.bold_reading.get(nickname): bold_reading = True languages_understood = \ get_understood_languages(base_dir, self.server.http_prefix, nickname, self.server.domain_full, self.server.person_cache) msg = \ html_new_post(edit_post_params, media_instance, translate, base_dir, http_prefix, path, in_reply_to_url, reply_to_list, share_description, None, reply_page_number, reply_category, nickname, domain, domain_full, self.server.default_timeline, self.server.newswire, self.server.theme_name, no_drop_down, access_keys, custom_submit_text, conversation_id, self.server.recent_posts_cache, self.server.max_recent_posts, curr_session, self.server.cached_webfingers, self.server.person_cache, self.server.port, post_json_object, self.server.project_version, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.peertube_instances, self.server.allow_local_network_access, self.server.system_language, languages_understood, self.server.max_like_count, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, self.server.default_timeline, reply_is_chat, bold_reading, self.server.dogwhistles, self.server.min_images_for_accounts, new_post_month, new_post_year, default_post_language) if not msg: print('Error replying to ' + in_reply_to_url) self._404() return True msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_new_post', self.server.debug) return True return False def _show_known_crawlers(self, calling_domain: str, path: str, base_dir: str, known_crawlers: {}) -> bool: """Show a list of known web crawlers """ if '/users/' not in path: return False if not path.endswith('/crawlers'): return False nickname = get_nickname_from_actor(path) if not nickname: return False if not is_moderator(base_dir, nickname): return False crawlers_list = [] curr_time = int(time.time()) recent_crawlers = 60 * 60 * 24 * 30 for ua_str, item in known_crawlers.items(): if item['lastseen'] - curr_time < recent_crawlers: hits_str = str(item['hits']).zfill(8) crawlers_list.append(hits_str + ' ' + ua_str) crawlers_list.sort(reverse=True) msg = '' for line_str in crawlers_list: msg += line_str + '\n' msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/plain; charset=utf-8', msglen, None, calling_domain, True) self._write(msg) return True def _edit_profile(self, calling_domain: str, path: str, translate: {}, base_dir: str, http_prefix: str, domain: str, port: int, cookie: str) -> bool: """Show the edit profile screen """ if '/users/' in path and path.endswith('/editprofile'): peertube_instances = self.server.peertube_instances nickname = get_nickname_from_actor(path) access_keys = self.server.access_keys if '/users/' in path: if self.server.key_shortcuts.get(nickname): access_keys = self.server.key_shortcuts[nickname] default_reply_interval_hrs = self.server.default_reply_interval_hrs msg = html_edit_profile(self.server, translate, base_dir, path, domain, port, self.server.default_timeline, self.server.theme_name, peertube_instances, self.server.text_mode_banner, self.server.user_agents_blocked, self.server.crawlers_allowed, access_keys, default_reply_interval_hrs, self.server.cw_lists, self.server.lists_enabled, self.server.system_language, self.server.min_images_for_accounts, self.server.max_recent_posts, self.server.reverse_sequence) 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, self.server.theme_name).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 if self._show_conversation_thread(authorized, calling_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.port, self.server.debug, self.server.session): fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_show_conversation_thread', 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 \ not self.path.startswith('/.well-known/protocol-handler') 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, cookie): 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, self.server.theme_name).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 if '?remotetag=' in self.path and \ '/users/' in self.path and authorized: actor = self.path.split('?remotetag=')[0] nickname = get_nickname_from_actor(actor) hashtag_url = self.path.split('?remotetag=')[1] if ';' in hashtag_url: hashtag_url = hashtag_url.split(';')[0] hashtag_url = hashtag_url.replace('--', '/') page_number = 1 if ';page=' in self.path: page_number_str = self.path.split(';page=')[1] if ';' in page_number_str: page_number_str = page_number_str.split(';')[0] if page_number_str.isdigit(): page_number = int(page_number_str) allow_local_network_access = self.server.allow_local_network_access show_published_date_only = self.server.show_published_date_only twitter_replacement_domain = self.server.twitter_replacement_domain timezone = None if self.server.account_timezone.get(nickname): timezone = \ self.server.account_timezone.get(nickname) msg = \ html_hashtag_search_remote(nickname, self.server.domain, self.server.port, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, self.server.base_dir, hashtag_url, page_number, MAX_POSTS_IN_FEED, self.server.session, self.server.cached_webfingers, self.server.person_cache, self.server.http_prefix, self.server.project_version, self.server.yt_replace_domain, twitter_replacement_domain, show_published_date_only, self.server.peertube_instances, 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, self.server.bold_reading, self.server.dogwhistles, self.server.min_images_for_accounts, self.server.debug) 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 self._404() 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 if not html_getreq: self._hashtag_search_json(calling_domain, referer_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.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) self.path = get_default_path(self.server.media_instance, self.server.blogs_instance, nickname) # search for a fediverse address, shared item or emoji # from the web interface by selecting search icon if html_getreq and users_in_path: if self.path.endswith('/search') or \ '/search?' in self.path: if '?' in self.path: self.path = self.path.split('?')[0] nickname = self.path.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] access_keys = self.server.access_keys if self.server.key_shortcuts.get(nickname): access_keys = self.server.key_shortcuts[nickname] # show the search screen msg = html_search(self.server.translate, self.server.base_dir, self.path, self.server.domain, self.server.default_timeline, self.server.theme_name, self.server.text_mode_banner, access_keys) if msg: msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'search screen shown', self.server.debug) self.server.getreq_busy = False return # show a hashtag category from the search screen if html_getreq and '/category/' in self.path: msg = html_search_hashtag_category(self.server.translate, self.server.base_dir, self.path, self.server.domain, self.server.theme_name) if msg: msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'hashtag category screen shown', self.server.debug) self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'search screen shown done', self.server.debug) # Show the html calendar for a user if html_getreq and users_in_path: if '/calendar' in self.path: nickname = self.path.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] access_keys = self.server.access_keys if self.server.key_shortcuts.get(nickname): access_keys = self.server.key_shortcuts[nickname] # show the calendar screen msg = html_calendar(self.server.person_cache, self.server.translate, self.server.base_dir, self.path, self.server.http_prefix, self.server.domain_full, self.server.text_mode_banner, access_keys, False, self.server.system_language, self.server.default_timeline, self.server.theme_name) if msg: msg = msg.encode('utf-8') msglen = len(msg) if 'ical=true' in self.path: self._set_headers('text/calendar', msglen, cookie, calling_domain, False) else: self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'calendar shown', self.server.debug) else: self._404() self.server.getreq_busy = False return # Show the icalendar for a user if icalendar_getreq and users_in_path: if '/calendar' in self.path: nickname = self.path.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] access_keys = self.server.access_keys if self.server.key_shortcuts.get(nickname): access_keys = self.server.key_shortcuts[nickname] # show the calendar screen msg = html_calendar(self.server.person_cache, self.server.translate, self.server.base_dir, self.path, self.server.http_prefix, self.server.domain_full, self.server.text_mode_banner, access_keys, True, self.server.system_language, self.server.default_timeline, self.server.theme_name) if msg: msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/calendar', msglen, cookie, calling_domain, False) self._write(msg) else: self._404() fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'icalendar shown', self.server.debug) self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'calendar shown done', self.server.debug) # Show confirmation for deleting a calendar event if html_getreq and users_in_path: if '/eventdelete' in self.path and \ '?time=' in self.path and \ '?eventid=' in self.path: if self._confirm_delete_event(calling_domain, self.path, self.server.base_dir, self.server.http_prefix, cookie, self.server.translate, self.server.domain_full, self.server.onion_domain, self.server.i2p_domain, getreq_start_time): self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'calendar delete shown done', self.server.debug) # search for emoji by name if html_getreq and users_in_path: if self.path.endswith('/searchemoji'): # show the search screen msg = \ html_search_emoji_text_entry(self.server.translate, self.server.base_dir, self.path).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'emoji search shown', self.server.debug) self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'emoji search shown done', self.server.debug) repeat_private = False if html_getreq and '?repeatprivate=' in self.path: repeat_private = True self.path = self.path.replace('?repeatprivate=', '?repeat=') # announce/repeat button was pressed if authorized and html_getreq and '?repeat=' in self.path: self._announce_button(calling_domain, self.path, self.server.base_dir, cookie, proxy_type, self.server.http_prefix, self.server.domain, self.server.domain_full, self.server.port, self.server.onion_domain, self.server.i2p_domain, getreq_start_time, repeat_private, self.server.debug, curr_session) self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'show announce done', self.server.debug) if authorized and html_getreq and '?unrepeatprivate=' in self.path: self.path = self.path.replace('?unrepeatprivate=', '?unrepeat=') # undo an announce/repeat from the web interface if authorized and html_getreq and '?unrepeat=' in self.path: self._announce_button_undo(calling_domain, self.path, self.server.base_dir, cookie, proxy_type, self.server.http_prefix, self.server.domain, self.server.domain_full, self.server.onion_domain, self.server.i2p_domain, getreq_start_time, self.server.debug, self.server.recent_posts_cache, curr_session) self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'unannounce done', self.server.debug) # send a newswire moderation vote from the web interface if authorized and '/newswirevote=' in self.path and \ self.path.startswith('/users/'): self._newswire_vote(calling_domain, self.path, cookie, self.server.base_dir, self.server.http_prefix, self.server.domain_full, self.server.onion_domain, self.server.i2p_domain, getreq_start_time, self.server.newswire) self.server.getreq_busy = False return # send a newswire moderation unvote from the web interface if authorized and '/newswireunvote=' in self.path and \ self.path.startswith('/users/'): self._newswire_unvote(calling_domain, self.path, cookie, self.server.base_dir, self.server.http_prefix, self.server.domain_full, self.server.onion_domain, self.server.i2p_domain, getreq_start_time, self.server.debug, self.server.newswire) self.server.getreq_busy = False return # send a follow request approval from the web interface if authorized and '/followapprove=' in self.path and \ self.path.startswith('/users/'): self._follow_approve_button(calling_domain, self.path, cookie, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.domain_full, self.server.port, self.server.onion_domain, self.server.i2p_domain, getreq_start_time, proxy_type, self.server.debug, curr_session) self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'follow approve done', self.server.debug) # deny a follow request from the web interface if authorized and '/followdeny=' in self.path and \ self.path.startswith('/users/'): self._follow_deny_button(calling_domain, self.path, cookie, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.domain_full, self.server.port, self.server.onion_domain, self.server.i2p_domain, getreq_start_time, self.server.debug) self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'follow deny done', self.server.debug) # like from the web interface icon if authorized and html_getreq and '?like=' in self.path: self._like_button(calling_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.domain_full, self.server.onion_domain, self.server.i2p_domain, getreq_start_time, proxy_type, cookie, self.server.debug, curr_session) self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'like button done', self.server.debug) # undo a like from the web interface icon if authorized and html_getreq and '?unlike=' in self.path: self._undo_like_button(calling_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.domain_full, self.server.onion_domain, self.server.i2p_domain, getreq_start_time, proxy_type, cookie, self.server.debug, curr_session) self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'unlike button done', self.server.debug) # emoji reaction from the web interface icon if authorized and html_getreq and \ '?react=' in self.path and \ '?actor=' in self.path: self._reaction_button(calling_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.domain_full, self.server.onion_domain, self.server.i2p_domain, getreq_start_time, proxy_type, cookie, self.server.debug, curr_session) self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'emoji reaction button done', self.server.debug) # undo an emoji reaction from the web interface icon if authorized and html_getreq and \ '?unreact=' in self.path and \ '?actor=' in self.path: self._undo_reaction_button(calling_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.domain_full, self.server.onion_domain, self.server.i2p_domain, getreq_start_time, proxy_type, cookie, self.server.debug, curr_session) self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'unreaction button done', self.server.debug) # bookmark from the web interface icon if authorized and html_getreq and '?bookmark=' in self.path: self._bookmark_button(calling_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.domain_full, self.server.port, self.server.onion_domain, self.server.i2p_domain, getreq_start_time, proxy_type, cookie, self.server.debug, curr_session) self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'bookmark shown done', self.server.debug) # emoji recation from the web interface bottom icon if authorized and html_getreq and '?selreact=' in self.path: self._reaction_picker(calling_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.port, getreq_start_time, cookie, self.server.debug, curr_session) self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'bookmark shown done', self.server.debug) # undo a bookmark from the web interface icon if authorized and html_getreq and '?unbookmark=' in self.path: self._undo_bookmark_button(calling_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.domain_full, self.server.port, self.server.onion_domain, self.server.i2p_domain, getreq_start_time, proxy_type, cookie, self.server.debug, curr_session) self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'unbookmark shown done', self.server.debug) # delete button is pressed on a post if authorized and html_getreq and '?delete=' in self.path: self._delete_button(calling_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain_full, self.server.onion_domain, self.server.i2p_domain, getreq_start_time, proxy_type, cookie, self.server.debug, curr_session) self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'delete shown done', self.server.debug) # The mute button is pressed if authorized and html_getreq and '?mute=' in self.path: self._mute_button(calling_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.domain_full, self.server.port, self.server.onion_domain, self.server.i2p_domain, getreq_start_time, cookie, self.server.debug, curr_session) self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'post muted done', self.server.debug) # unmute a post from the web interface icon if authorized and html_getreq and '?unmute=' in self.path: self._undo_mute_button(calling_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.domain_full, self.server.port, self.server.onion_domain, self.server.i2p_domain, getreq_start_time, cookie, self.server.debug, curr_session) self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'unmute activated done', self.server.debug) # reply from the web interface icon in_reply_to_url = None # replyWithDM = False reply_to_list = [] reply_page_number = 1 reply_category = '' share_description = None conversation_id = None # replytoActor = None if html_getreq: if '?conversationId=' in self.path: conversation_id = self.path.split('?conversationId=')[1] if '?' in conversation_id: conversation_id = conversation_id.split('?')[0] # public reply if '?replyto=' in self.path: in_reply_to_url = self.path.split('?replyto=')[1] if '?' in in_reply_to_url: mentions_list = in_reply_to_url.split('?') for ment in mentions_list: if ment.startswith('mention='): reply_handle = ment.replace('mention=', '') if reply_handle not in reply_to_list: reply_to_list.append(reply_handle) if ment.startswith('page='): reply_page_str = ment.replace('page=', '') if len(reply_page_str) > 5: reply_page_str = "1" if reply_page_str.isdigit(): reply_page_number = int(reply_page_str) # if m.startswith('actor='): # replytoActor = m.replace('actor=', '') in_reply_to_url = mentions_list[0] self.path = self.path.split('?replyto=')[0] + '/newpost' if self.server.debug: print('DEBUG: replyto path ' + self.path) # unlisted reply if '?replyunlisted=' in self.path: in_reply_to_url = self.path.split('?replyunlisted=')[1] if '?' in in_reply_to_url: mentions_list = in_reply_to_url.split('?') for ment in mentions_list: if ment.startswith('mention='): reply_handle = ment.replace('mention=', '') if reply_handle not in reply_to_list: reply_to_list.append(reply_handle) if ment.startswith('page='): reply_page_str = ment.replace('page=', '') if len(reply_page_str) > 5: reply_page_str = "1" if reply_page_str.isdigit(): reply_page_number = int(reply_page_str) in_reply_to_url = mentions_list[0] self.path = \ self.path.split('?replyunlisted=')[0] + '/newunlisted' if self.server.debug: print('DEBUG: replyunlisted path ' + self.path) # reply to followers if '?replyfollowers=' in self.path: in_reply_to_url = self.path.split('?replyfollowers=')[1] if '?' in in_reply_to_url: mentions_list = in_reply_to_url.split('?') for ment in mentions_list: if ment.startswith('mention='): reply_handle = ment.replace('mention=', '') ment2 = ment.replace('mention=', '') if ment2 not in reply_to_list: reply_to_list.append(reply_handle) if ment.startswith('page='): reply_page_str = ment.replace('page=', '') if len(reply_page_str) > 5: reply_page_str = "1" if reply_page_str.isdigit(): reply_page_number = int(reply_page_str) # if m.startswith('actor='): # replytoActor = m.replace('actor=', '') in_reply_to_url = mentions_list[0] self.path = self.path.split('?replyfollowers=')[0] + \ '/newfollowers' if self.server.debug: print('DEBUG: replyfollowers path ' + self.path) # replying as a direct message, # for moderation posts or the dm timeline reply_is_chat = False if '?replydm=' in self.path or '?replychat=' in self.path: reply_type = 'replydm' if '?replychat=' in self.path: reply_type = 'replychat' reply_is_chat = True in_reply_to_url = self.path.split('?' + reply_type + '=')[1] in_reply_to_url = urllib.parse.unquote_plus(in_reply_to_url) if '?' in in_reply_to_url: # multiple parameters mentions_list = in_reply_to_url.split('?') for ment in mentions_list: if ment.startswith('mention='): reply_handle = ment.replace('mention=', '') in_reply_to_url = reply_handle if reply_handle not in reply_to_list: reply_to_list.append(reply_handle) elif ment.startswith('page='): reply_page_str = ment.replace('page=', '') if len(reply_page_str) > 5: reply_page_str = "1" if reply_page_str.isdigit(): reply_page_number = int(reply_page_str) elif ment.startswith('category='): reply_category = ment.replace('category=', '') elif ment.startswith('sharedesc:'): # get the title for the shared item share_description = \ ment.replace('sharedesc:', '').strip() share_description = \ share_description.replace('_', ' ') in_reply_to_url = mentions_list[0] else: # single parameter if in_reply_to_url.startswith('mention='): reply_handle = in_reply_to_url.replace('mention=', '') in_reply_to_url = reply_handle if reply_handle not in reply_to_list: reply_to_list.append(reply_handle) elif in_reply_to_url.startswith('sharedesc:'): # get the title for the shared item share_description = \ in_reply_to_url.replace('sharedesc:', '').strip() share_description = \ share_description.replace('_', ' ') self.path = \ self.path.split('?' + reply_type + '=')[0] + '/newdm' if self.server.debug: print('DEBUG: ' + reply_type + ' path ' + self.path) # Edit a blog post if authorized and \ '/users/' in self.path and \ '?editblogpost=' in self.path and \ ';actor=' in self.path: message_id = self.path.split('?editblogpost=')[1] if ';' in message_id: message_id = message_id.split(';')[0] actor = self.path.split(';actor=')[1] if ';' in actor: actor = actor.split(';')[0] nickname = get_nickname_from_actor(self.path.split('?')[0]) if not nickname: self._404() self.server.getreq_busy = False return if nickname == actor: post_url = \ local_actor_url(self.server.http_prefix, nickname, self.server.domain_full) + \ '/statuses/' + message_id msg = html_edit_blog(self.server.media_instance, self.server.translate, self.server.base_dir, self.path, reply_page_number, nickname, self.server.domain, post_url, self.server.system_language) if msg: msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) self.server.getreq_busy = False return # Edit a post edit_post_params = {} if authorized and \ '/users/' in self.path and \ '?postedit=' in self.path and \ ';scope=' in self.path and \ ';actor=' in self.path: post_scope = self.path.split(';scope=')[1] if ';' in post_scope: post_scope = post_scope.split(';')[0] edit_post_params['scope'] = post_scope message_id = self.path.split('?postedit=')[1] if ';' in message_id: message_id = message_id.split(';')[0] if ';replyTo=' in self.path: reply_to = self.path.split(';replyTo=')[1] if ';' in reply_to: reply_to = message_id.split(';')[0] edit_post_params['replyTo'] = reply_to actor = self.path.split(';actor=')[1] if ';' in actor: actor = actor.split(';')[0] edit_post_params['actor'] = actor nickname = get_nickname_from_actor(self.path.split('?')[0]) edit_post_params['nickname'] = nickname if not nickname: self._404() self.server.getreq_busy = False return if nickname != actor: self._404() self.server.getreq_busy = False return post_url = \ local_actor_url(self.server.http_prefix, nickname, self.server.domain_full) + \ '/statuses/' + message_id edit_post_params['post_url'] = post_url # use the new post functions, but using edit_post_params new_post_scope = post_scope if post_scope == 'public': new_post_scope = 'post' self.path = '/users/' + nickname + '/new' + new_post_scope # list of known crawlers accessing nodeinfo or masto API if self._show_known_crawlers(calling_domain, self.path, self.server.base_dir, self.server.known_crawlers): self.server.getreq_busy = False return # edit profile in web interface if self._edit_profile(calling_domain, self.path, self.server.translate, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.port, cookie): self.server.getreq_busy = False return # edit links from the left column of the timeline in web interface if self._edit_links(calling_domain, self.path, self.server.translate, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.port, cookie, self.server.theme_name): self.server.getreq_busy = False return # edit newswire from the right column of the timeline if self._edit_newswire(calling_domain, self.path, self.server.translate, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.port, cookie): self.server.getreq_busy = False return # edit news post if self._edit_news_post2(calling_domain, self.path, self.server.translate, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.port, self.server.domain_full, cookie): self.server.getreq_busy = False return if self._show_new_post(edit_post_params, calling_domain, self.path, self.server.media_instance, self.server.translate, self.server.base_dir, self.server.http_prefix, in_reply_to_url, reply_to_list, reply_is_chat, share_description, reply_page_number, reply_category, self.server.domain, self.server.domain_full, getreq_start_time, cookie, no_drop_down, conversation_id, curr_session): self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'new post done', self.server.debug) # get an individual post from the path /@nickname/statusnumber if self._show_individual_at_post(ssml_getreq, authorized, calling_domain, referer_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.domain_full, self.server.port, self.server.onion_domain, self.server.i2p_domain, getreq_start_time, proxy_type, cookie, self.server.debug, curr_session): self.server.getreq_busy = False return # show the likers of a post if self._show_likers_of_post(authorized, calling_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.port, getreq_start_time, cookie, self.server.debug, curr_session): self.server.getreq_busy = False return # show the announcers/repeaters of a post if self._show_announcers_of_post(authorized, calling_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.port, getreq_start_time, cookie, self.server.debug, curr_session): self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'individual post done', self.server.debug) # get replies to a post /users/nickname/statuses/number/replies if self.path.endswith('/replies') or '/replies?page=' in self.path: if self._show_replies_to_post(authorized, calling_domain, referer_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.domain_full, self.server.port, getreq_start_time, proxy_type, cookie, self.server.debug, curr_session): self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'post replies done', self.server.debug) # roles on profile screen if self.path.endswith('/roles') and users_in_path: if self._show_roles(calling_domain, referer_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, getreq_start_time, proxy_type, cookie, self.server.debug, curr_session): self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'show roles done', self.server.debug) # show skills on the profile page if self.path.endswith('/skills') and users_in_path: if self._show_skills(calling_domain, referer_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, getreq_start_time, proxy_type, cookie, self.server.debug, curr_session): self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'show skills done', self.server.debug) if '?notifypost=' in self.path and users_in_path and authorized: if self._show_notify_post(authorized, calling_domain, referer_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.port, getreq_start_time, proxy_type, cookie, self.server.debug, curr_session): self.server.getreq_busy = False return # get an individual post from the path # /users/nickname/statuses/number if '/statuses/' in self.path and users_in_path: if self._show_individual_post(ssml_getreq, authorized, calling_domain, referer_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.domain_full, self.server.port, getreq_start_time, proxy_type, cookie, self.server.debug, curr_session): self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'show status done', self.server.debug) # get the inbox timeline for a given person if self.path.endswith('/inbox') or '/inbox?page=' in self.path: if self._show_inbox(authorized, calling_domain, referer_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.port, getreq_start_time, cookie, self.server.debug, self.server.recent_posts_cache, curr_session, self.server.default_timeline, self.server.max_recent_posts, self.server.translate, self.server.cached_webfingers, self.server.person_cache, self.server.allow_deletion, self.server.project_version, self.server.yt_replace_domain, self.server.twitter_replacement_domain, ua_str): self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'show inbox done', self.server.debug) # get the direct messages timeline for a given person if self.path.endswith('/dm') or '/dm?page=' in self.path: if self._show_dms(authorized, calling_domain, referer_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.port, getreq_start_time, cookie, self.server.debug, curr_session, ua_str): self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'show dms done', self.server.debug) # get the replies timeline for a given person if self.path.endswith('/tlreplies') or '/tlreplies?page=' in self.path: if self._show_replies(authorized, calling_domain, referer_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.port, getreq_start_time, cookie, self.server.debug, curr_session, ua_str): self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'show replies 2 done', self.server.debug) # get the media timeline for a given person if self.path.endswith('/tlmedia') or '/tlmedia?page=' in self.path: if self._show_media_timeline(authorized, calling_domain, referer_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.port, getreq_start_time, cookie, self.server.debug, curr_session, ua_str): self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'show media 2 done', self.server.debug) # get the blogs for a given person if self.path.endswith('/tlblogs') or '/tlblogs?page=' in self.path: if self._show_blogs_timeline(authorized, calling_domain, referer_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.port, getreq_start_time, cookie, self.server.debug, curr_session, ua_str): self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'show blogs 2 done', self.server.debug) # get the news for a given person if self.path.endswith('/tlnews') or '/tlnews?page=' in self.path: if self._show_news_timeline(authorized, calling_domain, referer_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.port, getreq_start_time, cookie, self.server.debug, curr_session, ua_str): self.server.getreq_busy = False return # get features (local blogs) for a given person if self.path.endswith('/tlfeatures') or \ '/tlfeatures?page=' in self.path: if self._show_features_timeline(authorized, calling_domain, referer_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.port, getreq_start_time, cookie, self.server.debug, curr_session, ua_str): self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'show news 2 done', self.server.debug) # get the shared items timeline for a given person if self.path.endswith('/tlshares') or '/tlshares?page=' in self.path: if self._show_shares_timeline(authorized, calling_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.port, getreq_start_time, cookie, self.server.debug, curr_session, ua_str): self.server.getreq_busy = False return # get the wanted items timeline for a given person if self.path.endswith('/tlwanted') or '/tlwanted?page=' in self.path: if self._show_wanted_timeline(authorized, calling_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.port, getreq_start_time, cookie, self.server.debug, curr_session, ua_str): self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'show shares 2 done', self.server.debug) # block a domain from html_account_info if authorized and users_in_path and \ '/accountinfo?blockdomain=' in self.path and \ '?handle=' in self.path: nickname = self.path.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] if not is_moderator(self.server.base_dir, nickname): self._400() self.server.getreq_busy = False return block_domain = self.path.split('/accountinfo?blockdomain=')[1] search_handle = block_domain.split('?handle=')[1] search_handle = urllib.parse.unquote_plus(search_handle) block_domain = block_domain.split('?handle=')[0] block_domain = urllib.parse.unquote_plus(block_domain.strip()) if '?' in block_domain: block_domain = block_domain.split('?')[0] add_global_block(self.server.base_dir, '*', block_domain, None) 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_moved_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 moved 4 done', self.server.debug) if self._show_inactive_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.dormant_months): self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'show inactive 5 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 5 done', self.server.debug) # look up a person if self._show_person_profile(authorized, calling_domain, referer_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.onion_domain, self.server.i2p_domain, getreq_start_time, proxy_type, cookie, self.server.debug, curr_session): self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'show profile posts done', self.server.debug) # check that a json file was requested if not self.path.endswith('.json'): if self.server.debug: print('DEBUG: GET Not json: ' + self.path + ' ' + self.server.base_dir) self._404() self.server.getreq_busy = False return if not self._secure_mode(curr_session, proxy_type): if self.server.debug: print('WARN: Unauthorized GET') self._404() self.server.getreq_busy = False return fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'authorized fetch', self.server.debug) # check that the file exists filename = self.server.base_dir + self.path if os.path.isfile(filename): content = None try: with open(filename, 'r', encoding='utf-8') as rfile: content = rfile.read() except OSError: print('EX: unable to read file ' + filename) if content: content_json = json.loads(content) msg_str = json.dumps(content_json, ensure_ascii=False) msg_str = self._convert_domains(calling_domain, referer_domain, msg_str) msg = msg_str.encode('utf-8') msglen = len(msg) accept_str = self.headers['Accept'] protocol_str = \ get_json_content_from_accept(accept_str) self._set_headers(protocol_str, msglen, None, calling_domain, False) self._write(msg) fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'arbitrary json', self.server.debug) else: if self.server.debug: print('DEBUG: GET Unknown file') self._404() self.server.getreq_busy = False fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'end benchmarks', self.server.debug) def _dav_handler(self, endpoint_type: str, debug: bool): calling_domain = self.server.domain_full if not self._has_accept(calling_domain): self._400() return accept_str = self.headers['Accept'] if 'application/xml' not in accept_str: if debug: print(endpoint_type.upper() + ' is not of xml type') self._400() return if not self.headers.get('Content-length'): print(endpoint_type.upper() + ' has no content-length') self._400() return # check that the content length string is not too long if isinstance(self.headers['Content-length'], str): max_content_size = len(str(self.server.maxMessageLength)) if len(self.headers['Content-length']) > max_content_size: self._400() return length = int(self.headers['Content-length']) if length > self.server.max_post_length: print(endpoint_type.upper() + ' request size too large ' + self.path) self._400() return if not self.path.startswith('/calendars/'): print(endpoint_type.upper() + ' without /calendars ' + self.path) self._404() return if debug: print(endpoint_type.upper() + ' checking authorization') if not self._is_authorized(): print(endpoint_type.upper() + ' not authorized') self._403() return nickname = self.path.split('/calendars/')[1] if '/' in nickname: nickname = nickname.split('/')[0] if not nickname: print(endpoint_type.upper() + ' no nickname ' + self.path) self._400() return if not os.path.isdir(self.server.base_dir + '/accounts/' + nickname + '@' + self.server.domain): print(endpoint_type.upper() + ' for non-existent account ' + self.path) self._404() return propfind_bytes = None try: propfind_bytes = self.rfile.read(length) except SocketError as ex: if ex.errno == errno.ECONNRESET: print('EX: ' + endpoint_type.upper() + ' connection reset by peer') else: print('EX: ' + endpoint_type.upper() + ' socket error') self._400() return except ValueError as ex: print('EX: ' + endpoint_type.upper() + ' rfile.read failed, ' + str(ex)) self._400() return if not propfind_bytes: self._404() return depth = 0 if self.headers.get('Depth'): depth = self.headers['Depth'] propfind_xml = propfind_bytes.decode('utf-8') response_str = None if endpoint_type == 'propfind': response_str = \ dav_propfind_response(nickname, propfind_xml) elif endpoint_type == 'put': response_str = \ dav_put_response(self.server.base_dir, nickname, self.server.domain, propfind_xml, self.server.http_prefix, self.server.system_language, self.server.recent_dav_etags) elif endpoint_type == 'report': curr_etag = None if self.headers.get('ETag'): curr_etag = self.headers['ETag'] elif self.headers.get('Etag'): curr_etag = self.headers['Etag'] response_str = \ dav_report_response(self.server.base_dir, nickname, self.server.domain, propfind_xml, self.server.person_cache, self.server.http_prefix, curr_etag, self.server.recent_dav_etags, self.server.domain_full, self.server.system_language) elif endpoint_type == 'delete': response_str = \ dav_delete_response(self.server.base_dir, nickname, self.server.domain, depth, self.path, self.server.http_prefix, self.server.debug, self.server.recent_posts_cache) if not response_str: self._404() return if response_str == 'Not modified': if endpoint_type == 'put': self._200() return self._304() return if response_str.startswith('ETag:') and endpoint_type == 'put': response_etag = response_str.split('ETag:', 1)[1] self._201(response_etag) elif response_str != 'Ok': message_xml = response_str.encode('utf-8') message_xml_len = len(message_xml) self._set_headers('application/xml; charset=utf-8', message_xml_len, None, calling_domain, False) self._write(message_xml) if 'multistatus' in response_str: return self._207() self._200() def do_PROPFIND(self): self._dav_handler('propfind', self.server.debug) def do_PUT(self): self._dav_handler('put', self.server.debug) def do_REPORT(self): self._dav_handler('report', self.server.debug) def do_DELETE(self): self._dav_handler('delete', self.server.debug) def do_HEAD(self): calling_domain = self.server.domain_full if self.headers.get('Host'): calling_domain = decoded_host(self.headers['Host']) if self.server.onion_domain: if calling_domain not in (self.server.domain, self.server.domain_full, self.server.onion_domain): print('HEAD domain blocked: ' + calling_domain) self._400() return else: if calling_domain not in (self.server.domain, self.server.domain_full): print('HEAD domain blocked: ' + calling_domain) self._400() return check_path = self.path etag = None file_length = -1 last_modified_time_str = None if '/media/' in self.path or \ '/accounts/avatars/' in self.path or \ '/accounts/headers/' in self.path: if is_image_file(self.path) or \ path_is_video(self.path) or \ path_is_audio(self.path): if '/media/' in self.path: media_str = self.path.split('/media/')[1] media_filename = \ self.server.base_dir + '/media/' + media_str elif '/accounts/avatars/' in self.path: avatar_file = self.path.split('/accounts/avatars/')[1] if '/' not in avatar_file: self._404() return nickname = avatar_file.split('/')[0] avatar_file = avatar_file.split('/')[1] avatar_file_ext = avatar_file.split('.')[-1] # remove any numbers, eg. avatar123.png becomes avatar.png if avatar_file.startswith('avatar'): avatar_file = 'avatar.' + avatar_file_ext media_filename = \ self.server.base_dir + '/accounts/' + \ nickname + '@' + self.server.domain + '/' + \ avatar_file else: banner_file = self.path.split('/accounts/headers/')[1] if '/' not in banner_file: self._404() return nickname = banner_file.split('/')[0] banner_file = banner_file.split('/')[1] banner_file_ext = banner_file.split('.')[-1] # remove any numbers, eg. banner123.png becomes banner.png if banner_file.startswith('banner'): banner_file = 'banner.' + banner_file_ext media_filename = \ self.server.base_dir + '/accounts/' + \ nickname + '@' + self.server.domain + '/' + \ banner_file if os.path.isfile(media_filename): check_path = media_filename file_length = os.path.getsize(media_filename) media_tm = os.path.getmtime(media_filename) last_modified_time = \ datetime.datetime.fromtimestamp(media_tm) time_format_str = '%a, %d %b %Y %H:%M:%S GMT' last_modified_time_str = \ last_modified_time.strftime(time_format_str) media_tag_filename = media_filename + '.etag' if os.path.isfile(media_tag_filename): try: with open(media_tag_filename, 'r', encoding='utf-8') as efile: etag = efile.read() except OSError: print('EX: do_HEAD unable to read ' + media_tag_filename) else: media_binary = None try: with open(media_filename, 'rb') as av_file: media_binary = av_file.read() except OSError: print('EX: unable to read media binary ' + media_filename) if media_binary: etag = md5(media_binary).hexdigest() # nosec try: with open(media_tag_filename, 'w+', encoding='utf-8') as efile: efile.write(etag) except OSError: print('EX: do_HEAD unable to write ' + media_tag_filename) else: self._404() return media_file_type = media_file_mime_type(check_path) self._set_headers_head(media_file_type, file_length, etag, calling_domain, False, last_modified_time_str) def _receive_new_post_process(self, post_type: str, path: str, headers: {}, length: int, post_bytes, boundary: str, calling_domain: str, cookie: str, content_license_url: str, curr_session, proxy_type: str) -> int: # Note: this needs to happen synchronously # 0=this is not a new post # 1=new post success # -1=new post failed # 2=new post canceled if self.server.debug: print('DEBUG: receiving POST') if ' boundary=' in headers['Content-Type']: if self.server.debug: print('DEBUG: receiving POST headers ' + headers['Content-Type'] + ' path ' + path) nickname = None nickname_str = path.split('/users/')[1] if '?' in nickname_str: nickname_str = nickname_str.split('?')[0] if '/' in nickname_str: nickname = nickname_str.split('/')[0] else: nickname = nickname_str if self.server.debug: print('DEBUG: POST nickname ' + str(nickname)) if not nickname: print('WARN: no nickname found when receiving ' + post_type + ' path ' + path) return -1 # get the message id of an edited post edited_postid = None print('DEBUG: edited_postid path ' + path) if '?editid=' in path: edited_postid = path.split('?editid=')[1] if '?' in edited_postid: edited_postid = edited_postid.split('?')[0] print('DEBUG: edited_postid ' + edited_postid) # get the published date of an edited post edited_published = None if '?editpub=' in path: edited_published = path.split('?editpub=')[1] if '?' in edited_published: edited_published = \ edited_published.split('?')[0] print('DEBUG: edited_published ' + edited_published) length = int(headers['Content-Length']) if length > self.server.max_post_length: print('POST size too large') return -1 boundary = headers['Content-Type'].split('boundary=')[1] if ';' in boundary: boundary = boundary.split(';')[0] # Note: we don't use cgi here because it's due to be deprecated # in Python 3.8/3.10 # Instead we use the multipart mime parser from the email module if self.server.debug: print('DEBUG: extracting media from POST') media_bytes, post_bytes = \ extract_media_in_form_post(post_bytes, boundary, 'attachpic') if self.server.debug: if media_bytes: print('DEBUG: media was found. ' + str(len(media_bytes)) + ' bytes') else: print('DEBUG: no media was found in POST') # Note: a .temp extension is used here so that at no time is # an image with metadata publicly exposed, even for a few mS filename_base = \ acct_dir(self.server.base_dir, nickname, self.server.domain) + '/upload.temp' filename, attachment_media_type = \ save_media_in_form_post(media_bytes, self.server.debug, filename_base) if self.server.debug: if filename: print('DEBUG: POST media filename is ' + filename) else: print('DEBUG: no media filename in POST') if filename: if is_image_file(filename): post_image_filename = filename.replace('.temp', '') print('Removing metadata from ' + post_image_filename) city = get_spoofed_city(self.server.city, self.server.base_dir, nickname, self.server.domain) if self.server.low_bandwidth: convert_image_to_low_bandwidth(filename) process_meta_data(self.server.base_dir, nickname, self.server.domain, filename, post_image_filename, city, content_license_url) if os.path.isfile(post_image_filename): print('POST media saved to ' + post_image_filename) else: print('ERROR: POST media could not be saved to ' + post_image_filename) else: if os.path.isfile(filename): new_filename = filename.replace('.temp', '') os.rename(filename, new_filename) filename = new_filename fields = \ extract_text_fields_in_post(post_bytes, boundary, self.server.debug) if self.server.debug: if fields: print('DEBUG: text field extracted from POST ' + str(fields)) else: print('WARN: no text fields could be extracted from POST') # was the citations button pressed on the newblog screen? citations_button_press = False if post_type == 'newblog' and fields.get('submitCitations'): if fields['submitCitations'] == \ self.server.translate['Citations']: citations_button_press = True if not citations_button_press: # process the received text fields from the POST if not fields.get('message') and \ not fields.get('imageDescription') and \ not fields.get('pinToProfile'): print('WARN: no message, image description or pin') return -1 submit_text1 = self.server.translate['Publish'] submit_text2 = self.server.translate['Send'] submit_text3 = submit_text2 custom_submit_text = \ get_config_param(self.server.base_dir, 'customSubmitText') if custom_submit_text: submit_text3 = custom_submit_text if fields.get('submitPost'): if fields['submitPost'] != submit_text1 and \ fields['submitPost'] != submit_text2 and \ fields['submitPost'] != submit_text3: print('WARN: no submit field ' + fields['submitPost']) return -1 else: print('WARN: no submitPost') return 2 if not fields.get('imageDescription'): fields['imageDescription'] = None if not fields.get('subject'): fields['subject'] = None if not fields.get('replyTo'): fields['replyTo'] = None if not fields.get('schedulePost'): fields['schedulePost'] = False else: fields['schedulePost'] = True print('DEBUG: shedulePost ' + str(fields['schedulePost'])) if not fields.get('eventDate'): fields['eventDate'] = None if not fields.get('eventTime'): fields['eventTime'] = None if not fields.get('eventEndTime'): fields['eventEndTime'] = None if not fields.get('location'): fields['location'] = None if not fields.get('languagesDropdown'): fields['languagesDropdown'] = self.server.system_language set_default_post_language(self.server.base_dir, nickname, self.server.domain, fields['languagesDropdown']) self.server.default_post_language[nickname] = \ fields['languagesDropdown'] if not citations_button_press: # Store a file which contains the time in seconds # since epoch when an attempt to post something was made. # This is then used for active monthly users counts last_used_filename = \ acct_dir(self.server.base_dir, nickname, self.server.domain) + '/.lastUsed' try: with open(last_used_filename, 'w+', encoding='utf-8') as lastfile: lastfile.write(str(int(time.time()))) except OSError: print('EX: _receive_new_post_process unable to write ' + last_used_filename) mentions_str = '' if fields.get('mentions'): mentions_str = fields['mentions'].strip() + ' ' if not fields.get('commentsEnabled'): comments_enabled = False else: comments_enabled = True if post_type == 'newpost': if not fields.get('pinToProfile'): pin_to_profile = False else: pin_to_profile = True # is the post message empty? if not fields['message']: # remove the pinned content from profile screen undo_pinned_post(self.server.base_dir, nickname, self.server.domain) return 1 city = get_spoofed_city(self.server.city, self.server.base_dir, nickname, self.server.domain) conversation_id = None if fields.get('conversationId'): conversation_id = fields['conversationId'] languages_understood = \ get_understood_languages(self.server.base_dir, self.server.http_prefix, nickname, self.server.domain_full, self.server.person_cache) message_json = \ create_public_post(self.server.base_dir, nickname, self.server.domain, self.server.port, self.server.http_prefix, mentions_str + fields['message'], False, False, comments_enabled, filename, attachment_media_type, fields['imageDescription'], city, fields['replyTo'], fields['replyTo'], fields['subject'], fields['schedulePost'], fields['eventDate'], fields['eventTime'], fields['eventEndTime'], fields['location'], False, fields['languagesDropdown'], conversation_id, self.server.low_bandwidth, self.server.content_license_url, languages_understood, self.server.translate) if message_json: if edited_postid: recent_posts_cache = self.server.recent_posts_cache allow_local_network_access = \ self.server.allow_local_network_access signing_priv_key_pem = \ self.server.signing_priv_key_pem twitter_replacement_domain = \ self.server.twitter_replacement_domain show_published_date_only = \ self.server.show_published_date_only min_images_for_accounts = \ self.server.min_images_for_accounts peertube_instances = \ self.server.peertube_instances self._update_edited_post(self.server.base_dir, nickname, self.server.domain, message_json, edited_published, edited_postid, recent_posts_cache, 'outbox', self.server.max_mentions, self.server.max_emoji, allow_local_network_access, self.server.debug, self.server.system_language, self.server.http_prefix, self.server.domain_full, self.server.person_cache, signing_priv_key_pem, self.server.max_recent_posts, self.server.translate, curr_session, self.server.cached_webfingers, self.server.port, self.server.allow_deletion, self.server.yt_replace_domain, twitter_replacement_domain, show_published_date_only, peertube_instances, self.server.theme_name, self.server.max_like_count, self.server.cw_lists, self.server.dogwhistles, min_images_for_accounts, self.server.max_hashtags) print('DEBUG: sending edited public post ' + str(message_json)) if fields['schedulePost']: return 1 if pin_to_profile: sys_language = self.server.system_language content_str = \ get_base_content_from_post(message_json, sys_language) pin_post(self.server.base_dir, nickname, self.server.domain, content_str) return 1 if self._post_to_outbox(message_json, self.server.project_version, nickname, curr_session, proxy_type): populate_replies(self.server.base_dir, self.server.http_prefix, self.server.domain_full, message_json, self.server.max_replies, self.server.debug) return 1 return -1 elif post_type == 'newblog': # citations button on newblog screen if citations_button_press: message_json = \ html_citations(self.server.base_dir, nickname, self.server.domain, self.server.http_prefix, self.server.default_timeline, self.server.translate, self.server.newswire, fields['subject'], fields['message'], filename, attachment_media_type, fields['imageDescription'], self.server.theme_name) if message_json: message_json = message_json.encode('utf-8') message_json_len = len(message_json) self._set_headers('text/html', message_json_len, cookie, calling_domain, False) self._write(message_json) return 1 else: return -1 if not fields['subject']: print('WARN: blog posts must have a title') return -1 if not fields['message']: print('WARN: blog posts must have content') return -1 # submit button on newblog screen save_to_file = False client_to_server = False city = None conversation_id = None if fields.get('conversationId'): conversation_id = fields['conversationId'] languages_understood = \ get_understood_languages(self.server.base_dir, self.server.http_prefix, nickname, self.server.domain_full, self.server.person_cache) message_json = \ create_blog_post(self.server.base_dir, nickname, self.server.domain, self.server.port, self.server.http_prefix, fields['message'], save_to_file, client_to_server, comments_enabled, filename, attachment_media_type, fields['imageDescription'], city, fields['replyTo'], fields['replyTo'], fields['subject'], fields['schedulePost'], fields['eventDate'], fields['eventTime'], fields['eventEndTime'], fields['location'], fields['languagesDropdown'], conversation_id, self.server.low_bandwidth, self.server.content_license_url, languages_understood, self.server.translate) if message_json: if fields['schedulePost']: return 1 if self._post_to_outbox(message_json, self.server.project_version, nickname, curr_session, proxy_type): refresh_newswire(self.server.base_dir) populate_replies(self.server.base_dir, self.server.http_prefix, self.server.domain_full, message_json, self.server.max_replies, self.server.debug) return 1 return -1 elif post_type == 'editblogpost': print('Edited blog post received') post_filename = \ locate_post(self.server.base_dir, nickname, self.server.domain, fields['postUrl']) if os.path.isfile(post_filename): post_json_object = load_json(post_filename) if post_json_object: cached_filename = \ acct_dir(self.server.base_dir, nickname, self.server.domain) + \ '/postcache/' + \ fields['postUrl'].replace('/', '#') + '.html' if os.path.isfile(cached_filename): print('Edited blog post, removing cached html') try: os.remove(cached_filename) except OSError: print('EX: _receive_new_post_process ' + 'unable to delete ' + cached_filename) # remove from memory cache remove_post_from_cache(post_json_object, self.server.recent_posts_cache) # change the blog post title post_json_object['object']['summary'] = \ fields['subject'] # format message tags = [] hashtags_dict = {} mentioned_recipients = [] fields['message'] = \ add_html_tags(self.server.base_dir, self.server.http_prefix, nickname, self.server.domain, fields['message'], mentioned_recipients, hashtags_dict, self.server.translate, True) # replace emoji with unicode tags = [] for _, tag in hashtags_dict.items(): tags.append(tag) # get list of tags fields['message'] = \ replace_emoji_from_tags(curr_session, self.server.base_dir, fields['message'], tags, 'content', self.server.debug, True) post_json_object['object']['content'] = \ fields['message'] content_map = post_json_object['object']['contentMap'] content_map[self.server.system_language] = \ fields['message'] img_description = '' if fields.get('imageDescription'): img_description = fields['imageDescription'] if filename: city = get_spoofed_city(self.server.city, self.server.base_dir, nickname, self.server.domain) post_json_object['object'] = \ attach_media(self.server.base_dir, self.server.http_prefix, nickname, self.server.domain, self.server.port, post_json_object['object'], filename, attachment_media_type, img_description, city, self.server.low_bandwidth, self.server.content_license_url) replace_you_tube(post_json_object, self.server.yt_replace_domain, self.server.system_language) replace_twitter(post_json_object, self.server.twitter_replacement_domain, self.server.system_language) save_json(post_json_object, post_filename) # also save to the news actor if nickname != 'news': post_filename = \ post_filename.replace('#users#' + nickname + '#', '#users#news#') save_json(post_json_object, post_filename) print('Edited blog post, resaved ' + post_filename) return 1 else: print('Edited blog post, unable to load json for ' + post_filename) else: print('Edited blog post not found ' + str(fields['postUrl'])) return -1 elif post_type == 'newunlisted': city = get_spoofed_city(self.server.city, self.server.base_dir, nickname, self.server.domain) save_to_file = False client_to_server = False conversation_id = None if fields.get('conversationId'): conversation_id = fields['conversationId'] languages_understood = \ get_understood_languages(self.server.base_dir, self.server.http_prefix, nickname, self.server.domain_full, self.server.person_cache) message_json = \ create_unlisted_post(self.server.base_dir, nickname, self.server.domain, self.server.port, self.server.http_prefix, mentions_str + fields['message'], save_to_file, client_to_server, comments_enabled, filename, attachment_media_type, fields['imageDescription'], city, fields['replyTo'], fields['replyTo'], fields['subject'], fields['schedulePost'], fields['eventDate'], fields['eventTime'], fields['eventEndTime'], fields['location'], fields['languagesDropdown'], conversation_id, self.server.low_bandwidth, self.server.content_license_url, languages_understood, self.server.translate) if message_json: if edited_postid: recent_posts_cache = self.server.recent_posts_cache allow_local_network_access = \ self.server.allow_local_network_access signing_priv_key_pem = \ self.server.signing_priv_key_pem twitter_replacement_domain = \ self.server.twitter_replacement_domain show_published_date_only = \ self.server.show_published_date_only min_images_for_accounts = \ self.server.min_images_for_accounts peertube_instances = \ self.server.peertube_instances self._update_edited_post(self.server.base_dir, nickname, self.server.domain, message_json, edited_published, edited_postid, recent_posts_cache, 'outbox', self.server.max_mentions, self.server.max_emoji, allow_local_network_access, self.server.debug, self.server.system_language, self.server.http_prefix, self.server.domain_full, self.server.person_cache, signing_priv_key_pem, self.server.max_recent_posts, self.server.translate, curr_session, self.server.cached_webfingers, self.server.port, self.server.allow_deletion, self.server.yt_replace_domain, twitter_replacement_domain, show_published_date_only, peertube_instances, self.server.theme_name, self.server.max_like_count, self.server.cw_lists, self.server.dogwhistles, min_images_for_accounts, self.server.max_hashtags) print('DEBUG: sending edited unlisted post ' + str(message_json)) if fields['schedulePost']: return 1 if self._post_to_outbox(message_json, self.server.project_version, nickname, curr_session, proxy_type): populate_replies(self.server.base_dir, self.server.http_prefix, self.server.domain, message_json, self.server.max_replies, self.server.debug) return 1 return -1 elif post_type == 'newfollowers': city = get_spoofed_city(self.server.city, self.server.base_dir, nickname, self.server.domain) save_to_file = False client_to_server = False conversation_id = None if fields.get('conversationId'): conversation_id = fields['conversationId'] mentions_message = mentions_str + fields['message'] languages_understood = \ get_understood_languages(self.server.base_dir, self.server.http_prefix, nickname, self.server.domain_full, self.server.person_cache) message_json = \ create_followers_only_post(self.server.base_dir, nickname, self.server.domain, self.server.port, self.server.http_prefix, mentions_message, save_to_file, client_to_server, comments_enabled, filename, attachment_media_type, fields['imageDescription'], city, fields['replyTo'], fields['replyTo'], fields['subject'], fields['schedulePost'], fields['eventDate'], fields['eventTime'], fields['eventEndTime'], fields['location'], fields['languagesDropdown'], conversation_id, self.server.low_bandwidth, self.server.content_license_url, languages_understood, self.server.translate) if message_json: if edited_postid: recent_posts_cache = self.server.recent_posts_cache allow_local_network_access = \ self.server.allow_local_network_access signing_priv_key_pem = \ self.server.signing_priv_key_pem twitter_replacement_domain = \ self.server.twitter_replacement_domain show_published_date_only = \ self.server.show_published_date_only min_images_for_accounts = \ self.server.min_images_for_accounts peertube_instances = \ self.server.peertube_instances self._update_edited_post(self.server.base_dir, nickname, self.server.domain, message_json, edited_published, edited_postid, recent_posts_cache, 'outbox', self.server.max_mentions, self.server.max_emoji, allow_local_network_access, self.server.debug, self.server.system_language, self.server.http_prefix, self.server.domain_full, self.server.person_cache, signing_priv_key_pem, self.server.max_recent_posts, self.server.translate, curr_session, self.server.cached_webfingers, self.server.port, self.server.allow_deletion, self.server.yt_replace_domain, twitter_replacement_domain, show_published_date_only, peertube_instances, self.server.theme_name, self.server.max_like_count, self.server.cw_lists, self.server.dogwhistles, min_images_for_accounts, self.server.max_hashtags) print('DEBUG: sending edited followers post ' + str(message_json)) if fields['schedulePost']: return 1 if self._post_to_outbox(message_json, self.server.project_version, nickname, curr_session, proxy_type): populate_replies(self.server.base_dir, self.server.http_prefix, self.server.domain, message_json, self.server.max_replies, self.server.debug) return 1 return -1 elif post_type == 'newdm': message_json = None print('A DM was posted') if '@' in mentions_str: city = get_spoofed_city(self.server.city, self.server.base_dir, nickname, self.server.domain) save_to_file = False client_to_server = False conversation_id = None if fields.get('conversationId'): conversation_id = fields['conversationId'] content_license_url = self.server.content_license_url languages_understood = \ get_understood_languages(self.server.base_dir, self.server.http_prefix, nickname, self.server.domain_full, self.server.person_cache) reply_is_chat = False if fields.get('replychatmsg'): reply_is_chat = fields['replychatmsg'] message_json = \ create_direct_message_post(self.server.base_dir, nickname, self.server.domain, self.server.port, self.server.http_prefix, mentions_str + fields['message'], save_to_file, client_to_server, comments_enabled, filename, attachment_media_type, fields['imageDescription'], city, fields['replyTo'], fields['replyTo'], fields['subject'], True, fields['schedulePost'], fields['eventDate'], fields['eventTime'], fields['eventEndTime'], fields['location'], fields['languagesDropdown'], conversation_id, self.server.low_bandwidth, content_license_url, languages_understood, reply_is_chat, self.server.translate) if message_json: print('DEBUG: posting DM edited_postid ' + str(edited_postid)) if edited_postid: recent_posts_cache = self.server.recent_posts_cache allow_local_network_access = \ self.server.allow_local_network_access signing_priv_key_pem = \ self.server.signing_priv_key_pem twitter_replacement_domain = \ self.server.twitter_replacement_domain show_published_date_only = \ self.server.show_published_date_only min_images_for_accounts = \ self.server.min_images_for_accounts peertube_instances = \ self.server.peertube_instances self._update_edited_post(self.server.base_dir, nickname, self.server.domain, message_json, edited_published, edited_postid, recent_posts_cache, 'outbox', self.server.max_mentions, self.server.max_emoji, allow_local_network_access, self.server.debug, self.server.system_language, self.server.http_prefix, self.server.domain_full, self.server.person_cache, signing_priv_key_pem, self.server.max_recent_posts, self.server.translate, curr_session, self.server.cached_webfingers, self.server.port, self.server.allow_deletion, self.server.yt_replace_domain, twitter_replacement_domain, show_published_date_only, peertube_instances, self.server.theme_name, self.server.max_like_count, self.server.cw_lists, self.server.dogwhistles, min_images_for_accounts, self.server.max_hashtags) print('DEBUG: sending edited dm post ' + str(message_json)) if fields['schedulePost']: return 1 print('Sending new DM to ' + str(message_json['object']['to'])) if self._post_to_outbox(message_json, self.server.project_version, nickname, curr_session, proxy_type): populate_replies(self.server.base_dir, self.server.http_prefix, self.server.domain, message_json, self.server.max_replies, self.server.debug) return 1 return -1 elif post_type == 'newreminder': message_json = None handle = nickname + '@' + self.server.domain_full print('A reminder was posted for ' + handle) if '@' + handle not in mentions_str: mentions_str = '@' + handle + ' ' + mentions_str city = get_spoofed_city(self.server.city, self.server.base_dir, nickname, self.server.domain) save_to_file = False client_to_server = False comments_enabled = False conversation_id = None mentions_message = mentions_str + fields['message'] languages_understood = \ get_understood_languages(self.server.base_dir, self.server.http_prefix, nickname, self.server.domain_full, self.server.person_cache) message_json = \ create_direct_message_post(self.server.base_dir, nickname, self.server.domain, self.server.port, self.server.http_prefix, mentions_message, save_to_file, client_to_server, comments_enabled, filename, attachment_media_type, fields['imageDescription'], city, None, None, fields['subject'], True, fields['schedulePost'], fields['eventDate'], fields['eventTime'], fields['eventEndTime'], fields['location'], fields['languagesDropdown'], conversation_id, self.server.low_bandwidth, self.server.content_license_url, languages_understood, False, self.server.translate) if message_json: if fields['schedulePost']: return 1 print('DEBUG: new reminder to ' + str(message_json['object']['to']) + ' ' + str(edited_postid)) if edited_postid: recent_posts_cache = self.server.recent_posts_cache allow_local_network_access = \ self.server.allow_local_network_access signing_priv_key_pem = \ self.server.signing_priv_key_pem twitter_replacement_domain = \ self.server.twitter_replacement_domain show_published_date_only = \ self.server.show_published_date_only min_images_for_accounts = \ self.server.min_images_for_accounts peertube_instances = \ self.server.peertube_instances self._update_edited_post(self.server.base_dir, nickname, self.server.domain, message_json, edited_published, edited_postid, recent_posts_cache, 'dm', self.server.max_mentions, self.server.max_emoji, allow_local_network_access, self.server.debug, self.server.system_language, self.server.http_prefix, self.server.domain_full, self.server.person_cache, signing_priv_key_pem, self.server.max_recent_posts, self.server.translate, curr_session, self.server.cached_webfingers, self.server.port, self.server.allow_deletion, self.server.yt_replace_domain, twitter_replacement_domain, show_published_date_only, peertube_instances, self.server.theme_name, self.server.max_like_count, self.server.cw_lists, self.server.dogwhistles, min_images_for_accounts, self.server.max_hashtags) print('DEBUG: sending edited reminder post ' + str(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 == 'newreport': if attachment_media_type: if attachment_media_type != 'image': return -1 # So as to be sure that this only goes to moderators # and not accounts being reported we disable any # included fediverse addresses by replacing '@' with '-at-' fields['message'] = fields['message'].replace('@', '-at-') city = get_spoofed_city(self.server.city, self.server.base_dir, nickname, self.server.domain) languages_understood = \ get_understood_languages(self.server.base_dir, self.server.http_prefix, nickname, self.server.domain_full, self.server.person_cache) message_json = \ create_report_post(self.server.base_dir, nickname, self.server.domain, self.server.port, self.server.http_prefix, mentions_str + fields['message'], False, False, True, filename, attachment_media_type, fields['imageDescription'], city, self.server.debug, fields['subject'], fields['languagesDropdown'], self.server.low_bandwidth, self.server.content_license_url, languages_understood, self.server.translate) if message_json: if self._post_to_outbox(message_json, self.server.project_version, nickname, curr_session, proxy_type): return 1 return -1 elif post_type == 'newquestion': if not fields.get('duration'): return -1 if not fields.get('message'): return -1 # questionStr = fields['message'] q_options = [] for question_ctr in range(8): if fields.get('questionOption' + str(question_ctr)): q_options.append(fields['questionOption' + str(question_ctr)]) if not q_options: return -1 city = get_spoofed_city(self.server.city, self.server.base_dir, nickname, self.server.domain) if isinstance(fields['duration'], str): if len(fields['duration']) > 5: return -1 int_duration_days = int(fields['duration']) languages_understood = \ get_understood_languages(self.server.base_dir, self.server.http_prefix, nickname, self.server.domain_full, self.server.person_cache) message_json = \ create_question_post(self.server.base_dir, nickname, self.server.domain, self.server.port, self.server.http_prefix, fields['message'], q_options, False, False, comments_enabled, filename, attachment_media_type, fields['imageDescription'], city, fields['subject'], int_duration_days, fields['languagesDropdown'], self.server.low_bandwidth, self.server.content_license_url, languages_understood, self.server.translate) if message_json: if self.server.debug: print('DEBUG: new Question') if self._post_to_outbox(message_json, self.server.project_version, nickname, curr_session, proxy_type): return 1 return -1 elif post_type in ('newshare', 'newwanted'): if not fields.get('itemQty'): print(post_type + ' no itemQty') return -1 if not fields.get('itemType'): print(post_type + ' no itemType') return -1 if 'itemPrice' not in fields: print(post_type + ' no itemPrice') return -1 if 'itemCurrency' not in fields: print(post_type + ' no itemCurrency') return -1 if not fields.get('category'): print(post_type + ' no category') return -1 if not fields.get('duration'): print(post_type + ' no duratio') return -1 if attachment_media_type: if attachment_media_type != 'image': print('Attached media is not an image') return -1 duration_str = fields['duration'] if duration_str: if ' ' not in duration_str: duration_str = duration_str + ' days' city = get_spoofed_city(self.server.city, self.server.base_dir, nickname, self.server.domain) item_qty = 1 if fields['itemQty']: if is_float(fields['itemQty']): item_qty = float(fields['itemQty']) item_price = "0.00" item_currency = "EUR" if fields['itemPrice']: item_price, item_currency = \ get_price_from_string(fields['itemPrice']) if fields['itemCurrency']: item_currency = fields['itemCurrency'] if post_type == 'newshare': print('Adding shared item') shares_file_type = 'shares' else: print('Adding wanted item') shares_file_type = 'wanted' add_share(self.server.base_dir, self.server.http_prefix, nickname, self.server.domain, self.server.port, fields['subject'], fields['message'], filename, item_qty, fields['itemType'], fields['category'], fields['location'], duration_str, self.server.debug, city, item_price, item_currency, fields['languagesDropdown'], self.server.translate, shares_file_type, self.server.low_bandwidth, self.server.content_license_url) if filename: if os.path.isfile(filename): try: os.remove(filename) except OSError: print('EX: _receive_new_post_process ' + 'unable to delete ' + filename) self.post_to_nickname = nickname return 1 return -1 def _receive_new_post(self, post_type: str, path: str, calling_domain: str, cookie: str, content_license_url: str, curr_session, proxy_type: str) -> int: """A new post has been created This creates a thread to send the new post """ page_number = 1 original_path = path if '/users/' not in path: print('Not receiving new post for ' + path + ' because /users/ not in path') return None if '?' + post_type + '?' not in path: print('Not receiving new post for ' + path + ' because ?' + post_type + '? not in path') return None print('New post begins: ' + post_type + ' ' + path) if '?page=' in path: page_number_str = path.split('?page=')[1] if '?' in page_number_str: page_number_str = page_number_str.split('?')[0] if '#' in page_number_str: page_number_str = page_number_str.split('#')[0] if len(page_number_str) > 5: page_number_str = "1" if page_number_str.isdigit(): page_number = int(page_number_str) path = path.split('?page=')[0] # get the username who posted new_post_thread_name = None if '/users/' in path: new_post_thread_name = path.split('/users/')[1] if '/' in new_post_thread_name: new_post_thread_name = new_post_thread_name.split('/')[0] if not new_post_thread_name: new_post_thread_name = '*' if self.server.new_post_thread.get(new_post_thread_name): print('Waiting for previous new post thread to end') wait_ctr = 0 np_thread = self.server.new_post_thread[new_post_thread_name] while np_thread.is_alive() and wait_ctr < 8: time.sleep(1) wait_ctr += 1 if wait_ctr >= 8: print('Killing previous new post thread for ' + new_post_thread_name) np_thread.kill() # make a copy of self.headers headers = copy.deepcopy(self.headers) headers_without_cookie = copy.deepcopy(headers) if 'cookie' in headers_without_cookie: del headers_without_cookie['cookie'] if 'Cookie' in headers_without_cookie: del headers_without_cookie['Cookie'] print('New post headers: ' + str(headers_without_cookie)) length = int(headers['Content-Length']) if length > self.server.max_post_length: print('POST size too large') return None if not headers.get('Content-Type'): if headers.get('Content-type'): headers['Content-Type'] = headers['Content-type'] elif headers.get('content-type'): headers['Content-Type'] = headers['content-type'] if headers.get('Content-Type'): if ' boundary=' in headers['Content-Type']: boundary = headers['Content-Type'].split('boundary=')[1] if ';' in boundary: boundary = boundary.split(';')[0] try: post_bytes = self.rfile.read(length) except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: POST post_bytes ' + 'connection reset by peer') else: print('WARN: POST post_bytes socket error') return None except ValueError as ex: print('EX: POST post_bytes rfile.read failed, ' + str(ex)) return None # second length check from the bytes received # since Content-Length could be untruthful length = len(post_bytes) if length > self.server.max_post_length: print('POST size too large') return None # Note sending new posts needs to be synchronous, # otherwise any attachments can get mangled if # other events happen during their decoding print('Creating new post from: ' + new_post_thread_name) self._receive_new_post_process(post_type, original_path, headers, length, post_bytes, boundary, calling_domain, cookie, content_license_url, curr_session, proxy_type) return page_number def _crypto_ap_iread_handle(self): """Reads handle """ message_bytes = None max_device_id_length = 2048 length = int(self.headers['Content-length']) if length >= max_device_id_length: print('WARN: handle post to crypto API is too long ' + str(length) + ' bytes') return {} try: message_bytes = self.rfile.read(length) except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: handle POST message_bytes ' + 'connection reset by peer') else: print('WARN: handle POST message_bytes socket error') return {} except ValueError as ex: print('EX: handle POST message_bytes rfile.read failed ' + str(ex)) return {} len_message = len(message_bytes) if len_message > 2048: print('WARN: handle post to crypto API is too long ' + str(len_message) + ' bytes') return {} handle = message_bytes.decode("utf-8") if not handle: return None if '@' not in handle: return None if '[' in handle: return json.loads(message_bytes) if handle.startswith('@'): handle = handle[1:] if '@' not in handle: return None return handle.strip() def _crypto_ap_iread_json(self) -> {}: """Obtains json from POST to the crypto API """ message_bytes = None max_crypto_message_length = 10240 length = int(self.headers['Content-length']) if length >= max_crypto_message_length: print('WARN: post to crypto API is too long ' + str(length) + ' bytes') return {} try: message_bytes = self.rfile.read(length) except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: POST message_bytes ' + 'connection reset by peer') else: print('WARN: POST message_bytes socket error') return {} except ValueError as ex: print('EX: POST message_bytes rfile.read failed, ' + str(ex)) return {} len_message = len(message_bytes) if len_message > 10240: print('WARN: post to crypto API is too long ' + str(len_message) + ' bytes') return {} return json.loads(message_bytes) def _crypto_api_query(self, calling_domain: str, referer_domain: str) -> bool: handle = self._crypto_ap_iread_handle() if not handle: return False if isinstance(handle, str): person_dir = acct_handle_dir(self.server.base_dir, 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 or \ '/question?firstpost=' in self.path or \ '/question?lastpost=' 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 = acct_handle_dir(base_dir, 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(max_hashtags: int, 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 update_moved_actors(base_dir, debug) if unit_test: server_address = (domain, proxy_port) pub_handler = partial(PubServerUnitTest) else: server_address = ('', proxy_port) pub_handler = partial(PubServer) if not os.path.isdir(base_dir + '/accounts'): print('Creating accounts directory') os.mkdir(base_dir + '/accounts') try: httpd = EpicyonServer(server_address, pub_handler) except SocketError as ex: if ex.errno == errno.ECONNREFUSED: print('EX: HTTP server address is already in use. ' + str(server_address)) return False print('EX: HTTP server failed to start. ' + str(ex)) print('server_address: ' + str(server_address)) return False # scan the theme directory for any svg files containing scripts assert not scan_themes_for_scripts(base_dir) # which accounts should minimize all attached images by default httpd.min_images_for_accounts = load_min_images_for_accounts(base_dir) # default language for each account when creating a new post httpd.default_post_language = load_default_post_languages(base_dir) # caches css files httpd.css_cache = {} httpd.reverse_sequence = load_reverse_timeline(base_dir) 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-nc/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', 'moveButton': 'm', '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.maxCacheAgeDays = 30 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, httpd.maxCacheAgeDays), 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) httpd.max_mentions = max_mentions httpd.max_emoji = max_emoji httpd.max_hashtags = max_hashtags 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, httpd.max_hashtags), 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()