__filename__ = "daemon.py" __author__ = "Bob Mottram" __license__ = "AGPL3+" __version__ = "1.2.0" __maintainer__ = "Bob Mottram" __email__ = "bob@libreserver.org" __status__ = "Production" __module_group__ = "Core" from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer, HTTPServer import sys import json import time import urllib.parse import datetime from socket import error as SocketError import errno from functools import partial import pyqrcode # for saving images from hashlib import sha256 from hashlib import md5 from shutil import copyfile from session import create_session from webfinger import webfinger_meta from webfinger import webfinger_node_info from webfinger import webfinger_lookup from webfinger import webfinger_update from mastoapiv1 import masto_api_v1_response from metadata import meta_data_node_info from metadata import metadata_custom_emoji from enigma import get_enigma_pub_key from enigma import set_enigma_pub_key from pgp import 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 jami import get_jami_address from jami import set_jami_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 person import add_actor_update_timestamp from person import set_person_notes from person import get_default_person_context from person import get_actor_update_json from person import save_person_qrcode from person import randomize_actor_images from person import person_upgrade_actor from person import activate_account from person import deactivate_account from person import register_account from person import person_lookup from person import person_box_json from person import create_shared_inbox from person import create_news_inbox from person import suspend_account from person import reenable_account from person import remove_account from person import can_remove_post from person import person_snooze from person import person_unsnooze from posts import get_original_post_from_announce_url from posts import save_post_to_box from posts import get_instance_actor_key from posts import remove_post_interactions from posts import outbox_message_create_wrap from posts import get_pinned_post_as_json from posts import pin_post from posts import json_pin_post from posts import undo_pinned_post from posts import is_moderator from posts import create_question_post from posts import create_public_post from posts import create_blog_post from posts import create_report_post from posts import create_unlisted_post from posts import create_followers_only_post from posts import create_direct_message_post from posts import populate_replies_json from posts import add_to_field from posts import expire_cache from inbox import clear_queue_items from inbox import inbox_permitted_message from inbox import inbox_message_has_params from inbox import run_inbox_queue from inbox import run_inbox_queue_watchdog from inbox import save_post_to_inbox_queue from inbox import populate_replies from follow import follower_approval_active from follow import is_following_actor from follow import get_following_feed from follow import send_follow_request from follow import unfollow_account from follow import create_initial_last_seen from skills import get_skills_from_list from skills import no_of_actor_skills from skills import actor_has_skill from skills import actor_skill_value from skills import set_actor_skill_level from auth import record_login_failure from auth import authorize from auth import create_password from auth import create_basic_auth_header from auth import authorize_basic from auth import store_basic_credentials from threads import thread_with_trace from threads import remove_dormant_threads from media import process_meta_data from media import convert_image_to_low_bandwidth from media import replace_you_tube from media import replace_twitter from media import attach_media from media import path_is_video from media import path_is_audio from blocking import get_cw_list_variable from blocking import load_cw_lists from blocking import update_blocked_cache from blocking import mute_post from blocking import unmute_post from blocking import set_broch_mode from blocking import broch_mode_is_active from blocking import add_block from blocking import remove_block from blocking import add_global_block from blocking import remove_global_block from blocking import is_blocked_hashtag from blocking import is_blocked_domain from blocking import get_domain_blocklist from roles import get_actor_roles_list from roles import set_role from roles import clear_moderator_status from roles import clear_editor_status from roles import clear_counselor_status from roles import clear_artist_status 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_theme_designer import html_theme_designer from webapp_minimalbutton import set_minimal from webapp_minimalbutton import is_minimal from webapp_utils import get_avatar_image_url from webapp_utils import html_hashtag_blocked from webapp_utils import html_following_list from webapp_utils import set_blog_address from webapp_utils import html_show_share from webapp_calendar import html_calendar_delete_confirm from webapp_calendar import html_calendar from webapp_about import html_about 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_unblock 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_d_ms from webapp_timeline import html_inbox_replies from webapp_timeline import html_inbox_media from webapp_timeline import html_inbox_blogs from webapp_timeline import html_inbox_news from webapp_timeline import html_inbox_features from webapp_timeline import html_outbox from webapp_media import load_peertube_instances from webapp_moderation import html_account_info from webapp_moderation import html_moderation from webapp_moderation import html_moderation_info from webapp_create_post import html_new_post from webapp_login import html_login from webapp_login import html_get_login_credentials from webapp_suspended import html_suspended from webapp_tos import html_terms_of_service from webapp_confirm import html_confirm_follow from webapp_confirm import html_confirm_unfollow from webapp_post import html_emoji_reaction_picker from webapp_post import html_post_replies from webapp_post import html_individual_post from webapp_post import individual_post_as_html from webapp_profile import html_edit_profile from webapp_profile import html_profile_after_search from webapp_profile import html_profile from webapp_column_left import html_links_mobile from webapp_column_left import html_edit_links from webapp_column_right import html_newswire_mobile from webapp_column_right import html_edit_newswire from webapp_column_right import html_citations from webapp_column_right import html_edit_news_post from webapp_search import html_skills_search from webapp_search import html_history_search from webapp_search import html_hashtag_search from webapp_search import rss_hashtag_search from webapp_search import html_search_emoji from webapp_search import html_search_shared_items from webapp_search import html_search_emoji_text_entry from webapp_search import html_search from webapp_hashtagswarm import get_hashtag_categories_feed from webapp_hashtagswarm import html_search_hashtag_category from webapp_welcome import welcome_screen_is_complete from webapp_welcome import html_welcome_screen from webapp_welcome import is_welcome_screen_complete from webapp_welcome_profile import html_welcome_profile from webapp_welcome_final import html_welcome_final from shares import merge_shared_item_tokens from shares import run_federated_shares_daemon from shares import run_federated_shares_watchdog from shares import update_shared_item_federation_token from shares import create_shared_item_federation_token from shares import authorize_shared_items from shares import generate_shared_item_federation_tokens from shares import get_shares_feed_for_person from shares import add_share from shares import remove_shared_item from shares import expire_shares from shares import shares_catalog_endpoint from shares import shares_catalog_account_endpoint from shares import shares_catalog_csv_endpoint from categories import set_hashtag_category from categories import update_hashtag_categories from languages import get_actor_languages from languages import set_actor_languages from like import update_likes_collection from reaction import update_reaction_collection 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 remove_line_endings 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 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 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 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_ssm_lbox 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 import os # maximum number of posts to list in outbox feed max_posts_in_feed = 12 # maximum number of posts in a hashtag feed max_posts_in_hashtag_feed = 6 # reduced posts for media feed because it can take a while max_posts_in_media_feed = 6 # Blogs can be longer, so don't show many per page max_posts_in_blogs_feed = 4 max_posts_in_news_feed = 10 # Maximum number of entries in returned rss.xml max_posts_in_rss_feed = 10 # number of follows/followers per page follows_per_page = 6 # number of item shares per page shares_per_page = 12 def save_domain_qrcode(base_dir: str, http_prefix: str, domain_full: str, scale=6) -> None: """Saves a qrcode image for the domain name This helps to transfer onion or i2p domains to a mobile device """ qrcodeFilename = base_dir + '/accounts/qrcode.png' url = pyqrcode.create(http_prefix + '://' + domain_full) url.png(qrcodeFilename, scale) class PubServer(BaseHTTPRequestHandler): protocol_version = 'HTTP/1.1' def _update_known_crawlers(self, uaStr: str) -> None: """Updates a dictionary of known crawlers accessing nodeinfo or the masto API """ if not uaStr: return curr_time = int(time.time()) if self.server.knownCrawlers.get(uaStr): self.server.knownCrawlers[uaStr]['hits'] += 1 self.server.knownCrawlers[uaStr]['lastseen'] = curr_time else: self.server.knownCrawlers[uaStr] = { "lastseen": curr_time, "hits": 1 } if curr_time - self.server.lastKnownCrawler >= 30: # remove any old observations removeCrawlers = [] for ua, item in self.server.knownCrawlers.items(): if curr_time - item['lastseen'] >= 60 * 60 * 24 * 30: removeCrawlers.append(ua) for ua in removeCrawlers: del self.server.knownCrawlers[ua] # save the list of crawlers save_json(self.server.knownCrawlers, self.server.base_dir + '/accounts/knownCrawlers.json') self.server.lastKnownCrawler = curr_time 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: instanceUrl = 'http://' + self.server.onion_domain elif (calling_domain.endswith('.i2p') and self.server.i2p_domain): instanceUrl = 'http://' + self.server.i2p_domain else: instanceUrl = \ self.server.http_prefix + '://' + self.server.domain_full return instanceUrl 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'] elif self.headers.get('signature-input'): return self.headers['signature-input'] elif 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)) pass def _send_reply_to_question(self, nickname: str, messageId: str, answer: str) -> None: """Sends a reply to a question """ votesFilename = \ acct_dir(self.server.base_dir, nickname, self.server.domain) + \ '/questions.txt' if os.path.isfile(votesFilename): # have we already voted on this? if messageId in open(votesFilename).read(): print('Already voted on message ' + messageId) return print('Voting on message ' + messageId) print('Vote for: ' + answer) commentsEnabled = True attachImageFilename = None mediaType = None imageDescription = None inReplyTo = messageId inReplyToAtomUri = messageId subject = None schedulePost = False eventDate = None eventTime = None location = None conversationId = None city = get_spoofed_city(self.server.city, self.server.base_dir, nickname, self.server.domain) message_json = \ create_public_post(self.server.base_dir, nickname, self.server.domain, self.server.port, self.server.http_prefix, answer, False, False, False, commentsEnabled, attachImageFilename, mediaType, imageDescription, city, inReplyTo, inReplyToAtomUri, subject, schedulePost, eventDate, eventTime, location, False, self.server.system_language, conversationId, self.server.low_bandwidth, self.server.content_license_url) 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): post_filename = \ locate_post(self.server.base_dir, nickname, self.server.domain, messageId) 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(votesFilename, 'a+') as votesFile: votesFile.write(messageId + '\n') except OSError: print('EX: unable to write vote ' + votesFilename) # ensure that the cached post is removed if it exists, # so that it then will be recreated cachedPostFilename = \ get_cached_post_filename(self.server.base_dir, nickname, self.server.domain, post_json_object) if cachedPostFilename: if os.path.isfile(cachedPostFilename): try: os.remove(cachedPostFilename) except OSError: print('EX: _send_reply_to_question ' + 'unable to delete ' + cachedPostFilename) # 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 _blocked_user_agent(self, calling_domain: str, agentStr: str) -> bool: """Should a GET or POST be blocked based upon its user agent? """ if not agentStr: return False agentStrLower = agentStr.lower() defaultAgentBlocks = [ 'fedilist' ] for uaBlock in defaultAgentBlocks: if uaBlock in agentStrLower: print('Blocked User agent: ' + uaBlock) return True agentDomain = None if agentStr: # is this a web crawler? If so the block it if 'bot/' in agentStrLower or 'bot-' in agentStrLower: if self.server.news_instance: return False print('Blocked Crawler: ' + agentStr) return True # get domain name from User-Agent agentDomain = user_agent_domain(agentStr, self.server.debug) else: # no User-Agent header is present return True # is the User-Agent type blocked? eg. "Mastodon" if self.server.user_agents_blocked: blockedUA = False for agentName in self.server.user_agents_blocked: if agentName in agentStr: blockedUA = True break if blockedUA: return True if not agentDomain: return False # is the User-Agent domain blocked blockedUA = False if not agentDomain.startswith(calling_domain): self.server.blockedCacheLastUpdated = \ update_blocked_cache(self.server.base_dir, self.server.blockedCache, self.server.blockedCacheLastUpdated, self.server.blockedCacheUpdateSecs) blockedUA = is_blocked_domain(self.server.base_dir, agentDomain, self.server.blockedCache) # if self.server.debug: if blockedUA: print('Blocked User agent: ' + agentDomain) return blockedUA def _request_csv(self) -> bool: """Should a csv response be given? """ if not self.headers.get('Accept'): return False acceptStr = self.headers['Accept'] if 'text/csv' in acceptStr: return True return False def _request_http(self) -> bool: """Should a http response be given? """ if not self.headers.get('Accept'): return False acceptStr = self.headers['Accept'] if self.server.debug: print('ACCEPT: ' + acceptStr) if 'application/ssml' in acceptStr: if 'text/html' not in acceptStr: return False if 'image/' in acceptStr: if 'text/html' not in acceptStr: return False if 'video/' in acceptStr: if 'text/html' not in acceptStr: return False if 'audio/' in acceptStr: if 'text/html' not in acceptStr: return False if acceptStr.startswith('*'): if self.headers.get('User-Agent'): if 'ELinks' in self.headers['User-Agent'] or \ 'Lynx' in self.headers['User-Agent']: return True return False if 'json' in acceptStr: return False return True def _signed_ge_tkey_id(self) -> str: """Returns the actor from the signed GET keyId """ 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 keyId, which is typically the instance actor keyId = None signatureParams = signature.split(',') for signatureItem in signatureParams: if signatureItem.startswith('keyId='): if '"' in signatureItem: keyId = signatureItem.split('"')[1] # remove #main-key if '#' in keyId: keyId = keyId.split('#')[0] return keyId return None def _establish_session(self, callingFunction: str) -> bool: """Recreates session if needed """ if self.server.session: return True print('DEBUG: creating new session during ' + callingFunction) self.server.session = create_session(self.server.proxy_type) if self.server.session: return True print('ERROR: GET failed to create session during ' + callingFunction) return False def _secure_mode(self, force: bool = False) -> bool: """http authentication of GET requests for json """ if not self.server.secure_mode and not force: return True keyId = self._signed_ge_tkey_id() if not keyId: if self.server.debug: print('AUTH: secure mode, ' + 'failed to obtain keyId from signature') return False # is the keyId (actor) valid? if not url_permitted(keyId, self.server.federation_list): if self.server.debug: print('AUTH: Secure mode GET request not permitted: ' + keyId) return False if not self._establish_session("secure mode"): return False # obtain the public key pubKey = \ get_person_pub_key(self.server.base_dir, self.server.session, keyId, self.server.person_cache, self.server.debug, self.server.project_version, self.server.http_prefix, self.server.domain, self.server.onion_domain, self.server.signing_priv_key_pem) if not pubKey: if self.server.debug: print('AUTH: secure mode failed to ' + 'obtain public key for ' + keyId) return False # verify the GET request without any digest if verify_post_headers(self.server.http_prefix, self.server.domain_full, pubKey, self.headers, self.path, True, None, '', self.server.debug): return True if self.server.debug: print('AUTH: secure mode authorization failed for ' + keyId) return False def _login_headers(self, fileFormat: str, length: int, calling_domain: str) -> None: self.send_response(200) self.send_header('Content-type', fileFormat) 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, fileFormat: str, length: int, calling_domain: str) -> None: self.send_response(200) self.send_header('Content-type', fileFormat) 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 lastStr = redirect.split('/')[-1] return redirect.replace('/' + lastStr, '/' + urllib.parse.quote_plus(lastStr)) def _logout_redirect(self, redirect: str, cookie: str, calling_domain: str) -> None: if '://' not in redirect: print('REDIRECT ERROR: redirect is not an absolute url ' + 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, fileFormat: str, length: int, cookie: str, calling_domain: str, permissive: bool) -> None: self.send_response(200) self.send_header('Content-type', fileFormat) if 'image/' in fileFormat or \ 'audio/' in fileFormat or \ 'video/' in fileFormat: 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', 'GNU Natalie Nguyen') if cookie: cookieStr = cookie if 'HttpOnly;' not in cookieStr: if self.server.http_prefix == 'https': cookieStr += '; Secure' cookieStr += '; HttpOnly; SameSite=Strict' self.send_header('Cookie', cookieStr) def _set_headers(self, fileFormat: str, length: int, cookie: str, calling_domain: str, permissive: bool) -> None: self._set_headers_base(fileFormat, length, cookie, calling_domain, permissive) self.end_headers() def _set_headers_head(self, fileFormat: str, length: int, etag: str, calling_domain: str, permissive: bool) -> None: self._set_headers_base(fileFormat, length, None, calling_domain, permissive) if etag: self.send_header('ETag', '"' + etag + '"') self.end_headers() def _set_headers_etag(self, mediaFilename: str, fileFormat: str, data, cookie: str, calling_domain: str, permissive: bool, lastModified: str) -> None: datalen = len(data) self._set_headers_base(fileFormat, datalen, cookie, calling_domain, permissive) etag = None if os.path.isfile(mediaFilename + '.etag'): try: with open(mediaFilename + '.etag', 'r') as etagFile: etag = etagFile.read() except OSError: print('EX: _set_headers_etag ' + 'unable to read ' + mediaFilename + '.etag') if not etag: etag = md5(data).hexdigest() # nosec try: with open(mediaFilename + '.etag', 'w+') as etagFile: etagFile.write(etag) except OSError: print('EX: _set_headers_etag ' + 'unable to write ' + mediaFilename + '.etag') # if etag: # self.send_header('ETag', '"' + etag + '"') if lastModified: self.send_header('last-modified', lastModified) self.end_headers() def _etag_exists(self, mediaFilename: str) -> bool: """Does an etag header exist for the given file? """ etagHeader = 'If-None-Match' if not self.headers.get(etagHeader): etagHeader = 'if-none-match' if not self.headers.get(etagHeader): etagHeader = 'If-none-match' if self.headers.get(etagHeader): oldEtag = self.headers[etagHeader].replace('"', '') if os.path.isfile(mediaFilename + '.etag'): # load the etag from file currEtag = '' try: with open(mediaFilename + '.etag', 'r') as etagFile: currEtag = etagFile.read() except OSError: print('EX: _etag_exists unable to read ' + str(mediaFilename)) if currEtag and oldEtag == currEtag: # The file has not changed return True return False def _redirect_headers(self, redirect: str, cookie: str, calling_domain: str) -> None: if '://' not in redirect: print('REDIRECT ERROR: redirect is not an absolute url ' + redirect) self.send_response(303) if cookie: cookieStr = cookie.replace('SET:', '').strip() if 'HttpOnly;' not in cookieStr: if self.server.http_prefix == 'https': cookieStr += '; Secure' cookieStr += '; HttpOnly; SameSite=Strict' if not cookie.startswith('SET:'): self.send_header('Cookie', cookieStr) else: self.send_header('Set-Cookie', cookieStr) 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, httpCode: int, httpDescription: str, longDescription: str) -> None: msg = \ '' + str(httpCode) + '' \ '' \ '
' + str(httpCode) + '
' \ '

' + httpDescription + '

' \ '
' + longDescription + '
' \ '' msg = msg.encode('utf-8') self.send_response(httpCode) self.send_header('Content-Type', 'text/html; charset=utf-8') msgLenStr = str(len(msg)) self.send_header('Content-Length', msgLenStr) self.end_headers() if not self._write(msg): print('Error when showing ' + str(httpCode)) 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) else: self._http_return_code(200, 'Ok', 'This is nothing less ' + 'than an utter triumph') def _403(self) -> None: if self.server.translate: self._http_return_code(403, self.server.translate['Forbidden'], self.server.translate["You're not allowed"]) else: self._http_return_code(403, 'Forbidden', "You're not allowed") 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']) else: self._http_return_code(404, 'Not Found', 'These are not the ' + 'droids you are ' + 'looking for') 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']) else: self._http_return_code(304, 'Not changed', 'The contents of ' + 'your local cache ' + 'are up to date') 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']) else: self._http_return_code(400, 'Bad Request', 'Better luck next time') 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) else: self._http_return_code(503, 'Unavailable', 'The server is busy. Please try again ' + 'later') 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('ERROR: _write error ' + str(tries) + ' ' + str(ex)) break except Exception as ex: print('ERROR: _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, uaStr: 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, customEmoji: [], show_node_info_accounts: bool) -> 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 print('mastodon api v1: ' + path) print('mastodon api v1: authorized ' + str(authorized)) print('mastodon api v1: nickname ' + str(nickname)) self._update_known_crawlers(uaStr) broch_mode = broch_mode_is_active(base_dir) sendJson, sendJsonStr = \ masto_api_v1_response(path, calling_domain, uaStr, authorized, http_prefix, base_dir, nickname, domain, domain_full, onion_domain, i2p_domain, translate, registration, system_language, project_version, customEmoji, show_node_info_accounts, broch_mode) if sendJson is not None: msg = json.dumps(sendJson).encode('utf-8') msglen = len(msg) if self._has_accept(calling_domain): if 'application/ld+json' in self.headers['Accept']: self._set_headers('application/ld+json', msglen, None, calling_domain, True) else: self._set_headers('application/json', msglen, None, calling_domain, True) else: self._set_headers('application/ld+json', msglen, None, calling_domain, True) self._write(msg) if sendJsonStr: print(sendJsonStr) return True # no api endpoints were matched self._404() return True def _masto_api(self, path: str, calling_domain: str, uaStr: 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, customEmoji: [], show_node_info_accounts: bool) -> bool: return self._masto_api_v1(path, calling_domain, uaStr, authorized, http_prefix, base_dir, nickname, domain, domain_full, onion_domain, i2p_domain, translate, registration, system_language, project_version, customEmoji, show_node_info_accounts) def _nodeinfo(self, uaStr: str, calling_domain: str) -> bool: if not self.path.startswith('/nodeinfo/2.0'): return False if self.server.debug: print('DEBUG: nodeinfo ' + self.path) self._update_known_crawlers(uaStr) # 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) nodeInfoVersion = self.server.project_version if not self.server.show_node_info_version or broch_mode: nodeInfoVersion = '0.0.0' show_node_info_accounts = self.server.show_node_info_accounts if broch_mode: show_node_info_accounts = False instanceUrl = self._get_instance_url(calling_domain) aboutUrl = instanceUrl + '/about' termsOfServiceUrl = instanceUrl + '/terms' info = meta_data_node_info(self.server.base_dir, aboutUrl, termsOfServiceUrl, self.server.registration, nodeInfoVersion, show_node_info_accounts) if info: msg = json.dumps(info).encode('utf-8') msglen = len(msg) if self._has_accept(calling_domain): if 'application/ld+json' in self.headers['Accept']: self._set_headers('application/ld+json', msglen, None, calling_domain, True) else: self._set_headers('application/json', msglen, None, calling_domain, True) else: self._set_headers('application/ld+json', msglen, None, calling_domain, True) self._write(msg) print('nodeinfo sent to ' + calling_domain) return True self._404() return True def _webfinger(self, calling_domain: str) -> bool: if not self.path.startswith('/.well-known'): return False if self.server.debug: print('DEBUG: WEBFINGER well-known') if self.server.debug: print('DEBUG: WEBFINGER host-meta') if self.path.startswith('/.well-known/host-meta'): if calling_domain.endswith('.onion') and \ self.server.onion_domain: wfResult = \ webfinger_meta('http', self.server.onion_domain) elif (calling_domain.endswith('.i2p') and self.server.i2p_domain): wfResult = \ webfinger_meta('http', self.server.i2p_domain) else: wfResult = \ webfinger_meta(self.server.http_prefix, self.server.domain_full) if wfResult: msg = wfResult.encode('utf-8') msglen = len(msg) self._set_headers('application/xrd+xml', msglen, None, calling_domain, True) self._write(msg) return True self._404() return True if self.path.startswith('/api/statusnet') or \ self.path.startswith('/api/gnusocial') or \ self.path.startswith('/siteinfo') or \ self.path.startswith('/poco') or \ self.path.startswith('/friendi'): self._404() return True if self.path.startswith('/.well-known/nodeinfo') or \ self.path.startswith('/.well-known/x-nodeinfo'): if calling_domain.endswith('.onion') and \ self.server.onion_domain: wfResult = \ webfinger_node_info('http', self.server.onion_domain) elif (calling_domain.endswith('.i2p') and self.server.i2p_domain): wfResult = \ webfinger_node_info('http', self.server.i2p_domain) else: wfResult = \ webfinger_node_info(self.server.http_prefix, self.server.domain_full) if wfResult: msg = json.dumps(wfResult).encode('utf-8') msglen = len(msg) if self._has_accept(calling_domain): if 'application/ld+json' in self.headers['Accept']: self._set_headers('application/ld+json', msglen, None, calling_domain, True) else: self._set_headers('application/json', 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)) wfResult = \ webfinger_lookup(self.path, self.server.base_dir, self.server.domain, self.server.onion_domain, self.server.port, self.server.debug) if wfResult: msg = json.dumps(wfResult).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, postToNickname: str) -> bool: """post is received by the outbox Client to server message post https://www.w3.org/TR/activitypub/#client-to-server-outbox-delivery """ city = self.server.city if postToNickname: print('Posting to nickname ' + postToNickname) self.postToNickname = postToNickname city = get_spoofed_city(self.server.city, self.server.base_dir, postToNickname, self.server.domain) shared_items_federated_domains = \ self.server.shared_items_federated_domains return post_message_to_outbox(self.server.session, self.server.translate, message_json, self.postToNickname, 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, self.server.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, self.server.sharedItemFederationTokens, 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) def _get_outbox_thread_index(self, nickname: str, maxOutboxThreadsPerAccount: 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 """ accountOutboxThreadName = nickname if not accountOutboxThreadName: accountOutboxThreadName = '*' # create the buffer for the given account if not self.server.outboxThread.get(accountOutboxThreadName): self.server.outboxThread[accountOutboxThreadName] = \ [None] * maxOutboxThreadsPerAccount self.server.outbox_thread_index[accountOutboxThreadName] = 0 return 0 # increment the ring buffer index index = self.server.outbox_thread_index[accountOutboxThreadName] + 1 if index >= maxOutboxThreadsPerAccount: index = 0 self.server.outbox_thread_index[accountOutboxThreadName] = index # remove any existing thread from the current index in the buffer if self.server.outboxThread.get(accountOutboxThreadName): acct = accountOutboxThreadName if self.server.outboxThread[acct][index].is_alive(): self.server.outboxThread[acct][index].kill() return index def _post_to_outbox_thread(self, message_json: {}) -> bool: """Creates a thread to send a post """ accountOutboxThreadName = self.postToNickname if not accountOutboxThreadName: accountOutboxThreadName = '*' index = self._get_outbox_thread_index(accountOutboxThreadName, 8) print('Creating outbox thread ' + accountOutboxThreadName + '/' + str(self.server.outbox_thread_index[accountOutboxThreadName])) self.server.outboxThread[accountOutboxThreadName][index] = \ thread_with_trace(target=self._post_to_outbox, args=(message_json.copy(), self.server.project_version, None), daemon=True) print('Starting outbox thread') self.server.outboxThread[accountOutboxThreadName][index].start() return True def _update_inbox_queue(self, nickname: str, message_json: {}, messageBytes: str) -> int: """Update the inbox queue """ if self.server.restartInboxQueueInProgress: self._503() print('Message arrived but currently restarting inbox queue') self.server.POSTbusy = False return 2 # check that the incoming message has a fully recognized # linked data context if not has_valid_context(message_json): print('Message arriving at inbox queue has no valid context') self._400() self.server.POSTbusy = False return 3 # check for blocked domains so that they can be rejected early messageDomain = None if not has_actor(message_json, self.server.debug): print('Message arriving at inbox queue has no actor') self._400() self.server.POSTbusy = False return 3 # actor should be a string if not isinstance(message_json['actor'], str): self._400() self.server.POSTbusy = False return 3 # check that some additional fields are strings stringFields = ('id', 'type', 'published') for checkField in stringFields: if not message_json.get(checkField): continue if not isinstance(message_json[checkField], str): self._400() self.server.POSTbusy = False return 3 # check that to/cc fields are lists listFields = ('to', 'cc') for checkField in listFields: if not message_json.get(checkField): continue if not isinstance(message_json[checkField], list): self._400() self.server.POSTbusy = False return 3 if has_object_dict(message_json): stringFields = ( 'id', 'actor', 'type', 'content', 'published', 'summary', 'url', 'attributedTo' ) for checkField in stringFields: if not message_json['object'].get(checkField): continue if not isinstance(message_json['object'][checkField], str): self._400() self.server.POSTbusy = False return 3 # check that some fields are lists listFields = ('to', 'cc', 'attachment') for checkField in listFields: if not message_json['object'].get(checkField): continue if not isinstance(message_json['object'][checkField], list): self._400() self.server.POSTbusy = False return 3 # actor should look like a url if '://' not in message_json['actor'] or \ '.' not in message_json['actor']: print('POST actor does not look like a url ' + message_json['actor']) self._400() self.server.POSTbusy = False return 3 # sent by an actor on a local network address? if not self.server.allow_local_network_access: localNetworkPatternList = get_local_network_addresses() for localNetworkPattern in localNetworkPatternList: if localNetworkPattern in message_json['actor']: print('POST actor contains local network address ' + message_json['actor']) self._400() self.server.POSTbusy = False return 3 messageDomain, messagePort = \ get_domain_from_actor(message_json['actor']) self.server.blockedCacheLastUpdated = \ update_blocked_cache(self.server.base_dir, self.server.blockedCache, self.server.blockedCacheLastUpdated, self.server.blockedCacheUpdateSecs) if is_blocked_domain(self.server.base_dir, messageDomain, self.server.blockedCache): print('POST from blocked domain ' + messageDomain) self._400() self.server.POSTbusy = False return 3 # if the inbox queue is full then return a busy code if len(self.server.inbox_queue) >= self.server.max_queue_length: if messageDomain: print('Queue: Inbox queue is full. Incoming post from ' + message_json['actor']) else: print('Queue: Inbox queue is full') self._503() clear_queue_items(self.server.base_dir, self.server.inbox_queue) if not self.server.restartInboxQueueInProgress: self.server.restartInboxQueue = True self.server.POSTbusy = False return 2 # Convert the headers needed for signature verification to dict headersDict = {} headersDict['host'] = self.headers['host'] headersDict['signature'] = self.headers['signature'] if self.headers.get('Date'): headersDict['Date'] = self.headers['Date'] elif self.headers.get('date'): headersDict['Date'] = self.headers['date'] if self.headers.get('digest'): headersDict['digest'] = self.headers['digest'] if self.headers.get('Collection-Synchronization'): headersDict['Collection-Synchronization'] = \ self.headers['Collection-Synchronization'] if self.headers.get('Content-type'): headersDict['Content-type'] = self.headers['Content-type'] if self.headers.get('Content-Length'): headersDict['Content-Length'] = self.headers['Content-Length'] elif self.headers.get('content-length'): headersDict['content-length'] = self.headers['content-length'] originalMessageJson = message_json.copy() # whether to add a 'to' field to the message add_to_fieldTypes = ( 'Follow', 'Like', 'EmojiReact', 'Add', 'Remove', 'Ignore' ) for addToType in add_to_fieldTypes: message_json, toFieldExists = \ add_to_field(addToType, message_json, self.server.debug) beginSaveTime = time.time() # save the json for later queue processing messageBytesDecoded = messageBytes.decode('utf-8') if contains_invalid_local_links(messageBytesDecoded): print('WARN: post contains invalid local links ' + str(originalMessageJson)) return 5 self.server.blockedCacheLastUpdated = \ update_blocked_cache(self.server.base_dir, self.server.blockedCache, self.server.blockedCacheLastUpdated, self.server.blockedCacheUpdateSecs) queueFilename = \ save_post_to_inbox_queue(self.server.base_dir, self.server.http_prefix, nickname, self.server.domain_full, message_json, originalMessageJson, messageBytesDecoded, headersDict, self.path, self.server.debug, self.server.blockedCache, self.server.system_language) if queueFilename: # add json to the queue if queueFilename not in self.server.inbox_queue: self.server.inbox_queue.append(queueFilename) if self.server.debug: timeDiff = int((time.time() - beginSaveTime) * 1000) if timeDiff > 200: print('SLOW: slow save of inbox queue item ' + queueFilename + ' took ' + str(timeDiff) + ' mS') self.send_response(201) self.end_headers() self.server.POSTbusy = False return 0 self._503() self.server.POSTbusy = False return 1 def _is_authorized(self) -> bool: self.authorizedNickname = None notAuthPaths = ( '/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 notAuthStr in notAuthPaths: if self.path.startswith(notAuthStr): return False # token based authenticated used by the web interface if self.headers.get('Cookie'): if self.headers['Cookie'].startswith('epicyon='): tokenStr = self.headers['Cookie'].split('=', 1)[1].strip() if ';' in tokenStr: tokenStr = tokenStr.split(';')[0].strip() if self.server.tokens_lookup.get(tokenStr): nickname = self.server.tokens_lookup[tokenStr] if not is_system_account(nickname): self.authorizedNickname = 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 elif '/' + nickname + '?' in self.path: return True elif 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=', '') + ' tokenStr=' + tokenStr + ' tokens=' + str(self.server.tokens_lookup)) 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 _show_login_screen(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) -> None: """Shows the login screen """ # 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.POSTbusy = 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.send_response(401) self.end_headers() self.server.POSTbusy = False return try: loginParams = self.rfile.read(length).decode('utf-8') except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: POST login read ' + 'connection reset by peer') else: print('WARN: POST login read socket error') self.send_response(400) self.end_headers() self.server.POSTbusy = False return except ValueError as ex: print('ERROR: POST login read failed, ' + str(ex)) self.send_response(400) self.end_headers() self.server.POSTbusy = False return loginNickname, loginPassword, register = \ html_get_login_credentials(loginParams, self.server.last_login_time, self.server.domain) if loginNickname: if is_system_account(loginNickname): print('Invalid username login: ' + loginNickname + ' (system account)') self._clear_login_details(loginNickname, calling_domain) self.server.POSTbusy = False return self.server.last_login_time = int(time.time()) if register: if not valid_password(loginPassword): self.server.POSTbusy = 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, loginNickname, loginPassword, self.server.manual_follower_approval): self.server.POSTbusy = 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 authHeader = \ create_basic_auth_header(loginNickname, loginPassword) if self.headers.get('X-Forward-For'): ipAddress = self.headers['X-Forward-For'] elif self.headers.get('X-Forwarded-For'): ipAddress = self.headers['X-Forwarded-For'] else: ipAddress = self.client_address[0] if not domain.endswith('.onion'): if not is_local_network_address(ipAddress): print('Login attempt from IP: ' + str(ipAddress)) if not authorize_basic(base_dir, '/users/' + loginNickname + '/outbox', authHeader, False): print('Login failed: ' + loginNickname) self._clear_login_details(loginNickname, calling_domain) failTime = int(time.time()) self.server.last_login_failure = failTime if not domain.endswith('.onion'): if not is_local_network_address(ipAddress): record_login_failure(base_dir, ipAddress, self.server.login_failure_count, failTime, self.server.log_login_failures) self.server.POSTbusy = False return else: if self.server.login_failure_count.get(ipAddress): del self.server.login_failure_count[ipAddress] if is_suspended(base_dir, loginNickname): msg = \ html_suspended(self.server.css_cache, base_dir).encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) self.server.POSTbusy = False return # login success - redirect with authorization print('Login success: ' + loginNickname) # re-activate account if needed activate_account(base_dir, loginNickname, domain) # This produces a deterministic token based # on nick+password+salt saltFilename = \ acct_dir(base_dir, loginNickname, domain) + '/.salt' salt = create_password(32) if os.path.isfile(saltFilename): try: with open(saltFilename, 'r') as fp: salt = fp.read() except OSError as ex: print('EX: Unable to read salt for ' + loginNickname + ' ' + str(ex)) else: try: with open(saltFilename, 'w+') as fp: fp.write(salt) except OSError as ex: print('EX: Unable to save salt for ' + loginNickname + ' ' + str(ex)) tokenText = loginNickname + loginPassword + salt token = sha256(tokenText.encode('utf-8')).hexdigest() self.server.tokens[loginNickname] = token loginHandle = loginNickname + '@' + domain tokenFilename = \ base_dir + '/accounts/' + \ loginHandle + '/.token' try: with open(tokenFilename, 'w+') as fp: fp.write(token) except OSError as ex: print('EX: Unable to save token for ' + loginNickname + ' ' + str(ex)) person_upgrade_actor(base_dir, None, loginHandle, base_dir + '/accounts/' + loginHandle + '.json') index = self.server.tokens[loginNickname] self.server.tokens_lookup[index] = loginNickname cookieStr = 'SET:epicyon=' + \ self.server.tokens[loginNickname] + '; SameSite=Strict' if calling_domain.endswith('.onion') and onion_domain: self._redirect_headers('http://' + onion_domain + '/users/' + loginNickname + '/' + self.server.defaultTimeline, cookieStr, calling_domain) elif (calling_domain.endswith('.i2p') and i2p_domain): self._redirect_headers('http://' + i2p_domain + '/users/' + loginNickname + '/' + self.server.defaultTimeline, cookieStr, calling_domain) else: self._redirect_headers(http_prefix + '://' + domain_full + '/users/' + loginNickname + '/' + self.server.defaultTimeline, cookieStr, calling_domain) self.server.POSTbusy = False return self._200() self.server.POSTbusy = False def _moderator_actions(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) -> None: """Actions on the moderator screen """ usersPath = path.replace('/moderationaction', '') nickname = usersPath.replace('/users/', '') actorStr = self._get_instance_url(calling_domain) + usersPath if not is_moderator(self.server.base_dir, nickname): self._redirect_headers(actorStr + '/moderation', cookie, calling_domain) self.server.POSTbusy = False return length = int(self.headers['Content-length']) try: moderationParams = self.rfile.read(length).decode('utf-8') except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: POST moderationParams connection was reset') else: print('WARN: POST moderationParams ' + 'rfile.read socket error') self.send_response(400) self.end_headers() self.server.POSTbusy = False return except ValueError as ex: print('ERROR: POST moderationParams rfile.read failed, ' + str(ex)) self.send_response(400) self.end_headers() self.server.POSTbusy = False return if '&' in moderationParams: moderationText = None moderationButton = None for moderationStr in moderationParams.split('&'): if moderationStr.startswith('moderationAction'): if '=' in moderationStr: moderationText = \ moderationStr.split('=')[1].strip() modText = moderationText.replace('+', ' ') moderationText = \ urllib.parse.unquote_plus(modText.strip()) elif moderationStr.startswith('submitInfo'): searchHandle = moderationText if searchHandle: if '/@' in searchHandle: searchNickname = \ get_nickname_from_actor(searchHandle) searchDomain, searchPort = \ get_domain_from_actor(searchHandle) searchHandle = \ searchNickname + '@' + searchDomain if '@' not in searchHandle: if searchHandle.startswith('http'): searchNickname = \ get_nickname_from_actor(searchHandle) searchDomain, searchPort = \ get_domain_from_actor(searchHandle) searchHandle = \ searchNickname + '@' + searchDomain if '@' not in searchHandle: # is this a local nickname on this instance? localHandle = \ searchHandle + '@' + self.server.domain if os.path.isdir(self.server.base_dir + '/accounts/' + localHandle): searchHandle = localHandle else: searchHandle = None if searchHandle: msg = \ html_account_info(self.server.css_cache, self.server.translate, base_dir, http_prefix, nickname, self.server.domain, self.server.port, searchHandle, self.server.debug, self.server.system_language, self.server.signing_priv_key_pem) else: msg = \ html_moderation_info(self.server.css_cache, self.server.translate, base_dir, http_prefix, nickname) msg = msg.encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) self.server.POSTbusy = False return elif moderationStr.startswith('submitBlock'): moderationButton = 'block' elif moderationStr.startswith('submitUnblock'): moderationButton = 'unblock' elif moderationStr.startswith('submitFilter'): moderationButton = 'filter' elif moderationStr.startswith('submitUnfilter'): moderationButton = 'unfilter' elif moderationStr.startswith('submitSuspend'): moderationButton = 'suspend' elif moderationStr.startswith('submitUnsuspend'): moderationButton = 'unsuspend' elif moderationStr.startswith('submitRemove'): moderationButton = 'remove' if moderationButton and moderationText: if debug: print('moderationButton: ' + moderationButton) print('moderationText: ' + moderationText) nickname = moderationText if nickname.startswith('http') or \ nickname.startswith('hyper'): nickname = get_nickname_from_actor(nickname) if '@' in nickname: nickname = nickname.split('@')[0] if moderationButton == 'suspend': suspend_account(base_dir, nickname, domain) if moderationButton == 'unsuspend': reenable_account(base_dir, nickname) if moderationButton == 'filter': add_global_filter(base_dir, moderationText) if moderationButton == 'unfilter': remove_global_filter(base_dir, moderationText) if moderationButton == 'block': fullBlockDomain = None if moderationText.startswith('http') or \ moderationText.startswith('hyper'): # https://domain blockDomain, blockPort = \ get_domain_from_actor(moderationText) fullBlockDomain = \ get_full_domain(blockDomain, blockPort) if '@' in moderationText: # nick@domain or *@domain fullBlockDomain = moderationText.split('@')[1] else: # assume the text is a domain name if not fullBlockDomain and '.' in moderationText: nickname = '*' fullBlockDomain = moderationText.strip() if fullBlockDomain or nickname.startswith('#'): add_global_block(base_dir, nickname, fullBlockDomain) if moderationButton == 'unblock': fullBlockDomain = None if moderationText.startswith('http') or \ moderationText.startswith('hyper'): # https://domain blockDomain, blockPort = \ get_domain_from_actor(moderationText) fullBlockDomain = \ get_full_domain(blockDomain, blockPort) if '@' in moderationText: # nick@domain or *@domain fullBlockDomain = moderationText.split('@')[1] else: # assume the text is a domain name if not fullBlockDomain and '.' in moderationText: nickname = '*' fullBlockDomain = moderationText.strip() if fullBlockDomain or nickname.startswith('#'): remove_global_block(base_dir, nickname, fullBlockDomain) if moderationButton == 'remove': if '/statuses/' not in moderationText: remove_account(base_dir, nickname, domain, port) else: # remove a post or thread post_filename = \ locate_post(base_dir, nickname, domain, moderationText) if post_filename: if can_remove_post(base_dir, nickname, domain, port, moderationText): delete_post(base_dir, http_prefix, nickname, domain, post_filename, debug, self.server.recent_posts_cache) 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, moderationText) if post_filename: if can_remove_post(base_dir, 'news', domain, port, moderationText): delete_post(base_dir, http_prefix, 'news', domain, post_filename, debug, self.server.recent_posts_cache) self._redirect_headers(actorStr + '/moderation', cookie, calling_domain) self.server.POSTbusy = False return def _key_shortcuts(self, path: str, calling_domain: str, cookie: str, base_dir: str, http_prefix: str, nickname: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, debug: bool, accessKeys: {}, defaultTimeline: str) -> None: """Receive POST from webapp_accesskeys """ usersPath = '/users/' + nickname originPathStr = \ http_prefix + '://' + domain_full + usersPath + '/' + \ defaultTimeline length = int(self.headers['Content-length']) try: accessKeysParams = self.rfile.read(length).decode('utf-8') except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: POST accessKeysParams ' + 'connection reset by peer') else: print('WARN: POST accessKeysParams socket error') self.send_response(400) self.end_headers() self.server.POSTbusy = False return except ValueError as ex: print('ERROR: POST accessKeysParams rfile.read failed, ' + str(ex)) self.send_response(400) self.end_headers() self.server.POSTbusy = False return accessKeysParams = \ urllib.parse.unquote_plus(accessKeysParams) # key shortcuts screen, back button # See html_access_keys if 'submitAccessKeysCancel=' in accessKeysParams or \ 'submitAccessKeys=' not in accessKeysParams: if calling_domain.endswith('.onion') and onion_domain: originPathStr = \ 'http://' + onion_domain + usersPath + '/' + \ defaultTimeline elif calling_domain.endswith('.i2p') and i2p_domain: originPathStr = \ 'http://' + i2p_domain + usersPath + '/' + defaultTimeline self._redirect_headers(originPathStr, cookie, calling_domain) self.server.POSTbusy = False return saveKeys = False accessKeysTemplate = self.server.accessKeys for variableName, key in accessKeysTemplate.items(): if not accessKeys.get(variableName): accessKeys[variableName] = accessKeysTemplate[variableName] variableName2 = variableName.replace(' ', '_') if variableName2 + '=' in accessKeysParams: newKey = accessKeysParams.split(variableName2 + '=')[1] if '&' in newKey: newKey = newKey.split('&')[0] if newKey: if len(newKey) > 1: newKey = newKey[0] if newKey != accessKeys[variableName]: accessKeys[variableName] = newKey saveKeys = True if saveKeys: accessKeysFilename = \ acct_dir(base_dir, nickname, domain) + '/accessKeys.json' save_json(accessKeys, accessKeysFilename) if not self.server.keyShortcuts.get(nickname): self.server.keyShortcuts[nickname] = accessKeys.copy() # redirect back from key shortcuts screen if calling_domain.endswith('.onion') and onion_domain: originPathStr = \ 'http://' + onion_domain + usersPath + '/' + defaultTimeline elif calling_domain.endswith('.i2p') and i2p_domain: originPathStr = \ 'http://' + i2p_domain + usersPath + '/' + defaultTimeline self._redirect_headers(originPathStr, cookie, calling_domain) self.server.POSTbusy = False return def _theme_designer_edit(self, path: str, calling_domain: str, cookie: str, base_dir: str, http_prefix: str, nickname: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, debug: bool, accessKeys: {}, defaultTimeline: str, theme_name: str, allow_local_network_access: bool, system_language: str) -> None: """Receive POST from webapp_theme_designer """ usersPath = '/users/' + nickname originPathStr = \ http_prefix + '://' + domain_full + usersPath + '/' + \ defaultTimeline length = int(self.headers['Content-length']) try: themeParams = self.rfile.read(length).decode('utf-8') except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: POST themeParams ' + 'connection reset by peer') else: print('WARN: POST themeParams socket error') self.send_response(400) self.end_headers() self.server.POSTbusy = False return except ValueError as ex: print('ERROR: POST themeParams rfile.read failed, ' + str(ex)) self.send_response(400) self.end_headers() self.server.POSTbusy = False return themeParams = \ urllib.parse.unquote_plus(themeParams) # theme designer screen, reset button # See html_theme_designer if 'submitThemeDesignerReset=' in themeParams or \ 'submitThemeDesigner=' not in themeParams: if 'submitThemeDesignerReset=' in themeParams: reset_theme_designer_settings(base_dir, theme_name, domain, allow_local_network_access, system_language) set_theme(base_dir, theme_name, domain, allow_local_network_access, system_language) if calling_domain.endswith('.onion') and onion_domain: originPathStr = \ 'http://' + onion_domain + usersPath + '/' + \ defaultTimeline elif calling_domain.endswith('.i2p') and i2p_domain: originPathStr = \ 'http://' + i2p_domain + usersPath + '/' + defaultTimeline self._redirect_headers(originPathStr, cookie, calling_domain) self.server.POSTbusy = False return fields = {} fieldsList = themeParams.split('&') for fieldStr in fieldsList: if '=' not in fieldStr: continue fieldValue = fieldStr.split('=')[1].strip() if not fieldValue: continue if fieldValue == 'on': fieldValue = 'True' fields[fieldStr.split('=')[0]] = fieldValue # Check for boolean values which are False. # These don't come through via themeParams, # so need to be checked separately themeFilename = base_dir + '/theme/' + theme_name + '/theme.json' themeJson = load_json(themeFilename) if themeJson: for variableName, value in themeJson.items(): variableName = 'themeSetting_' + variableName if value.lower() == 'false' or value.lower() == 'true': if variableName not in fields: fields[variableName] = 'False' # get the parameters from the theme designer screen themeDesignerParams = {} for variableName, key in fields.items(): if variableName.startswith('themeSetting_'): variableName = variableName.replace('themeSetting_', '') themeDesignerParams[variableName] = key set_theme_from_designer(base_dir, theme_name, domain, themeDesignerParams, allow_local_network_access, system_language) # set boolean values if 'rss-icon-at-top' in themeDesignerParams: if themeDesignerParams['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 themeDesignerParams: if themeDesignerParams['publish-button-at-top'].lower() == 'true': self.server.publish_button_at_top = True else: self.server.publish_button_at_top = False if 'newswire-publish-icon' in themeDesignerParams: if themeDesignerParams['newswire-publish-icon'].lower() == 'true': self.server.show_publish_as_icon = True else: self.server.show_publish_as_icon = False if 'icons-as-buttons' in themeDesignerParams: if themeDesignerParams['icons-as-buttons'].lower() == 'true': self.server.icons_as_buttons = True else: self.server.icons_as_buttons = False if 'full-width-timeline-buttons' in themeDesignerParams: themeValue = themeDesignerParams['full-width-timeline-buttons'] if themeValue.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: originPathStr = \ 'http://' + onion_domain + usersPath + '/' + defaultTimeline elif calling_domain.endswith('.i2p') and i2p_domain: originPathStr = \ 'http://' + i2p_domain + usersPath + '/' + defaultTimeline self._redirect_headers(originPathStr, cookie, calling_domain) self.server.POSTbusy = 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) -> None: """Receive POST from person options screen """ pageNumber = 1 usersPath = path.split('/personoptions')[0] originPathStr = http_prefix + '://' + domain_full + usersPath chooserNickname = get_nickname_from_actor(originPathStr) if not chooserNickname: if calling_domain.endswith('.onion') and onion_domain: originPathStr = 'http://' + onion_domain + usersPath elif (calling_domain.endswith('.i2p') and i2p_domain): originPathStr = 'http://' + i2p_domain + usersPath print('WARN: unable to find nickname in ' + originPathStr) self._redirect_headers(originPathStr, cookie, calling_domain) self.server.POSTbusy = False return length = int(self.headers['Content-length']) try: optionsConfirmParams = self.rfile.read(length).decode('utf-8') except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: POST optionsConfirmParams ' + 'connection reset by peer') else: print('WARN: POST optionsConfirmParams socket error') self.send_response(400) self.end_headers() self.server.POSTbusy = False return except ValueError as ex: print('ERROR: ' + 'POST optionsConfirmParams rfile.read failed, ' + str(ex)) self.send_response(400) self.end_headers() self.server.POSTbusy = False return optionsConfirmParams = \ urllib.parse.unquote_plus(optionsConfirmParams) # page number to return to if 'pageNumber=' in optionsConfirmParams: pageNumberStr = optionsConfirmParams.split('pageNumber=')[1] if '&' in pageNumberStr: pageNumberStr = pageNumberStr.split('&')[0] if pageNumberStr.isdigit(): pageNumber = int(pageNumberStr) # actor for the person optionsActor = optionsConfirmParams.split('actor=')[1] if '&' in optionsActor: optionsActor = optionsActor.split('&')[0] # url of the avatar optionsAvatarUrl = optionsConfirmParams.split('avatarUrl=')[1] if '&' in optionsAvatarUrl: optionsAvatarUrl = optionsAvatarUrl.split('&')[0] # link to a post, which can then be included in reports postUrl = None if 'postUrl' in optionsConfirmParams: postUrl = optionsConfirmParams.split('postUrl=')[1] if '&' in postUrl: postUrl = postUrl.split('&')[0] # petname for this person petname = None if 'optionpetname' in optionsConfirmParams: petname = optionsConfirmParams.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 personNotes = None if 'optionnotes' in optionsConfirmParams: personNotes = optionsConfirmParams.split('optionnotes=')[1] if '&' in personNotes: personNotes = personNotes.split('&')[0] personNotes = urllib.parse.unquote_plus(personNotes.strip()) # Limit the length of the notes if len(personNotes) > 64000: personNotes = None # get the nickname optionsNickname = get_nickname_from_actor(optionsActor) if not optionsNickname: if calling_domain.endswith('.onion') and onion_domain: originPathStr = 'http://' + onion_domain + usersPath elif (calling_domain.endswith('.i2p') and i2p_domain): originPathStr = 'http://' + i2p_domain + usersPath print('WARN: unable to find nickname in ' + optionsActor) self._redirect_headers(originPathStr, cookie, calling_domain) self.server.POSTbusy = False return optionsDomain, optionsPort = get_domain_from_actor(optionsActor) optionsDomainFull = get_full_domain(optionsDomain, optionsPort) if chooserNickname == optionsNickname and \ optionsDomain == domain and \ optionsPort == port: if debug: print('You cannot perform an option action on yourself') # person options screen, view button # See html_person_options if '&submitView=' in optionsConfirmParams: if debug: print('Viewing ' + optionsActor) self._redirect_headers(optionsActor, cookie, calling_domain) self.server.POSTbusy = False return # person options screen, petname submit button # See html_person_options if '&submitPetname=' in optionsConfirmParams and petname: if debug: print('Change petname to ' + petname) handle = optionsNickname + '@' + optionsDomainFull set_pet_name(base_dir, chooserNickname, domain, handle, petname) usersPathStr = \ usersPath + '/' + self.server.defaultTimeline + \ '?page=' + str(pageNumber) self._redirect_headers(usersPathStr, cookie, calling_domain) self.server.POSTbusy = False return # person options screen, person notes submit button # See html_person_options if '&submitPersonNotes=' in optionsConfirmParams: if debug: print('Change person notes') handle = optionsNickname + '@' + optionsDomainFull if not personNotes: personNotes = '' set_person_notes(base_dir, chooserNickname, domain, handle, personNotes) usersPathStr = \ usersPath + '/' + self.server.defaultTimeline + \ '?page=' + str(pageNumber) self._redirect_headers(usersPathStr, cookie, calling_domain) self.server.POSTbusy = False return # person options screen, on calendar checkbox # See html_person_options if '&submitOnCalendar=' in optionsConfirmParams: onCalendar = None if 'onCalendar=' in optionsConfirmParams: onCalendar = optionsConfirmParams.split('onCalendar=')[1] if '&' in onCalendar: onCalendar = onCalendar.split('&')[0] if onCalendar == 'on': add_person_to_calendar(base_dir, chooserNickname, domain, optionsNickname, optionsDomainFull) else: remove_person_from_calendar(base_dir, chooserNickname, domain, optionsNickname, optionsDomainFull) usersPathStr = \ usersPath + '/' + self.server.defaultTimeline + \ '?page=' + str(pageNumber) self._redirect_headers(usersPathStr, cookie, calling_domain) self.server.POSTbusy = False return # person options screen, on notify checkbox # See html_person_options if '&submitNotifyOnPost=' in optionsConfirmParams: notify = None if 'notifyOnPost=' in optionsConfirmParams: notify = optionsConfirmParams.split('notifyOnPost=')[1] if '&' in notify: notify = notify.split('&')[0] if notify == 'on': add_notify_on_post(base_dir, chooserNickname, domain, optionsNickname, optionsDomainFull) else: remove_notify_on_post(base_dir, chooserNickname, domain, optionsNickname, optionsDomainFull) usersPathStr = \ usersPath + '/' + self.server.defaultTimeline + \ '?page=' + str(pageNumber) self._redirect_headers(usersPathStr, cookie, calling_domain) self.server.POSTbusy = False return # person options screen, permission to post to newswire # See html_person_options if '&submitPostToNews=' in optionsConfirmParams: adminNickname = get_config_param(self.server.base_dir, 'admin') if (chooserNickname != optionsNickname and (chooserNickname == adminNickname or (is_moderator(self.server.base_dir, chooserNickname) and not is_moderator(self.server.base_dir, optionsNickname)))): postsToNews = None if 'postsToNews=' in optionsConfirmParams: postsToNews = optionsConfirmParams.split('postsToNews=')[1] if '&' in postsToNews: postsToNews = postsToNews.split('&')[0] accountDir = acct_dir(self.server.base_dir, optionsNickname, optionsDomain) newswireBlockedFilename = accountDir + '/.nonewswire' if postsToNews == 'on': if os.path.isfile(newswireBlockedFilename): try: os.remove(newswireBlockedFilename) except OSError: print('EX: _person_options unable to delete ' + newswireBlockedFilename) refresh_newswire(self.server.base_dir) else: if os.path.isdir(accountDir): nwFilename = newswireBlockedFilename nwWritten = False try: with open(nwFilename, 'w+') as noNewswireFile: noNewswireFile.write('\n') nwWritten = True except OSError as ex: print('EX: unable to write ' + nwFilename + ' ' + str(ex)) if nwWritten: refresh_newswire(self.server.base_dir) usersPathStr = \ usersPath + '/' + self.server.defaultTimeline + \ '?page=' + str(pageNumber) self._redirect_headers(usersPathStr, cookie, calling_domain) self.server.POSTbusy = False return # person options screen, permission to post to featured articles # See html_person_options if '&submitPostToFeatures=' in optionsConfirmParams: adminNickname = get_config_param(self.server.base_dir, 'admin') if (chooserNickname != optionsNickname and (chooserNickname == adminNickname or (is_moderator(self.server.base_dir, chooserNickname) and not is_moderator(self.server.base_dir, optionsNickname)))): postsToFeatures = None if 'postsToFeatures=' in optionsConfirmParams: postsToFeatures = \ optionsConfirmParams.split('postsToFeatures=')[1] if '&' in postsToFeatures: postsToFeatures = postsToFeatures.split('&')[0] accountDir = acct_dir(self.server.base_dir, optionsNickname, optionsDomain) featuresBlockedFilename = accountDir + '/.nofeatures' if postsToFeatures == 'on': if os.path.isfile(featuresBlockedFilename): try: os.remove(featuresBlockedFilename) except OSError: print('EX: _person_options unable to delete ' + featuresBlockedFilename) refresh_newswire(self.server.base_dir) else: if os.path.isdir(accountDir): featFilename = featuresBlockedFilename featWritten = False try: with open(featFilename, 'w+') as noFeaturesFile: noFeaturesFile.write('\n') featWritten = True except OSError as ex: print('EX: unable to write ' + featFilename + ' ' + str(ex)) if featWritten: refresh_newswire(self.server.base_dir) usersPathStr = \ usersPath + '/' + self.server.defaultTimeline + \ '?page=' + str(pageNumber) self._redirect_headers(usersPathStr, cookie, calling_domain) self.server.POSTbusy = False return # person options screen, permission to post to newswire # See html_person_options if '&submitModNewsPosts=' in optionsConfirmParams: adminNickname = get_config_param(self.server.base_dir, 'admin') if (chooserNickname != optionsNickname and (chooserNickname == adminNickname or (is_moderator(self.server.base_dir, chooserNickname) and not is_moderator(self.server.base_dir, optionsNickname)))): modPostsToNews = None if 'modNewsPosts=' in optionsConfirmParams: modPostsToNews = \ optionsConfirmParams.split('modNewsPosts=')[1] if '&' in modPostsToNews: modPostsToNews = modPostsToNews.split('&')[0] accountDir = acct_dir(self.server.base_dir, optionsNickname, optionsDomain) newswireModFilename = accountDir + '/.newswiremoderated' if modPostsToNews != 'on': if os.path.isfile(newswireModFilename): try: os.remove(newswireModFilename) except OSError: print('EX: _person_options unable to delete ' + newswireModFilename) else: if os.path.isdir(accountDir): nwFilename = newswireModFilename try: with open(nwFilename, 'w+') as modNewswireFile: modNewswireFile.write('\n') except OSError: print('EX: unable to write ' + nwFilename) usersPathStr = \ usersPath + '/' + self.server.defaultTimeline + \ '?page=' + str(pageNumber) self._redirect_headers(usersPathStr, cookie, calling_domain) self.server.POSTbusy = False return # person options screen, block button # See html_person_options if '&submitBlock=' in optionsConfirmParams: print('Adding block by ' + chooserNickname + ' of ' + optionsActor) if add_block(base_dir, chooserNickname, domain, optionsNickname, optionsDomainFull): # send block activity self._send_block(http_prefix, chooserNickname, domain_full, optionsNickname, optionsDomainFull) # person options screen, unblock button # See html_person_options if '&submitUnblock=' in optionsConfirmParams: if debug: print('Unblocking ' + optionsActor) msg = \ html_confirm_unblock(self.server.css_cache, self.server.translate, base_dir, usersPath, optionsActor, optionsAvatarUrl).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) self.server.POSTbusy = False return # person options screen, follow button # See html_person_options followStr if '&submitFollow=' in optionsConfirmParams or \ '&submitJoin=' in optionsConfirmParams: if debug: print('Following ' + optionsActor) msg = \ html_confirm_follow(self.server.css_cache, self.server.translate, base_dir, usersPath, optionsActor, optionsAvatarUrl).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) self.server.POSTbusy = False return # person options screen, unfollow button # See html_person_options followStr if '&submitUnfollow=' in optionsConfirmParams or \ '&submitLeave=' in optionsConfirmParams: print('Unfollowing ' + optionsActor) msg = \ html_confirm_unfollow(self.server.css_cache, self.server.translate, base_dir, usersPath, optionsActor, optionsAvatarUrl).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) self.server.POSTbusy = False return # person options screen, DM button # See html_person_options if '&submitDM=' in optionsConfirmParams: if debug: print('Sending DM to ' + optionsActor) reportPath = path.replace('/personoptions', '') + '/newdm' accessKeys = self.server.accessKeys if '/users/' in path: nickname = path.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] if self.server.keyShortcuts.get(nickname): accessKeys = self.server.keyShortcuts[nickname] customSubmitText = get_config_param(base_dir, 'customSubmitText') conversationId = None msg = html_new_post(self.server.css_cache, False, self.server.translate, base_dir, http_prefix, reportPath, None, [optionsActor], None, None, pageNumber, '', chooserNickname, domain, domain_full, self.server.defaultTimeline, self.server.newswire, self.server.theme_name, True, accessKeys, customSubmitText, conversationId, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.session, self.server.cached_webfingers, self.server.person_cache, self.server.port, None, self.server.project_version, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.peertube_instances, self.server.allow_local_network_access, self.server.system_language, self.server.max_like_count, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, self.server.defaultTimeline).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) self.server.POSTbusy = False return # person options screen, Info button # See html_person_options if '&submitPersonInfo=' in optionsConfirmParams: if is_moderator(self.server.base_dir, chooserNickname): if debug: print('Showing info for ' + optionsActor) signing_priv_key_pem = self.server.signing_priv_key_pem msg = \ html_account_info(self.server.css_cache, self.server.translate, base_dir, http_prefix, chooserNickname, domain, self.server.port, optionsActor, self.server.debug, self.server.system_language, signing_priv_key_pem).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) self.server.POSTbusy = False return else: self._404() return # person options screen, snooze button # See html_person_options if '&submitSnooze=' in optionsConfirmParams: usersPath = path.split('/personoptions')[0] thisActor = http_prefix + '://' + domain_full + usersPath if debug: print('Snoozing ' + optionsActor + ' ' + thisActor) if '/users/' in thisActor: nickname = thisActor.split('/users/')[1] person_snooze(base_dir, nickname, domain, optionsActor) if calling_domain.endswith('.onion') and onion_domain: thisActor = 'http://' + onion_domain + usersPath elif (calling_domain.endswith('.i2p') and i2p_domain): thisActor = 'http://' + i2p_domain + usersPath actorPathStr = \ thisActor + '/' + self.server.defaultTimeline + \ '?page=' + str(pageNumber) self._redirect_headers(actorPathStr, cookie, calling_domain) self.server.POSTbusy = False return # person options screen, unsnooze button # See html_person_options if '&submitUnSnooze=' in optionsConfirmParams: usersPath = path.split('/personoptions')[0] thisActor = http_prefix + '://' + domain_full + usersPath if debug: print('Unsnoozing ' + optionsActor + ' ' + thisActor) if '/users/' in thisActor: nickname = thisActor.split('/users/')[1] person_unsnooze(base_dir, nickname, domain, optionsActor) if calling_domain.endswith('.onion') and onion_domain: thisActor = 'http://' + onion_domain + usersPath elif (calling_domain.endswith('.i2p') and i2p_domain): thisActor = 'http://' + i2p_domain + usersPath actorPathStr = \ thisActor + '/' + self.server.defaultTimeline + \ '?page=' + str(pageNumber) self._redirect_headers(actorPathStr, cookie, calling_domain) self.server.POSTbusy = False return # person options screen, report button # See html_person_options if '&submitReport=' in optionsConfirmParams: if debug: print('Reporting ' + optionsActor) reportPath = \ path.replace('/personoptions', '') + '/newreport' accessKeys = self.server.accessKeys if '/users/' in path: nickname = path.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] if self.server.keyShortcuts.get(nickname): accessKeys = self.server.keyShortcuts[nickname] customSubmitText = get_config_param(base_dir, 'customSubmitText') conversationId = None msg = html_new_post(self.server.css_cache, False, self.server.translate, base_dir, http_prefix, reportPath, None, [], None, postUrl, pageNumber, '', chooserNickname, domain, domain_full, self.server.defaultTimeline, self.server.newswire, self.server.theme_name, True, accessKeys, customSubmitText, conversationId, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.session, self.server.cached_webfingers, self.server.person_cache, self.server.port, None, self.server.project_version, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.peertube_instances, self.server.allow_local_network_access, self.server.system_language, self.server.max_like_count, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, self.server.defaultTimeline).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) self.server.POSTbusy = False return # redirect back from person options screen if calling_domain.endswith('.onion') and onion_domain: originPathStr = 'http://' + onion_domain + usersPath elif calling_domain.endswith('.i2p') and i2p_domain: originPathStr = 'http://' + i2p_domain + usersPath self._redirect_headers(originPathStr, cookie, calling_domain) self.server.POSTbusy = False return def _unfollow_confirm(self, calling_domain: str, cookie: str, authorized: bool, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, debug: bool) -> None: """Confirm to unfollow """ usersPath = path.split('/unfollowconfirm')[0] originPathStr = http_prefix + '://' + domain_full + usersPath followerNickname = get_nickname_from_actor(originPathStr) length = int(self.headers['Content-length']) try: followConfirmParams = self.rfile.read(length).decode('utf-8') except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: POST followConfirmParams ' + 'connection was reset') else: print('WARN: POST followConfirmParams socket error') self.send_response(400) self.end_headers() self.server.POSTbusy = False return except ValueError as ex: print('ERROR: POST followConfirmParams rfile.read failed, ' + str(ex)) self.send_response(400) self.end_headers() self.server.POSTbusy = False return if '&submitYes=' in followConfirmParams: followingActor = \ urllib.parse.unquote_plus(followConfirmParams) followingActor = followingActor.split('actor=')[1] if '&' in followingActor: followingActor = followingActor.split('&')[0] followingNickname = get_nickname_from_actor(followingActor) followingDomain, followingPort = \ get_domain_from_actor(followingActor) followingDomainFull = \ get_full_domain(followingDomain, followingPort) if followerNickname == followingNickname and \ followingDomain == domain and \ followingPort == port: if debug: print('You cannot unfollow yourself!') else: if debug: print(followerNickname + ' stops following ' + followingActor) followActor = \ local_actor_url(http_prefix, followerNickname, domain_full) statusNumber, published = get_status_number() followId = followActor + '/statuses/' + str(statusNumber) unfollowJson = { '@context': 'https://www.w3.org/ns/activitystreams', 'id': followId + '/undo', 'type': 'Undo', 'actor': followActor, 'object': { 'id': followId, 'type': 'Follow', 'actor': followActor, 'object': followingActor } } pathUsersSection = path.split('/users/')[1] self.postToNickname = pathUsersSection.split('/')[0] group_account = has_group_type(self.server.base_dir, followingActor, self.server.person_cache) unfollow_account(self.server.base_dir, self.postToNickname, self.server.domain, followingNickname, followingDomainFull, self.server.debug, group_account) self._post_to_outbox_thread(unfollowJson) if calling_domain.endswith('.onion') and onion_domain: originPathStr = 'http://' + onion_domain + usersPath elif (calling_domain.endswith('.i2p') and i2p_domain): originPathStr = 'http://' + i2p_domain + usersPath self._redirect_headers(originPathStr, cookie, calling_domain) self.server.POSTbusy = False def _follow_confirm(self, calling_domain: str, cookie: str, authorized: bool, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, debug: bool) -> None: """Confirm to follow """ usersPath = path.split('/followconfirm')[0] originPathStr = http_prefix + '://' + domain_full + usersPath followerNickname = get_nickname_from_actor(originPathStr) length = int(self.headers['Content-length']) try: followConfirmParams = self.rfile.read(length).decode('utf-8') except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: POST followConfirmParams ' + 'connection was reset') else: print('WARN: POST followConfirmParams socket error') self.send_response(400) self.end_headers() self.server.POSTbusy = False return except ValueError as ex: print('ERROR: POST followConfirmParams rfile.read failed, ' + str(ex)) self.send_response(400) self.end_headers() self.server.POSTbusy = False return if '&submitView=' in followConfirmParams: followingActor = \ urllib.parse.unquote_plus(followConfirmParams) followingActor = followingActor.split('actor=')[1] if '&' in followingActor: followingActor = followingActor.split('&')[0] self._redirect_headers(followingActor, cookie, calling_domain) self.server.POSTbusy = False return if '&submitYes=' in followConfirmParams: followingActor = \ urllib.parse.unquote_plus(followConfirmParams) followingActor = followingActor.split('actor=')[1] if '&' in followingActor: followingActor = followingActor.split('&')[0] followingNickname = get_nickname_from_actor(followingActor) followingDomain, followingPort = \ get_domain_from_actor(followingActor) if followerNickname == followingNickname and \ followingDomain == domain and \ followingPort == port: if debug: print('You cannot follow yourself!') elif (followingNickname == 'news' and followingDomain == domain and followingPort == port): if debug: print('You cannot follow the news actor') else: print('Sending follow request from ' + followerNickname + ' to ' + followingActor) if not self.server.signing_priv_key_pem: print('Sending follow request with no signing key') send_follow_request(self.server.session, base_dir, followerNickname, domain, port, http_prefix, followingNickname, followingDomain, followingActor, followingPort, 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) if calling_domain.endswith('.onion') and onion_domain: originPathStr = 'http://' + onion_domain + usersPath elif (calling_domain.endswith('.i2p') and i2p_domain): originPathStr = 'http://' + i2p_domain + usersPath self._redirect_headers(originPathStr, cookie, calling_domain) self.server.POSTbusy = False def _block_confirm(self, calling_domain: str, cookie: str, authorized: bool, 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 block """ usersPath = path.split('/blockconfirm')[0] originPathStr = http_prefix + '://' + domain_full + usersPath blockerNickname = get_nickname_from_actor(originPathStr) if not blockerNickname: if calling_domain.endswith('.onion') and onion_domain: originPathStr = 'http://' + onion_domain + usersPath elif (calling_domain.endswith('.i2p') and i2p_domain): originPathStr = 'http://' + i2p_domain + usersPath print('WARN: unable to find nickname in ' + originPathStr) self._redirect_headers(originPathStr, cookie, calling_domain) self.server.POSTbusy = False return length = int(self.headers['Content-length']) try: blockConfirmParams = self.rfile.read(length).decode('utf-8') except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: POST blockConfirmParams ' + 'connection was reset') else: print('WARN: POST blockConfirmParams socket error') self.send_response(400) self.end_headers() self.server.POSTbusy = False return except ValueError as ex: print('ERROR: POST blockConfirmParams rfile.read failed, ' + str(ex)) self.send_response(400) self.end_headers() self.server.POSTbusy = False return if '&submitYes=' in blockConfirmParams: blockingActor = \ urllib.parse.unquote_plus(blockConfirmParams) blockingActor = blockingActor.split('actor=')[1] if '&' in blockingActor: blockingActor = blockingActor.split('&')[0] blockingNickname = get_nickname_from_actor(blockingActor) if not blockingNickname: if calling_domain.endswith('.onion') and onion_domain: originPathStr = 'http://' + onion_domain + usersPath elif (calling_domain.endswith('.i2p') and i2p_domain): originPathStr = 'http://' + i2p_domain + usersPath print('WARN: unable to find nickname in ' + blockingActor) self._redirect_headers(originPathStr, cookie, calling_domain) self.server.POSTbusy = False return blockingDomain, blockingPort = \ get_domain_from_actor(blockingActor) blockingDomainFull = get_full_domain(blockingDomain, blockingPort) if blockerNickname == blockingNickname and \ blockingDomain == domain and \ blockingPort == port: if debug: print('You cannot block yourself!') else: print('Adding block by ' + blockerNickname + ' of ' + blockingActor) if add_block(base_dir, blockerNickname, domain, blockingNickname, blockingDomainFull): # send block activity self._send_block(http_prefix, blockerNickname, domain_full, blockingNickname, blockingDomainFull) if calling_domain.endswith('.onion') and onion_domain: originPathStr = 'http://' + onion_domain + usersPath elif (calling_domain.endswith('.i2p') and i2p_domain): originPathStr = 'http://' + i2p_domain + usersPath self._redirect_headers(originPathStr, cookie, calling_domain) self.server.POSTbusy = False def _unblock_confirm(self, calling_domain: str, cookie: str, authorized: bool, 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 """ usersPath = path.split('/unblockconfirm')[0] originPathStr = http_prefix + '://' + domain_full + usersPath blockerNickname = get_nickname_from_actor(originPathStr) if not blockerNickname: if calling_domain.endswith('.onion') and onion_domain: originPathStr = 'http://' + onion_domain + usersPath elif (calling_domain.endswith('.i2p') and i2p_domain): originPathStr = 'http://' + i2p_domain + usersPath print('WARN: unable to find nickname in ' + originPathStr) self._redirect_headers(originPathStr, cookie, calling_domain) self.server.POSTbusy = False return length = int(self.headers['Content-length']) try: blockConfirmParams = self.rfile.read(length).decode('utf-8') except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: POST blockConfirmParams ' + 'connection was reset') else: print('WARN: POST blockConfirmParams socket error') self.send_response(400) self.end_headers() self.server.POSTbusy = False return except ValueError as ex: print('ERROR: POST blockConfirmParams rfile.read failed, ' + str(ex)) self.send_response(400) self.end_headers() self.server.POSTbusy = False return if '&submitYes=' in blockConfirmParams: blockingActor = \ urllib.parse.unquote_plus(blockConfirmParams) blockingActor = blockingActor.split('actor=')[1] if '&' in blockingActor: blockingActor = blockingActor.split('&')[0] blockingNickname = get_nickname_from_actor(blockingActor) if not blockingNickname: if calling_domain.endswith('.onion') and onion_domain: originPathStr = 'http://' + onion_domain + usersPath elif (calling_domain.endswith('.i2p') and i2p_domain): originPathStr = 'http://' + i2p_domain + usersPath print('WARN: unable to find nickname in ' + blockingActor) self._redirect_headers(originPathStr, cookie, calling_domain) self.server.POSTbusy = False return blockingDomain, blockingPort = \ get_domain_from_actor(blockingActor) blockingDomainFull = get_full_domain(blockingDomain, blockingPort) if blockerNickname == blockingNickname and \ blockingDomain == domain and \ blockingPort == port: if debug: print('You cannot unblock yourself!') else: if debug: print(blockerNickname + ' stops blocking ' + blockingActor) remove_block(base_dir, blockerNickname, domain, blockingNickname, blockingDomainFull) if calling_domain.endswith('.onion') and onion_domain: originPathStr = 'http://' + onion_domain + usersPath elif (calling_domain.endswith('.i2p') and i2p_domain): originPathStr = 'http://' + i2p_domain + usersPath self._redirect_headers(originPathStr, cookie, calling_domain) self.server.POSTbusy = 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, searchForEmoji: bool, onion_domain: str, i2p_domain: str, GETstartTime, GETtimings: {}, debug: bool) -> None: """Receive a search query """ # get the page number pageNumber = 1 if '/searchhandle?page=' in path: pageNumberStr = path.split('/searchhandle?page=')[1] if '#' in pageNumberStr: pageNumberStr = pageNumberStr.split('#')[0] if pageNumberStr.isdigit(): pageNumber = int(pageNumberStr) path = path.split('?page=')[0] usersPath = path.replace('/searchhandle', '') actorStr = self._get_instance_url(calling_domain) + usersPath length = int(self.headers['Content-length']) try: searchParams = self.rfile.read(length).decode('utf-8') except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: POST searchParams connection was reset') else: print('WARN: POST searchParams socket error') self.send_response(400) self.end_headers() self.server.POSTbusy = False return except ValueError as ex: print('ERROR: POST searchParams rfile.read failed, ' + str(ex)) self.send_response(400) self.end_headers() self.server.POSTbusy = False return if 'submitBack=' in searchParams: # go back on search screen self._redirect_headers(actorStr + '/' + self.server.defaultTimeline, cookie, calling_domain) self.server.POSTbusy = False return if 'searchtext=' in searchParams: searchStr = searchParams.split('searchtext=')[1] if '&' in searchStr: searchStr = searchStr.split('&')[0] searchStr = \ urllib.parse.unquote_plus(searchStr.strip()) searchStr = searchStr.lower().strip() print('searchStr: ' + searchStr) if searchForEmoji: searchStr = ':' + searchStr + ':' if searchStr.startswith('#'): nickname = get_nickname_from_actor(actorStr) # hashtag search hashtagStr = \ html_hashtag_search(self.server.css_cache, nickname, domain, port, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, base_dir, searchStr[1:], 1, max_posts_in_hashtag_feed, self.server.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) if hashtagStr: msg = hashtagStr.encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) self.server.POSTbusy = False return elif (searchStr.startswith('*') or searchStr.endswith(' skill')): possibleEndings = ( ' skill' ) for possEnding in possibleEndings: if searchStr.endswith(possEnding): searchStr = searchStr.replace(possEnding, '') break # skill search searchStr = searchStr.replace('*', '').strip() skillStr = \ html_skills_search(actorStr, self.server.css_cache, self.server.translate, base_dir, http_prefix, searchStr, self.server.instance_only_skills_search, 64) if skillStr: msg = skillStr.encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) self.server.POSTbusy = False return elif (searchStr.startswith("'") or searchStr.endswith(' history') or searchStr.endswith(' in sent') or searchStr.endswith(' in outbox') or searchStr.endswith(' in outgoing') or searchStr.endswith(' in sent items') or searchStr.endswith(' in sent posts') or searchStr.endswith(' in outgoing posts') or searchStr.endswith(' in my history') or searchStr.endswith(' in my outbox') or searchStr.endswith(' in my posts')): possibleEndings = ( ' 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 possEnding in possibleEndings: if searchStr.endswith(possEnding): searchStr = searchStr.replace(possEnding, '') break # your post history search nickname = get_nickname_from_actor(actorStr) searchStr = searchStr.replace("'", '', 1).strip() historyStr = \ html_history_search(self.server.css_cache, self.server.translate, base_dir, http_prefix, nickname, domain, searchStr, max_posts_in_feed, pageNumber, self.server.project_version, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.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) if historyStr: msg = historyStr.encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) self.server.POSTbusy = False return elif (searchStr.startswith('-') or searchStr.endswith(' in my saved items') or searchStr.endswith(' in my saved posts') or searchStr.endswith(' in my bookmarks') or searchStr.endswith(' in my saved') or searchStr.endswith(' in my saves') or searchStr.endswith(' in saved posts') or searchStr.endswith(' in saved items') or searchStr.endswith(' in bookmarks') or searchStr.endswith(' in saved') or searchStr.endswith(' in saves') or searchStr.endswith(' bookmark')): possibleEndings = ( ' 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 possEnding in possibleEndings: if searchStr.endswith(possEnding): searchStr = searchStr.replace(possEnding, '') break # bookmark search nickname = get_nickname_from_actor(actorStr) searchStr = searchStr.replace('-', '', 1).strip() bookmarksStr = \ html_history_search(self.server.css_cache, self.server.translate, base_dir, http_prefix, nickname, domain, searchStr, max_posts_in_feed, pageNumber, self.server.project_version, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.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) if bookmarksStr: msg = bookmarksStr.encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) self.server.POSTbusy = False return elif ('@' in searchStr or ('://' in searchStr and has_users_path(searchStr))): if searchStr.endswith(':') or \ searchStr.endswith(';') or \ searchStr.endswith('.'): actorStr = \ self._get_instance_url(calling_domain) + usersPath self._redirect_headers(actorStr + '/search', cookie, calling_domain) self.server.POSTbusy = False return # profile search nickname = get_nickname_from_actor(actorStr) if not self._establish_session("handle search"): self.server.POSTbusy = False return profilePathStr = path.replace('/searchhandle', '') # are we already following the searched for handle? if is_following_actor(base_dir, nickname, domain, searchStr): if not has_users_path(searchStr): searchNickname = get_nickname_from_actor(searchStr) searchDomain, searchPort = \ get_domain_from_actor(searchStr) searchDomainFull = \ get_full_domain(searchDomain, searchPort) actor = \ local_actor_url(http_prefix, searchNickname, searchDomainFull) else: actor = searchStr avatarUrl = \ get_avatar_image_url(self.server.session, base_dir, http_prefix, actor, self.server.person_cache, None, True, self.server.signing_priv_key_pem) profilePathStr += \ '?options=' + actor + ';1;' + avatarUrl self._show_person_options(calling_domain, profilePathStr, base_dir, http_prefix, domain, domain_full, GETstartTime, onion_domain, i2p_domain, cookie, debug, authorized) return else: show_published_date_only = \ self.server.show_published_date_only allow_local_network_access = \ self.server.allow_local_network_access accessKeys = self.server.accessKeys if self.server.keyShortcuts.get(nickname): accessKeys = self.server.keyShortcuts[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 profileStr = \ html_profile_after_search(self.server.css_cache, recent_posts_cache, self.server.max_recent_posts, self.server.translate, base_dir, profilePathStr, http_prefix, nickname, domain, port, searchStr, self.server.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.defaultTimeline, peertube_instances, allow_local_network_access, self.server.theme_name, accessKeys, self.server.system_language, self.server.max_like_count, signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled) if profileStr: msg = profileStr.encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) self.server.POSTbusy = False return else: actorStr = \ self._get_instance_url(calling_domain) + usersPath self._redirect_headers(actorStr + '/search', cookie, calling_domain) self.server.POSTbusy = False return elif (searchStr.startswith(':') or searchStr.endswith(' emoji')): # eg. "cat emoji" if searchStr.endswith(' emoji'): searchStr = \ searchStr.replace(' emoji', '') # emoji search emojiStr = \ html_search_emoji(self.server.css_cache, self.server.translate, base_dir, http_prefix, searchStr) if emojiStr: msg = emojiStr.encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) self.server.POSTbusy = False return elif searchStr.startswith('.'): # wanted items search shared_items_federated_domains = \ self.server.shared_items_federated_domains wantedItemsStr = \ html_search_shared_items(self.server.css_cache, self.server.translate, base_dir, searchStr[1:], pageNumber, max_posts_in_feed, http_prefix, domain_full, actorStr, calling_domain, shared_items_federated_domains, 'wanted') if wantedItemsStr: msg = wantedItemsStr.encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) self.server.POSTbusy = False return else: # shared items search shared_items_federated_domains = \ self.server.shared_items_federated_domains sharedItemsStr = \ html_search_shared_items(self.server.css_cache, self.server.translate, base_dir, searchStr, pageNumber, max_posts_in_feed, http_prefix, domain_full, actorStr, calling_domain, shared_items_federated_domains, 'shares') if sharedItemsStr: msg = sharedItemsStr.encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) self.server.POSTbusy = False return actorStr = self._get_instance_url(calling_domain) + usersPath self._redirect_headers(actorStr + '/' + self.server.defaultTimeline, cookie, calling_domain) self.server.POSTbusy = False def _receive_vote(self, calling_domain: str, cookie: str, authorized: bool, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, onion_domain: str, i2p_domain: str, debug: bool) -> None: """Receive a vote via POST """ pageNumber = 1 if '?page=' in path: pageNumberStr = path.split('?page=')[1] if '#' in pageNumberStr: pageNumberStr = pageNumberStr.split('#')[0] if pageNumberStr.isdigit(): pageNumber = int(pageNumberStr) path = path.split('?page=')[0] # the actor who votes usersPath = path.replace('/question', '') actor = http_prefix + '://' + domain_full + usersPath nickname = get_nickname_from_actor(actor) if not nickname: if calling_domain.endswith('.onion') and onion_domain: actor = 'http://' + onion_domain + usersPath elif (calling_domain.endswith('.i2p') and i2p_domain): actor = 'http://' + i2p_domain + usersPath actorPathStr = \ actor + '/' + self.server.defaultTimeline + \ '?page=' + str(pageNumber) self._redirect_headers(actorPathStr, cookie, calling_domain) self.server.POSTbusy = False return # get the parameters length = int(self.headers['Content-length']) try: questionParams = self.rfile.read(length).decode('utf-8') except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: POST questionParams connection was reset') else: print('WARN: POST questionParams socket error') self.send_response(400) self.end_headers() self.server.POSTbusy = False return except ValueError as ex: print('ERROR: POST questionParams rfile.read failed, ' + str(ex)) self.send_response(400) self.end_headers() self.server.POSTbusy = False return questionParams = questionParams.replace('+', ' ') questionParams = questionParams.replace('%3F', '') questionParams = \ urllib.parse.unquote_plus(questionParams.strip()) # post being voted on messageId = None if 'messageId=' in questionParams: messageId = questionParams.split('messageId=')[1] if '&' in messageId: messageId = messageId.split('&')[0] answer = None if 'answer=' in questionParams: answer = questionParams.split('answer=')[1] if '&' in answer: answer = answer.split('&')[0] self._send_reply_to_question(nickname, messageId, answer) if calling_domain.endswith('.onion') and onion_domain: actor = 'http://' + onion_domain + usersPath elif (calling_domain.endswith('.i2p') and i2p_domain): actor = 'http://' + i2p_domain + usersPath actorPathStr = \ actor + '/' + self.server.defaultTimeline + \ '?page=' + str(pageNumber) self._redirect_headers(actorPathStr, cookie, calling_domain) self.server.POSTbusy = False return def _receive_image(self, length: int, calling_domain: str, cookie: str, authorized: bool, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, onion_domain: str, i2p_domain: str, debug: bool) -> None: """Receives an image via POST """ if not self.outboxAuthenticated: if debug: print('DEBUG: unauthenticated attempt to ' + 'post image to outbox') self.send_response(403) self.end_headers() self.server.POSTbusy = False return pathUsersSection = path.split('/users/')[1] if '/' not in pathUsersSection: self._404() self.server.POSTbusy = False return self.postFromNickname = pathUsersSection.split('/')[0] accountsDir = acct_dir(base_dir, self.postFromNickname, domain) if not os.path.isdir(accountsDir): self._404() self.server.POSTbusy = False return try: mediaBytes = self.rfile.read(length) except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: POST mediaBytes ' + 'connection reset by peer') else: print('WARN: POST mediaBytes socket error') self.send_response(400) self.end_headers() self.server.POSTbusy = False return except ValueError as ex: print('ERROR: POST mediaBytes rfile.read failed, ' + str(ex)) self.send_response(400) self.end_headers() self.server.POSTbusy = False return mediaFilenameBase = accountsDir + '/upload' mediaFilename = \ mediaFilenameBase + '.' + \ get_image_extension_from_mime_type(self.headers['Content-type']) try: with open(mediaFilename, 'wb') as avFile: avFile.write(mediaBytes) except OSError: print('EX: unable to write ' + mediaFilename) if debug: print('DEBUG: image saved to ' + mediaFilename) self.send_response(201) self.end_headers() self.server.POSTbusy = False def _remove_share(self, calling_domain: str, cookie: str, authorized: bool, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, onion_domain: str, i2p_domain: str, debug: bool) -> None: """Removes a shared item """ usersPath = path.split('/rmshare')[0] originPathStr = http_prefix + '://' + domain_full + usersPath length = int(self.headers['Content-length']) try: removeShareConfirmParams = \ self.rfile.read(length).decode('utf-8') except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: POST removeShareConfirmParams ' + 'connection was reset') else: print('WARN: POST removeShareConfirmParams socket error') self.send_response(400) self.end_headers() self.server.POSTbusy = False return except ValueError as ex: print('ERROR: POST removeShareConfirmParams rfile.read failed, ' + str(ex)) self.send_response(400) self.end_headers() self.server.POSTbusy = False return if '&submitYes=' in removeShareConfirmParams and authorized: removeShareConfirmParams = \ removeShareConfirmParams.replace('+', ' ').strip() removeShareConfirmParams = \ urllib.parse.unquote_plus(removeShareConfirmParams) shareActor = removeShareConfirmParams.split('actor=')[1] if '&' in shareActor: shareActor = shareActor.split('&')[0] adminNickname = get_config_param(base_dir, 'admin') adminActor = \ local_actor_url(http_prefix, adminNickname, domain_full) actor = originPathStr actorNickname = get_nickname_from_actor(actor) if actor == shareActor or actor == adminActor or \ is_moderator(base_dir, actorNickname): itemID = removeShareConfirmParams.split('itemID=')[1] if '&' in itemID: itemID = itemID.split('&')[0] shareNickname = get_nickname_from_actor(shareActor) if shareNickname: shareDomain, sharePort = get_domain_from_actor(shareActor) remove_shared_item(base_dir, shareNickname, shareDomain, itemID, http_prefix, domain_full, 'shares') if calling_domain.endswith('.onion') and onion_domain: originPathStr = 'http://' + onion_domain + usersPath elif (calling_domain.endswith('.i2p') and i2p_domain): originPathStr = 'http://' + i2p_domain + usersPath self._redirect_headers(originPathStr + '/tlshares', cookie, calling_domain) self.server.POSTbusy = False def _remove_wanted(self, calling_domain: str, cookie: str, authorized: bool, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, onion_domain: str, i2p_domain: str, debug: bool) -> None: """Removes a wanted item """ usersPath = path.split('/rmwanted')[0] originPathStr = http_prefix + '://' + domain_full + usersPath length = int(self.headers['Content-length']) try: removeShareConfirmParams = \ self.rfile.read(length).decode('utf-8') except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: POST removeShareConfirmParams ' + 'connection was reset') else: print('WARN: POST removeShareConfirmParams socket error') self.send_response(400) self.end_headers() self.server.POSTbusy = False return except ValueError as ex: print('ERROR: POST removeShareConfirmParams rfile.read failed, ' + str(ex)) self.send_response(400) self.end_headers() self.server.POSTbusy = False return if '&submitYes=' in removeShareConfirmParams and authorized: removeShareConfirmParams = \ removeShareConfirmParams.replace('+', ' ').strip() removeShareConfirmParams = \ urllib.parse.unquote_plus(removeShareConfirmParams) shareActor = removeShareConfirmParams.split('actor=')[1] if '&' in shareActor: shareActor = shareActor.split('&')[0] adminNickname = get_config_param(base_dir, 'admin') adminActor = \ local_actor_url(http_prefix, adminNickname, domain_full) actor = originPathStr actorNickname = get_nickname_from_actor(actor) if actor == shareActor or actor == adminActor or \ is_moderator(base_dir, actorNickname): itemID = removeShareConfirmParams.split('itemID=')[1] if '&' in itemID: itemID = itemID.split('&')[0] shareNickname = get_nickname_from_actor(shareActor) if shareNickname: shareDomain, sharePort = get_domain_from_actor(shareActor) remove_shared_item(base_dir, shareNickname, shareDomain, itemID, http_prefix, domain_full, 'wanted') if calling_domain.endswith('.onion') and onion_domain: originPathStr = 'http://' + onion_domain + usersPath elif (calling_domain.endswith('.i2p') and i2p_domain): originPathStr = 'http://' + i2p_domain + usersPath self._redirect_headers(originPathStr + '/tlwanted', cookie, calling_domain) self.server.POSTbusy = False def _receive_remove_post(self, calling_domain: str, cookie: str, authorized: bool, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, onion_domain: str, i2p_domain: str, debug: bool) -> None: """Endpoint for removing posts after confirmation """ pageNumber = 1 usersPath = path.split('/rmpost')[0] originPathStr = \ http_prefix + '://' + \ domain_full + usersPath length = int(self.headers['Content-length']) try: removePostConfirmParams = \ self.rfile.read(length).decode('utf-8') except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: POST removePostConfirmParams ' + 'connection was reset') else: print('WARN: POST removePostConfirmParams socket error') self.send_response(400) self.end_headers() self.server.POSTbusy = False return except ValueError as ex: print('ERROR: POST removePostConfirmParams rfile.read failed, ' + str(ex)) self.send_response(400) self.end_headers() self.server.POSTbusy = False return if '&submitYes=' in removePostConfirmParams: removePostConfirmParams = \ urllib.parse.unquote_plus(removePostConfirmParams) removeMessageId = \ removePostConfirmParams.split('messageId=')[1] if '&' in removeMessageId: removeMessageId = removeMessageId.split('&')[0] if 'pageNumber=' in removePostConfirmParams: pageNumberStr = \ removePostConfirmParams.split('pageNumber=')[1] if '&' in pageNumberStr: pageNumberStr = pageNumberStr.split('&')[0] if pageNumberStr.isdigit(): pageNumber = int(pageNumberStr) yearStr = None if 'year=' in removePostConfirmParams: yearStr = removePostConfirmParams.split('year=')[1] if '&' in yearStr: yearStr = yearStr.split('&')[0] monthStr = None if 'month=' in removePostConfirmParams: monthStr = removePostConfirmParams.split('month=')[1] if '&' in monthStr: monthStr = monthStr.split('&')[0] if '/statuses/' in removeMessageId: removePostActor = removeMessageId.split('/statuses/')[0] if originPathStr in removePostActor: toList = ['https://www.w3.org/ns/activitystreams#Public', removePostActor] deleteJson = { "@context": "https://www.w3.org/ns/activitystreams", 'actor': removePostActor, 'object': removeMessageId, 'to': toList, 'cc': [removePostActor + '/followers'], 'type': 'Delete' } self.postToNickname = get_nickname_from_actor(removePostActor) if self.postToNickname: if monthStr and yearStr: if monthStr.isdigit() and yearStr.isdigit(): yearInt = int(yearStr) monthInt = int(monthStr) remove_calendar_event(base_dir, self.postToNickname, domain, yearInt, monthInt, removeMessageId) self._post_to_outbox_thread(deleteJson) if calling_domain.endswith('.onion') and onion_domain: originPathStr = 'http://' + onion_domain + usersPath elif (calling_domain.endswith('.i2p') and i2p_domain): originPathStr = 'http://' + i2p_domain + usersPath if pageNumber == 1: self._redirect_headers(originPathStr + '/outbox', cookie, calling_domain) else: pageNumberStr = str(pageNumber) actorPathStr = originPathStr + '/outbox?page=' + pageNumberStr self._redirect_headers(actorPathStr, cookie, calling_domain) self.server.POSTbusy = False def _links_update(self, calling_domain: str, cookie: str, authorized: bool, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, onion_domain: str, i2p_domain: str, debug: bool, defaultTimeline: str, allow_local_network_access: bool) -> None: """Updates the left links column of the timeline """ usersPath = path.replace('/linksdata', '') usersPath = usersPath.replace('/editlinks', '') actorStr = self._get_instance_url(calling_domain) + usersPath 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(actorStr) 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 ' + actorStr) else: print('WARN: nickname is not a moderator' + actorStr) self._redirect_headers(actorStr, cookie, calling_domain) self.server.POSTbusy = False return 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(actorStr, cookie, calling_domain) self.server.POSTbusy = False return try: # read the bytes of the http form POST postBytes = self.rfile.read(length) except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: connection was reset while ' + 'reading bytes from http form POST') else: print('WARN: error while reading bytes ' + 'from http form POST') self.send_response(400) self.end_headers() self.server.POSTbusy = False return except ValueError as ex: print('ERROR: failed to read bytes for POST, ' + str(ex)) self.send_response(400) self.end_headers() self.server.POSTbusy = False return linksFilename = base_dir + '/accounts/links.txt' aboutFilename = base_dir + '/accounts/about.md' TOSFilename = base_dir + '/accounts/tos.md' # extract all of the text fields into a dict fields = \ extract_text_fields_in_post(postBytes, boundary, debug) if fields.get('editedLinks'): linksStr = fields['editedLinks'] if fields.get('newColLink'): if linksStr: if not linksStr.endswith('\n'): linksStr += '\n' linksStr += fields['newColLink'] + '\n' try: with open(linksFilename, 'w+') as linksFile: linksFile.write(linksStr) except OSError: print('EX: _links_update unable to write ' + linksFilename) else: if fields.get('newColLink'): # the text area is empty but there is a new link added linksStr = fields['newColLink'] + '\n' try: with open(linksFilename, 'w+') as linksFile: linksFile.write(linksStr) except OSError: print('EX: _links_update unable to write ' + linksFilename) else: if os.path.isfile(linksFilename): try: os.remove(linksFilename) except OSError: print('EX: _links_update unable to delete ' + linksFilename) adminNickname = \ get_config_param(base_dir, 'admin') if nickname == adminNickname: if fields.get('editedAbout'): aboutStr = fields['editedAbout'] if not dangerous_markup(aboutStr, allow_local_network_access): try: with open(aboutFilename, 'w+') as aboutFile: aboutFile.write(aboutStr) except OSError: print('EX: unable to write about ' + aboutFilename) else: if os.path.isfile(aboutFilename): try: os.remove(aboutFilename) except OSError: print('EX: _links_update unable to delete ' + aboutFilename) if fields.get('editedTOS'): TOSStr = fields['editedTOS'] if not dangerous_markup(TOSStr, allow_local_network_access): try: with open(TOSFilename, 'w+') as TOSFile: TOSFile.write(TOSStr) except OSError: print('EX: unable to write TOS ' + TOSFilename) else: if os.path.isfile(TOSFilename): try: os.remove(TOSFilename) except OSError: print('EX: _links_update unable to delete ' + TOSFilename) # redirect back to the default timeline self._redirect_headers(actorStr + '/' + defaultTimeline, cookie, calling_domain) self.server.POSTbusy = False def _set_hashtag_category(self, calling_domain: str, cookie: str, authorized: bool, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, onion_domain: str, i2p_domain: str, debug: bool, defaultTimeline: str, allow_local_network_access: bool) -> None: """On the screen after selecting a hashtag from the swarm, this sets the category for that tag """ usersPath = path.replace('/sethashtagcategory', '') hashtag = '' if '/tags/' not in usersPath: # no hashtag is specified within the path self._404() return hashtag = usersPath.split('/tags/')[1].strip() hashtag = urllib.parse.unquote_plus(hashtag) if not hashtag: # no hashtag was given in the path self._404() return hashtagFilename = base_dir + '/tags/' + hashtag + '.txt' if not os.path.isfile(hashtagFilename): # the hashtag does not exist self._404() return usersPath = usersPath.split('/tags/')[0] actorStr = self._get_instance_url(calling_domain) + usersPath tagScreenStr = actorStr + '/tags/' + hashtag 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(actorStr) 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 ' + actorStr) else: print('WARN: nickname is not a moderator' + actorStr) self._redirect_headers(tagScreenStr, cookie, calling_domain) self.server.POSTbusy = False return 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(tagScreenStr, cookie, calling_domain) self.server.POSTbusy = False return try: # read the bytes of the http form POST postBytes = self.rfile.read(length) except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: connection was reset while ' + 'reading bytes from http form POST') else: print('WARN: error while reading bytes ' + 'from http form POST') self.send_response(400) self.end_headers() self.server.POSTbusy = False return except ValueError as ex: print('ERROR: failed to read bytes for POST, ' + str(ex)) self.send_response(400) self.end_headers() self.server.POSTbusy = False return # extract all of the text fields into a dict fields = \ extract_text_fields_in_post(postBytes, boundary, debug) if fields.get('hashtagCategory'): categoryStr = fields['hashtagCategory'].lower() if not is_blocked_hashtag(base_dir, categoryStr) and \ not is_filtered(base_dir, nickname, domain, categoryStr): set_hashtag_category(base_dir, hashtag, categoryStr, False) else: categoryFilename = base_dir + '/tags/' + hashtag + '.category' if os.path.isfile(categoryFilename): try: os.remove(categoryFilename) except OSError: print('EX: _set_hashtag_category unable to delete ' + categoryFilename) # redirect back to the default timeline self._redirect_headers(tagScreenStr, cookie, calling_domain) self.server.POSTbusy = False def _newswire_update(self, calling_domain: str, cookie: str, authorized: bool, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, onion_domain: str, i2p_domain: str, debug: bool, defaultTimeline: str) -> None: """Updates the right newswire column of the timeline """ usersPath = path.replace('/newswiredata', '') usersPath = usersPath.replace('/editnewswire', '') actorStr = self._get_instance_url(calling_domain) + usersPath 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(actorStr) 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 ' + actorStr) else: print('WARN: nickname is not a moderator' + actorStr) self._redirect_headers(actorStr, cookie, calling_domain) self.server.POSTbusy = False return 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(actorStr, cookie, calling_domain) self.server.POSTbusy = False return try: # read the bytes of the http form POST postBytes = self.rfile.read(length) except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: connection was reset while ' + 'reading bytes from http form POST') else: print('WARN: error while reading bytes ' + 'from http form POST') self.send_response(400) self.end_headers() self.server.POSTbusy = False return except ValueError as ex: print('ERROR: failed to read bytes for POST, ' + str(ex)) self.send_response(400) self.end_headers() self.server.POSTbusy = False return newswireFilename = base_dir + '/accounts/newswire.txt' # extract all of the text fields into a dict fields = \ extract_text_fields_in_post(postBytes, boundary, debug) if fields.get('editedNewswire'): newswireStr = fields['editedNewswire'] # append a new newswire entry if fields.get('newNewswireFeed'): if newswireStr: if not newswireStr.endswith('\n'): newswireStr += '\n' newswireStr += fields['newNewswireFeed'] + '\n' try: with open(newswireFilename, 'w+') as newswireFile: newswireFile.write(newswireStr) except OSError: print('EX: unable to write ' + newswireFilename) else: if fields.get('newNewswireFeed'): # the text area is empty but there is a new feed added newswireStr = fields['newNewswireFeed'] + '\n' try: with open(newswireFilename, 'w+') as newswireFile: newswireFile.write(newswireStr) except OSError: print('EX: unable to write ' + newswireFilename) else: # text area has been cleared and there is no new feed if os.path.isfile(newswireFilename): try: os.remove(newswireFilename) except OSError: print('EX: _newswire_update unable to delete ' + newswireFilename) # save filtered words list for the newswire filterNewswireFilename = \ base_dir + '/accounts/' + \ 'news@' + domain + '/filters.txt' if fields.get('filteredWordsNewswire'): try: with open(filterNewswireFilename, 'w+') as filterfile: filterfile.write(fields['filteredWordsNewswire']) except OSError: print('EX: unable to write ' + filterNewswireFilename) else: if os.path.isfile(filterNewswireFilename): try: os.remove(filterNewswireFilename) except OSError: print('EX: _newswire_update unable to delete ' + filterNewswireFilename) # save news tagging rules hashtagRulesFilename = \ base_dir + '/accounts/hashtagrules.txt' if fields.get('hashtagRulesList'): try: with open(hashtagRulesFilename, 'w+') as rulesfile: rulesfile.write(fields['hashtagRulesList']) except OSError: print('EX: unable to write ' + hashtagRulesFilename) else: if os.path.isfile(hashtagRulesFilename): try: os.remove(hashtagRulesFilename) except OSError: print('EX: _newswire_update unable to delete ' + hashtagRulesFilename) newswireTrustedFilename = \ base_dir + '/accounts/newswiretrusted.txt' if fields.get('trustedNewswire'): newswireTrusted = fields['trustedNewswire'] if not newswireTrusted.endswith('\n'): newswireTrusted += '\n' try: with open(newswireTrustedFilename, 'w+') as trustFile: trustFile.write(newswireTrusted) except OSError: print('EX: unable to write ' + newswireTrustedFilename) else: if os.path.isfile(newswireTrustedFilename): try: os.remove(newswireTrustedFilename) except OSError: print('EX: _newswire_update unable to delete ' + newswireTrustedFilename) # redirect back to the default timeline self._redirect_headers(actorStr + '/' + defaultTimeline, cookie, calling_domain) self.server.POSTbusy = False def _citations_update(self, calling_domain: str, cookie: str, authorized: bool, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, onion_domain: str, i2p_domain: str, debug: bool, defaultTimeline: str, newswire: {}) -> None: """Updates the citations for a blog post after hitting update button on the citations screen """ usersPath = path.replace('/citationsdata', '') actorStr = self._get_instance_url(calling_domain) + usersPath nickname = get_nickname_from_actor(actorStr) citationsFilename = \ acct_dir(base_dir, nickname, domain) + '/.citations.txt' # remove any existing citations file if os.path.isfile(citationsFilename): try: os.remove(citationsFilename) except OSError: print('EX: _citations_update unable to delete ' + citationsFilename) 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(actorStr, cookie, calling_domain) self.server.POSTbusy = False return try: # read the bytes of the http form POST postBytes = self.rfile.read(length) except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: connection was reset while ' + 'reading bytes from http form ' + 'citation screen POST') else: print('WARN: error while reading bytes ' + 'from http form citations screen POST') self.send_response(400) self.end_headers() self.server.POSTbusy = False return except ValueError as ex: print('ERROR: failed to read bytes for ' + 'citations screen POST, ' + str(ex)) self.send_response(400) self.end_headers() self.server.POSTbusy = False return # extract all of the text fields into a dict fields = \ extract_text_fields_in_post(postBytes, boundary, debug) print('citationstest: ' + str(fields)) citations = [] for ctr in range(0, 128): fieldName = 'newswire' + str(ctr) if not fields.get(fieldName): continue citations.append(fields[fieldName]) if citations: citationsStr = '' for citationDate in citations: citationsStr += citationDate + '\n' # save citations dates, so that they can be added when # reloading the newblog screen try: with open(citationsFilename, 'w+') as citationsFile: citationsFile.write(citationsStr) except OSError: print('EX: unable to write ' + citationsFilename) # redirect back to the default timeline self._redirect_headers(actorStr + '/newblog', cookie, calling_domain) self.server.POSTbusy = False def _news_post_edit(self, calling_domain: str, cookie: str, authorized: bool, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, onion_domain: str, i2p_domain: str, debug: bool, defaultTimeline: str) -> None: """edits a news post after receiving POST """ usersPath = path.replace('/newseditdata', '') usersPath = usersPath.replace('/editnewspost', '') actorStr = self._get_instance_url(calling_domain) + usersPath 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(actorStr) editorRole = None if nickname: editorRole = is_editor(base_dir, nickname) if not nickname or not editorRole: if not nickname: print('WARN: nickname not found in ' + actorStr) else: print('WARN: nickname is not an editor' + actorStr) if self.server.news_instance: self._redirect_headers(actorStr + '/tlfeatures', cookie, calling_domain) else: self._redirect_headers(actorStr + '/tlnews', cookie, calling_domain) self.server.POSTbusy = False return 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(actorStr + '/tlfeatures', cookie, calling_domain) else: self._redirect_headers(actorStr + '/tlnews', cookie, calling_domain) self.server.POSTbusy = False return try: # read the bytes of the http form POST postBytes = self.rfile.read(length) except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: connection was reset while ' + 'reading bytes from http form POST') else: print('WARN: error while reading bytes ' + 'from http form POST') self.send_response(400) self.end_headers() self.server.POSTbusy = False return except ValueError as ex: print('ERROR: failed to read bytes for POST, ' + str(ex)) self.send_response(400) self.end_headers() self.server.POSTbusy = False return # extract all of the text fields into a dict fields = \ extract_text_fields_in_post(postBytes, boundary, debug) newsPostUrl = None newsPostTitle = None newsPostContent = None if fields.get('newsPostUrl'): newsPostUrl = fields['newsPostUrl'] if fields.get('newsPostTitle'): newsPostTitle = fields['newsPostTitle'] if fields.get('editedNewsPost'): newsPostContent = fields['editedNewsPost'] if newsPostUrl and newsPostContent and newsPostTitle: # load the post post_filename = \ locate_post(base_dir, nickname, domain, newsPostUrl) if post_filename: post_json_object = load_json(post_filename) # update the content and title post_json_object['object']['summary'] = \ newsPostTitle post_json_object['object']['content'] = \ newsPostContent contentMap = post_json_object['object']['contentMap'] contentMap[self.server.system_language] = newsPostContent # update newswire pubDate = post_json_object['object']['published'] publishedDate = \ datetime.datetime.strptime(pubDate, "%Y-%m-%dT%H:%M:%SZ") if self.server.newswire.get(str(publishedDate)): self.server.newswire[publishedDate][0] = \ newsPostTitle self.server.newswire[publishedDate][4] = \ first_paragraph_from_string(newsPostContent) # save newswire newswireStateFilename = \ base_dir + '/accounts/.newswirestate.json' try: save_json(self.server.newswire, newswireStateFilename) except Exception as ex: print('ERROR: saving newswire state, ' + str(ex)) # remove any previous cached news posts newsId = remove_id_ending(post_json_object['object']['id']) newsId = newsId.replace('/', '#') clear_from_post_caches(base_dir, self.server.recent_posts_cache, newsId) # 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(actorStr + '/tlfeatures', cookie, calling_domain) else: self._redirect_headers(actorStr + '/tlnews', cookie, calling_domain) self.server.POSTbusy = False def _profile_edit(self, calling_domain: str, cookie: str, authorized: bool, 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) -> None: """Updates your user profile after editing via the Edit button on the profile screen """ usersPath = path.replace('/profiledata', '') usersPath = usersPath.replace('/editprofile', '') actorStr = self._get_instance_url(calling_domain) + usersPath 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(actorStr) if not nickname: print('WARN: nickname not found in ' + actorStr) self._redirect_headers(actorStr, cookie, calling_domain) self.server.POSTbusy = False return 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(actorStr, cookie, calling_domain) self.server.POSTbusy = False return try: # read the bytes of the http form POST postBytes = self.rfile.read(length) except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: connection was reset while ' + 'reading bytes from http form POST') else: print('WARN: error while reading bytes ' + 'from http form POST') self.send_response(400) self.end_headers() self.server.POSTbusy = False return except ValueError as ex: print('ERROR: failed to read bytes for POST, ' + str(ex)) self.send_response(400) self.end_headers() self.server.POSTbusy = False return adminNickname = get_config_param(self.server.base_dir, 'admin') # get the various avatar, banner and background images actorChanged = True profileMediaTypes = ( 'avatar', 'image', 'banner', 'search_banner', 'instanceLogo', 'left_col_image', 'right_col_image', 'submitImportTheme' ) profileMediaTypesUploaded = {} for mType in profileMediaTypes: # some images can only be changed by the admin if mType == 'instanceLogo': if nickname != adminNickname: print('WARN: only the admin can change ' + 'instance logo') continue if debug: print('DEBUG: profile update extracting ' + mType + ' image, zip or font from POST') mediaBytes, postBytes = \ extract_media_in_form_post(postBytes, boundary, mType) if mediaBytes: if debug: print('DEBUG: profile update ' + mType + ' image, zip or font was found. ' + str(len(mediaBytes)) + ' bytes') else: if debug: print('DEBUG: profile update, no ' + mType + ' image, zip 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 mType == 'instanceLogo': filenameBase = \ base_dir + '/accounts/login.temp' elif mType == 'submitImportTheme': if not os.path.isdir(base_dir + '/imports'): os.mkdir(base_dir + '/imports') filenameBase = \ base_dir + '/imports/newtheme.zip' if os.path.isfile(filenameBase): try: os.remove(filenameBase) except OSError: print('EX: _profile_edit unable to delete ' + filenameBase) else: filenameBase = \ acct_dir(base_dir, nickname, domain) + \ '/' + mType + '.temp' filename, attachmentMediaType = \ save_media_in_form_post(mediaBytes, debug, filenameBase) if filename: print('Profile update POST ' + mType + ' media, zip or font filename is ' + filename) else: print('Profile update, no ' + mType + ' media, zip or font filename in POST') continue if mType == 'submitImportTheme': if nickname == adminNickname 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_imageFilename = filename.replace('.temp', '') if debug: print('DEBUG: POST ' + mType + ' media removing metadata') # remove existing etag if os.path.isfile(post_imageFilename + '.etag'): try: os.remove(post_imageFilename + '.etag') except OSError: print('EX: _profile_edit unable to delete ' + post_imageFilename + '.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_imageFilename, city, content_license_url) if os.path.isfile(post_imageFilename): print('profile update POST ' + mType + ' image, zip or font saved to ' + post_imageFilename) if mType != 'instanceLogo': lastPartOfImageFilename = \ post_imageFilename.split('/')[-1] profileMediaTypesUploaded[mType] = \ lastPartOfImageFilename actorChanged = True else: print('ERROR: profile update POST ' + mType + ' image or font could not be saved to ' + post_imageFilename) postBytesStr = postBytes.decode('utf-8') redirectPath = '' checkNameAndBio = False onFinalWelcomeScreen = False if 'name="previewAvatar"' in postBytesStr: redirectPath = '/welcome_profile' elif 'name="initialWelcomeScreen"' in postBytesStr: redirectPath = '/welcome' elif 'name="finalWelcomeScreen"' in postBytesStr: checkNameAndBio = True redirectPath = '/welcome_final' elif 'name="welcomeCompleteButton"' in postBytesStr: redirectPath = '/' + self.server.defaultTimeline welcome_screen_is_complete(self.server.base_dir, nickname, self.server.domain) onFinalWelcomeScreen = True elif 'name="submitExportTheme"' in postBytesStr: print('submitExportTheme') themeDownloadPath = actorStr if export_theme(self.server.base_dir, self.server.theme_name): themeDownloadPath += \ '/exports/' + self.server.theme_name + '.zip' print('submitExportTheme path=' + themeDownloadPath) self._redirect_headers(themeDownloadPath, cookie, calling_domain) self.server.POSTbusy = False return # extract all of the text fields into a dict fields = \ extract_text_fields_in_post(postBytes, 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 actorFilename = \ acct_dir(base_dir, nickname, domain) + '.json' if os.path.isfile(actorFilename): actor_json = load_json(actorFilename) if actor_json: if not actor_json.get('discoverable'): # discoverable in profile directory # which isn't implemented in Epicyon actor_json['discoverable'] = True actorChanged = True if actor_json.get('capabilityAcquisitionEndpoint'): del actor_json['capabilityAcquisitionEndpoint'] actorChanged = True # update the avatar/image url file extension uploads = profileMediaTypesUploaded.items() for mType, lastPart in uploads: repStr = '/' + lastPart if mType == 'avatar': actorUrl = actor_json['icon']['url'] lastPartOfUrl = actorUrl.split('/')[-1] srchStr = '/' + lastPartOfUrl actorUrl = actorUrl.replace(srchStr, repStr) actor_json['icon']['url'] = actorUrl print('actorUrl: ' + actorUrl) if '.' in actorUrl: imgExt = actorUrl.split('.')[-1] if imgExt == 'jpg': imgExt = 'jpeg' actor_json['icon']['mediaType'] = \ 'image/' + imgExt elif mType == 'image': lastPartOfUrl = \ actor_json['image']['url'].split('/')[-1] srchStr = '/' + lastPartOfUrl actor_json['image']['url'] = \ actor_json['image']['url'].replace(srchStr, repStr) if '.' in actor_json['image']['url']: imgExt = \ actor_json['image']['url'].split('.')[-1] if imgExt == 'jpg': imgExt = 'jpeg' actor_json['image']['mediaType'] = \ 'image/' + imgExt # set skill levels skillCtr = 1 actorSkillsCtr = no_of_actor_skills(actor_json) while skillCtr < 10: skillName = \ fields.get('skillName' + str(skillCtr)) if not skillName: skillCtr += 1 continue if is_filtered(base_dir, nickname, domain, skillName): skillCtr += 1 continue skillValue = \ fields.get('skillValue' + str(skillCtr)) if not skillValue: skillCtr += 1 continue if not actor_has_skill(actor_json, skillName): actorChanged = True else: if actor_skill_value(actor_json, skillName) != \ int(skillValue): actorChanged = True set_actor_skill_level(actor_json, skillName, int(skillValue)) skillsStr = self.server.translate['Skills'] skillsStr = skillsStr.lower() set_hashtag_category(base_dir, skillName, skillsStr, False) skillCtr += 1 if no_of_actor_skills(actor_json) != \ actorSkillsCtr: actorChanged = True # change password if fields.get('password') and \ fields.get('passwordconfirm'): fields['password'] = \ remove_line_endings(fields['password']) fields['passwordconfirm'] = \ remove_line_endings(fields['passwordconfirm']) 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'): cityFilename = \ acct_dir(base_dir, nickname, domain) + '/city.txt' try: with open(cityFilename, 'w+') as fp: fp.write(fields['cityDropdown']) except OSError: print('EX: unable to write city ' + cityFilename) # change displayed name if fields.get('displayNickname'): if fields['displayNickname'] != actor_json['name']: displayName = \ remove_html(fields['displayNickname']) if not is_filtered(base_dir, nickname, domain, displayName): actor_json['name'] = displayName else: actor_json['name'] = nickname if checkNameAndBio: redirectPath = 'previewAvatar' actorChanged = True else: if checkNameAndBio: redirectPath = 'previewAvatar' if nickname == adminNickname or \ is_artist(base_dir, nickname): # change theme if fields.get('themeDropdown'): self.server.theme_name = fields['themeDropdown'] set_theme(base_dir, self.server.theme_name, domain, allow_local_network_access, system_language) self.server.text_mode_banner = \ get_text_mode_banner(self.server.base_dir) self.server.iconsCache = {} self.server.fontsCache = {} self.server.show_publish_as_icon = \ get_config_param(self.server.base_dir, 'show_publish_as_icon') self.server.full_width_tl_button_header = \ get_config_param(self.server.base_dir, 'full_width_tl_button_header') self.server.icons_as_buttons = \ get_config_param(self.server.base_dir, 'icons_as_buttons') self.server.rss_icon_at_top = \ get_config_param(self.server.base_dir, 'rss_icon_at_top') self.server.publish_button_at_top = \ get_config_param(self.server.base_dir, 'publish_button_at_top') set_news_avatar(base_dir, fields['themeDropdown'], http_prefix, domain, domain_full) if nickname == adminNickname: # change media instance status if fields.get('media_instance'): self.server.media_instance = False self.server.defaultTimeline = 'inbox' if fields['media_instance'] == 'on': self.server.media_instance = True self.server.blogs_instance = False self.server.news_instance = False self.server.defaultTimeline = 'tlmedia' set_config_param(base_dir, "media_instance", self.server.media_instance) set_config_param(base_dir, "blogs_instance", self.server.blogs_instance) set_config_param(base_dir, "news_instance", self.server.news_instance) else: if self.server.media_instance: self.server.media_instance = False self.server.defaultTimeline = 'inbox' set_config_param(base_dir, "media_instance", self.server.media_instance) # is this a news theme? if is_news_theme_name(self.server.base_dir, self.server.theme_name): fields['news_instance'] = 'on' # change news instance status if fields.get('news_instance'): self.server.news_instance = False self.server.defaultTimeline = 'inbox' if fields['news_instance'] == 'on': self.server.news_instance = True self.server.blogs_instance = False self.server.media_instance = False self.server.defaultTimeline = 'tlfeatures' set_config_param(base_dir, "media_instance", self.server.media_instance) set_config_param(base_dir, "blogs_instance", self.server.blogs_instance) set_config_param(base_dir, "news_instance", self.server.news_instance) else: if self.server.news_instance: self.server.news_instance = False self.server.defaultTimeline = 'inbox' set_config_param(base_dir, "news_instance", self.server.media_instance) # change blog instance status if fields.get('blogs_instance'): self.server.blogs_instance = False self.server.defaultTimeline = 'inbox' if fields['blogs_instance'] == 'on': self.server.blogs_instance = True self.server.media_instance = False self.server.news_instance = False self.server.defaultTimeline = 'tlblogs' set_config_param(base_dir, "blogs_instance", self.server.blogs_instance) set_config_param(base_dir, "media_instance", self.server.media_instance) set_config_param(base_dir, "news_instance", self.server.news_instance) else: if self.server.blogs_instance: self.server.blogs_instance = False self.server.defaultTimeline = 'inbox' set_config_param(base_dir, "blogs_instance", self.server.blogs_instance) # change instance title if fields.get('instanceTitle'): currInstanceTitle = \ get_config_param(base_dir, 'instanceTitle') if fields['instanceTitle'] != currInstanceTitle: set_config_param(base_dir, 'instanceTitle', fields['instanceTitle']) # change YouTube alternate domain if fields.get('ytdomain'): currYTDomain = self.server.yt_replace_domain if fields['ytdomain'] != currYTDomain: newYTDomain = fields['ytdomain'] if '://' in newYTDomain: newYTDomain = newYTDomain.split('://')[1] if '/' in newYTDomain: newYTDomain = newYTDomain.split('/')[0] if '.' in newYTDomain: set_config_param(base_dir, 'youtubedomain', newYTDomain) self.server.yt_replace_domain = \ newYTDomain else: set_config_param(base_dir, 'youtubedomain', '') self.server.yt_replace_domain = None # change twitter alternate domain if fields.get('twitterdomain'): currTwitterDomain = \ self.server.twitter_replacement_domain if fields['twitterdomain'] != currTwitterDomain: newTwitterDomain = fields['twitterdomain'] if '://' in newTwitterDomain: newTwitterDomain = \ newTwitterDomain.split('://')[1] if '/' in newTwitterDomain: newTwitterDomain = \ newTwitterDomain.split('/')[0] if '.' in newTwitterDomain: set_config_param(base_dir, 'twitterdomain', newTwitterDomain) self.server.twitter_replacement_domain = \ newTwitterDomain else: set_config_param(base_dir, 'twitterdomain', '') self.server.twitter_replacement_domain = None # change custom post submit button text currCustomSubmitText = \ get_config_param(base_dir, 'customSubmitText') if fields.get('customSubmitText'): if fields['customSubmitText'] != \ currCustomSubmitText: customText = fields['customSubmitText'] set_config_param(base_dir, 'customSubmitText', customText) else: if currCustomSubmitText: set_config_param(base_dir, 'customSubmitText', '') # libretranslate URL currLibretranslateUrl = \ get_config_param(base_dir, 'libretranslateUrl') if fields.get('libretranslateUrl'): if fields['libretranslateUrl'] != \ currLibretranslateUrl: ltUrl = fields['libretranslateUrl'] if '://' in ltUrl and \ '.' in ltUrl: set_config_param(base_dir, 'libretranslateUrl', ltUrl) else: if currLibretranslateUrl: set_config_param(base_dir, 'libretranslateUrl', '') # libretranslate API Key currLibretranslateApiKey = \ get_config_param(base_dir, 'libretranslateApiKey') if fields.get('libretranslateApiKey'): if fields['libretranslateApiKey'] != \ currLibretranslateApiKey: ltApiKey = fields['libretranslateApiKey'] set_config_param(base_dir, 'libretranslateApiKey', ltApiKey) else: if currLibretranslateApiKey: set_config_param(base_dir, 'libretranslateApiKey', '') # change instance short description if fields.get('content_license_url'): if fields['content_license_url'] != \ self.server.content_license_url: licenseStr = fields['content_license_url'] set_config_param(base_dir, 'content_license_url', licenseStr) self.server.content_license_url = \ licenseStr else: licenseStr = \ 'https://creativecommons.org/licenses/by/4.0' set_config_param(base_dir, 'content_license_url', licenseStr) self.server.content_license_url = licenseStr # change instance short description currInstanceDescriptionShort = \ get_config_param(base_dir, 'instanceDescriptionShort') if fields.get('instanceDescriptionShort'): if fields['instanceDescriptionShort'] != \ currInstanceDescriptionShort: iDesc = fields['instanceDescriptionShort'] set_config_param(base_dir, 'instanceDescriptionShort', iDesc) else: if currInstanceDescriptionShort: set_config_param(base_dir, 'instanceDescriptionShort', '') # change instance description currInstanceDescription = \ get_config_param(base_dir, 'instanceDescription') if fields.get('instanceDescription'): if fields['instanceDescription'] != \ currInstanceDescription: set_config_param(base_dir, 'instanceDescription', fields['instanceDescription']) else: if currInstanceDescription: set_config_param(base_dir, 'instanceDescription', '') # change email address currentEmailAddress = get_email_address(actor_json) if fields.get('email'): if fields['email'] != currentEmailAddress: set_email_address(actor_json, fields['email']) actorChanged = True else: if currentEmailAddress: set_email_address(actor_json, '') actorChanged = True # change xmpp address currentXmppAddress = get_xmpp_address(actor_json) if fields.get('xmppAddress'): if fields['xmppAddress'] != currentXmppAddress: set_xmpp_address(actor_json, fields['xmppAddress']) actorChanged = True else: if currentXmppAddress: set_xmpp_address(actor_json, '') actorChanged = True # change matrix address currentMatrixAddress = get_matrix_address(actor_json) if fields.get('matrixAddress'): if fields['matrixAddress'] != currentMatrixAddress: set_matrix_address(actor_json, fields['matrixAddress']) actorChanged = True else: if currentMatrixAddress: set_matrix_address(actor_json, '') actorChanged = True # change SSB address currentSSBAddress = get_ssb_address(actor_json) if fields.get('ssbAddress'): if fields['ssbAddress'] != currentSSBAddress: set_ssb_address(actor_json, fields['ssbAddress']) actorChanged = True else: if currentSSBAddress: set_ssb_address(actor_json, '') actorChanged = True # change blog address currentBlogAddress = get_blog_address(actor_json) if fields.get('blogAddress'): if fields['blogAddress'] != currentBlogAddress: set_blog_address(actor_json, fields['blogAddress']) actorChanged = True else: if currentBlogAddress: set_blog_address(actor_json, '') actorChanged = True # change Languages address currentShowLanguages = get_actor_languages(actor_json) if fields.get('showLanguages'): if fields['showLanguages'] != currentShowLanguages: set_actor_languages(base_dir, actor_json, fields['showLanguages']) actorChanged = True else: if currentShowLanguages: set_actor_languages(base_dir, actor_json, '') actorChanged = True # change tox address currentToxAddress = get_tox_address(actor_json) if fields.get('toxAddress'): if fields['toxAddress'] != currentToxAddress: set_tox_address(actor_json, fields['toxAddress']) actorChanged = True else: if currentToxAddress: set_tox_address(actor_json, '') actorChanged = True # change briar address currentBriarAddress = get_briar_address(actor_json) if fields.get('briarAddress'): if fields['briarAddress'] != currentBriarAddress: set_briar_address(actor_json, fields['briarAddress']) actorChanged = True else: if currentBriarAddress: set_briar_address(actor_json, '') actorChanged = True # change jami address currentJamiAddress = get_jami_address(actor_json) if fields.get('jamiAddress'): if fields['jamiAddress'] != currentJamiAddress: set_jami_address(actor_json, fields['jamiAddress']) actorChanged = True else: if currentJamiAddress: set_jami_address(actor_json, '') actorChanged = True # change cwtch address currentCwtchAddress = get_cwtch_address(actor_json) if fields.get('cwtchAddress'): if fields['cwtchAddress'] != currentCwtchAddress: set_cwtch_address(actor_json, fields['cwtchAddress']) actorChanged = True else: if currentCwtchAddress: set_cwtch_address(actor_json, '') actorChanged = True # change Enigma public key currentEnigmaPubKey = get_enigma_pub_key(actor_json) if fields.get('enigmapubkey'): if fields['enigmapubkey'] != currentEnigmaPubKey: set_enigma_pub_key(actor_json, fields['enigmapubkey']) actorChanged = True else: if currentEnigmaPubKey: set_enigma_pub_key(actor_json, '') actorChanged = True # change PGP public key currentPGPpubKey = get_pgp_pub_key(actor_json) if fields.get('pgp'): if fields['pgp'] != currentPGPpubKey: set_pgp_pub_key(actor_json, fields['pgp']) actorChanged = True else: if currentPGPpubKey: set_pgp_pub_key(actor_json, '') actorChanged = True # change PGP fingerprint currentPGPfingerprint = get_pgp_fingerprint(actor_json) if fields.get('openpgp'): if fields['openpgp'] != currentPGPfingerprint: set_pgp_fingerprint(actor_json, fields['openpgp']) actorChanged = True else: if currentPGPfingerprint: set_pgp_fingerprint(actor_json, '') actorChanged = True # change donation link currentDonateUrl = get_donation_url(actor_json) if fields.get('donateUrl'): if fields['donateUrl'] != currentDonateUrl: set_donation_url(actor_json, fields['donateUrl']) actorChanged = True else: if currentDonateUrl: set_donation_url(actor_json, '') actorChanged = True # change website currentWebsite = \ get_website(actor_json, self.server.translate) if fields.get('websiteUrl'): if fields['websiteUrl'] != currentWebsite: set_website(actor_json, fields['websiteUrl'], self.server.translate) actorChanged = True else: if currentWebsite: set_website(actor_json, '', self.server.translate) actorChanged = True # account moved to new address movedTo = '' if actor_json.get('movedTo'): movedTo = actor_json['movedTo'] if fields.get('movedTo'): if fields['movedTo'] != movedTo and \ '://' in fields['movedTo'] and \ '.' in fields['movedTo']: actor_json['movedTo'] = movedTo actorChanged = True else: if movedTo: del actor_json['movedTo'] actorChanged = True # Other accounts (alsoKnownAs) occupationName = get_occupation_name(actor_json) if fields.get('occupationName'): fields['occupationName'] = \ remove_html(fields['occupationName']) if occupationName != \ fields['occupationName']: set_occupation_name(actor_json, fields['occupationName']) actorChanged = True else: if occupationName: set_occupation_name(actor_json, '') actorChanged = True # Other accounts (alsoKnownAs) alsoKnownAs = [] if actor_json.get('alsoKnownAs'): alsoKnownAs = actor_json['alsoKnownAs'] if fields.get('alsoKnownAs'): alsoKnownAsStr = '' alsoKnownAsCtr = 0 for altActor in alsoKnownAs: if alsoKnownAsCtr > 0: alsoKnownAsStr += ', ' alsoKnownAsStr += altActor alsoKnownAsCtr += 1 if fields['alsoKnownAs'] != alsoKnownAsStr and \ '://' in fields['alsoKnownAs'] and \ '@' not in fields['alsoKnownAs'] and \ '.' in fields['alsoKnownAs']: if ';' in fields['alsoKnownAs']: fields['alsoKnownAs'] = \ fields['alsoKnownAs'].replace(';', ',') newAlsoKnownAs = fields['alsoKnownAs'].split(',') alsoKnownAs = [] for altActor in newAlsoKnownAs: altActor = altActor.strip() if '://' in altActor and '.' in altActor: if altActor not in alsoKnownAs: alsoKnownAs.append(altActor) actor_json['alsoKnownAs'] = alsoKnownAs actorChanged = True else: if alsoKnownAs: del actor_json['alsoKnownAs'] actorChanged = True # change user bio if fields.get('bio'): if fields['bio'] != actor_json['summary']: bioStr = remove_html(fields['bio']) if not is_filtered(base_dir, nickname, domain, bioStr): actorTags = {} actor_json['summary'] = \ add_html_tags(base_dir, http_prefix, nickname, domain_full, bioStr, [], actorTags) if actorTags: actor_json['tag'] = [] for tagName, tag in actorTags.items(): actor_json['tag'].append(tag) actorChanged = True else: if checkNameAndBio: redirectPath = 'previewAvatar' else: if checkNameAndBio: redirectPath = 'previewAvatar' adminNickname = \ get_config_param(base_dir, 'admin') if adminNickname: # whether to require jsonld signatures # on all incoming posts if path.startswith('/users/' + adminNickname + '/'): show_node_info_accounts = False if fields.get('show_node_info_accounts'): if fields['show_node_info_accounts'] == 'on': show_node_info_accounts = True self.server.show_node_info_accounts = \ show_node_info_accounts set_config_param(base_dir, "show_node_info_accounts", show_node_info_accounts) show_node_info_version = False if fields.get('show_node_info_version'): if fields['show_node_info_version'] == 'on': show_node_info_version = True self.server.show_node_info_version = \ show_node_info_version set_config_param(base_dir, "show_node_info_version", 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, "verify_all_signatures", verify_all_signatures) broch_mode = False if fields.get('broch_mode'): if fields['broch_mode'] == 'on': broch_mode = True currBrochMode = \ get_config_param(base_dir, "broch_mode") if broch_mode != currBrochMode: set_broch_mode(self.server.base_dir, self.server.domain_full, broch_mode) set_config_param(base_dir, "broch_mode", broch_mode) # shared item federation domains siDomainUpdated = False fed_domains_variable = \ "shared_items_federated_domains" fed_domains_str = \ get_config_param(base_dir, fed_domains_variable) if not fed_domains_str: fed_domains_str = '' sharedItemsFormStr = '' if fields.get('shareDomainList'): sharedItemsList = \ fed_domains_str.split(',') for sharedFederatedDomain in sharedItemsList: sharedItemsFormStr += \ sharedFederatedDomain.strip() + '\n' shareDomainList = fields['shareDomainList'] if shareDomainList != \ sharedItemsFormStr: sharedItemsFormStr2 = \ shareDomainList.replace('\n', ',') sharedItemsField = \ "shared_items_federated_domains" set_config_param(base_dir, sharedItemsField, sharedItemsFormStr2) siDomainUpdated = True else: if fed_domains_str: sharedItemsField = \ "shared_items_federated_domains" set_config_param(base_dir, sharedItemsField, '') siDomainUpdated = True if siDomainUpdated: siDomains = sharedItemsFormStr.split('\n') siTokens = \ self.server.sharedItemFederationTokens self.server.shared_items_federated_domains = \ siDomains domain_full = self.server.domain_full base_dir = \ self.server.base_dir self.server.sharedItemFederationTokens = \ merge_shared_item_tokens(base_dir, domain_full, siDomains, siTokens) # change moderators list if fields.get('moderators'): if path.startswith('/users/' + adminNickname + '/'): moderatorsFile = \ base_dir + \ '/accounts/moderators.txt' clear_moderator_status(base_dir) if ',' in fields['moderators']: # if the list was given as comma separated mods = fields['moderators'].split(',') try: with open(moderatorsFile, 'w+') as modFile: for modNick in mods: modNick = modNick.strip() modDir = base_dir + \ '/accounts/' + modNick + \ '@' + domain if os.path.isdir(modDir): modFile.write(modNick + '\n') except OSError: print('EX: ' + 'unable to write moderators ' + moderatorsFile) for modNick in mods: modNick = modNick.strip() modDir = base_dir + \ '/accounts/' + modNick + \ '@' + domain if os.path.isdir(modDir): set_role(base_dir, modNick, domain, 'moderator') else: # nicknames on separate lines mods = fields['moderators'].split('\n') try: with open(moderatorsFile, 'w+') as modFile: for modNick in mods: modNick = modNick.strip() modDir = \ base_dir + \ '/accounts/' + modNick + \ '@' + domain if os.path.isdir(modDir): modFile.write(modNick + '\n') except OSError: print('EX: ' + 'unable to write moderators 2 ' + moderatorsFile) for modNick in mods: modNick = modNick.strip() modDir = \ base_dir + \ '/accounts/' + \ modNick + '@' + \ domain if os.path.isdir(modDir): set_role(base_dir, modNick, domain, 'moderator') # change site editors list if fields.get('editors'): if path.startswith('/users/' + adminNickname + '/'): editorsFile = \ base_dir + \ '/accounts/editors.txt' clear_editor_status(base_dir) if ',' in fields['editors']: # if the list was given as comma separated eds = fields['editors'].split(',') try: with open(editorsFile, 'w+') as edFile: for edNick in eds: edNick = edNick.strip() edDir = base_dir + \ '/accounts/' + edNick + \ '@' + domain if os.path.isdir(edDir): edFile.write(edNick + '\n') except OSError as ex: print('EX: unable to write editors ' + editorsFile + ' ' + str(ex)) for edNick in eds: edNick = edNick.strip() edDir = base_dir + \ '/accounts/' + edNick + \ '@' + domain if os.path.isdir(edDir): set_role(base_dir, edNick, domain, 'editor') else: # nicknames on separate lines eds = fields['editors'].split('\n') try: with open(editorsFile, 'w+') as edFile: for edNick in eds: edNick = edNick.strip() edDir = \ base_dir + \ '/accounts/' + edNick + \ '@' + domain if os.path.isdir(edDir): edFile.write(edNick + '\n') except OSError as ex: print('EX: unable to write editors ' + editorsFile + ' ' + str(ex)) for edNick in eds: edNick = edNick.strip() edDir = \ base_dir + \ '/accounts/' + \ edNick + '@' + \ domain if os.path.isdir(edDir): set_role(base_dir, edNick, domain, 'editor') # change site counselors list if fields.get('counselors'): if path.startswith('/users/' + adminNickname + '/'): counselorsFile = \ base_dir + \ '/accounts/counselors.txt' clear_counselor_status(base_dir) if ',' in fields['counselors']: # if the list was given as comma separated eds = fields['counselors'].split(',') try: with open(counselorsFile, 'w+') as edFile: for edNick in eds: edNick = edNick.strip() edDir = base_dir + \ '/accounts/' + edNick + \ '@' + domain if os.path.isdir(edDir): edFile.write(edNick + '\n') except OSError as ex: print('EX: ' + 'unable to write counselors ' + counselorsFile + ' ' + str(ex)) for edNick in eds: edNick = edNick.strip() edDir = base_dir + \ '/accounts/' + edNick + \ '@' + domain if os.path.isdir(edDir): set_role(base_dir, edNick, domain, 'counselor') else: # nicknames on separate lines eds = fields['counselors'].split('\n') try: with open(counselorsFile, 'w+') as edFile: for edNick in eds: edNick = edNick.strip() edDir = \ base_dir + \ '/accounts/' + edNick + \ '@' + domain if os.path.isdir(edDir): edFile.write(edNick + '\n') except OSError as ex: print('EX: ' + 'unable to write counselors ' + counselorsFile + ' ' + str(ex)) for edNick in eds: edNick = edNick.strip() edDir = \ base_dir + \ '/accounts/' + \ edNick + '@' + \ domain if os.path.isdir(edDir): set_role(base_dir, edNick, domain, 'counselor') # change site artists list if fields.get('artists'): if path.startswith('/users/' + adminNickname + '/'): artistsFile = \ base_dir + \ '/accounts/artists.txt' clear_artist_status(base_dir) if ',' in fields['artists']: # if the list was given as comma separated eds = fields['artists'].split(',') try: with open(artistsFile, 'w+') as edFile: for edNick in eds: edNick = edNick.strip() edDir = base_dir + \ '/accounts/' + edNick + \ '@' + domain if os.path.isdir(edDir): edFile.write(edNick + '\n') except OSError as ex: print('EX: unable to write artists ' + artistsFile + ' ' + str(ex)) for edNick in eds: edNick = edNick.strip() edDir = base_dir + \ '/accounts/' + edNick + \ '@' + domain if os.path.isdir(edDir): set_role(base_dir, edNick, domain, 'artist') else: # nicknames on separate lines eds = fields['artists'].split('\n') try: with open(artistsFile, 'w+') as edFile: for edNick in eds: edNick = edNick.strip() edDir = \ base_dir + \ '/accounts/' + edNick + \ '@' + domain if os.path.isdir(edDir): edFile.write(edNick + '\n') except OSError as ex: print('EX: unable to write artists ' + artistsFile + ' ' + str(ex)) for edNick in eds: edNick = edNick.strip() edDir = \ base_dir + \ '/accounts/' + \ edNick + '@' + \ domain if os.path.isdir(edDir): set_role(base_dir, edNick, domain, 'artist') # remove scheduled posts if fields.get('remove_scheduled_posts'): if fields['remove_scheduled_posts'] == 'on': remove_scheduled_posts(base_dir, nickname, domain) # approve followers if onFinalWelcomeScreen: # Default setting created via the welcome screen actor_json['manuallyApprovesFollowers'] = True actorChanged = True else: approveFollowers = False if fields.get('approveFollowers'): if fields['approveFollowers'] == 'on': approveFollowers = True if approveFollowers != \ actor_json['manuallyApprovesFollowers']: actor_json['manuallyApprovesFollowers'] = \ approveFollowers actorChanged = True # remove a custom font if fields.get('removeCustomFont'): if (fields['removeCustomFont'] == 'on' and (is_artist(base_dir, nickname) or path.startswith('/users/' + adminNickname + '/'))): fontExt = ('woff', 'woff2', 'otf', 'ttf') for ext in fontExt: 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') currTheme = get_theme(base_dir) if currTheme: self.server.theme_name = currTheme allow_local_network_access = \ self.server.allow_local_network_access set_theme(base_dir, currTheme, domain, allow_local_network_access, system_language) 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, 'show_publish_as_icon') self.server.full_width_tl_button_header = \ get_config_param(base_dir, 'fullWidthTimeline' + 'ButtonHeader') self.server.icons_as_buttons = \ get_config_param(base_dir, 'icons_as_buttons') self.server.rss_icon_at_top = \ get_config_param(base_dir, 'rss_icon_at_top') self.server.publish_button_at_top = \ get_config_param(base_dir, 'publish_button_at_top') # only receive DMs from accounts you follow followDMsFilename = \ acct_dir(base_dir, nickname, domain) + '/.followDMs' if onFinalWelcomeScreen: # initial default setting created via # the welcome screen try: with open(followDMsFilename, 'w+') as fFile: fFile.write('\n') except OSError: print('EX: unable to write follow DMs ' + followDMsFilename) actorChanged = True else: followDMsActive = False if fields.get('followDMs'): if fields['followDMs'] == 'on': followDMsActive = True try: with open(followDMsFilename, 'w+') as fFile: fFile.write('\n') except OSError: print('EX: unable to write follow DMs 2 ' + followDMsFilename) if not followDMsActive: if os.path.isfile(followDMsFilename): try: os.remove(followDMsFilename) except OSError: print('EX: _profile_edit ' + 'unable to delete ' + followDMsFilename) # remove Twitter retweets removeTwitterFilename = \ acct_dir(base_dir, nickname, domain) + \ '/.removeTwitter' removeTwitterActive = False if fields.get('removeTwitter'): if fields['removeTwitter'] == 'on': removeTwitterActive = True try: with open(removeTwitterFilename, 'w+') as rFile: rFile.write('\n') except OSError: print('EX: unable to write remove twitter ' + removeTwitterFilename) if not removeTwitterActive: if os.path.isfile(removeTwitterFilename): try: os.remove(removeTwitterFilename) except OSError: print('EX: _profile_edit ' + 'unable to delete ' + removeTwitterFilename) # hide Like button hideLikeButtonFile = \ acct_dir(base_dir, nickname, domain) + \ '/.hideLikeButton' notifyLikesFilename = \ acct_dir(base_dir, nickname, domain) + \ '/.notifyLikes' hideLikeButtonActive = False if fields.get('hideLikeButton'): if fields['hideLikeButton'] == 'on': hideLikeButtonActive = True try: with open(hideLikeButtonFile, 'w+') as rFile: rFile.write('\n') except OSError: print('EX: unable to write hide like ' + hideLikeButtonFile) # remove notify likes selection if os.path.isfile(notifyLikesFilename): try: os.remove(notifyLikesFilename) except OSError: print('EX: _profile_edit ' + 'unable to delete ' + notifyLikesFilename) if not hideLikeButtonActive: if os.path.isfile(hideLikeButtonFile): try: os.remove(hideLikeButtonFile) except OSError: print('EX: _profile_edit ' + 'unable to delete ' + hideLikeButtonFile) # hide Reaction button hideReactionButtonFile = \ acct_dir(base_dir, nickname, domain) + \ '/.hideReactionButton' notifyReactionsFilename = \ acct_dir(base_dir, nickname, domain) + \ '/.notifyReactions' hideReactionButtonActive = False if fields.get('hideReactionButton'): if fields['hideReactionButton'] == 'on': hideReactionButtonActive = True try: with open(hideReactionButtonFile, 'w+') as rFile: rFile.write('\n') except OSError: print('EX: unable to write hide reaction ' + hideReactionButtonFile) # remove notify Reaction selection if os.path.isfile(notifyReactionsFilename): try: os.remove(notifyReactionsFilename) except OSError: print('EX: _profile_edit ' + 'unable to delete ' + notifyReactionsFilename) if not hideReactionButtonActive: if os.path.isfile(hideReactionButtonFile): try: os.remove(hideReactionButtonFile) except OSError: print('EX: _profile_edit ' + 'unable to delete ' + hideReactionButtonFile) # notify about new Likes if onFinalWelcomeScreen: # default setting from welcome screen try: with open(notifyLikesFilename, 'w+') as rFile: rFile.write('\n') except OSError: print('EX: unable to write notify likes ' + notifyLikesFilename) actorChanged = True else: notifyLikesActive = False if fields.get('notifyLikes'): if fields['notifyLikes'] == 'on' and \ not hideLikeButtonActive: notifyLikesActive = True try: with open(notifyLikesFilename, 'w+') as rFile: rFile.write('\n') except OSError: print('EX: unable to write notify likes ' + notifyLikesFilename) if not notifyLikesActive: if os.path.isfile(notifyLikesFilename): try: os.remove(notifyLikesFilename) except OSError: print('EX: _profile_edit ' + 'unable to delete ' + notifyLikesFilename) notifyReactionsFilename = \ acct_dir(base_dir, nickname, domain) + \ '/.notifyReactions' if onFinalWelcomeScreen: # default setting from welcome screen try: with open(notifyReactionsFilename, 'w+') as rFile: rFile.write('\n') except OSError: print('EX: unable to write notify reactions ' + notifyReactionsFilename) actorChanged = True else: notifyReactionsActive = False if fields.get('notifyReactions'): if fields['notifyReactions'] == 'on' and \ not hideReactionButtonActive: notifyReactionsActive = True try: with open(notifyReactionsFilename, 'w+') as rFile: rFile.write('\n') except OSError: print('EX: unable to write ' + 'notify reactions ' + notifyReactionsFilename) if not notifyReactionsActive: if os.path.isfile(notifyReactionsFilename): try: os.remove(notifyReactionsFilename) except OSError: print('EX: _profile_edit ' + 'unable to delete ' + notifyReactionsFilename) # this account is a bot if fields.get('isBot'): if fields['isBot'] == 'on': if actor_json['type'] != 'Service': actor_json['type'] = 'Service' actorChanged = True else: # this account is a group if fields.get('isGroup'): if fields['isGroup'] == 'on': if actor_json['type'] != 'Group': # only allow admin to create groups if path.startswith('/users/' + adminNickname + '/'): actor_json['type'] = 'Group' actorChanged = True else: # this account is a person (default) if actor_json['type'] != 'Person': actor_json['type'] = 'Person' actorChanged = True # grayscale theme if path.startswith('/users/' + adminNickname + '/') 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) # low bandwidth images checkbox if path.startswith('/users/' + adminNickname + '/') or \ is_artist(base_dir, nickname): currLowBandwidth = \ get_config_param(base_dir, 'low_bandwidth') low_bandwidth = False if fields.get('low_bandwidth'): if fields['low_bandwidth'] == 'on': low_bandwidth = True if currLowBandwidth != low_bandwidth: set_config_param(base_dir, 'low_bandwidth', low_bandwidth) self.server.low_bandwidth = low_bandwidth # save filtered words list filterFilename = \ acct_dir(base_dir, nickname, domain) + \ '/filters.txt' if fields.get('filteredWords'): try: with open(filterFilename, 'w+') as filterfile: filterfile.write(fields['filteredWords']) except OSError: print('EX: unable to write filter ' + filterFilename) else: if os.path.isfile(filterFilename): try: os.remove(filterFilename) except OSError: print('EX: _profile_edit ' + 'unable to delete filter ' + filterFilename) # save filtered words within bio list filterBioFilename = \ acct_dir(base_dir, nickname, domain) + \ '/filters_bio.txt' if fields.get('filteredWordsBio'): try: with open(filterBioFilename, 'w+') as filterfile: filterfile.write(fields['filteredWordsBio']) except OSError: print('EX: unable to write bio filter ' + filterBioFilename) else: if os.path.isfile(filterBioFilename): try: os.remove(filterBioFilename) except OSError: print('EX: _profile_edit ' + 'unable to delete bio filter ' + filterBioFilename) # word replacements switchFilename = \ acct_dir(base_dir, nickname, domain) + \ '/replacewords.txt' if fields.get('switchwords'): try: with open(switchFilename, 'w+') as switchfile: switchfile.write(fields['switchwords']) except OSError: print('EX: unable to write switches ' + switchFilename) else: if os.path.isfile(switchFilename): try: os.remove(switchFilename) except OSError: print('EX: _profile_edit ' + 'unable to delete ' + switchFilename) # autogenerated tags autoTagsFilename = \ acct_dir(base_dir, nickname, domain) + \ '/autotags.txt' if fields.get('autoTags'): try: with open(autoTagsFilename, 'w+') as autoTagsFile: autoTagsFile.write(fields['autoTags']) except OSError: print('EX: unable to write auto tags ' + autoTagsFilename) else: if os.path.isfile(autoTagsFilename): try: os.remove(autoTagsFilename) except OSError: print('EX: _profile_edit ' + 'unable to delete ' + autoTagsFilename) # autogenerated content warnings autoCWFilename = \ acct_dir(base_dir, nickname, domain) + \ '/autocw.txt' if fields.get('autoCW'): try: with open(autoCWFilename, 'w+') as autoCWFile: autoCWFile.write(fields['autoCW']) except OSError: print('EX: unable to write auto CW ' + autoCWFilename) else: if os.path.isfile(autoCWFilename): try: os.remove(autoCWFilename) except OSError: print('EX: _profile_edit ' + 'unable to delete ' + autoCWFilename) # save blocked accounts list blockedFilename = \ acct_dir(base_dir, nickname, domain) + \ '/blocking.txt' if fields.get('blocked'): try: with open(blockedFilename, 'w+') as blockedfile: blockedfile.write(fields['blocked']) except OSError: print('EX: unable to write blocked accounts ' + blockedFilename) else: if os.path.isfile(blockedFilename): try: os.remove(blockedFilename) except OSError: print('EX: _profile_edit ' + 'unable to delete ' + blockedFilename) # Save DM allowed instances list. # The allow list for incoming DMs, # if the .followDMs flag file exists dmAllowedInstancesFilename = \ acct_dir(base_dir, nickname, domain) + \ '/dmAllowedinstances.txt' if fields.get('dmAllowedInstances'): try: with open(dmAllowedInstancesFilename, 'w+') as aFile: aFile.write(fields['dmAllowedInstances']) except OSError: print('EX: unable to write allowed DM instances ' + dmAllowedInstancesFilename) else: if os.path.isfile(dmAllowedInstancesFilename): try: os.remove(dmAllowedInstancesFilename) except OSError: print('EX: _profile_edit ' + 'unable to delete ' + dmAllowedInstancesFilename) # save allowed instances list # This is the account level allow list allowedInstancesFilename = \ acct_dir(base_dir, nickname, domain) + \ '/allowedinstances.txt' if fields.get('allowedInstances'): try: with open(allowedInstancesFilename, 'w+') as aFile: aFile.write(fields['allowedInstances']) except OSError: print('EX: unable to write allowed instances ' + allowedInstancesFilename) else: if os.path.isfile(allowedInstancesFilename): try: os.remove(allowedInstancesFilename) except OSError: print('EX: _profile_edit ' + 'unable to delete ' + allowedInstancesFilename) if is_moderator(self.server.base_dir, nickname): # set selected content warning lists newListsEnabled = '' for name, item in self.server.cw_lists.items(): listVarName = get_cw_list_variable(name) if fields.get(listVarName): if fields[listVarName] == 'on': if newListsEnabled: newListsEnabled += ', ' + name else: newListsEnabled += name if newListsEnabled != self.server.lists_enabled: self.server.lists_enabled = newListsEnabled set_config_param(self.server.base_dir, "lists_enabled", newListsEnabled) # save blocked user agents user_agents_blocked = [] if fields.get('user_agents_blockedStr'): user_agents_blockedStr = \ fields['user_agents_blockedStr'] user_agents_blockedList = \ user_agents_blockedStr.split('\n') for ua in user_agents_blockedList: if ua in user_agents_blocked: continue user_agents_blocked.append(ua.strip()) if str(self.server.user_agents_blocked) != \ str(user_agents_blocked): self.server.user_agents_blocked = \ user_agents_blocked user_agents_blockedStr = '' for ua in user_agents_blocked: if user_agents_blockedStr: user_agents_blockedStr += ',' user_agents_blockedStr += ua set_config_param(base_dir, 'user_agents_blocked', user_agents_blockedStr) # save peertube instances list peertube_instancesFile = \ base_dir + '/accounts/peertube.txt' if fields.get('ptInstances'): self.server.peertube_instances.clear() try: with open(peertube_instancesFile, 'w+') as aFile: aFile.write(fields['ptInstances']) except OSError: print('EX: unable to write peertube ' + peertube_instancesFile) ptInstancesList = \ fields['ptInstances'].split('\n') if ptInstancesList: for url in ptInstancesList: 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_instancesFile): try: os.remove(peertube_instancesFile) except OSError: print('EX: _profile_edit ' + 'unable to delete ' + peertube_instancesFile) self.server.peertube_instances.clear() # save git project names list gitProjectsFilename = \ acct_dir(base_dir, nickname, domain) + \ '/gitprojects.txt' if fields.get('gitProjects'): try: with open(gitProjectsFilename, 'w+') as aFile: aFile.write(fields['gitProjects'].lower()) except OSError: print('EX: unable to write git ' + gitProjectsFilename) else: if os.path.isfile(gitProjectsFilename): try: os.remove(gitProjectsFilename) except OSError: print('EX: _profile_edit ' + 'unable to delete ' + gitProjectsFilename) # save actor json file within accounts if actorChanged: # 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, actorFilename) webfinger_update(base_dir, nickname, domain, onion_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 idStr = actor_json['id'].replace('/', '-') remove_avatar_from_cache(base_dir, idStr) # save the actor to the cache actorCacheFilename = \ base_dir + '/cache/actors/' + \ actor_json['id'].replace('/', '#') + '.json' save_json(actor_json, actorCacheFilename) # send profile update to followers pubNumber, pubDate = get_status_number() updateActorJson = get_actor_update_json(actor_json) print('Sending actor update: ' + str(updateActorJson)) self._post_to_outbox(updateActorJson, self.server.project_version, nickname) # 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.POSTbusy = False return # redirect back to the profile screen self._redirect_headers(actorStr + redirectPath, cookie, calling_domain) self.server.POSTbusy = False def _progressive_web_app_manifest(self, calling_domain: str, GETstartTime) -> None: """gets the PWA manifest """ app1 = "https://f-droid.org/en/packages/eu.siacs.conversations" app2 = "https://staging.f-droid.org/en/packages/im.vector.app" manifest = { "name": "Epicyon", "short_name": "Epicyon", "start_url": "/index.html", "display": "standalone", "background_color": "black", "theme_color": "grey", "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 } ] } msg = json.dumps(manifest, ensure_ascii=False).encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) if self.server.debug: print('Sent manifest: ' + calling_domain) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_progressive_web_app_manifest', self.server.debug) def _browser_config(self, calling_domain: str, GETstartTime) -> None: """Used by MS Windows to put an icon on the desktop if you link to a website """ xmlStr = \ '\n' + \ '\n' + \ ' \n' + \ ' \n' + \ ' \n' + \ ' #eeeeee\n' + \ ' \n' + \ ' \n' + \ '' msg = json.dumps(xmlStr, ensure_ascii=False).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(GETstartTime, self.server.fitness, '_GET', '_browser_config', self.server.debug) def _get_favicon(self, calling_domain: str, base_dir: str, debug: bool, favFilename: str) -> None: """Return the site favicon or default newswire favicon """ favType = 'image/x-icon' if self._has_accept(calling_domain): if 'image/webp' in self.headers['Accept']: favType = 'image/webp' favFilename = favFilename.split('.')[0] + '.webp' if 'image/avif' in self.headers['Accept']: favType = 'image/avif' favFilename = favFilename.split('.')[0] + '.avif' 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 faviconFilename = \ base_dir + '/theme/' + self.server.theme_name + \ '/icons/' + favFilename if not favFilename.endswith('.ico'): if not os.path.isfile(faviconFilename): if favFilename.endswith('.webp'): favFilename = favFilename.replace('.webp', '.ico') elif favFilename.endswith('.avif'): favFilename = favFilename.replace('.avif', '.ico') if not os.path.isfile(faviconFilename): # default favicon faviconFilename = \ base_dir + '/theme/default/icons/' + favFilename if self._etag_exists(faviconFilename): # The file has not changed if debug: print('favicon icon has not changed: ' + calling_domain) self._304() return if self.server.iconsCache.get(favFilename): favBinary = self.server.iconsCache[favFilename] self._set_headers_etag(faviconFilename, favType, favBinary, None, self.server.domain_full, False, None) self._write(favBinary) if debug: print('Sent favicon from cache: ' + calling_domain) return else: if os.path.isfile(faviconFilename): favBinary = None try: with open(faviconFilename, 'rb') as favFile: favBinary = favFile.read() except OSError: print('EX: unable to read favicon ' + faviconFilename) if favBinary: self._set_headers_etag(faviconFilename, favType, favBinary, None, self.server.domain_full, False, None) self._write(favBinary) self.server.iconsCache[favFilename] = favBinary 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, path: str, base_dir: str, domain: str, debug: bool) -> None: """Returns the speaker file used for TTS and accessed via c2s """ nickname = path.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] speakerFilename = \ acct_dir(base_dir, nickname, domain) + '/speaker.json' if not os.path.isfile(speakerFilename): self._404() return speakerJson = load_json(speakerFilename) msg = json.dumps(speakerJson, ensure_ascii=False).encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) def _get_exported_theme(self, calling_domain: str, path: str, base_dir: str, domain_full: str, debug: bool) -> None: """Returns an exported theme zip file """ filename = path.split('/exports/', 1)[1] filename = base_dir + '/exports/' + filename if os.path.isfile(filename): exportBinary = None try: with open(filename, 'rb') as fp: exportBinary = fp.read() except OSError: print('EX: unable to read theme export ' + filename) if exportBinary: exportType = 'application/zip' self._set_headers_etag(filename, exportType, exportBinary, None, domain_full, False, None) self._write(exportBinary) self._404() def _get_fonts(self, calling_domain: str, path: str, base_dir: str, debug: bool, GETstartTime) -> None: """Returns a font """ fontStr = path.split('/fonts/')[1] if fontStr.endswith('.otf') or \ fontStr.endswith('.ttf') or \ fontStr.endswith('.woff') or \ fontStr.endswith('.woff2'): if fontStr.endswith('.otf'): fontType = 'font/otf' elif fontStr.endswith('.ttf'): fontType = 'font/ttf' elif fontStr.endswith('.woff'): fontType = 'font/woff' else: fontType = 'font/woff2' fontFilename = \ base_dir + '/fonts/' + fontStr if self._etag_exists(fontFilename): # The file has not changed self._304() return if self.server.fontsCache.get(fontStr): fontBinary = self.server.fontsCache[fontStr] self._set_headers_etag(fontFilename, fontType, fontBinary, None, self.server.domain_full, False, None) self._write(fontBinary) if debug: print('font sent from cache: ' + path + ' ' + calling_domain) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_get_fonts cache', self.server.debug) return else: if os.path.isfile(fontFilename): fontBinary = None try: with open(fontFilename, 'rb') as fontFile: fontBinary = fontFile.read() except OSError: print('EX: unable to load font ' + fontFilename) if fontBinary: self._set_headers_etag(fontFilename, fontType, fontBinary, None, self.server.domain_full, False, None) self._write(fontBinary) self.server.fontsCache[fontStr] = fontBinary if debug: print('font sent from file: ' + path + ' ' + calling_domain) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_get_fonts', self.server.debug) return if debug: print('font not found: ' + path + ' ' + calling_domain) self._404() def _get_rss2feed(self, authorized: bool, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, port: int, proxy_type: str, GETstartTime, debug: bool) -> 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.'): accountDir = acct_dir(self.server.base_dir, nickname, domain) if os.path.isdir(accountDir): if not self._establish_session("RSS request"): return msg = \ html_blog_page_rss2(authorized, self.server.session, 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(GETstartTime, 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, authorized: bool, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain_full: str, port: int, proxy_type: str, translate: {}, GETstartTime, debug: bool) -> None: """Returns an RSS2 feed for all blogs on this instance """ if not self._establish_session("get_rss2site"): self._404() return msg = '' for subdir, dirs, files 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(authorized, self.server.session, 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(GETstartTime, 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, authorized: bool, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, port: int, proxy_type: str, GETstartTime, debug: bool) -> None: """Returns the newswire feed """ if not self._establish_session("getNewswireFeed"): 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(GETstartTime, 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, authorized: bool, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, port: int, proxy_type: str, GETstartTime, debug: bool) -> None: """Returns the hashtag categories feed """ if not self._establish_session("get_hashtag_categories_feed"): self._404() return hashtagCategories = None msg = \ get_hashtag_categories_feed(base_dir, hashtagCategories) 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(GETstartTime, 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, authorized: bool, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, port: int, proxy_type: str, GETstartTime, debug: bool, system_language: str) -> None: """Returns an RSS3 feed """ nickname = path.split('/blog/')[1] if '/' in nickname: nickname = nickname.split('/')[0] if not nickname.startswith('rss.'): accountDir = acct_dir(base_dir, nickname, domain) if os.path.isdir(accountDir): if not self._establish_session("get_rss3Feed"): self._404() return msg = \ html_blog_page_rss3(authorized, self.server.session, base_dir, http_prefix, self.server.translate, 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(GETstartTime, 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, GETstartTime, onion_domain: str, i2p_domain: str, cookie: str, debug: bool, authorized: bool) -> None: """Show person options screen """ backToPath = '' optionsStr = path.split('?options=')[1] originPathStr = path.split('?options=')[0] if ';' in optionsStr and '/users/news/' not in path: pageNumber = 1 optionsList = optionsStr.split(';') optionsActor = optionsList[0] optionsPageNumber = optionsList[1] optionsProfileUrl = optionsList[2] if '.' in optionsProfileUrl and \ optionsProfileUrl.startswith('/members/'): ext = optionsProfileUrl.split('.')[-1] optionsProfileUrl = optionsProfileUrl.split('/members/')[1] optionsProfileUrl = optionsProfileUrl.replace('.' + ext, '') optionsProfileUrl = \ '/users/' + optionsProfileUrl + '/avatar.' + ext backToPath = 'moderation' if optionsPageNumber.isdigit(): pageNumber = int(optionsPageNumber) optionsLink = None if len(optionsList) > 3: optionsLink = optionsList[3] isGroup = False donateUrl = None websiteUrl = None EnigmaPubKey = None PGPpubKey = None PGPfingerprint = None xmppAddress = None matrixAddress = None blogAddress = None toxAddress = None briarAddress = None jamiAddress = None cwtchAddress = None ssbAddress = None emailAddress = None lockedAccount = False alsoKnownAs = None movedTo = '' actor_json = \ get_person_from_cache(base_dir, optionsActor, self.server.person_cache, True) if actor_json: if actor_json.get('movedTo'): movedTo = actor_json['movedTo'] if '"' in movedTo: movedTo = movedTo.split('"')[1] if actor_json['type'] == 'Group': isGroup = True lockedAccount = get_locked_account(actor_json) donateUrl = get_donation_url(actor_json) websiteUrl = get_website(actor_json, self.server.translate) xmppAddress = get_xmpp_address(actor_json) matrixAddress = get_matrix_address(actor_json) ssbAddress = get_ssb_address(actor_json) blogAddress = get_blog_address(actor_json) toxAddress = get_tox_address(actor_json) briarAddress = get_briar_address(actor_json) jamiAddress = get_jami_address(actor_json) cwtchAddress = get_cwtch_address(actor_json) emailAddress = get_email_address(actor_json) EnigmaPubKey = get_enigma_pub_key(actor_json) PGPpubKey = get_pgp_pub_key(actor_json) PGPfingerprint = get_pgp_fingerprint(actor_json) if actor_json.get('alsoKnownAs'): alsoKnownAs = actor_json['alsoKnownAs'] if self.server.session: check_for_changed_actor(self.server.session, self.server.base_dir, self.server.http_prefix, self.server.domain_full, optionsActor, optionsProfileUrl, self.server.person_cache, 5) accessKeys = self.server.accessKeys if '/users/' in path: nickname = path.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] if self.server.keyShortcuts.get(nickname): accessKeys = self.server.keyShortcuts[nickname] msg = \ html_person_options(self.server.defaultTimeline, self.server.css_cache, self.server.translate, base_dir, domain, domain_full, originPathStr, optionsActor, optionsProfileUrl, optionsLink, pageNumber, donateUrl, websiteUrl, xmppAddress, matrixAddress, ssbAddress, blogAddress, toxAddress, briarAddress, jamiAddress, cwtchAddress, EnigmaPubKey, PGPpubKey, PGPfingerprint, emailAddress, self.server.dormant_months, backToPath, lockedAccount, movedTo, alsoKnownAs, self.server.text_mode_banner, self.server.news_instance, authorized, accessKeys, isGroup).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_person_options', debug) return if '/users/news/' in path: self._redirect_headers(originPathStr + '/tlfeatures', cookie, calling_domain) return if calling_domain.endswith('.onion') and onion_domain: originPathStrAbsolute = \ 'http://' + onion_domain + originPathStr elif calling_domain.endswith('.i2p') and i2p_domain: originPathStrAbsolute = \ 'http://' + i2p_domain + originPathStr else: originPathStrAbsolute = \ http_prefix + '://' + domain_full + originPathStr self._redirect_headers(originPathStrAbsolute, cookie, calling_domain) def _show_media(self, calling_domain: str, path: str, base_dir: str, GETstartTime) -> None: """Returns a media file """ if is_image_file(path) or \ path_is_video(path) or \ path_is_audio(path): mediaStr = path.split('/media/')[1] mediaFilename = base_dir + '/media/' + mediaStr if os.path.isfile(mediaFilename): if self._etag_exists(mediaFilename): # The file has not changed self._304() return mediaFileType = media_file_mime_type(mediaFilename) t = os.path.getmtime(mediaFilename) lastModifiedTime = datetime.datetime.fromtimestamp(t) lastModifiedTimeStr = \ lastModifiedTime.strftime('%a, %d %b %Y %H:%M:%S GMT') mediaBinary = None try: with open(mediaFilename, 'rb') as avFile: mediaBinary = avFile.read() except OSError: print('EX: unable to read media binary ' + mediaFilename) if mediaBinary: self._set_headers_etag(mediaFilename, mediaFileType, mediaBinary, None, None, True, lastModifiedTimeStr) self._write(mediaBinary) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_media', self.server.debug) return self._404() def _get_ontology(self, calling_domain: str, path: str, base_dir: str, GETstartTime) -> None: """Returns an ontology file """ if '.owl' in path or '.rdf' in path or '.json' in path: if '/ontologies/' in path: ontologyStr = path.split('/ontologies/')[1].replace('#', '') else: ontologyStr = path.split('/data/')[1].replace('#', '') ontologyFilename = None ontologyFileType = 'application/rdf+xml' if ontologyStr.startswith('DFC_'): ontologyFilename = base_dir + '/ontology/DFC/' + ontologyStr else: ontologyStr = ontologyStr.replace('/data/', '') ontologyFilename = base_dir + '/ontology/' + ontologyStr if ontologyStr.endswith('.json'): ontologyFileType = 'application/ld+json' if os.path.isfile(ontologyFilename): ontologyFile = None try: with open(ontologyFilename, 'r') as fp: ontologyFile = fp.read() except OSError: print('EX: unable to read ontology ' + ontologyFilename) if ontologyFile: ontologyFile = \ ontologyFile.replace('static.datafoodconsortium.org', calling_domain) if not calling_domain.endswith('.i2p') and \ not calling_domain.endswith('.onion'): ontologyFile = \ ontologyFile.replace('http://' + calling_domain, 'https://' + calling_domain) msg = ontologyFile.encode('utf-8') msglen = len(msg) self._set_headers(ontologyFileType, msglen, None, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_get_ontology', self.server.debug) return self._404() def _show_emoji(self, calling_domain: str, path: str, base_dir: str, GETstartTime) -> None: """Returns an emoji image """ if is_image_file(path): emojiStr = path.split('/emoji/')[1] emojiFilename = base_dir + '/emoji/' + emojiStr if not os.path.isfile(emojiFilename): emojiFilename = base_dir + '/emojicustom/' + emojiStr if os.path.isfile(emojiFilename): if self._etag_exists(emojiFilename): # The file has not changed self._304() return mediaImageType = get_image_mime_type(emojiFilename) mediaBinary = None try: with open(emojiFilename, 'rb') as avFile: mediaBinary = avFile.read() except OSError: print('EX: unable to read emoji image ' + emojiFilename) if mediaBinary: self._set_headers_etag(emojiFilename, mediaImageType, mediaBinary, None, self.server.domain_full, False, None) self._write(mediaBinary) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_emoji', self.server.debug) return self._404() def _show_icon(self, calling_domain: str, path: str, base_dir: str, GETstartTime) -> None: """Shows an icon """ if not path.endswith('.png'): self._404() return mediaStr = path.split('/icons/')[1] if '/' not in mediaStr: if not self.server.theme_name: theme = 'default' else: theme = self.server.theme_name iconFilename = mediaStr else: theme = mediaStr.split('/')[0] iconFilename = mediaStr.split('/')[1] mediaFilename = \ base_dir + '/theme/' + theme + '/icons/' + iconFilename if self._etag_exists(mediaFilename): # The file has not changed self._304() return if self.server.iconsCache.get(mediaStr): mediaBinary = self.server.iconsCache[mediaStr] mimeTypeStr = media_file_mime_type(mediaFilename) self._set_headers_etag(mediaFilename, mimeTypeStr, mediaBinary, None, self.server.domain_full, False, None) self._write(mediaBinary) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_icon', self.server.debug) return else: if os.path.isfile(mediaFilename): mediaBinary = None try: with open(mediaFilename, 'rb') as avFile: mediaBinary = avFile.read() except OSError: print('EX: unable to read icon image ' + mediaFilename) if mediaBinary: mimeType = media_file_mime_type(mediaFilename) self._set_headers_etag(mediaFilename, mimeType, mediaBinary, None, self.server.domain_full, False, None) self._write(mediaBinary) self.server.iconsCache[mediaStr] = mediaBinary fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_icon', self.server.debug) return self._404() def _show_help_screen_image(self, calling_domain: str, path: str, base_dir: str, GETstartTime) -> None: """Shows a help screen image """ if not is_image_file(path): return mediaStr = path.split('/helpimages/')[1] if '/' not in mediaStr: if not self.server.theme_name: theme = 'default' else: theme = self.server.theme_name iconFilename = mediaStr else: theme = mediaStr.split('/')[0] iconFilename = mediaStr.split('/')[1] mediaFilename = \ base_dir + '/theme/' + theme + '/helpimages/' + iconFilename # if there is no theme-specific help image then use the default one if not os.path.isfile(mediaFilename): mediaFilename = \ base_dir + '/theme/default/helpimages/' + iconFilename if self._etag_exists(mediaFilename): # The file has not changed self._304() return if os.path.isfile(mediaFilename): mediaBinary = None try: with open(mediaFilename, 'rb') as avFile: mediaBinary = avFile.read() except OSError: print('EX: unable to read help image ' + mediaFilename) if mediaBinary: mimeType = media_file_mime_type(mediaFilename) self._set_headers_etag(mediaFilename, mimeType, mediaBinary, None, self.server.domain_full, False, None) self._write(mediaBinary) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_help_screen_image', self.server.debug) return self._404() def _show_cached_favicon(self, refererDomain: str, path: str, base_dir: str, GETstartTime) -> None: """Shows a favicon image obtained from the cache """ favFile = path.replace('/favicons/', '') favFilename = base_dir + urllib.parse.unquote_plus(path) print('showCachedFavicon: ' + favFilename) if self.server.favicons_cache.get(favFile): mediaBinary = self.server.favicons_cache[favFile] mimeType = media_file_mime_type(favFilename) self._set_headers_etag(favFilename, mimeType, mediaBinary, None, refererDomain, False, None) self._write(mediaBinary) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_cached_favicon2', self.server.debug) return if not os.path.isfile(favFilename): self._404() return if self._etag_exists(favFilename): # The file has not changed self._304() return mediaBinary = None try: with open(favFilename, 'rb') as avFile: mediaBinary = avFile.read() except OSError: print('EX: unable to read cached favicon ' + favFilename) if mediaBinary: mimeType = media_file_mime_type(favFilename) self._set_headers_etag(favFilename, mimeType, mediaBinary, None, refererDomain, False, None) self._write(mediaBinary) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_cached_favicon', self.server.debug) self.server.favicons_cache[favFile] = mediaBinary return self._404() def _show_cached_avatar(self, refererDomain: str, path: str, base_dir: str, GETstartTime) -> None: """Shows an avatar image obtained from the cache """ mediaFilename = base_dir + '/cache' + path if os.path.isfile(mediaFilename): if self._etag_exists(mediaFilename): # The file has not changed self._304() return mediaBinary = None try: with open(mediaFilename, 'rb') as avFile: mediaBinary = avFile.read() except OSError: print('EX: unable to read cached avatar ' + mediaFilename) if mediaBinary: mimeType = media_file_mime_type(mediaFilename) self._set_headers_etag(mediaFilename, mimeType, mediaBinary, None, refererDomain, False, None) self._write(mediaBinary) fitness_performance(GETstartTime, 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, GETstartTime) -> None: """Return the result of a hashtag search """ pageNumber = 1 if '?page=' in path: pageNumberStr = path.split('?page=')[1] if '#' in pageNumberStr: pageNumberStr = pageNumberStr.split('#')[0] if pageNumberStr.isdigit(): pageNumber = int(pageNumberStr) 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(self.server.css_cache, 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] hashtagStr = \ html_hashtag_search(self.server.css_cache, nickname, domain, port, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, base_dir, hashtag, pageNumber, max_posts_in_hashtag_feed, self.server.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) if hashtagStr: msg = hashtagStr.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) else: originPathStr = path.split('/tags/')[0] originPathStrAbsolute = \ http_prefix + '://' + domain_full + originPathStr if calling_domain.endswith('.onion') and onion_domain: originPathStrAbsolute = \ 'http://' + onion_domain + originPathStr elif (calling_domain.endswith('.i2p') and onion_domain): originPathStrAbsolute = \ 'http://' + i2p_domain + originPathStr self._redirect_headers(originPathStrAbsolute + '/search', cookie, calling_domain) fitness_performance(GETstartTime, 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, GETstartTime) -> 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) hashtagStr = \ 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, self.server.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 hashtagStr: msg = hashtagStr.encode('utf-8') msglen = len(msg) self._set_headers('text/xml', msglen, cookie, calling_domain, False) self._write(msg) else: originPathStr = path.split('/tags/rss2/')[0] originPathStrAbsolute = \ http_prefix + '://' + domain_full + originPathStr if calling_domain.endswith('.onion') and onion_domain: originPathStrAbsolute = \ 'http://' + onion_domain + originPathStr elif (calling_domain.endswith('.i2p') and onion_domain): originPathStrAbsolute = \ 'http://' + i2p_domain + originPathStr self._redirect_headers(originPathStrAbsolute + '/search', cookie, calling_domain) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_hashtag_search_rss2', self.server.debug) def _announce_button(self, calling_domain: str, path: str, base_dir: str, cookie: str, proxy_type: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, repeatPrivate: bool, debug: bool) -> None: """The announce/repeat button was pressed on a post """ pageNumber = 1 repeatUrl = path.split('?repeat=')[1] if '?' in repeatUrl: repeatUrl = repeatUrl.split('?')[0] timelineBookmark = '' if '?bm=' in path: timelineBookmark = path.split('?bm=')[1] if '?' in timelineBookmark: timelineBookmark = timelineBookmark.split('?')[0] timelineBookmark = '#' + timelineBookmark if '?page=' in path: pageNumberStr = path.split('?page=')[1] if '?' in pageNumberStr: pageNumberStr = pageNumberStr.split('?')[0] if '#' in pageNumberStr: pageNumberStr = pageNumberStr.split('#')[0] if pageNumberStr.isdigit(): pageNumber = int(pageNumberStr) timelineStr = 'inbox' if '?tl=' in path: timelineStr = path.split('?tl=')[1] if '?' in timelineStr: timelineStr = timelineStr.split('?')[0] actor = path.split('?repeat=')[0] self.postToNickname = get_nickname_from_actor(actor) if not self.postToNickname: print('WARN: unable to find nickname in ' + actor) actorAbsolute = self._get_instance_url(calling_domain) + actor actorPathStr = \ actorAbsolute + '/' + timelineStr + \ '?page=' + str(pageNumber) self._redirect_headers(actorPathStr, cookie, calling_domain) return if not self._establish_session("announceButton"): self._404() return self.server.actorRepeat = path.split('?actor=')[1] announceToStr = \ local_actor_url(http_prefix, self.postToNickname, domain_full) + \ '/followers' if not repeatPrivate: announceToStr = 'https://www.w3.org/ns/activitystreams#Public' announceJson = \ create_announce(self.server.session, base_dir, self.server.federation_list, self.postToNickname, domain, port, announceToStr, None, http_prefix, repeatUrl, 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) announceFilename = None if announceJson: # 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 announceId = remove_id_ending(announceJson['id']) announceFilename = \ save_post_to_box(base_dir, http_prefix, announceId, self.postToNickname, domain_full, announceJson, '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(announceJson, self.server.project_version, self.postToNickname) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_announce_button postToOutboxThread', self.server.debug) # generate the html for the announce if announceJson and announceFilename: if debug: print('Generating html post for announce') cachedPostFilename = \ get_cached_post_filename(base_dir, self.postToNickname, domain, announceJson) if debug: print('Announced post json: ' + str(announceJson)) print('Announced post nickname: ' + self.postToNickname + ' ' + domain) print('Announced post cache: ' + str(cachedPostFilename)) showIndividualPostIcons = True manuallyApproveFollowers = \ follower_approval_active(base_dir, self.postToNickname, domain) showRepeats = not is_dm(announceJson) individual_post_as_html(self.server.signing_priv_key_pem, False, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, pageNumber, base_dir, self.server.session, self.server.cached_webfingers, self.server.person_cache, self.postToNickname, domain, self.server.port, announceJson, None, True, self.server.allow_deletion, http_prefix, self.server.project_version, timelineStr, 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, showRepeats, showIndividualPostIcons, manuallyApproveFollowers, False, True, False, self.server.cw_lists, self.server.lists_enabled) actorAbsolute = self._get_instance_url(calling_domain) + actor actorPathStr = \ actorAbsolute + '/' + timelineStr + '?page=' + \ str(pageNumber) + timelineBookmark fitness_performance(GETstartTime, self.server.fitness, '_GET', '_announce_button', self.server.debug) self._redirect_headers(actorPathStr, cookie, calling_domain) def _undo_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, GETstartTime, repeatPrivate: bool, debug: bool, recent_posts_cache: {}) -> None: """Undo announce/repeat button was pressed """ pageNumber = 1 # the post which was referenced by the announce post repeatUrl = path.split('?unrepeat=')[1] if '?' in repeatUrl: repeatUrl = repeatUrl.split('?')[0] timelineBookmark = '' if '?bm=' in path: timelineBookmark = path.split('?bm=')[1] if '?' in timelineBookmark: timelineBookmark = timelineBookmark.split('?')[0] timelineBookmark = '#' + timelineBookmark if '?page=' in path: pageNumberStr = path.split('?page=')[1] if '?' in pageNumberStr: pageNumberStr = pageNumberStr.split('?')[0] if '#' in pageNumberStr: pageNumberStr = pageNumberStr.split('#')[0] if pageNumberStr.isdigit(): pageNumber = int(pageNumberStr) timelineStr = 'inbox' if '?tl=' in path: timelineStr = path.split('?tl=')[1] if '?' in timelineStr: timelineStr = timelineStr.split('?')[0] actor = path.split('?unrepeat=')[0] self.postToNickname = get_nickname_from_actor(actor) if not self.postToNickname: print('WARN: unable to find nickname in ' + actor) actorAbsolute = self._get_instance_url(calling_domain) + actor actorPathStr = \ actorAbsolute + '/' + timelineStr + '?page=' + \ str(pageNumber) self._redirect_headers(actorPathStr, cookie, calling_domain) return if not self._establish_session("undoAnnounceButton"): self._404() return undoAnnounceActor = \ http_prefix + '://' + domain_full + \ '/users/' + self.postToNickname unRepeatToStr = 'https://www.w3.org/ns/activitystreams#Public' newUndoAnnounce = { "@context": "https://www.w3.org/ns/activitystreams", 'actor': undoAnnounceActor, 'type': 'Undo', 'cc': [undoAnnounceActor + '/followers'], 'to': [unRepeatToStr], 'object': { 'actor': undoAnnounceActor, 'cc': [undoAnnounceActor + '/followers'], 'object': repeatUrl, 'to': [unRepeatToStr], '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: announceUrl = path.split('?unannounce=')[1] if '?' in announceUrl: announceUrl = announceUrl.split('?')[0] post_filename = None nickname = get_nickname_from_actor(announceUrl) if nickname: if domain_full + '/users/' + nickname + '/' in announceUrl: post_filename = \ locate_post(base_dir, nickname, domain, announceUrl) if post_filename: delete_post(base_dir, http_prefix, nickname, domain, post_filename, debug, recent_posts_cache) self._post_to_outbox(newUndoAnnounce, self.server.project_version, self.postToNickname) actorAbsolute = self._get_instance_url(calling_domain) + actor actorPathStr = \ actorAbsolute + '/' + timelineStr + '?page=' + \ str(pageNumber) + timelineBookmark fitness_performance(GETstartTime, self.server.fitness, '_GET', '_undo_announce_button', self.server.debug) self._redirect_headers(actorPathStr, 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, GETstartTime, proxy_type: str, debug: bool) -> None: """Follow approve button was pressed """ originPathStr = path.split('/followapprove=')[0] followerNickname = originPathStr.replace('/users/', '') followingHandle = path.split('/followapprove=')[1] if '://' in followingHandle: handleNickname = get_nickname_from_actor(followingHandle) handleDomain, handlePort = get_domain_from_actor(followingHandle) followingHandle = \ handleNickname + '@' + \ get_full_domain(handleDomain, handlePort) if '@' in followingHandle: if not self._establish_session("followApproveButton"): self._404() return signing_priv_key_pem = \ self.server.signing_priv_key_pem manual_approve_follow_request_thread(self.server.session, base_dir, http_prefix, followerNickname, domain, port, followingHandle, 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) originPathStrAbsolute = \ http_prefix + '://' + domain_full + originPathStr if calling_domain.endswith('.onion') and onion_domain: originPathStrAbsolute = \ 'http://' + onion_domain + originPathStr elif (calling_domain.endswith('.i2p') and i2p_domain): originPathStrAbsolute = \ 'http://' + i2p_domain + originPathStr fitness_performance(GETstartTime, self.server.fitness, '_GET', '_follow_approve_button', self.server.debug) self._redirect_headers(originPathStrAbsolute, cookie, calling_domain) def _newswire_vote(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, GETstartTime, proxy_type: str, debug: bool, newswire: {}): """Vote for a newswire item """ originPathStr = path.split('/newswirevote=')[0] dateStr = \ path.split('/newswirevote=')[1].replace('T', ' ') dateStr = dateStr.replace(' 00:00', '').replace('+00:00', '') dateStr = urllib.parse.unquote_plus(dateStr) + '+00:00' nickname = urllib.parse.unquote_plus(originPathStr.split('/users/')[1]) if '/' in nickname: nickname = nickname.split('/')[0] print('Newswire item date: ' + dateStr) if newswire.get(dateStr): if is_moderator(base_dir, nickname): newswireItem = newswire[dateStr] print('Voting on newswire item: ' + str(newswireItem)) votesIndex = 2 filenameIndex = 3 if 'vote:' + nickname not in newswireItem[votesIndex]: newswireItem[votesIndex].append('vote:' + nickname) filename = newswireItem[filenameIndex] newswireStateFilename = \ base_dir + '/accounts/.newswirestate.json' try: save_json(newswire, newswireStateFilename) except Exception as ex: print('ERROR: saving newswire state, ' + str(ex)) if filename: save_json(newswireItem[votesIndex], filename + '.votes') else: print('No newswire item with date: ' + dateStr + ' ' + str(newswire)) originPathStrAbsolute = \ http_prefix + '://' + domain_full + originPathStr + '/' + \ self.server.defaultTimeline if calling_domain.endswith('.onion') and onion_domain: originPathStrAbsolute = \ 'http://' + onion_domain + originPathStr elif (calling_domain.endswith('.i2p') and i2p_domain): originPathStrAbsolute = \ 'http://' + i2p_domain + originPathStr fitness_performance(GETstartTime, self.server.fitness, '_GET', '_newswire_vote', self.server.debug) self._redirect_headers(originPathStrAbsolute, cookie, calling_domain) def _newswire_unvote(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, GETstartTime, proxy_type: str, debug: bool, newswire: {}): """Remove vote for a newswire item """ originPathStr = path.split('/newswireunvote=')[0] dateStr = \ path.split('/newswireunvote=')[1].replace('T', ' ') dateStr = dateStr.replace(' 00:00', '').replace('+00:00', '') dateStr = urllib.parse.unquote_plus(dateStr) + '+00:00' nickname = urllib.parse.unquote_plus(originPathStr.split('/users/')[1]) if '/' in nickname: nickname = nickname.split('/')[0] if newswire.get(dateStr): if is_moderator(base_dir, nickname): votesIndex = 2 filenameIndex = 3 newswireItem = newswire[dateStr] if 'vote:' + nickname in newswireItem[votesIndex]: newswireItem[votesIndex].remove('vote:' + nickname) filename = newswireItem[filenameIndex] newswireStateFilename = \ base_dir + '/accounts/.newswirestate.json' try: save_json(newswire, newswireStateFilename) except Exception as ex: print('ERROR: saving newswire state, ' + str(ex)) if filename: save_json(newswireItem[votesIndex], filename + '.votes') else: print('No newswire item with date: ' + dateStr + ' ' + str(newswire)) originPathStrAbsolute = \ http_prefix + '://' + domain_full + originPathStr + '/' + \ self.server.defaultTimeline if calling_domain.endswith('.onion') and onion_domain: originPathStrAbsolute = \ 'http://' + onion_domain + originPathStr elif (calling_domain.endswith('.i2p') and i2p_domain): originPathStrAbsolute = \ 'http://' + i2p_domain + originPathStr self._redirect_headers(originPathStrAbsolute, cookie, calling_domain) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_newswire_unvote', self.server.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, GETstartTime, proxy_type: str, debug: bool) -> None: """Follow deny button was pressed """ originPathStr = path.split('/followdeny=')[0] followerNickname = originPathStr.replace('/users/', '') followingHandle = path.split('/followdeny=')[1] if '://' in followingHandle: handleNickname = get_nickname_from_actor(followingHandle) handleDomain, handlePort = get_domain_from_actor(followingHandle) followingHandle = \ handleNickname + '@' + \ get_full_domain(handleDomain, handlePort) if '@' in followingHandle: manual_deny_follow_request_thread(self.server.session, base_dir, http_prefix, followerNickname, domain, port, followingHandle, 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) originPathStrAbsolute = \ http_prefix + '://' + domain_full + originPathStr if calling_domain.endswith('.onion') and onion_domain: originPathStrAbsolute = \ 'http://' + onion_domain + originPathStr elif calling_domain.endswith('.i2p') and i2p_domain: originPathStrAbsolute = \ 'http://' + i2p_domain + originPathStr self._redirect_headers(originPathStrAbsolute, cookie, calling_domain) fitness_performance(GETstartTime, 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, GETstartTime, proxy_type: str, cookie: str, debug: str) -> None: """Press the like button """ pageNumber = 1 likeUrl = path.split('?like=')[1] if '?' in likeUrl: likeUrl = likeUrl.split('?')[0] timelineBookmark = '' if '?bm=' in path: timelineBookmark = path.split('?bm=')[1] if '?' in timelineBookmark: timelineBookmark = timelineBookmark.split('?')[0] timelineBookmark = '#' + timelineBookmark actor = path.split('?like=')[0] if '?page=' in path: pageNumberStr = path.split('?page=')[1] if '?' in pageNumberStr: pageNumberStr = pageNumberStr.split('?')[0] if '#' in pageNumberStr: pageNumberStr = pageNumberStr.split('#')[0] if pageNumberStr.isdigit(): pageNumber = int(pageNumberStr) timelineStr = 'inbox' if '?tl=' in path: timelineStr = path.split('?tl=')[1] if '?' in timelineStr: timelineStr = timelineStr.split('?')[0] self.postToNickname = get_nickname_from_actor(actor) if not self.postToNickname: print('WARN: unable to find nickname in ' + actor) actorAbsolute = self._get_instance_url(calling_domain) + actor actorPathStr = \ actorAbsolute + '/' + timelineStr + \ '?page=' + str(pageNumber) + timelineBookmark self._redirect_headers(actorPathStr, cookie, calling_domain) return if not self._establish_session("likeButton"): self._404() return likeActor = \ local_actor_url(http_prefix, self.postToNickname, domain_full) actorLiked = path.split('?actor=')[1] if '?' in actorLiked: actorLiked = actorLiked.split('?')[0] # if this is an announce then send the like to the original post origActor, origPostUrl, origFilename = \ get_original_post_from_announce_url(likeUrl, base_dir, self.postToNickname, domain) likeUrl2 = likeUrl likedPostFilename = origFilename if origActor and origPostUrl: actorLiked = origActor likeUrl2 = origPostUrl likedPostFilename = None likeJson = { "@context": "https://www.w3.org/ns/activitystreams", 'type': 'Like', 'actor': likeActor, 'to': [actorLiked], 'object': likeUrl2 } # send out the like to followers self._post_to_outbox(likeJson, self.server.project_version, None) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_like_button postToOutbox', self.server.debug) print('Locating liked post ' + likeUrl) # directly like the post file if not likedPostFilename: likedPostFilename = \ locate_post(base_dir, self.postToNickname, domain, likeUrl) if likedPostFilename: recent_posts_cache = self.server.recent_posts_cache likedPostJson = load_json(likedPostFilename, 0, 1) if origFilename and origPostUrl: update_likes_collection(recent_posts_cache, base_dir, likedPostFilename, likeUrl, likeActor, self.postToNickname, domain, debug, likedPostJson) likeUrl = origPostUrl likedPostFilename = origFilename if debug: print('Updating likes for ' + likedPostFilename) update_likes_collection(recent_posts_cache, base_dir, likedPostFilename, likeUrl, likeActor, self.postToNickname, 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 likedPostJson: cachedPostFilename = \ get_cached_post_filename(base_dir, self.postToNickname, domain, likedPostJson) if debug: print('Liked post json: ' + str(likedPostJson)) print('Liked post nickname: ' + self.postToNickname + ' ' + domain) print('Liked post cache: ' + str(cachedPostFilename)) showIndividualPostIcons = True manuallyApproveFollowers = \ follower_approval_active(base_dir, self.postToNickname, domain) showRepeats = not is_dm(likedPostJson) individual_post_as_html(self.server.signing_priv_key_pem, False, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, pageNumber, base_dir, self.server.session, self.server.cached_webfingers, self.server.person_cache, self.postToNickname, domain, self.server.port, likedPostJson, None, True, self.server.allow_deletion, http_prefix, self.server.project_version, timelineStr, 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, showRepeats, showIndividualPostIcons, manuallyApproveFollowers, False, True, False, self.server.cw_lists, self.server.lists_enabled) else: print('WARN: Liked post not found: ' + likedPostFilename) # 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 ' + likeUrl) actorAbsolute = self._get_instance_url(calling_domain) + actor actorPathStr = \ actorAbsolute + '/' + timelineStr + \ '?page=' + str(pageNumber) + timelineBookmark fitness_performance(GETstartTime, self.server.fitness, '_GET', '_like_button', self.server.debug) self._redirect_headers(actorPathStr, 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, GETstartTime, proxy_type: str, cookie: str, debug: str) -> None: """A button is pressed to undo """ pageNumber = 1 likeUrl = path.split('?unlike=')[1] if '?' in likeUrl: likeUrl = likeUrl.split('?')[0] timelineBookmark = '' if '?bm=' in path: timelineBookmark = path.split('?bm=')[1] if '?' in timelineBookmark: timelineBookmark = timelineBookmark.split('?')[0] timelineBookmark = '#' + timelineBookmark if '?page=' in path: pageNumberStr = path.split('?page=')[1] if '?' in pageNumberStr: pageNumberStr = pageNumberStr.split('?')[0] if '#' in pageNumberStr: pageNumberStr = pageNumberStr.split('#')[0] if pageNumberStr.isdigit(): pageNumber = int(pageNumberStr) timelineStr = 'inbox' if '?tl=' in path: timelineStr = path.split('?tl=')[1] if '?' in timelineStr: timelineStr = timelineStr.split('?')[0] actor = path.split('?unlike=')[0] self.postToNickname = get_nickname_from_actor(actor) if not self.postToNickname: print('WARN: unable to find nickname in ' + actor) actorAbsolute = self._get_instance_url(calling_domain) + actor actorPathStr = \ actorAbsolute + '/' + timelineStr + \ '?page=' + str(pageNumber) self._redirect_headers(actorPathStr, cookie, calling_domain) return if not self._establish_session("undoLikeButton"): self._404() return undoActor = \ local_actor_url(http_prefix, self.postToNickname, domain_full) actorLiked = path.split('?actor=')[1] if '?' in actorLiked: actorLiked = actorLiked.split('?')[0] # if this is an announce then send the like to the original post origActor, origPostUrl, origFilename = \ get_original_post_from_announce_url(likeUrl, base_dir, self.postToNickname, domain) likeUrl2 = likeUrl likedPostFilename = origFilename if origActor and origPostUrl: actorLiked = origActor likeUrl2 = origPostUrl likedPostFilename = None undoLikeJson = { "@context": "https://www.w3.org/ns/activitystreams", 'type': 'Undo', 'actor': undoActor, 'to': [actorLiked], 'object': { 'type': 'Like', 'actor': undoActor, 'to': [actorLiked], 'object': likeUrl2 } } # send out the undo like to followers self._post_to_outbox(undoLikeJson, self.server.project_version, None) # directly undo the like within the post file if not likedPostFilename: likedPostFilename = locate_post(base_dir, self.postToNickname, domain, likeUrl) if likedPostFilename: recent_posts_cache = self.server.recent_posts_cache likedPostJson = load_json(likedPostFilename, 0, 1) if origFilename and origPostUrl: undo_likes_collection_entry(recent_posts_cache, base_dir, likedPostFilename, likeUrl, undoActor, domain, debug, likedPostJson) likeUrl = origPostUrl likedPostFilename = origFilename if debug: print('Removing likes for ' + likedPostFilename) undo_likes_collection_entry(recent_posts_cache, base_dir, likedPostFilename, likeUrl, undoActor, domain, debug, None) if debug: print('Regenerating html post for changed likes collection') if likedPostJson: showIndividualPostIcons = True manuallyApproveFollowers = \ follower_approval_active(base_dir, self.postToNickname, domain) showRepeats = not is_dm(likedPostJson) individual_post_as_html(self.server.signing_priv_key_pem, False, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, pageNumber, base_dir, self.server.session, self.server.cached_webfingers, self.server.person_cache, self.postToNickname, domain, self.server.port, likedPostJson, None, True, self.server.allow_deletion, http_prefix, self.server.project_version, timelineStr, 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, showRepeats, showIndividualPostIcons, manuallyApproveFollowers, False, True, False, self.server.cw_lists, self.server.lists_enabled) else: print('WARN: Unliked post not found: ' + likedPostFilename) # 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'] actorAbsolute = self._get_instance_url(calling_domain) + actor actorPathStr = \ actorAbsolute + '/' + timelineStr + \ '?page=' + str(pageNumber) + timelineBookmark fitness_performance(GETstartTime, self.server.fitness, '_GET', '_undo_like_button', self.server.debug) self._redirect_headers(actorPathStr, 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, GETstartTime, proxy_type: str, cookie: str, debug: str) -> None: """Press an emoji reaction button Note that this is not the emoji reaction selection icon at the bottom of the post """ pageNumber = 1 reactionUrl = path.split('?react=')[1] if '?' in reactionUrl: reactionUrl = reactionUrl.split('?')[0] timelineBookmark = '' if '?bm=' in path: timelineBookmark = path.split('?bm=')[1] if '?' in timelineBookmark: timelineBookmark = timelineBookmark.split('?')[0] timelineBookmark = '#' + timelineBookmark actor = path.split('?react=')[0] if '?page=' in path: pageNumberStr = path.split('?page=')[1] if '?' in pageNumberStr: pageNumberStr = pageNumberStr.split('?')[0] if '#' in pageNumberStr: pageNumberStr = pageNumberStr.split('#')[0] if pageNumberStr.isdigit(): pageNumber = int(pageNumberStr) timelineStr = 'inbox' if '?tl=' in path: timelineStr = path.split('?tl=')[1] if '?' in timelineStr: timelineStr = timelineStr.split('?')[0] emojiContentEncoded = None if '?emojreact=' in path: emojiContentEncoded = path.split('?emojreact=')[1] if '?' in emojiContentEncoded: emojiContentEncoded = emojiContentEncoded.split('?')[0] if not emojiContentEncoded: print('WARN: no emoji reaction ' + actor) actorAbsolute = self._get_instance_url(calling_domain) + actor actorPathStr = \ actorAbsolute + '/' + timelineStr + \ '?page=' + str(pageNumber) + timelineBookmark self._redirect_headers(actorPathStr, cookie, calling_domain) return emojiContent = urllib.parse.unquote_plus(emojiContentEncoded) self.postToNickname = get_nickname_from_actor(actor) if not self.postToNickname: print('WARN: unable to find nickname in ' + actor) actorAbsolute = self._get_instance_url(calling_domain) + actor actorPathStr = \ actorAbsolute + '/' + timelineStr + \ '?page=' + str(pageNumber) + timelineBookmark self._redirect_headers(actorPathStr, cookie, calling_domain) return if not self._establish_session("reactionButton"): self._404() return reactionActor = \ local_actor_url(http_prefix, self.postToNickname, domain_full) actorReaction = path.split('?actor=')[1] if '?' in actorReaction: actorReaction = actorReaction.split('?')[0] # if this is an announce then send the emoji reaction # to the original post origActor, origPostUrl, origFilename = \ get_original_post_from_announce_url(reactionUrl, base_dir, self.postToNickname, domain) reactionUrl2 = reactionUrl reaction_postFilename = origFilename if origActor and origPostUrl: actorReaction = origActor reactionUrl2 = origPostUrl reaction_postFilename = None reactionJson = { "@context": "https://www.w3.org/ns/activitystreams", 'type': 'EmojiReact', 'actor': reactionActor, 'to': [actorReaction], 'object': reactionUrl2, 'content': emojiContent } # send out the emoji reaction to followers self._post_to_outbox(reactionJson, self.server.project_version, None) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_reaction_button postToOutbox', self.server.debug) print('Locating emoji reaction post ' + reactionUrl) # directly emoji reaction the post file if not reaction_postFilename: reaction_postFilename = \ locate_post(base_dir, self.postToNickname, domain, reactionUrl) if reaction_postFilename: recent_posts_cache = self.server.recent_posts_cache reaction_post_json = load_json(reaction_postFilename, 0, 1) if origFilename and origPostUrl: update_reaction_collection(recent_posts_cache, base_dir, reaction_postFilename, reactionUrl, reactionActor, self.postToNickname, domain, debug, reaction_post_json, emojiContent) reactionUrl = origPostUrl reaction_postFilename = origFilename if debug: print('Updating emoji reaction for ' + reaction_postFilename) update_reaction_collection(recent_posts_cache, base_dir, reaction_postFilename, reactionUrl, reactionActor, self.postToNickname, domain, debug, None, emojiContent) 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: cachedPostFilename = \ get_cached_post_filename(base_dir, self.postToNickname, domain, reaction_post_json) if debug: print('Reaction post json: ' + str(reaction_post_json)) print('Reaction post nickname: ' + self.postToNickname + ' ' + domain) print('Reaction post cache: ' + str(cachedPostFilename)) showIndividualPostIcons = True manuallyApproveFollowers = \ follower_approval_active(base_dir, self.postToNickname, domain) showRepeats = not is_dm(reaction_post_json) individual_post_as_html(self.server.signing_priv_key_pem, False, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, pageNumber, base_dir, self.server.session, self.server.cached_webfingers, self.server.person_cache, self.postToNickname, domain, self.server.port, reaction_post_json, None, True, self.server.allow_deletion, http_prefix, self.server.project_version, timelineStr, 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, showRepeats, showIndividualPostIcons, manuallyApproveFollowers, False, True, False, self.server.cw_lists, self.server.lists_enabled) else: print('WARN: Emoji reaction post not found: ' + reaction_postFilename) else: print('WARN: unable to locate file for emoji reaction post ' + reactionUrl) actorAbsolute = self._get_instance_url(calling_domain) + actor actorPathStr = \ actorAbsolute + '/' + timelineStr + \ '?page=' + str(pageNumber) + timelineBookmark fitness_performance(GETstartTime, self.server.fitness, '_GET', '_reaction_button', self.server.debug) self._redirect_headers(actorPathStr, 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, GETstartTime, proxy_type: str, cookie: str, debug: str) -> None: """A button is pressed to undo emoji reaction """ pageNumber = 1 reactionUrl = path.split('?unreact=')[1] if '?' in reactionUrl: reactionUrl = reactionUrl.split('?')[0] timelineBookmark = '' if '?bm=' in path: timelineBookmark = path.split('?bm=')[1] if '?' in timelineBookmark: timelineBookmark = timelineBookmark.split('?')[0] timelineBookmark = '#' + timelineBookmark if '?page=' in path: pageNumberStr = path.split('?page=')[1] if '?' in pageNumberStr: pageNumberStr = pageNumberStr.split('?')[0] if '#' in pageNumberStr: pageNumberStr = pageNumberStr.split('#')[0] if pageNumberStr.isdigit(): pageNumber = int(pageNumberStr) timelineStr = 'inbox' if '?tl=' in path: timelineStr = path.split('?tl=')[1] if '?' in timelineStr: timelineStr = timelineStr.split('?')[0] actor = path.split('?unreact=')[0] self.postToNickname = get_nickname_from_actor(actor) if not self.postToNickname: print('WARN: unable to find nickname in ' + actor) actorAbsolute = self._get_instance_url(calling_domain) + actor actorPathStr = \ actorAbsolute + '/' + timelineStr + \ '?page=' + str(pageNumber) self._redirect_headers(actorPathStr, cookie, calling_domain) return emojiContentEncoded = None if '?emojreact=' in path: emojiContentEncoded = path.split('?emojreact=')[1] if '?' in emojiContentEncoded: emojiContentEncoded = emojiContentEncoded.split('?')[0] if not emojiContentEncoded: print('WARN: no emoji reaction ' + actor) actorAbsolute = self._get_instance_url(calling_domain) + actor actorPathStr = \ actorAbsolute + '/' + timelineStr + \ '?page=' + str(pageNumber) + timelineBookmark self._redirect_headers(actorPathStr, cookie, calling_domain) return emojiContent = urllib.parse.unquote_plus(emojiContentEncoded) if not self._establish_session("undoReactionButton"): self._404() return undoActor = \ local_actor_url(http_prefix, self.postToNickname, domain_full) actorReaction = path.split('?actor=')[1] if '?' in actorReaction: actorReaction = actorReaction.split('?')[0] # if this is an announce then send the emoji reaction # to the original post origActor, origPostUrl, origFilename = \ get_original_post_from_announce_url(reactionUrl, base_dir, self.postToNickname, domain) reactionUrl2 = reactionUrl reaction_postFilename = origFilename if origActor and origPostUrl: actorReaction = origActor reactionUrl2 = origPostUrl reaction_postFilename = None undoReactionJson = { "@context": "https://www.w3.org/ns/activitystreams", 'type': 'Undo', 'actor': undoActor, 'to': [actorReaction], 'object': { 'type': 'EmojiReact', 'actor': undoActor, 'to': [actorReaction], 'object': reactionUrl2 } } # send out the undo emoji reaction to followers self._post_to_outbox(undoReactionJson, self.server.project_version, None) # directly undo the emoji reaction within the post file if not reaction_postFilename: reaction_postFilename = \ locate_post(base_dir, self.postToNickname, domain, reactionUrl) if reaction_postFilename: recent_posts_cache = self.server.recent_posts_cache reaction_post_json = load_json(reaction_postFilename, 0, 1) if origFilename and origPostUrl: undo_reaction_collection_entry(recent_posts_cache, base_dir, reaction_postFilename, reactionUrl, undoActor, domain, debug, reaction_post_json, emojiContent) reactionUrl = origPostUrl reaction_postFilename = origFilename if debug: print('Removing emoji reaction for ' + reaction_postFilename) undo_reaction_collection_entry(recent_posts_cache, base_dir, reaction_postFilename, reactionUrl, undoActor, domain, debug, reaction_post_json, emojiContent) if debug: print('Regenerating html post for changed ' + 'emoji reaction collection') if reaction_post_json: showIndividualPostIcons = True manuallyApproveFollowers = \ follower_approval_active(base_dir, self.postToNickname, domain) showRepeats = not is_dm(reaction_post_json) individual_post_as_html(self.server.signing_priv_key_pem, False, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, pageNumber, base_dir, self.server.session, self.server.cached_webfingers, self.server.person_cache, self.postToNickname, domain, self.server.port, reaction_post_json, None, True, self.server.allow_deletion, http_prefix, self.server.project_version, timelineStr, 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, showRepeats, showIndividualPostIcons, manuallyApproveFollowers, False, True, False, self.server.cw_lists, self.server.lists_enabled) else: print('WARN: Unreaction post not found: ' + reaction_postFilename) actorAbsolute = self._get_instance_url(calling_domain) + actor actorPathStr = \ actorAbsolute + '/' + timelineStr + \ '?page=' + str(pageNumber) + timelineBookmark fitness_performance(GETstartTime, self.server.fitness, '_GET', '_undo_reaction_button', self.server.debug) self._redirect_headers(actorPathStr, cookie, calling_domain) def _reaction_picker(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, GETstartTime, proxy_type: str, cookie: str, debug: str) -> None: """Press the emoji reaction picker icon at the bottom of the post """ pageNumber = 1 reactionUrl = path.split('?selreact=')[1] if '?' in reactionUrl: reactionUrl = reactionUrl.split('?')[0] timelineBookmark = '' if '?bm=' in path: timelineBookmark = path.split('?bm=')[1] if '?' in timelineBookmark: timelineBookmark = timelineBookmark.split('?')[0] timelineBookmark = '#' + timelineBookmark actor = path.split('?selreact=')[0] if '?page=' in path: pageNumberStr = path.split('?page=')[1] if '?' in pageNumberStr: pageNumberStr = pageNumberStr.split('?')[0] if '#' in pageNumberStr: pageNumberStr = pageNumberStr.split('#')[0] if pageNumberStr.isdigit(): pageNumber = int(pageNumberStr) timelineStr = 'inbox' if '?tl=' in path: timelineStr = path.split('?tl=')[1] if '?' in timelineStr: timelineStr = timelineStr.split('?')[0] self.postToNickname = get_nickname_from_actor(actor) if not self.postToNickname: print('WARN: unable to find nickname in ' + actor) actorAbsolute = self._get_instance_url(calling_domain) + actor actorPathStr = \ actorAbsolute + '/' + timelineStr + \ '?page=' + str(pageNumber) + timelineBookmark self._redirect_headers(actorPathStr, cookie, calling_domain) return post_json_object = None reaction_postFilename = \ locate_post(self.server.base_dir, self.postToNickname, domain, reactionUrl) if reaction_postFilename: post_json_object = load_json(reaction_postFilename) if not reaction_postFilename or not post_json_object: print('WARN: unable to locate reaction post ' + reactionUrl) actorAbsolute = self._get_instance_url(calling_domain) + actor actorPathStr = \ actorAbsolute + '/' + timelineStr + \ '?page=' + str(pageNumber) + timelineBookmark self._redirect_headers(actorPathStr, cookie, calling_domain) return msg = \ html_emoji_reaction_picker(self.server.css_cache, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, self.server.base_dir, self.server.session, self.server.cached_webfingers, self.server.person_cache, self.postToNickname, domain, port, post_json_object, self.server.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, timelineStr, pageNumber) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_reaction_picker', self.server.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, GETstartTime, proxy_type: str, cookie: str, debug: str) -> None: """Bookmark button was pressed """ pageNumber = 1 bookmarkUrl = path.split('?bookmark=')[1] if '?' in bookmarkUrl: bookmarkUrl = bookmarkUrl.split('?')[0] timelineBookmark = '' if '?bm=' in path: timelineBookmark = path.split('?bm=')[1] if '?' in timelineBookmark: timelineBookmark = timelineBookmark.split('?')[0] timelineBookmark = '#' + timelineBookmark actor = path.split('?bookmark=')[0] if '?page=' in path: pageNumberStr = path.split('?page=')[1] if '?' in pageNumberStr: pageNumberStr = pageNumberStr.split('?')[0] if '#' in pageNumberStr: pageNumberStr = pageNumberStr.split('#')[0] if pageNumberStr.isdigit(): pageNumber = int(pageNumberStr) timelineStr = 'inbox' if '?tl=' in path: timelineStr = path.split('?tl=')[1] if '?' in timelineStr: timelineStr = timelineStr.split('?')[0] self.postToNickname = get_nickname_from_actor(actor) if not self.postToNickname: print('WARN: unable to find nickname in ' + actor) actorAbsolute = self._get_instance_url(calling_domain) + actor actorPathStr = \ actorAbsolute + '/' + timelineStr + \ '?page=' + str(pageNumber) self._redirect_headers(actorPathStr, cookie, calling_domain) return if not self._establish_session("bookmarkButton"): self._404() return bookmarkActor = \ local_actor_url(http_prefix, self.postToNickname, domain_full) ccList = [] bookmark_post(self.server.recent_posts_cache, self.server.session, base_dir, self.server.federation_list, self.postToNickname, domain, port, ccList, http_prefix, bookmarkUrl, bookmarkActor, False, self.server.send_threads, self.server.postLog, self.server.person_cache, self.server.cached_webfingers, self.server.debug, self.server.project_version) # clear the icon from the cache so that it gets updated if self.server.iconsCache.get('bookmark.png'): del self.server.iconsCache['bookmark.png'] bookmarkFilename = \ locate_post(base_dir, self.postToNickname, domain, bookmarkUrl) if bookmarkFilename: print('Regenerating html post for changed bookmark') bookmarkPostJson = load_json(bookmarkFilename, 0, 1) if bookmarkPostJson: cachedPostFilename = \ get_cached_post_filename(base_dir, self.postToNickname, domain, bookmarkPostJson) print('Bookmarked post json: ' + str(bookmarkPostJson)) print('Bookmarked post nickname: ' + self.postToNickname + ' ' + domain) print('Bookmarked post cache: ' + str(cachedPostFilename)) showIndividualPostIcons = True manuallyApproveFollowers = \ follower_approval_active(base_dir, self.postToNickname, domain) showRepeats = not is_dm(bookmarkPostJson) individual_post_as_html(self.server.signing_priv_key_pem, False, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, pageNumber, base_dir, self.server.session, self.server.cached_webfingers, self.server.person_cache, self.postToNickname, domain, self.server.port, bookmarkPostJson, None, True, self.server.allow_deletion, http_prefix, self.server.project_version, timelineStr, 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, showRepeats, showIndividualPostIcons, manuallyApproveFollowers, False, True, False, self.server.cw_lists, self.server.lists_enabled) else: print('WARN: Bookmarked post not found: ' + bookmarkFilename) # self._post_to_outbox(bookmarkJson, self.server.project_version, None) actorAbsolute = self._get_instance_url(calling_domain) + actor actorPathStr = \ actorAbsolute + '/' + timelineStr + \ '?page=' + str(pageNumber) + timelineBookmark fitness_performance(GETstartTime, self.server.fitness, '_GET', '_bookmark_button', self.server.debug) self._redirect_headers(actorPathStr, 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, GETstartTime, proxy_type: str, cookie: str, debug: str) -> None: """Button pressed to undo a bookmark """ pageNumber = 1 bookmarkUrl = path.split('?unbookmark=')[1] if '?' in bookmarkUrl: bookmarkUrl = bookmarkUrl.split('?')[0] timelineBookmark = '' if '?bm=' in path: timelineBookmark = path.split('?bm=')[1] if '?' in timelineBookmark: timelineBookmark = timelineBookmark.split('?')[0] timelineBookmark = '#' + timelineBookmark if '?page=' in path: pageNumberStr = path.split('?page=')[1] if '?' in pageNumberStr: pageNumberStr = pageNumberStr.split('?')[0] if '#' in pageNumberStr: pageNumberStr = pageNumberStr.split('#')[0] if pageNumberStr.isdigit(): pageNumber = int(pageNumberStr) timelineStr = 'inbox' if '?tl=' in path: timelineStr = path.split('?tl=')[1] if '?' in timelineStr: timelineStr = timelineStr.split('?')[0] actor = path.split('?unbookmark=')[0] self.postToNickname = get_nickname_from_actor(actor) if not self.postToNickname: print('WARN: unable to find nickname in ' + actor) actorAbsolute = self._get_instance_url(calling_domain) + actor actorPathStr = \ actorAbsolute + '/' + timelineStr + \ '?page=' + str(pageNumber) self._redirect_headers(actorPathStr, cookie, calling_domain) return if not self._establish_session("undo_bookmarkButton"): self._404() return undoActor = \ local_actor_url(http_prefix, self.postToNickname, domain_full) ccList = [] undo_bookmark_post(self.server.recent_posts_cache, self.server.session, base_dir, self.server.federation_list, self.postToNickname, domain, port, ccList, http_prefix, bookmarkUrl, undoActor, False, self.server.send_threads, self.server.postLog, self.server.person_cache, self.server.cached_webfingers, debug, self.server.project_version) # 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_bookmarkJson, # self.server.project_version, None) bookmarkFilename = \ locate_post(base_dir, self.postToNickname, domain, bookmarkUrl) if bookmarkFilename: print('Regenerating html post for changed unbookmark') bookmarkPostJson = load_json(bookmarkFilename, 0, 1) if bookmarkPostJson: cachedPostFilename = \ get_cached_post_filename(base_dir, self.postToNickname, domain, bookmarkPostJson) print('Unbookmarked post json: ' + str(bookmarkPostJson)) print('Unbookmarked post nickname: ' + self.postToNickname + ' ' + domain) print('Unbookmarked post cache: ' + str(cachedPostFilename)) showIndividualPostIcons = True manuallyApproveFollowers = \ follower_approval_active(base_dir, self.postToNickname, domain) showRepeats = not is_dm(bookmarkPostJson) individual_post_as_html(self.server.signing_priv_key_pem, False, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, pageNumber, base_dir, self.server.session, self.server.cached_webfingers, self.server.person_cache, self.postToNickname, domain, self.server.port, bookmarkPostJson, None, True, self.server.allow_deletion, http_prefix, self.server.project_version, timelineStr, 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, showRepeats, showIndividualPostIcons, manuallyApproveFollowers, False, True, False, self.server.cw_lists, self.server.lists_enabled) else: print('WARN: Unbookmarked post not found: ' + bookmarkFilename) actorAbsolute = self._get_instance_url(calling_domain) + actor actorPathStr = \ actorAbsolute + '/' + timelineStr + \ '?page=' + str(pageNumber) + timelineBookmark fitness_performance(GETstartTime, self.server.fitness, '_GET', '_undo_bookmark_button', self.server.debug) self._redirect_headers(actorPathStr, cookie, calling_domain) def _delete_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, GETstartTime, proxy_type: str, cookie: str, debug: str) -> None: """Delete button is pressed on a post """ if not cookie: print('ERROR: no cookie given when deleting ' + path) self._400() return pageNumber = 1 if '?page=' in path: pageNumberStr = path.split('?page=')[1] if '?' in pageNumberStr: pageNumberStr = pageNumberStr.split('?')[0] if '#' in pageNumberStr: pageNumberStr = pageNumberStr.split('#')[0] if pageNumberStr.isdigit(): pageNumber = int(pageNumberStr) deleteUrl = path.split('?delete=')[1] if '?' in deleteUrl: deleteUrl = deleteUrl.split('?')[0] timelineStr = self.server.defaultTimeline if '?tl=' in path: timelineStr = path.split('?tl=')[1] if '?' in timelineStr: timelineStr = timelineStr.split('?')[0] usersPath = path.split('?delete=')[0] actor = \ http_prefix + '://' + domain_full + usersPath if self.server.allow_deletion or \ deleteUrl.startswith(actor): if self.server.debug: print('DEBUG: deleteUrl=' + deleteUrl) print('DEBUG: actor=' + actor) if actor not in deleteUrl: # You can only delete your own posts if calling_domain.endswith('.onion') and onion_domain: actor = 'http://' + onion_domain + usersPath elif calling_domain.endswith('.i2p') and i2p_domain: actor = 'http://' + i2p_domain + usersPath self._redirect_headers(actor + '/' + timelineStr, cookie, calling_domain) return self.postToNickname = get_nickname_from_actor(actor) if not self.postToNickname: print('WARN: unable to find nickname in ' + actor) if calling_domain.endswith('.onion') and onion_domain: actor = 'http://' + onion_domain + usersPath elif calling_domain.endswith('.i2p') and i2p_domain: actor = 'http://' + i2p_domain + usersPath self._redirect_headers(actor + '/' + timelineStr, cookie, calling_domain) return if not self._establish_session("deleteButton"): self._404() return deleteStr = \ html_confirm_delete(self.server.css_cache, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, pageNumber, self.server.session, base_dir, deleteUrl, 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) if deleteStr: deleteStrLen = len(deleteStr) self._set_headers('text/html', deleteStrLen, cookie, calling_domain, False) self._write(deleteStr.encode('utf-8')) self.server.GETbusy = False return if calling_domain.endswith('.onion') and onion_domain: actor = 'http://' + onion_domain + usersPath elif (calling_domain.endswith('.i2p') and i2p_domain): actor = 'http://' + i2p_domain + usersPath fitness_performance(GETstartTime, self.server.fitness, '_GET', '_delete_button', self.server.debug) self._redirect_headers(actor + '/' + timelineStr, 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, GETstartTime, proxy_type: str, cookie: str, debug: str): """Mute button is pressed """ muteUrl = path.split('?mute=')[1] if '?' in muteUrl: muteUrl = muteUrl.split('?')[0] timelineBookmark = '' if '?bm=' in path: timelineBookmark = path.split('?bm=')[1] if '?' in timelineBookmark: timelineBookmark = timelineBookmark.split('?')[0] timelineBookmark = '#' + timelineBookmark timelineStr = self.server.defaultTimeline if '?tl=' in path: timelineStr = path.split('?tl=')[1] if '?' in timelineStr: timelineStr = timelineStr.split('?')[0] pageNumber = 1 if '?page=' in path: pageNumberStr = path.split('?page=')[1] if '?' in pageNumberStr: pageNumberStr = pageNumberStr.split('?')[0] if '#' in pageNumberStr: pageNumberStr = pageNumberStr.split('#')[0] if pageNumberStr.isdigit(): pageNumber = int(pageNumberStr) actor = \ http_prefix + '://' + domain_full + path.split('?mute=')[0] nickname = get_nickname_from_actor(actor) mute_post(base_dir, nickname, domain, port, http_prefix, muteUrl, self.server.recent_posts_cache, debug) muteFilename = \ locate_post(base_dir, nickname, domain, muteUrl) if muteFilename: print('mute_post: Regenerating html post for changed mute status') mute_post_json = load_json(muteFilename, 0, 1) if mute_post_json: cachedPostFilename = \ 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(cachedPostFilename)) showIndividualPostIcons = True manuallyApproveFollowers = \ follower_approval_active(base_dir, nickname, domain) showRepeats = not is_dm(mute_post_json) showPublicOnly = False storeToCache = True useCacheOnly = False allowDownloads = False showAvatarOptions = True avatarUrl = None individual_post_as_html(self.server.signing_priv_key_pem, allowDownloads, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, pageNumber, base_dir, self.server.session, self.server.cached_webfingers, self.server.person_cache, nickname, domain, self.server.port, mute_post_json, avatarUrl, showAvatarOptions, self.server.allow_deletion, http_prefix, self.server.project_version, timelineStr, 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, showRepeats, showIndividualPostIcons, manuallyApproveFollowers, showPublicOnly, storeToCache, useCacheOnly, self.server.cw_lists, self.server.lists_enabled) else: print('WARN: Muted post not found: ' + muteFilename) 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(GETstartTime, self.server.fitness, '_GET', '_mute_button', self.server.debug) self._redirect_headers(actor + '/' + timelineStr + timelineBookmark, 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, GETstartTime, proxy_type: str, cookie: str, debug: str): """Undo mute button is pressed """ muteUrl = path.split('?unmute=')[1] if '?' in muteUrl: muteUrl = muteUrl.split('?')[0] timelineBookmark = '' if '?bm=' in path: timelineBookmark = path.split('?bm=')[1] if '?' in timelineBookmark: timelineBookmark = timelineBookmark.split('?')[0] timelineBookmark = '#' + timelineBookmark timelineStr = self.server.defaultTimeline if '?tl=' in path: timelineStr = path.split('?tl=')[1] if '?' in timelineStr: timelineStr = timelineStr.split('?')[0] pageNumber = 1 if '?page=' in path: pageNumberStr = path.split('?page=')[1] if '?' in pageNumberStr: pageNumberStr = pageNumberStr.split('?')[0] if '#' in pageNumberStr: pageNumberStr = pageNumberStr.split('#')[0] if pageNumberStr.isdigit(): pageNumber = int(pageNumberStr) actor = \ http_prefix + '://' + domain_full + path.split('?unmute=')[0] nickname = get_nickname_from_actor(actor) unmute_post(base_dir, nickname, domain, port, http_prefix, muteUrl, self.server.recent_posts_cache, debug) muteFilename = \ locate_post(base_dir, nickname, domain, muteUrl) if muteFilename: print('unmute_post: ' + 'Regenerating html post for changed unmute status') mute_post_json = load_json(muteFilename, 0, 1) if mute_post_json: cachedPostFilename = \ 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(cachedPostFilename)) showIndividualPostIcons = True manuallyApproveFollowers = \ follower_approval_active(base_dir, nickname, domain) showRepeats = not is_dm(mute_post_json) showPublicOnly = False storeToCache = True useCacheOnly = False allowDownloads = False showAvatarOptions = True avatarUrl = None individual_post_as_html(self.server.signing_priv_key_pem, allowDownloads, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, pageNumber, base_dir, self.server.session, self.server.cached_webfingers, self.server.person_cache, nickname, domain, self.server.port, mute_post_json, avatarUrl, showAvatarOptions, self.server.allow_deletion, http_prefix, self.server.project_version, timelineStr, 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, showRepeats, showIndividualPostIcons, manuallyApproveFollowers, showPublicOnly, storeToCache, useCacheOnly, self.server.cw_lists, self.server.lists_enabled) else: print('WARN: Unmuted post not found: ' + muteFilename) 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(GETstartTime, self.server.fitness, '_GET', '_undo_mute_button', self.server.debug) self._redirect_headers(actor + '/' + timelineStr + timelineBookmark, cookie, calling_domain) def _show_replies_to_post(self, authorized: bool, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, proxy_type: str, cookie: str, debug: str) -> bool: """Shows the replies to a post """ if not ('/statuses/' in path and '/users/' in path): return False namedStatus = path.split('/users/')[1] if '/' not in namedStatus: return False postSections = namedStatus.split('/') if len(postSections) < 4: return False if not postSections[3].startswith('replies'): return False nickname = postSections[0] statusNumber = postSections[2] if not (len(statusNumber) > 10 and statusNumber.isdigit()): return False boxname = 'outbox' # get the replies file postDir = \ acct_dir(base_dir, nickname, domain) + '/' + boxname postRepliesFilename = \ postDir + '/' + \ http_prefix + ':##' + domain_full + '#users#' + \ nickname + '#statuses#' + statusNumber + '.replies' if not os.path.isfile(postRepliesFilename): # There are no replies, # so show empty collection contextStr = \ 'https://www.w3.org/ns/activitystreams' firstStr = \ local_actor_url(http_prefix, nickname, domain_full) + \ '/statuses/' + statusNumber + '/replies?page=true' idStr = \ local_actor_url(http_prefix, nickname, domain_full) + \ '/statuses/' + statusNumber + '/replies' lastStr = \ local_actor_url(http_prefix, nickname, domain_full) + \ '/statuses/' + statusNumber + '/replies?page=true' repliesJson = { '@context': contextStr, 'first': firstStr, 'id': idStr, 'last': lastStr, 'totalItems': 0, 'type': 'OrderedCollection' } if self._request_http(): if not self._establish_session("showRepliesToPost"): self._404() return True recent_posts_cache = self.server.recent_posts_cache max_recent_posts = self.server.max_recent_posts translate = self.server.translate session = self.server.session cached_webfingers = self.server.cached_webfingers person_cache = self.server.person_cache project_version = self.server.project_version ytDomain = self.server.yt_replace_domain twitter_replacement_domain = \ self.server.twitter_replacement_domain peertube_instances = self.server.peertube_instances msg = \ html_post_replies(self.server.css_cache, recent_posts_cache, max_recent_posts, translate, base_dir, session, cached_webfingers, person_cache, nickname, domain, port, repliesJson, http_prefix, project_version, ytDomain, 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) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_replies_to_post', self.server.debug) else: if self._secure_mode(): msg = json.dumps(repliesJson, ensure_ascii=False) msg = msg.encode('utf-8') protocolStr = 'application/json' msglen = len(msg) self._set_headers(protocolStr, msglen, None, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_replies_to_post json', self.server.debug) else: self._404() return True else: # replies exist. Itterate through the # text file containing message ids contextStr = 'https://www.w3.org/ns/activitystreams' idStr = \ local_actor_url(http_prefix, nickname, domain_full) + \ '/statuses/' + statusNumber + '?page=true' partOfStr = \ local_actor_url(http_prefix, nickname, domain_full) + \ '/statuses/' + statusNumber repliesJson = { '@context': contextStr, 'id': idStr, 'orderedItems': [ ], 'partOf': partOfStr, 'type': 'OrderedCollectionPage' } # populate the items list with replies populate_replies_json(base_dir, nickname, domain, postRepliesFilename, authorized, repliesJson) # send the replies json if self._request_http(): if not self._establish_session("showRepliesToPost2"): self._404() return True recent_posts_cache = self.server.recent_posts_cache max_recent_posts = self.server.max_recent_posts translate = self.server.translate session = self.server.session cached_webfingers = self.server.cached_webfingers person_cache = self.server.person_cache project_version = self.server.project_version ytDomain = self.server.yt_replace_domain twitter_replacement_domain = \ self.server.twitter_replacement_domain peertube_instances = self.server.peertube_instances msg = \ html_post_replies(self.server.css_cache, recent_posts_cache, max_recent_posts, translate, base_dir, session, cached_webfingers, person_cache, nickname, domain, port, repliesJson, http_prefix, project_version, ytDomain, 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) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_replies_to_post', self.server.debug) else: if self._secure_mode(): msg = json.dumps(repliesJson, ensure_ascii=False) msg = msg.encode('utf-8') protocolStr = 'application/json' msglen = len(msg) self._set_headers(protocolStr, msglen, None, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_replies_to_post json', self.server.debug) else: self._404() return True return False def _show_roles(self, authorized: bool, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, proxy_type: str, cookie: str, debug: str) -> bool: """Show roles within profile screen """ namedStatus = path.split('/users/')[1] if '/' not in namedStatus: return False postSections = namedStatus.split('/') nickname = postSections[0] actorFilename = acct_dir(base_dir, nickname, domain) + '.json' if not os.path.isfile(actorFilename): return False actor_json = load_json(actorFilename) if not actor_json: return False if actor_json.get('hasOccupation'): if self._request_http(): getPerson = \ person_lookup(domain, path.replace('/roles', ''), base_dir) if getPerson: defaultTimeline = \ self.server.defaultTimeline 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 accessKeys = self.server.accessKeys if self.server.keyShortcuts.get(nickname): accessKeys = self.server.keyShortcuts[nickname] rolesList = 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 msg = \ html_profile(self.server.signing_priv_key_pem, self.server.rss_icon_at_top, self.server.css_cache, icons_as_buttons, defaultTimeline, recent_posts_cache, self.server.max_recent_posts, self.server.translate, self.server.project_version, base_dir, http_prefix, True, getPerson, 'roles', self.server.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, accessKeys, city, self.server.system_language, self.server.max_like_count, shared_items_federated_domains, rolesList, None, None, self.server.cw_lists, self.server.lists_enabled, self.server.content_license_url) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_roles', self.server.debug) else: if self._secure_mode(): rolesList = get_actor_roles_list(actor_json) msg = json.dumps(rolesList, ensure_ascii=False) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_roles json', self.server.debug) else: self._404() return True return False def _show_skills(self, authorized: bool, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, proxy_type: str, cookie: str, debug: str) -> bool: """Show skills on the profile screen """ namedStatus = path.split('/users/')[1] if '/' in namedStatus: postSections = namedStatus.split('/') nickname = postSections[0] actorFilename = acct_dir(base_dir, nickname, domain) + '.json' if os.path.isfile(actorFilename): actor_json = load_json(actorFilename) if actor_json: if no_of_actor_skills(actor_json) > 0: if self._request_http(): getPerson = \ person_lookup(domain, path.replace('/skills', ''), base_dir) if getPerson: defaultTimeline = \ self.server.defaultTimeline 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 accessKeys = self.server.accessKeys if self.server.keyShortcuts.get(nickname): accessKeys = \ self.server.keyShortcuts[nickname] actorSkillsList = \ get_occupation_skills(actor_json) skills = get_skills_from_list(actorSkillsList) 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 msg = \ html_profile(signing_priv_key_pem, self.server.rss_icon_at_top, self.server.css_cache, icons_as_buttons, defaultTimeline, recent_posts_cache, self.server.max_recent_posts, self.server.translate, self.server.project_version, base_dir, http_prefix, True, getPerson, 'skills', self.server.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, accessKeys, 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) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_skills', self.server.debug) else: if self._secure_mode(): actorSkillsList = \ get_occupation_skills(actor_json) skills = get_skills_from_list(actorSkillsList) msg = json.dumps(skills, ensure_ascii=False) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_skills json', self.server.debug) else: self._404() return True actor = path.replace('/skills', '') actorAbsolute = self._get_instance_url(calling_domain) + actor self._redirect_headers(actorAbsolute, cookie, calling_domain) return True def _show_individual_at_post(self, authorized: bool, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, proxy_type: str, cookie: str, debug: str) -> bool: """get an individual post from the path /@nickname/statusnumber """ if '/@' not in path: return False likedBy = None if '?likedBy=' in path: likedBy = path.split('?likedBy=')[1].strip() if '?' in likedBy: likedBy = likedBy.split('?')[0] path = path.split('?likedBy=')[0] reactBy = None reactEmoji = None if '?reactBy=' in path: reactBy = path.split('?reactBy=')[1].strip() if ';' in reactBy: reactBy = reactBy.split(';')[0] if ';emoj=' in path: reactEmoji = path.split(';emoj=')[1].strip() if ';' in reactEmoji: reactEmoji = reactEmoji.split(';')[0] path = path.split('?reactBy=')[0] namedStatus = path.split('/@')[1] if '/' not in namedStatus: # show actor nickname = namedStatus return False postSections = namedStatus.split('/') if len(postSections) != 2: return False nickname = postSections[0] statusNumber = postSections[1] if len(statusNumber) <= 10 or not statusNumber.isdigit(): return False post_filename = \ acct_dir(base_dir, nickname, domain) + '/outbox/' + \ http_prefix + ':##' + domain_full + '#users#' + nickname + \ '#statuses#' + statusNumber + '.json' includeCreateWrapper = False if postSections[-1] == 'activity': includeCreateWrapper = True result = self._show_post_from_file(post_filename, likedBy, reactBy, reactEmoji, authorized, calling_domain, path, base_dir, http_prefix, nickname, domain, domain_full, port, onion_domain, i2p_domain, GETstartTime, proxy_type, cookie, debug, includeCreateWrapper) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_individual_at_post', self.server.debug) return result def _show_post_from_file(self, post_filename: str, likedBy: str, reactBy: str, reactEmoji: str, authorized: bool, calling_domain: str, path: str, base_dir: str, http_prefix: str, nickname: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, proxy_type: str, cookie: str, debug: str, includeCreateWrapper: bool) -> bool: """Shows an individual post from its filename """ if not os.path.isfile(post_filename): self._404() self.server.GETbusy = False return True post_json_object = load_json(post_filename) if not post_json_object: self.send_response(429) self.end_headers() self.server.GETbusy = 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.GETbusy = False return True remove_post_interactions(pjo, True) if self._request_http(): msg = \ html_individual_post(self.server.css_cache, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, base_dir, self.server.session, self.server.cached_webfingers, self.server.person_cache, nickname, domain, port, authorized, post_json_object, http_prefix, self.server.project_version, likedBy, reactBy, reactEmoji, 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) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_post_from_file', self.server.debug) else: if self._secure_mode(): if not includeCreateWrapper and \ post_json_object['type'] == 'Create' and \ has_object_dict(post_json_object): unwrappedJson = post_json_object['object'] unwrappedJson['@context'] = \ get_individual_post_context() msg = json.dumps(unwrappedJson, ensure_ascii=False) else: msg = json.dumps(post_json_object, ensure_ascii=False) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_post_from_file json', self.server.debug) else: self._404() self.server.GETbusy = False return True def _show_individual_post(self, authorized: bool, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, proxy_type: str, cookie: str, debug: str) -> bool: """Shows an individual post """ likedBy = None if '?likedBy=' in path: likedBy = path.split('?likedBy=')[1].strip() if '?' in likedBy: likedBy = likedBy.split('?')[0] path = path.split('?likedBy=')[0] reactBy = None reactEmoji = None if '?reactBy=' in path: reactBy = path.split('?reactBy=')[1].strip() if ';' in reactBy: reactBy = reactBy.split(';')[0] if ';emoj=' in path: reactEmoji = path.split(';emoj=')[1].strip() if ';' in reactEmoji: reactEmoji = reactEmoji.split(';')[0] path = path.split('?reactBy=')[0] namedStatus = path.split('/users/')[1] if '/' not in namedStatus: return False postSections = namedStatus.split('/') if len(postSections) < 3: return False nickname = postSections[0] statusNumber = postSections[2] if len(statusNumber) <= 10 or (not statusNumber.isdigit()): return False post_filename = \ acct_dir(base_dir, nickname, domain) + '/outbox/' + \ http_prefix + ':##' + domain_full + '#users#' + nickname + \ '#statuses#' + statusNumber + '.json' includeCreateWrapper = False if postSections[-1] == 'activity': includeCreateWrapper = True result = self._show_post_from_file(post_filename, likedBy, reactBy, reactEmoji, authorized, calling_domain, path, base_dir, http_prefix, nickname, domain, domain_full, port, onion_domain, i2p_domain, GETstartTime, proxy_type, cookie, debug, includeCreateWrapper) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_individual_post', self.server.debug) return result def _show_notify_post(self, authorized: bool, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, proxy_type: str, cookie: str, debug: str) -> bool: """Shows an individual post from an account which you are following and where you have the notify checkbox set on person options """ likedBy = None reactBy = None reactEmoji = 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 includeCreateWrapper = False if path.endswith('/activity'): includeCreateWrapper = True result = self._show_post_from_file(post_filename, likedBy, reactBy, reactEmoji, authorized, calling_domain, path, base_dir, http_prefix, nickname, domain, domain_full, port, onion_domain, i2p_domain, GETstartTime, proxy_type, cookie, debug, includeCreateWrapper) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_notify_post', self.server.debug) return result def _show_inbox(self, authorized: bool, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, proxy_type: str, cookie: str, debug: str, recent_posts_cache: {}, session, defaultTimeline: str, max_recent_posts: int, translate: {}, cached_webfingers: {}, person_cache: {}, allow_deletion: bool, project_version: str, yt_replace_domain: str, twitter_replacement_domain: str) -> bool: """Shows the inbox timeline """ if '/users/' in path: if authorized: inboxFeed = \ person_box_json(recent_posts_cache, session, base_dir, domain, port, path, http_prefix, max_posts_in_feed, 'inbox', authorized, 0, self.server.positive_voting, self.server.voting_time_mins) if inboxFeed: if GETstartTime: fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_inbox', self.server.debug) if self._request_http(): nickname = path.replace('/users/', '') nickname = nickname.replace('/inbox', '') pageNumber = 1 if '?page=' in nickname: pageNumber = nickname.split('?page=')[1] nickname = nickname.split('?page=')[0] if pageNumber.isdigit(): pageNumber = int(pageNumber) else: pageNumber = 1 if 'page=' not in path: # if no page was specified then show the first inboxFeed = \ person_box_json(recent_posts_cache, session, 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 GETstartTime: fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_inbox2', self.server.debug) full_width_tl_button_header = \ self.server.full_width_tl_button_header minimalNick = is_minimal(base_dir, domain, nickname) accessKeys = self.server.accessKeys if self.server.keyShortcuts.get(nickname): accessKeys = \ self.server.keyShortcuts[nickname] shared_items_federated_domains = \ self.server.shared_items_federated_domains allow_local_network_access = \ self.server.allow_local_network_access msg = html_inbox(self.server.css_cache, defaultTimeline, recent_posts_cache, max_recent_posts, translate, pageNumber, max_posts_in_feed, session, base_dir, cached_webfingers, person_cache, nickname, domain, port, inboxFeed, allow_deletion, http_prefix, project_version, minimalNick, 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, accessKeys, 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) if GETstartTime: fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_inbox3', 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) if GETstartTime: fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_inbox4', self.server.debug) else: # don't need authorized fetch here because # there is already the authorization check msg = json.dumps(inboxFeed, ensure_ascii=False) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, 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_d_ms(self, authorized: bool, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, proxy_type: str, cookie: str, debug: str) -> bool: """Shows the DMs timeline """ if '/users/' in path: if authorized: inboxDMFeed = \ person_box_json(self.server.recent_posts_cache, self.server.session, base_dir, domain, port, path, http_prefix, max_posts_in_feed, 'dm', authorized, 0, self.server.positive_voting, self.server.voting_time_mins) if inboxDMFeed: if self._request_http(): nickname = path.replace('/users/', '') nickname = nickname.replace('/dm', '') pageNumber = 1 if '?page=' in nickname: pageNumber = nickname.split('?page=')[1] nickname = nickname.split('?page=')[0] if pageNumber.isdigit(): pageNumber = int(pageNumber) else: pageNumber = 1 if 'page=' not in path: # if no page was specified then show the first inboxDMFeed = \ person_box_json(self.server.recent_posts_cache, self.server.session, 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 minimalNick = is_minimal(base_dir, domain, nickname) accessKeys = self.server.accessKeys if self.server.keyShortcuts.get(nickname): accessKeys = \ self.server.keyShortcuts[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 msg = \ html_inbox_d_ms(self.server.css_cache, self.server.defaultTimeline, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, pageNumber, max_posts_in_feed, self.server.session, base_dir, self.server.cached_webfingers, self.server.person_cache, nickname, domain, port, inboxDMFeed, self.server.allow_deletion, http_prefix, self.server.project_version, minimalNick, 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, accessKeys, 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) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_d_ms', self.server.debug) else: # don't need authorized fetch here because # there is already the authorization check msg = json.dumps(inboxDMFeed, ensure_ascii=False) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_d_ms 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, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, proxy_type: str, cookie: str, debug: str) -> bool: """Shows the replies timeline """ if '/users/' in path: if authorized: inboxRepliesFeed = \ person_box_json(self.server.recent_posts_cache, self.server.session, 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 inboxRepliesFeed: inboxRepliesFeed = [] if self._request_http(): nickname = path.replace('/users/', '') nickname = nickname.replace('/tlreplies', '') pageNumber = 1 if '?page=' in nickname: pageNumber = nickname.split('?page=')[1] nickname = nickname.split('?page=')[0] if pageNumber.isdigit(): pageNumber = int(pageNumber) else: pageNumber = 1 if 'page=' not in path: # if no page was specified then show the first inboxRepliesFeed = \ person_box_json(self.server.recent_posts_cache, self.server.session, 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 minimalNick = is_minimal(base_dir, domain, nickname) accessKeys = self.server.accessKeys if self.server.keyShortcuts.get(nickname): accessKeys = \ self.server.keyShortcuts[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 msg = \ html_inbox_replies(self.server.css_cache, self.server.defaultTimeline, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, pageNumber, max_posts_in_feed, self.server.session, base_dir, self.server.cached_webfingers, self.server.person_cache, nickname, domain, port, inboxRepliesFeed, self.server.allow_deletion, http_prefix, self.server.project_version, minimalNick, 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, accessKeys, 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) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_replies', self.server.debug) else: # don't need authorized fetch here because there is # already the authorization check msg = json.dumps(inboxRepliesFeed, ensure_ascii=False) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_replies json', self.server.debug) return True else: 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, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, proxy_type: str, cookie: str, debug: str) -> bool: """Shows the media timeline """ if '/users/' in path: if authorized: inboxMediaFeed = \ person_box_json(self.server.recent_posts_cache, self.server.session, 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 inboxMediaFeed: inboxMediaFeed = [] if self._request_http(): nickname = path.replace('/users/', '') nickname = nickname.replace('/tlmedia', '') pageNumber = 1 if '?page=' in nickname: pageNumber = nickname.split('?page=')[1] nickname = nickname.split('?page=')[0] if pageNumber.isdigit(): pageNumber = int(pageNumber) else: pageNumber = 1 if 'page=' not in path: # if no page was specified then show the first inboxMediaFeed = \ person_box_json(self.server.recent_posts_cache, self.server.session, 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 minimalNick = is_minimal(base_dir, domain, nickname) accessKeys = self.server.accessKeys if self.server.keyShortcuts.get(nickname): accessKeys = \ self.server.keyShortcuts[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 msg = \ html_inbox_media(self.server.css_cache, self.server.defaultTimeline, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, pageNumber, max_posts_in_media_feed, self.server.session, base_dir, self.server.cached_webfingers, self.server.person_cache, nickname, domain, port, inboxMediaFeed, self.server.allow_deletion, http_prefix, self.server.project_version, minimalNick, 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, accessKeys, 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) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, 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 = json.dumps(inboxMediaFeed, ensure_ascii=False) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_media_timeline json', self.server.debug) return True else: 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, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, proxy_type: str, cookie: str, debug: str) -> bool: """Shows the blogs timeline """ if '/users/' in path: if authorized: inboxBlogsFeed = \ person_box_json(self.server.recent_posts_cache, self.server.session, 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 inboxBlogsFeed: inboxBlogsFeed = [] if self._request_http(): nickname = path.replace('/users/', '') nickname = nickname.replace('/tlblogs', '') pageNumber = 1 if '?page=' in nickname: pageNumber = nickname.split('?page=')[1] nickname = nickname.split('?page=')[0] if pageNumber.isdigit(): pageNumber = int(pageNumber) else: pageNumber = 1 if 'page=' not in path: # if no page was specified then show the first inboxBlogsFeed = \ person_box_json(self.server.recent_posts_cache, self.server.session, 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 minimalNick = is_minimal(base_dir, domain, nickname) accessKeys = self.server.accessKeys if self.server.keyShortcuts.get(nickname): accessKeys = \ self.server.keyShortcuts[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 msg = \ html_inbox_blogs(self.server.css_cache, self.server.defaultTimeline, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, pageNumber, max_posts_in_blogs_feed, self.server.session, base_dir, self.server.cached_webfingers, self.server.person_cache, nickname, domain, port, inboxBlogsFeed, self.server.allow_deletion, http_prefix, self.server.project_version, minimalNick, 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, accessKeys, 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) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, 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 = json.dumps(inboxBlogsFeed, ensure_ascii=False) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_blogs_timeline json', self.server.debug) return True else: 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, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, proxy_type: str, cookie: str, debug: str) -> bool: """Shows the news timeline """ if '/users/' in path: if authorized: inboxNewsFeed = \ person_box_json(self.server.recent_posts_cache, self.server.session, 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 inboxNewsFeed: inboxNewsFeed = [] if self._request_http(): nickname = path.replace('/users/', '') nickname = nickname.replace('/tlnews', '') pageNumber = 1 if '?page=' in nickname: pageNumber = nickname.split('?page=')[1] nickname = nickname.split('?page=')[0] if pageNumber.isdigit(): pageNumber = int(pageNumber) else: pageNumber = 1 if 'page=' not in path: newswire_votes_threshold = \ self.server.newswire_votes_threshold # if no page was specified then show the first inboxNewsFeed = \ person_box_json(self.server.recent_posts_cache, self.server.session, 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) currNickname = path.split('/users/')[1] if '/' in currNickname: currNickname = currNickname.split('/')[0] moderator = is_moderator(base_dir, currNickname) editor = is_editor(base_dir, currNickname) artist = is_artist(base_dir, currNickname) full_width_tl_button_header = \ self.server.full_width_tl_button_header minimalNick = is_minimal(base_dir, domain, nickname) accessKeys = self.server.accessKeys if self.server.keyShortcuts.get(nickname): accessKeys = \ self.server.keyShortcuts[nickname] fed_domains = \ self.server.shared_items_federated_domains msg = \ html_inbox_news(self.server.css_cache, self.server.defaultTimeline, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, pageNumber, max_posts_in_news_feed, self.server.session, base_dir, self.server.cached_webfingers, self.server.person_cache, nickname, domain, port, inboxNewsFeed, self.server.allow_deletion, http_prefix, self.server.project_version, minimalNick, 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, accessKeys, 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) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, 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 = json.dumps(inboxNewsFeed, ensure_ascii=False) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_news_timeline json', self.server.debug) return True else: 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, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, proxy_type: str, cookie: str, debug: str) -> bool: """Shows the features timeline (all local blogs) """ if '/users/' in path: if authorized: inboxFeaturesFeed = \ person_box_json(self.server.recent_posts_cache, self.server.session, 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 inboxFeaturesFeed: inboxFeaturesFeed = [] if self._request_http(): nickname = path.replace('/users/', '') nickname = nickname.replace('/tlfeatures', '') pageNumber = 1 if '?page=' in nickname: pageNumber = nickname.split('?page=')[1] nickname = nickname.split('?page=')[0] if pageNumber.isdigit(): pageNumber = int(pageNumber) else: pageNumber = 1 if 'page=' not in path: newswire_votes_threshold = \ self.server.newswire_votes_threshold # if no page was specified then show the first inboxFeaturesFeed = \ person_box_json(self.server.recent_posts_cache, self.server.session, 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) currNickname = path.split('/users/')[1] if '/' in currNickname: currNickname = currNickname.split('/')[0] full_width_tl_button_header = \ self.server.full_width_tl_button_header minimalNick = is_minimal(base_dir, domain, nickname) accessKeys = self.server.accessKeys if self.server.keyShortcuts.get(nickname): accessKeys = \ self.server.keyShortcuts[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 msg = \ html_inbox_features(self.server.css_cache, self.server.defaultTimeline, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, pageNumber, max_posts_in_blogs_feed, self.server.session, base_dir, self.server.cached_webfingers, self.server.person_cache, nickname, domain, port, inboxFeaturesFeed, self.server.allow_deletion, http_prefix, self.server.project_version, minimalNick, 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, accessKeys, 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) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, 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 = json.dumps(inboxFeaturesFeed, ensure_ascii=False) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_features_timeline json', self.server.debug) return True else: 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, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, proxy_type: str, cookie: str, debug: str) -> bool: """Shows the shares timeline """ if '/users/' in path: if authorized: if self._request_http(): nickname = path.replace('/users/', '') nickname = nickname.replace('/tlshares', '') pageNumber = 1 if '?page=' in nickname: pageNumber = nickname.split('?page=')[1] nickname = nickname.split('?page=')[0] if pageNumber.isdigit(): pageNumber = int(pageNumber) else: pageNumber = 1 accessKeys = self.server.accessKeys if self.server.keyShortcuts.get(nickname): accessKeys = \ self.server.keyShortcuts[nickname] full_width_tl_button_header = \ self.server.full_width_tl_button_header msg = \ html_shares(self.server.css_cache, self.server.defaultTimeline, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, pageNumber, max_posts_in_feed, self.server.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, accessKeys, 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) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, 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, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, proxy_type: str, cookie: str, debug: str) -> bool: """Shows the wanted timeline """ if '/users/' in path: if authorized: if self._request_http(): nickname = path.replace('/users/', '') nickname = nickname.replace('/tlwanted', '') pageNumber = 1 if '?page=' in nickname: pageNumber = nickname.split('?page=')[1] nickname = nickname.split('?page=')[0] if pageNumber.isdigit(): pageNumber = int(pageNumber) else: pageNumber = 1 accessKeys = self.server.accessKeys if self.server.keyShortcuts.get(nickname): accessKeys = \ self.server.keyShortcuts[nickname] full_width_tl_button_header = \ self.server.full_width_tl_button_header msg = \ html_wanted(self.server.css_cache, self.server.defaultTimeline, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, pageNumber, max_posts_in_feed, self.server.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, accessKeys, 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) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, 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, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, proxy_type: str, cookie: str, debug: str) -> bool: """Shows the bookmarks timeline """ if '/users/' in path: if authorized: bookmarksFeed = \ person_box_json(self.server.recent_posts_cache, self.server.session, base_dir, domain, port, path, http_prefix, max_posts_in_feed, 'tlbookmarks', authorized, 0, self.server.positive_voting, self.server.voting_time_mins) if bookmarksFeed: if self._request_http(): nickname = path.replace('/users/', '') nickname = nickname.replace('/tlbookmarks', '') nickname = nickname.replace('/bookmarks', '') pageNumber = 1 if '?page=' in nickname: pageNumber = nickname.split('?page=')[1] nickname = nickname.split('?page=')[0] if pageNumber.isdigit(): pageNumber = int(pageNumber) else: pageNumber = 1 if 'page=' not in path: # if no page was specified then show the first bookmarksFeed = \ person_box_json(self.server.recent_posts_cache, self.server.session, 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 minimalNick = is_minimal(base_dir, domain, nickname) accessKeys = self.server.accessKeys if self.server.keyShortcuts.get(nickname): accessKeys = \ self.server.keyShortcuts[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 msg = \ html_bookmarks(self.server.css_cache, self.server.defaultTimeline, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, pageNumber, max_posts_in_feed, self.server.session, base_dir, self.server.cached_webfingers, self.server.person_cache, nickname, domain, port, bookmarksFeed, self.server.allow_deletion, http_prefix, self.server.project_version, minimalNick, 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, accessKeys, 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) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, 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 = json.dumps(bookmarksFeed, ensure_ascii=False) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, 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, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, proxy_type: str, cookie: str, debug: str) -> bool: """Shows the outbox timeline """ # get outbox feed for a person outboxFeed = \ person_box_json(self.server.recent_posts_cache, self.server.session, 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 outboxFeed: nickname = \ path.replace('/users/', '').replace('/outbox', '') pageNumber = 0 if '?page=' in nickname: pageNumber = nickname.split('?page=')[1] nickname = nickname.split('?page=')[0] if pageNumber.isdigit(): pageNumber = int(pageNumber) else: pageNumber = 1 else: if self._request_http(): pageNumber = 1 if authorized and pageNumber >= 1: # if a page wasn't specified then show the first one pageStr = '?page=' + str(pageNumber) outboxFeed = \ person_box_json(self.server.recent_posts_cache, self.server.session, base_dir, domain, port, path + pageStr, http_prefix, max_posts_in_feed, 'outbox', authorized, self.server.newswire_votes_threshold, self.server.positive_voting, self.server.voting_time_mins) else: pageNumber = 1 if self._request_http(): full_width_tl_button_header = \ self.server.full_width_tl_button_header minimalNick = is_minimal(base_dir, domain, nickname) accessKeys = self.server.accessKeys if self.server.keyShortcuts.get(nickname): accessKeys = \ self.server.keyShortcuts[nickname] msg = \ html_outbox(self.server.css_cache, self.server.defaultTimeline, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, pageNumber, max_posts_in_feed, self.server.session, base_dir, self.server.cached_webfingers, self.server.person_cache, nickname, domain, port, outboxFeed, self.server.allow_deletion, http_prefix, self.server.project_version, minimalNick, 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, accessKeys, 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) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_outbox_timeline', self.server.debug) else: if self._secure_mode(): msg = json.dumps(outboxFeed, ensure_ascii=False) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_outbox_timeline json', self.server.debug) else: self._404() return True return False def _show_mod_timeline(self, authorized: bool, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, proxy_type: str, cookie: str, debug: str) -> bool: """Shows the moderation timeline """ if '/users/' in path: if authorized: moderationFeed = \ person_box_json(self.server.recent_posts_cache, self.server.session, base_dir, domain, port, path, http_prefix, max_posts_in_feed, 'moderation', True, 0, self.server.positive_voting, self.server.voting_time_mins) if moderationFeed: if self._request_http(): nickname = path.replace('/users/', '') nickname = nickname.replace('/moderation', '') pageNumber = 1 if '?page=' in nickname: pageNumber = nickname.split('?page=')[1] nickname = nickname.split('?page=')[0] if pageNumber.isdigit(): pageNumber = int(pageNumber) else: pageNumber = 1 if 'page=' not in path: # if no page was specified then show the first moderationFeed = \ person_box_json(self.server.recent_posts_cache, self.server.session, 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 moderationActionStr = '' accessKeys = self.server.accessKeys if self.server.keyShortcuts.get(nickname): accessKeys = \ self.server.keyShortcuts[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 msg = \ html_moderation(self.server.css_cache, self.server.defaultTimeline, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, pageNumber, max_posts_in_feed, self.server.session, base_dir, self.server.cached_webfingers, self.server.person_cache, nickname, domain, port, moderationFeed, 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, moderationActionStr, self.server.theme_name, self.server.peertube_instances, allow_local_network_access, self.server.text_mode_banner, accessKeys, 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) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, 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 = json.dumps(moderationFeed, ensure_ascii=False) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, 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, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, proxy_type: str, cookie: str, debug: str, sharesFileType: str) -> bool: """Shows the shares feed """ shares = \ get_shares_feed_for_person(base_dir, domain, port, path, http_prefix, sharesFileType, shares_per_page) if shares: if self._request_http(): pageNumber = 1 if '?page=' not in path: searchPath = path # get a page of shares, not the summary shares = \ get_shares_feed_for_person(base_dir, domain, port, path + '?page=true', http_prefix, sharesFileType, shares_per_page) else: pageNumberStr = path.split('?page=')[1] if '#' in pageNumberStr: pageNumberStr = pageNumberStr.split('#')[0] if pageNumberStr.isdigit(): pageNumber = int(pageNumberStr) searchPath = path.split('?page=')[0] getPerson = \ person_lookup(domain, searchPath.replace('/' + sharesFileType, ''), base_dir) if getPerson: if not self._establish_session("showSharesFeed"): self._404() self.server.GETbusy = False return True accessKeys = self.server.accessKeys if '/users/' in path: nickname = path.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] if self.server.keyShortcuts.get(nickname): accessKeys = \ self.server.keyShortcuts[nickname] city = get_spoofed_city(self.server.city, base_dir, nickname, domain) shared_items_federated_domains = \ self.server.shared_items_federated_domains msg = \ html_profile(self.server.signing_priv_key_pem, self.server.rss_icon_at_top, self.server.css_cache, self.server.icons_as_buttons, self.server.defaultTimeline, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, self.server.project_version, base_dir, http_prefix, authorized, getPerson, sharesFileType, self.server.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, accessKeys, city, self.server.system_language, self.server.max_like_count, shared_items_federated_domains, shares, pageNumber, shares_per_page, self.server.cw_lists, self.server.lists_enabled, self.server.content_license_url) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_shares_feed', self.server.debug) self.server.GETbusy = False return True else: if self._secure_mode(): msg = json.dumps(shares, ensure_ascii=False) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_shares_feed json', self.server.debug) else: self._404() return True return False def _show_following_feed(self, authorized: bool, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, proxy_type: str, cookie: str, debug: str) -> 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(): pageNumber = 1 if '?page=' not in path: searchPath = 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: pageNumberStr = path.split('?page=')[1] if '#' in pageNumberStr: pageNumberStr = pageNumberStr.split('#')[0] if pageNumberStr.isdigit(): pageNumber = int(pageNumberStr) searchPath = path.split('?page=')[0] getPerson = \ person_lookup(domain, searchPath.replace('/following', ''), base_dir) if getPerson: if not self._establish_session("showFollowingFeed"): self._404() return True accessKeys = self.server.accessKeys city = None if '/users/' in path: nickname = path.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] if self.server.keyShortcuts.get(nickname): accessKeys = \ self.server.keyShortcuts[nickname] city = get_spoofed_city(self.server.city, base_dir, nickname, domain) content_license_url = \ self.server.content_license_url shared_items_federated_domains = \ self.server.shared_items_federated_domains msg = \ html_profile(self.server.signing_priv_key_pem, self.server.rss_icon_at_top, self.server.css_cache, self.server.icons_as_buttons, self.server.defaultTimeline, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, self.server.project_version, base_dir, http_prefix, authorized, getPerson, 'following', self.server.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, accessKeys, city, self.server.system_language, self.server.max_like_count, shared_items_federated_domains, following, pageNumber, follows_per_page, self.server.cw_lists, self.server.lists_enabled, content_license_url).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_following_feed', self.server.debug) return True else: if self._secure_mode(): msg = json.dumps(following, ensure_ascii=False).encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_following_feed json', self.server.debug) else: self._404() return True return False def _show_followers_feed(self, authorized: bool, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, proxy_type: str, cookie: str, debug: str) -> 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(): pageNumber = 1 if '?page=' not in path: searchPath = 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: pageNumberStr = path.split('?page=')[1] if '#' in pageNumberStr: pageNumberStr = pageNumberStr.split('#')[0] if pageNumberStr.isdigit(): pageNumber = int(pageNumberStr) searchPath = path.split('?page=')[0] getPerson = \ person_lookup(domain, searchPath.replace('/followers', ''), base_dir) if getPerson: if not self._establish_session("showFollowersFeed"): self._404() return True accessKeys = self.server.accessKeys city = None if '/users/' in path: nickname = path.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] if self.server.keyShortcuts.get(nickname): accessKeys = \ self.server.keyShortcuts[nickname] city = get_spoofed_city(self.server.city, base_dir, nickname, domain) content_license_url = \ self.server.content_license_url shared_items_federated_domains = \ self.server.shared_items_federated_domains msg = \ html_profile(self.server.signing_priv_key_pem, self.server.rss_icon_at_top, self.server.css_cache, self.server.icons_as_buttons, self.server.defaultTimeline, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.translate, self.server.project_version, base_dir, http_prefix, authorized, getPerson, 'followers', self.server.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, accessKeys, city, self.server.system_language, self.server.max_like_count, shared_items_federated_domains, followers, pageNumber, follows_per_page, self.server.cw_lists, self.server.lists_enabled, content_license_url).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_followers_feed', self.server.debug) return True else: if self._secure_mode(): msg = json.dumps(followers, ensure_ascii=False).encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_followers_feed json', self.server.debug) else: self._404() return True return False def _get_featured_collection(self, calling_domain: str, base_dir: str, path: str, http_prefix: str, nickname: str, domain: str, domain_full: str, system_language: str) -> None: """Returns the featured posts collections in actor/collections/featured """ featuredCollection = \ json_pin_post(base_dir, http_prefix, nickname, domain, domain_full, system_language) msg = json.dumps(featuredCollection, ensure_ascii=False).encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) def _get_featured_tags_collection(self, calling_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 """ postContext = get_individual_post_context() featuredTagsCollection = { '@context': postContext, 'id': http_prefix + '://' + domain_full + path, 'orderedItems': [], 'totalItems': 0, 'type': 'OrderedCollection' } msg = json.dumps(featuredTagsCollection, ensure_ascii=False).encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) def _show_person_profile(self, authorized: bool, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, proxy_type: str, cookie: str, debug: str) -> 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 if self._request_http(): if not self._establish_session("showPersonProfile"): self._404() return True accessKeys = self.server.accessKeys city = None if '/users/' in path: nickname = path.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] if self.server.keyShortcuts.get(nickname): accessKeys = \ self.server.keyShortcuts[nickname] city = get_spoofed_city(self.server.city, base_dir, nickname, domain) msg = \ html_profile(self.server.signing_priv_key_pem, self.server.rss_icon_at_top, self.server.css_cache, self.server.icons_as_buttons, self.server.defaultTimeline, 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', self.server.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, accessKeys, 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).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_person_profile', self.server.debug) else: if self._secure_mode(): acceptStr = self.headers['Accept'] msgStr = json.dumps(actor_json, ensure_ascii=False) msg = msgStr.encode('utf-8') msglen = len(msg) if 'application/ld+json' in acceptStr: self._set_headers('application/ld+json', msglen, cookie, calling_domain, False) elif 'application/jrd+json' in acceptStr: 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(GETstartTime, self.server.fitness, '_GET', '_show_person_profile json', self.server.debug) else: self._404() return True def _show_instance_actor(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, GETstartTime, proxy_type: str, 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 acceptStr = self.headers['Accept'] if onion_domain and calling_domain.endswith('.onion'): actorDomainUrl = 'http://' + onion_domain elif i2p_domain and calling_domain.endswith('.i2p'): actorDomainUrl = 'http://' + i2p_domain else: actorDomainUrl = http_prefix + '://' + domain_full actorUrl = actorDomainUrl + '/users/Actor' removeFields = ( 'icon', 'image', 'tts', 'shares', 'alsoKnownAs', 'hasOccupation', 'featured', 'featuredTags', 'discoverable', 'published', 'devices' ) for r in removeFields: if r in actor_json: del actor_json[r] actor_json['endpoints'] = {} if enable_shared_inbox: actor_json['endpoints'] = { 'sharedInbox': actorDomainUrl + '/inbox' } actor_json['name'] = 'ACTOR' actor_json['preferredUsername'] = domain_full actor_json['id'] = actorDomainUrl + '/actor' actor_json['type'] = 'Application' actor_json['summary'] = 'Instance Actor' actor_json['publicKey']['id'] = actorDomainUrl + '/actor#main-key' actor_json['publicKey']['owner'] = actorDomainUrl + '/actor' actor_json['url'] = actorDomainUrl + '/actor' actor_json['inbox'] = actorUrl + '/inbox' actor_json['followers'] = actorUrl + '/followers' actor_json['following'] = actorUrl + '/following' msgStr = json.dumps(actor_json, ensure_ascii=False) if onion_domain and calling_domain.endswith('.onion'): msgStr = msgStr.replace(http_prefix + '://' + domain_full, 'http://' + onion_domain) elif i2p_domain and calling_domain.endswith('.i2p'): msgStr = msgStr.replace(http_prefix + '://' + domain_full, 'http://' + i2p_domain) msg = msgStr.encode('utf-8') msglen = len(msg) if 'application/ld+json' in acceptStr: self._set_headers('application/ld+json', msglen, cookie, calling_domain, False) elif 'application/jrd+json' in acceptStr: 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(GETstartTime, self.server.fitness, '_GET', '_show_instance_actor', self.server.debug) return True def _show_blog_page(self, authorized: bool, calling_domain: str, path: str, base_dir: str, http_prefix: str, domain: str, domain_full: str, port: int, onion_domain: str, i2p_domain: str, GETstartTime, proxy_type: str, cookie: str, translate: {}, debug: str) -> bool: """Shows a blog page """ pageNumber = 1 nickname = path.split('/blog/')[1] if '/' in nickname: nickname = nickname.split('/')[0] if '?' in nickname: nickname = nickname.split('?')[0] if '?page=' in path: pageNumberStr = path.split('?page=')[1] if '?' in pageNumberStr: pageNumberStr = pageNumberStr.split('?')[0] if '#' in pageNumberStr: pageNumberStr = pageNumberStr.split('#')[0] if pageNumberStr.isdigit(): pageNumber = int(pageNumberStr) if pageNumber < 1: pageNumber = 1 elif pageNumber > 10: pageNumber = 10 if not self._establish_session("showBlogPage"): self._404() self.server.GETbusy = False return True msg = html_blog_page(authorized, self.server.session, base_dir, http_prefix, translate, nickname, domain, port, max_posts_in_blogs_feed, pageNumber, 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(GETstartTime, self.server.fitness, '_GET', '_show_blog_page', self.server.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, GETstartTime, authorized: bool, debug: bool): """Redirects to the login screen if necessary """ divertToLoginScreen = 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 \ '/avatars/' not in path and \ '/favicons/' not in path and \ '/headers/' not in path and \ '/fonts/' not in path and \ '/icons/' not in path: divertToLoginScreen = True if path.startswith('/users/'): nickStr = path.split('/users/')[1] if '/' not in nickStr and '?' not in nickStr: divertToLoginScreen = 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'): divertToLoginScreen = False if divertToLoginScreen and not authorized: divertPath = '/login' if self.server.news_instance: # for news instances if not logged in then show the # front page divertPath = '/users/news' # if debug: print('DEBUG: divertToLoginScreen=' + str(divertToLoginScreen)) print('DEBUG: authorized=' + str(authorized)) print('DEBUG: path=' + path) if calling_domain.endswith('.onion') and onion_domain: self._redirect_headers('http://' + onion_domain + divertPath, None, calling_domain) elif calling_domain.endswith('.i2p') and i2p_domain: self._redirect_headers('http://' + i2p_domain + divertPath, None, calling_domain) else: self._redirect_headers(http_prefix + '://' + domain_full + divertPath, None, calling_domain) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_redirect_to_login_screen', self.server.debug) return True return False def _get_style_sheet(self, calling_domain: str, path: str, GETstartTime) -> 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] if os.path.isfile(path): tries = 0 while tries < 5: try: css = get_css(self.server.base_dir, path, self.server.css_cache) if css: break except Exception as ex: print('ERROR: _get_style_sheet ' + str(tries) + ' ' + str(ex)) time.sleep(1) tries += 1 msg = css.encode('utf-8') msglen = len(msg) self._set_headers('text/css', msglen, None, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_get_style_sheet', self.server.debug) return True self._404() return True def _show_q_rcode(self, calling_domain: str, path: str, base_dir: str, domain: str, port: int, GETstartTime) -> bool: """Shows a QR code for an account """ nickname = get_nickname_from_actor(path) save_person_qrcode(base_dir, nickname, domain, port) qrFilename = \ acct_dir(base_dir, nickname, domain) + '/qrcode.png' if os.path.isfile(qrFilename): if self._etag_exists(qrFilename): # The file has not changed self._304() return tries = 0 mediaBinary = None while tries < 5: try: with open(qrFilename, 'rb') as avFile: mediaBinary = avFile.read() break except Exception as ex: print('ERROR: _show_q_rcode ' + str(tries) + ' ' + str(ex)) time.sleep(1) tries += 1 if mediaBinary: mimeType = media_file_mime_type(qrFilename) self._set_headers_etag(qrFilename, mimeType, mediaBinary, None, self.server.domain_full, False, None) self._write(mediaBinary) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_q_rcode', self.server.debug) return True self._404() return True def _search_screen_banner(self, calling_domain: str, path: str, base_dir: str, domain: str, port: int, GETstartTime) -> bool: """Shows a banner image on the search screen """ nickname = get_nickname_from_actor(path) bannerFilename = \ acct_dir(base_dir, nickname, domain) + '/search_banner.png' if not os.path.isfile(bannerFilename): if os.path.isfile(base_dir + '/theme/default/search_banner.png'): copyfile(base_dir + '/theme/default/search_banner.png', bannerFilename) if os.path.isfile(bannerFilename): if self._etag_exists(bannerFilename): # The file has not changed self._304() return True tries = 0 mediaBinary = None while tries < 5: try: with open(bannerFilename, 'rb') as avFile: mediaBinary = avFile.read() break except Exception as ex: print('ERROR: _search_screen_banner ' + str(tries) + ' ' + str(ex)) time.sleep(1) tries += 1 if mediaBinary: mimeType = media_file_mime_type(bannerFilename) self._set_headers_etag(bannerFilename, mimeType, mediaBinary, None, self.server.domain_full, False, None) self._write(mediaBinary) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_search_screen_banner', self.server.debug) return True self._404() return True def _column_image(self, side: str, calling_domain: str, path: str, base_dir: str, domain: str, port: int, GETstartTime) -> 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 bannerFilename = \ acct_dir(base_dir, nickname, domain) + '/' + \ side + '_col_image.png' if os.path.isfile(bannerFilename): if self._etag_exists(bannerFilename): # The file has not changed self._304() return True tries = 0 mediaBinary = None while tries < 5: try: with open(bannerFilename, 'rb') as avFile: mediaBinary = avFile.read() break except Exception as ex: print('ERROR: _column_image ' + str(tries) + ' ' + str(ex)) time.sleep(1) tries += 1 if mediaBinary: mimeType = media_file_mime_type(bannerFilename) self._set_headers_etag(bannerFilename, mimeType, mediaBinary, None, self.server.domain_full, False, None) self._write(mediaBinary) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_column_image ' + side, self.server.debug) return True self._404() return True def _show_background_image(self, calling_domain: str, path: str, base_dir: str, GETstartTime) -> bool: """Show a background image """ imageExtensions = get_image_extensions() for ext in imageExtensions: for bg in ('follow', 'options', 'login', 'welcome'): # follow screen background image if path.endswith('/' + bg + '-background.' + ext): bgFilename = \ base_dir + '/accounts/' + \ bg + '-background.' + ext if os.path.isfile(bgFilename): if self._etag_exists(bgFilename): # The file has not changed self._304() return True tries = 0 bgBinary = None while tries < 5: try: with open(bgFilename, 'rb') as avFile: bgBinary = avFile.read() break except Exception as ex: print('ERROR: _show_background_image ' + str(tries) + ' ' + str(ex)) time.sleep(1) tries += 1 if bgBinary: if ext == 'jpg': ext = 'jpeg' self._set_headers_etag(bgFilename, 'image/' + ext, bgBinary, None, self.server.domain_full, False, None) self._write(bgBinary) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_background_image', self.server.debug) return True self._404() return True def _show_default_profile_background(self, calling_domain: str, path: str, base_dir: str, theme_name: str, GETstartTime) -> bool: """If a background image is missing after searching for a handle then substitute this image """ imageExtensions = get_image_extensions() for ext in imageExtensions: bgFilename = \ base_dir + '/theme/' + theme_name + '/image.' + ext if os.path.isfile(bgFilename): if self._etag_exists(bgFilename): # The file has not changed self._304() return True tries = 0 bgBinary = None while tries < 5: try: with open(bgFilename, 'rb') as avFile: bgBinary = avFile.read() break except Exception as ex: print('ERROR: _show_default_profile_background ' + str(tries) + ' ' + str(ex)) time.sleep(1) tries += 1 if bgBinary: if ext == 'jpg': ext = 'jpeg' self._set_headers_etag(bgFilename, 'image/' + ext, bgBinary, None, self.server.domain_full, False, None) self._write(bgBinary) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_default_profile_background', self.server.debug) return True break self._404() return True def _show_share_image(self, calling_domain: str, path: str, base_dir: str, GETstartTime) -> bool: """Show a shared item image """ if not is_image_file(path): self._404() return True mediaStr = path.split('/sharefiles/')[1] mediaFilename = base_dir + '/sharefiles/' + mediaStr if not os.path.isfile(mediaFilename): self._404() return True if self._etag_exists(mediaFilename): # The file has not changed self._304() return True mediaFileType = get_image_mime_type(mediaFilename) mediaBinary = None try: with open(mediaFilename, 'rb') as avFile: mediaBinary = avFile.read() except OSError: print('EX: unable to read binary ' + mediaFilename) if mediaBinary: self._set_headers_etag(mediaFilename, mediaFileType, mediaBinary, None, self.server.domain_full, False, None) self._write(mediaBinary) fitness_performance(GETstartTime, self.server.fitness, '_GET', '_show_share_image', self.server.debug) return True def _show_avatar_or_banner(self, refererDomain: str, path: str, base_dir: str, domain: str, GETstartTime) -> 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: avatarStr = path.split('/system/accounts/avatars/')[1] elif '/accounts/avatars/' in path: avatarStr = path.split('/accounts/avatars/')[1] elif '/system/accounts/headers/' in path: avatarStr = path.split('/system/accounts/headers/')[1] elif '/accounts/headers/' in path: avatarStr = path.split('/accounts/headers/')[1] else: avatarStr = path.split('/users/')[1] if not ('/' in avatarStr and '.temp.' not in path): return False avatarNickname = avatarStr.split('/')[0] avatarFile = avatarStr.split('/')[1] avatarFileExt = avatarFile.split('.')[-1] # remove any numbers, eg. avatar123.png becomes avatar.png if avatarFile.startswith('avatar'): avatarFile = 'avatar.' + avatarFileExt elif avatarFile.startswith('banner'): avatarFile = 'banner.' + avatarFileExt elif avatarFile.startswith('search_banner'): avatarFile = 'search_banner.' + avatarFileExt elif avatarFile.startswith('image'): avatarFile = 'image.' + avatarFileExt elif avatarFile.startswith('left_col_image'): avatarFile = 'left_col_image.' + avatarFileExt elif avatarFile.startswith('right_col_image'): avatarFile = 'right_col_image.' + avatarFileExt avatarFilename = \ acct_dir(base_dir, avatarNickname, domain) + '/' + avatarFile if not os.path.isfile(avatarFilename): originalExt = avatarFileExt originalAvatarFile = avatarFile altExt = get_image_extensions() altFound = False for alt in altExt: if alt == originalExt: continue avatarFile = \ originalAvatarFile.replace('.' + originalExt, '.' + alt) avatarFilename = \ acct_dir(base_dir, avatarNickname, domain) + \ '/' + avatarFile if os.path.isfile(avatarFilename): altFound = True break if not altFound: return False if self._etag_exists(avatarFilename): # The file has not changed self._304() return True t = os.path.getmtime(avatarFilename) lastModifiedTime = datetime.datetime.fromtimestamp(t) lastModifiedTimeStr = \ lastModifiedTime.strftime('%a, %d %b %Y %H:%M:%S GMT') mediaImageType = get_image_mime_type(avatarFile) mediaBinary = None try: with open(avatarFilename, 'rb') as avFile: mediaBinary = avFile.read() except OSError: print('EX: unable to read avatar ' + avatarFilename) if mediaBinary: self._set_headers_etag(avatarFilename, mediaImageType, mediaBinary, None, refererDomain, True, lastModifiedTimeStr) self._write(mediaBinary) fitness_performance(GETstartTime, 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, GETstartTime) -> bool: """Confirm whether to delete a calendar event """ post_id = path.split('?eventid=')[1] if '?' in post_id: post_id = post_id.split('?')[0] postTime = path.split('?time=')[1] if '?' in postTime: postTime = postTime.split('?')[0] postYear = path.split('?year=')[1] if '?' in postYear: postYear = postYear.split('?')[0] postMonth = path.split('?month=')[1] if '?' in postMonth: postMonth = postMonth.split('?')[0] postDay = path.split('?day=')[1] if '?' in postDay: postDay = postDay.split('?')[0] # show the confirmation screen screen msg = html_calendar_delete_confirm(self.server.css_cache, translate, base_dir, path, http_prefix, domain_full, post_id, postTime, postYear, postMonth, postDay, 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(GETstartTime, self.server.fitness, '_GET', '_confirm_delete_event', self.server.debug) return True msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) return True def _show_new_post(self, calling_domain: str, path: str, media_instance: bool, translate: {}, base_dir: str, http_prefix: str, inReplyToUrl: str, replyToList: [], shareDescription: str, replyPageNumber: int, replyCategory: str, domain: str, domain_full: str, GETstartTime, cookie, noDropDown: bool, conversationId: str) -> bool: """Shows the new post screen """ isNewPostEndpoint = False if '/users/' in path and '/new' in path: # Various types of new post in the web interface newPostEndpoints = get_new_post_endpoints() for currPostType in newPostEndpoints: if path.endswith('/' + currPostType): isNewPostEndpoint = True break if isNewPostEndpoint: nickname = get_nickname_from_actor(path) if inReplyToUrl: replyIntervalHours = self.server.default_reply_interval_hrs if not can_reply_to(base_dir, nickname, domain, inReplyToUrl, replyIntervalHours): print('Reply outside of time window ' + inReplyToUrl + str(replyIntervalHours) + ' hours') self._403() return True elif self.server.debug: print('Reply is within time interval: ' + str(replyIntervalHours) + ' hours') accessKeys = self.server.accessKeys if self.server.keyShortcuts.get(nickname): accessKeys = self.server.keyShortcuts[nickname] customSubmitText = get_config_param(base_dir, 'customSubmitText') post_json_object = None if inReplyToUrl: replyPostFilename = \ locate_post(base_dir, nickname, domain, inReplyToUrl) if replyPostFilename: post_json_object = load_json(replyPostFilename) msg = html_new_post(self.server.css_cache, media_instance, translate, base_dir, http_prefix, path, inReplyToUrl, replyToList, shareDescription, None, replyPageNumber, replyCategory, nickname, domain, domain_full, self.server.defaultTimeline, self.server.newswire, self.server.theme_name, noDropDown, accessKeys, customSubmitText, conversationId, self.server.recent_posts_cache, self.server.max_recent_posts, self.server.session, self.server.cached_webfingers, self.server.person_cache, self.server.port, post_json_object, self.server.project_version, self.server.yt_replace_domain, self.server.twitter_replacement_domain, self.server.show_published_date_only, self.server.peertube_instances, self.server.allow_local_network_access, self.server.system_language, self.server.max_like_count, self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, self.server.defaultTimeline).encode('utf-8') if not msg: print('Error replying to ' + inReplyToUrl) self._404() return True msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, 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, knownCrawlers: {}) -> 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 crawlersList = [] curr_time = int(time.time()) recentCrawlers = 60 * 60 * 24 * 30 for uaStr, item in knownCrawlers.items(): if item['lastseen'] - curr_time < recentCrawlers: hitsStr = str(item['hits']).zfill(8) crawlersList.append(hitsStr + ' ' + uaStr) crawlersList.sort(reverse=True) msg = '' for lineStr in crawlersList: msg += lineStr + '\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) if nickname: city = get_spoofed_city(self.server.city, base_dir, nickname, domain) else: city = self.server.city accessKeys = self.server.accessKeys if '/users/' in path: if self.server.keyShortcuts.get(nickname): accessKeys = self.server.keyShortcuts[nickname] default_reply_interval_hrs = self.server.default_reply_interval_hrs msg = html_edit_profile(self.server.css_cache, translate, base_dir, path, domain, port, http_prefix, self.server.defaultTimeline, self.server.theme_name, peertube_instances, self.server.text_mode_banner, city, self.server.user_agents_blocked, accessKeys, default_reply_interval_hrs, self.server.cw_lists, self.server.lists_enabled).encode('utf-8') if msg: 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] accessKeys = self.server.accessKeys if self.server.keyShortcuts.get(nickname): accessKeys = self.server.keyShortcuts[nickname] msg = html_edit_links(self.server.css_cache, translate, base_dir, path, domain, port, http_prefix, self.server.defaultTimeline, theme, accessKeys).encode('utf-8') if msg: 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] accessKeys = self.server.accessKeys if self.server.keyShortcuts.get(nickname): accessKeys = self.server.keyShortcuts[nickname] msg = html_edit_newswire(self.server.css_cache, translate, base_dir, path, domain, port, http_prefix, self.server.defaultTimeline, self.server.theme_name, accessKeys).encode('utf-8') if msg: 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_post(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: postActor = 'news' if '?actor=' in path: postActor = path.split('?actor=')[1] if '?' in postActor: postActor = postActor.split('?')[0] post_id = path.split('/editnewspost=')[1] if '?' in post_id: post_id = post_id.split('?')[0] postUrl = local_actor_url(http_prefix, postActor, domain_full) + \ '/statuses/' + post_id path = path.split('/editnewspost=')[0] msg = html_edit_news_post(self.server.css_cache, translate, base_dir, path, domain, port, http_prefix, postUrl, 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, http_prefix: str, domain: str, port: int, followingItemsPerPage: int, debug: bool, listName='following') -> None: """Returns json collection for following.txt """ followingJson = \ get_following_feed(base_dir, domain, port, path, http_prefix, True, followingItemsPerPage, listName) if not followingJson: if debug: print(listName + ' json feed not found for ' + path) self._404() return msg = json.dumps(followingJson, ensure_ascii=False).encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) def _send_block(self, http_prefix: str, blockerNickname: str, blockerDomainFull: str, blockingNickname: str, blockingDomainFull: str) -> bool: if blockerDomainFull == blockingDomainFull: if blockerNickname == blockingNickname: # don't block self return False blockActor = \ local_actor_url(http_prefix, blockerNickname, blockerDomainFull) toUrl = 'https://www.w3.org/ns/activitystreams#Public' ccUrl = blockActor + '/followers' blockedUrl = \ http_prefix + '://' + blockingDomainFull + \ '/@' + blockingNickname blockJson = { "@context": "https://www.w3.org/ns/activitystreams", 'type': 'Block', 'actor': blockActor, 'object': blockedUrl, 'to': [toUrl], 'cc': [ccUrl] } self._post_to_outbox(blockJson, self.server.project_version, blockerNickname) return True def _get_referer_domain(self, uaStr: str) -> str: """Returns the referer domain Which domain is the GET request coming from? """ refererDomain = None if self.headers.get('referer'): refererDomain, refererPort = \ get_domain_from_actor(self.headers['referer']) refererDomain = get_full_domain(refererDomain, refererPort) elif self.headers.get('Referer'): refererDomain, refererPort = \ get_domain_from_actor(self.headers['Referer']) refererDomain = get_full_domain(refererDomain, refererPort) elif self.headers.get('Signature'): if 'keyId="' in self.headers['Signature']: refererDomain = self.headers['Signature'].split('keyId="')[1] if '/' in refererDomain: refererDomain = refererDomain.split('/')[0] elif '#' in refererDomain: refererDomain = refererDomain.split('#')[0] elif '"' in refererDomain: refererDomain = refererDomain.split('"')[0] elif uaStr: if '+https://' in uaStr: refererDomain = uaStr.split('+https://')[1] if '/' in refererDomain: refererDomain = refererDomain.split('/')[0] elif ')' in refererDomain: refererDomain = refererDomain.split(')')[0] elif '+http://' in uaStr: refererDomain = uaStr.split('+http://')[1] if '/' in refererDomain: refererDomain = refererDomain.split('/')[0] elif ')' in refererDomain: refererDomain = refererDomain.split(')')[0] return refererDomain def _get_user_agent(self) -> str: """Returns the user agent string from the headers """ uaStr = None if self.headers.get('User-Agent'): uaStr = self.headers['User-Agent'] elif self.headers.get('user-agent'): uaStr = self.headers['user-agent'] elif self.headers.get('User-agent'): uaStr = self.headers['User-agent'] return uaStr 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 != self.server.domain and \ calling_domain != self.server.domain_full and \ calling_domain != self.server.onion_domain: print('GET domain blocked: ' + calling_domain) self._400() return elif self.server.i2p_domain: if calling_domain != self.server.domain and \ calling_domain != self.server.domain_full and \ calling_domain != self.server.i2p_domain: print('GET domain blocked: ' + calling_domain) self._400() return else: if calling_domain != self.server.domain and \ calling_domain != self.server.domain_full: print('GET domain blocked: ' + calling_domain) self._400() return uaStr = self._get_user_agent() if not self._permitted_crawler_path(self.path): if self._blocked_user_agent(calling_domain, uaStr): self._400() return refererDomain = self._get_referer_domain(uaStr) GETstartTime = time.time() fitness_performance(GETstartTime, self.server.fitness, '_GET', 'start', self.server.debug) # Since fediverse crawlers are quite active, # make returning info to them high priority # get nodeinfo endpoint if self._nodeinfo(uaStr, calling_domain): return fitness_performance(GETstartTime, self.server.fitness, '_GET', '_nodeinfo[calling_domain]', self.server.debug) if self.path == '/logout': if not self.server.news_instance: msg = \ html_login(self.server.css_cache, self.server.translate, self.server.base_dir, self.server.http_prefix, self.server.domain_full, self.server.system_language, False).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(GETstartTime, self.server.fitness, '_GET', 'logout', self.server.debug) return fitness_performance(GETstartTime, 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: statusNumberStr = nickname.split('/')[1] if statusNumberStr.isdigit(): nickname = nickname.split('/')[0] self.path = \ self.path.replace('/users/' + nickname + '/', '/users/' + nickname + '/statuses/') # instance actor if self.path == '/actor' or \ self.path == '/users/actor' or \ self.path == '/Actor' or \ self.path == '/users/Actor': self.path = '/users/inbox' if self._show_instance_actor(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, GETstartTime, self.server.proxy_type, None, self.server.debug, self.server.enable_shared_inbox): return else: self._404() return # turn off dropdowns on new post screen noDropDown = False if self.path.endswith('?nodropdown'): noDropDown = 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.GETbusy)) if self.server.debug: print(str(self.headers)) cookie = None if self.headers.get('Cookie'): cookie = self.headers['Cookie'] fitness_performance(GETstartTime, 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(calling_domain, GETstartTime) return else: self.path = '/' if '/browserconfig.xml' in self.path: if self._has_accept(calling_domain): self._browser_config(calling_domain, GETstartTime) 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') else: print('GET Not authorized') fitness_performance(GETstartTime, self.server.fitness, '_GET', 'isAuthorized', self.server.debug) # 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): catalogAuthorized = authorized if not catalogAuthorized: 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'): permittedDomains = \ self.server.shared_items_federated_domains sharedItemTokens = self.server.sharedItemFederationTokens if authorize_shared_items(permittedDomains, self.server.base_dir, self.headers['Origin'], calling_domain, self.headers['Authorization'], self.server.debug, sharedItemTokens): catalogAuthorized = 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 catalogAuthorized: catalogType = 'json' if self.path.endswith('.csv') or self._request_csv(): catalogType = 'csv' elif self.path.endswith('.json') or not self._request_http(): catalogType = 'json' if self.server.debug: print('Preparing DFC catalog in format ' + catalogType) if catalogType == 'json': # catalog as a json if not self.path.startswith('/users/'): if self.server.debug: print('Catalog for the instance') catalogJson = \ 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 catalogJson = \ shares_catalog_account_endpoint(base_dir, http_prefix, nickname, self.server.domain, domain_full, self.path, self.server.debug, 'shares') msg = json.dumps(catalogJson, ensure_ascii=False).encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) return elif catalogType == '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): catalogAuthorized = authorized if not catalogAuthorized: 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'): permittedDomains = \ self.server.shared_items_federated_domains sharedItemTokens = self.server.sharedItemFederationTokens if authorize_shared_items(permittedDomains, self.server.base_dir, self.headers['Origin'], calling_domain, self.headers['Authorization'], self.server.debug, sharedItemTokens): catalogAuthorized = 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 catalogAuthorized: catalogType = 'json' if self.path.endswith('.csv') or self._request_csv(): catalogType = 'csv' elif self.path.endswith('.json') or not self._request_http(): catalogType = 'json' if self.server.debug: print('Preparing DFC wanted catalog in format ' + catalogType) if catalogType == 'json': # catalog as a json if not self.path.startswith('/users/'): if self.server.debug: print('Wanted catalog for the instance') catalogJson = \ 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 catalogJson = \ shares_catalog_account_endpoint(base_dir, http_prefix, nickname, self.server.domain, domain_full, self.path, self.server.debug, 'wanted') msg = json.dumps(catalogJson, ensure_ascii=False).encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) return elif catalogType == '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, uaStr, authorized, self.server.http_prefix, self.server.base_dir, self.authorizedNickname, 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.customEmoji, self.server.show_node_info_accounts): return fitness_performance(GETstartTime, self.server.fitness, '_GET', '_masto_api[calling_domain]', self.server.debug) if not self._establish_session("GET"): self._404() fitness_performance(GETstartTime, self.server.fitness, '_GET', 'session fail', self.server.debug) self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'create session', self.server.debug) # is this a html request? htmlGET = False if self._has_accept(calling_domain): if self._request_http(): htmlGET = 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(GETstartTime, 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(refererDomain, self.path, self.server.base_dir, GETstartTime) return # get css # Note that this comes before the busy flag to avoid conflicts if self.path.endswith('.css'): if self._get_style_sheet(calling_domain, self.path, GETstartTime): return if authorized and '/exports/' in self.path: self._get_exported_theme(calling_domain, self.path, self.server.base_dir, self.server.domain_full, self.server.debug) return # get fonts if '/fonts/' in self.path: self._get_fonts(calling_domain, self.path, self.server.base_dir, self.server.debug, GETstartTime) return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'fonts', self.server.debug) if self.path == '/sharedInbox' or \ self.path == '/users/inbox' or \ self.path == '/actor/inbox' or \ self.path == '/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(GETstartTime, self.server.fitness, '_GET', 'sharedInbox enabled', self.server.debug) if self.path == '/categories.xml': self._get_hashtag_categories_feed(authorized, calling_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.port, self.server.proxy_type, GETstartTime, self.server.debug) return if self.path == '/newswire.xml': self._get_newswire_feed(authorized, calling_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.port, self.server.proxy_type, GETstartTime, self.server.debug) 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(authorized, calling_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.port, self.server.proxy_type, GETstartTime, self.server.debug) else: self._get_rss2site(authorized, calling_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain_full, self.server.port, self.server.proxy_type, self.server.translate, GETstartTime, self.server.debug) return fitness_performance(GETstartTime, 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(authorized, calling_domain, self.path, self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.port, self.server.proxy_type, GETstartTime, self.server.debug, self.server.system_language) return usersInPath = False if '/users/' in self.path: usersInPath = True if authorized and not htmlGET and usersInPath: if '/following?page=' in self.path: self._get_following_json(self.server.base_dir, self.path, calling_domain, self.server.http_prefix, self.server.domain, self.server.port, self.server.followingItemsPerPage, self.server.debug, 'following') return elif '/followers?page=' in self.path: self._get_following_json(self.server.base_dir, self.path, calling_domain, self.server.http_prefix, self.server.domain, self.server.port, self.server.followingItemsPerPage, self.server.debug, 'followers') return elif '/followrequests?page=' in self.path: self._get_following_json(self.server.base_dir, self.path, calling_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 usersInPath and \ self.path.endswith('/speaker'): if 'application/ssml' not in self.headers['Accept']: # json endpoint self._get_speaker(calling_domain, self.path, self.server.base_dir, self.server.domain, self.server.debug) else: xmlStr = \ get_ssm_lbox(self.server.base_dir, self.path, self.server.domain, self.server.system_language, self.server.instanceTitle, 'inbox') if xmlStr: msg = xmlStr.encode('utf-8') msglen = len(msg) self._set_headers('application/xrd+xml', msglen, None, calling_domain, False) self._write(msg) return # redirect to the welcome screen if htmlGET and authorized and usersInPath 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.authorizedNickname 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 htmlGET and \ usersInPath and self.path.endswith('/pinned'): nickname = self.path.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] pinnedPostJson = \ 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 pinnedPostJson: post_id = remove_id_ending(pinnedPostJson['id']) message_json = \ outbox_message_create_wrap(self.server.http_prefix, nickname, self.server.domain, self.server.port, pinnedPostJson) 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 = json.dumps(message_json, ensure_ascii=False).encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) return if not htmlGET and \ usersInPath 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, self.server.base_dir, self.path, self.server.http_prefix, nickname, self.server.domain, self.server.domain_full, self.server.system_language) return if not htmlGET and \ usersInPath and self.path.endswith('/collections/featuredTags'): self._get_featured_tags_collection(calling_domain, self.path, self.server.http_prefix, self.server.domain_full) return fitness_performance(GETstartTime, 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 htmlGET and not graph.endswith('.json'): if graph == 'post': graph = '_POST' 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(GETstartTime, self.server.fitness, '_GET', 'graph', self.server.debug) return else: graph = graph.replace('.json', '') if graph == 'post': graph = '_POST' elif graph == 'get': graph = '_GET' watchPointsJson = \ sorted_watch_points(self.server.fitness, graph) msg = json.dumps(watchPointsJson, ensure_ascii=False).encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', 'graph json', self.server.debug) return # show the main blog page if htmlGET and (self.path == '/blog' or self.path == '/blog/' or self.path == '/blogs' or self.path == '/blogs/'): if '/rss.xml' not in self.path: if not self._establish_session("show the main blog page"): self._404() return msg = html_blog_view(authorized, self.server.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(GETstartTime, self.server.fitness, '_GET', 'blog view', self.server.debug) return self._404() return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'blog view done', self.server.debug) # show a particular page of blog entries # for a particular account if htmlGET 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.domain_full, self.server.port, self.server.onion_domain, self.server.i2p_domain, GETstartTime, self.server.proxy_type, cookie, self.server.translate, self.server.debug): return # list of registered devices for e2ee # see https://github.com/tootsuite/mastodon/pull/13820 if authorized and usersInPath: if self.path.endswith('/collections/devices'): nickname = self.path.split('/users/') if '/' in nickname: nickname = nickname.split('/')[0] devJson = e2e_edevices_collection(self.server.base_dir, nickname, self.server.domain, self.server.domain_full, self.server.http_prefix) msg = json.dumps(devJson, ensure_ascii=False).encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', 'registered devices', self.server.debug) return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'registered devices done', self.server.debug) if htmlGET and usersInPath: # 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, GETstartTime, self.server.onion_domain, self.server.i2p_domain, cookie, self.server.debug, authorized) return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'person options done', self.server.debug) # show blog post blogFilename, nickname = \ path_contains_blog_link(self.server.base_dir, self.server.http_prefix, self.server.domain, self.server.domain_full, self.path) if blogFilename and nickname: post_json_object = load_json(blogFilename) if is_blog_post(post_json_object): msg = html_blog_post(self.server.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(GETstartTime, self.server.fitness, '_GET', 'blog post 2', self.server.debug) return self._404() return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'blog post 2 done', self.server.debug) # after selecting a shared item from the left column then show it if htmlGET and '?showshare=' in self.path and '/users/' in self.path: itemID = self.path.split('?showshare=')[1] if '?' in itemID: itemID = itemID.split('?')[0] category = '' if '?category=' in self.path: category = self.path.split('?category=')[1] if '?' in category: category = category.split('?')[0] usersPath = self.path.split('?showshare=')[0] nickname = usersPath.replace('/users/', '') itemID = urllib.parse.unquote_plus(itemID.strip()) msg = \ html_show_share(self.server.base_dir, self.server.domain, nickname, self.server.http_prefix, self.server.domain_full, itemID, self.server.translate, self.server.shared_items_federated_domains, self.server.defaultTimeline, 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 + usersPath elif (calling_domain.endswith('.i2p') and self.server.i2p_domain): actor = 'http://' + self.server.i2p_domain + usersPath 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(GETstartTime, self.server.fitness, '_GET', 'html_show_share', self.server.debug) return # after selecting a wanted item from the left column then show it if htmlGET and '?showwanted=' in self.path and '/users/' in self.path: itemID = self.path.split('?showwanted=')[1] if ';' in itemID: itemID = itemID.split(';')[0] category = self.path.split('?category=')[1] if ';' in category: category = category.split(';')[0] usersPath = self.path.split('?showwanted=')[0] nickname = usersPath.replace('/users/', '') itemID = urllib.parse.unquote_plus(itemID.strip()) msg = \ html_show_share(self.server.base_dir, self.server.domain, nickname, self.server.http_prefix, self.server.domain_full, itemID, self.server.translate, self.server.shared_items_federated_domains, self.server.defaultTimeline, 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 + usersPath elif (calling_domain.endswith('.i2p') and self.server.i2p_domain): actor = 'http://' + self.server.i2p_domain + usersPath 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(GETstartTime, self.server.fitness, '_GET', 'htmlShowWanted', self.server.debug) return # remove a shared item if htmlGET and '?rmshare=' in self.path: itemID = self.path.split('?rmshare=')[1] itemID = urllib.parse.unquote_plus(itemID.strip()) usersPath = self.path.split('?rmshare=')[0] actor = \ self.server.http_prefix + '://' + \ self.server.domain_full + usersPath msg = html_confirm_remove_shared_item(self.server.css_cache, self.server.translate, self.server.base_dir, actor, itemID, calling_domain, 'shares') if not msg: if calling_domain.endswith('.onion') and \ self.server.onion_domain: actor = 'http://' + self.server.onion_domain + usersPath elif (calling_domain.endswith('.i2p') and self.server.i2p_domain): actor = 'http://' + self.server.i2p_domain + usersPath 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(GETstartTime, self.server.fitness, '_GET', 'remove shared item', self.server.debug) return # remove a wanted item if htmlGET and '?rmwanted=' in self.path: itemID = self.path.split('?rmwanted=')[1] itemID = urllib.parse.unquote_plus(itemID.strip()) usersPath = self.path.split('?rmwanted=')[0] actor = \ self.server.http_prefix + '://' + \ self.server.domain_full + usersPath msg = html_confirm_remove_shared_item(self.server.css_cache, self.server.translate, self.server.base_dir, actor, itemID, calling_domain, 'wanted') if not msg: if calling_domain.endswith('.onion') and \ self.server.onion_domain: actor = 'http://' + self.server.onion_domain + usersPath elif (calling_domain.endswith('.i2p') and self.server.i2p_domain): actor = 'http://' + self.server.i2p_domain + usersPath 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(GETstartTime, self.server.fitness, '_GET', 'remove shared item', self.server.debug) return fitness_performance(GETstartTime, 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.css_cache, 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.css_cache, self.server.base_dir, 'http', self.server.i2p_domain) else: msg = html_terms_of_service(self.server.css_cache, 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(GETstartTime, self.server.fitness, '_GET', 'terms of service shown', self.server.debug) return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'terms of service done', self.server.debug) # show a list of who you are following if htmlGET and authorized and usersInPath and \ self.path.endswith('/followingaccounts'): nickname = get_nickname_from_actor(self.path) followingFilename = \ acct_dir(self.server.base_dir, nickname, self.server.domain) + '/following.txt' if not os.path.isfile(followingFilename): self._404() return msg = html_following_list(self.server.css_cache, self.server.base_dir, followingFilename) msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg.encode('utf-8')) fitness_performance(GETstartTime, self.server.fitness, '_GET', 'following accounts shown', self.server.debug) return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'following accounts done', self.server.debug) if self.path.endswith('/about'): if calling_domain.endswith('.onion'): msg = \ html_about(self.server.css_cache, 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.css_cache, self.server.base_dir, 'http', self.server.i2p_domain, None, self.server.translate, self.server.system_language) else: msg = \ html_about(self.server.css_cache, 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(GETstartTime, self.server.fitness, '_GET', 'show about screen', self.server.debug) return if htmlGET and usersInPath and authorized and \ self.path.endswith('/accesskeys'): nickname = self.path.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] accessKeys = self.server.accessKeys if self.server.keyShortcuts.get(nickname): accessKeys = \ self.server.keyShortcuts[nickname] msg = \ html_access_keys(self.server.css_cache, self.server.base_dir, nickname, self.server.domain, self.server.translate, accessKeys, self.server.accessKeys, self.server.defaultTimeline) msg = msg.encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', 'show accesskeys screen', self.server.debug) return if htmlGET and usersInPath 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.css_cache, self.server.base_dir, nickname, self.server.domain, self.server.translate, self.server.defaultTimeline, self.server.theme_name, self.server.accessKeys) msg = msg.encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', 'show theme designer screen', self.server.debug) return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'show about screen done', self.server.debug) # the initial welcome screen after first logging in if htmlGET 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(GETstartTime, self.server.fitness, '_GET', 'show welcome screen', self.server.debug) return else: self.path = self.path.replace('/welcome', '') # the welcome screen which allows you to set an avatar image if htmlGET 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(GETstartTime, self.server.fitness, '_GET', 'show welcome profile screen', self.server.debug) return else: self.path = self.path.replace('/welcome_profile', '') # the final welcome screen if htmlGET 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.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(GETstartTime, self.server.fitness, '_GET', 'show welcome final screen', self.server.debug) return else: self.path = self.path.replace('/welcome_final', '') # if not authorized then show the login screen if htmlGET and self.path != '/login' and \ not is_image_file(self.path) and \ self.path != '/' and \ self.path != '/users/news/linksmobile' and \ self.path != '/users/news/newswiremobile': if self._redirect_to_login_screen(calling_domain, self.path, self.server.http_prefix, self.server.domain_full, self.server.onion_domain, self.server.i2p_domain, GETstartTime, authorized, self.server.debug): return fitness_performance(GETstartTime, 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 == '/logo72.png' or \ self.path == '/logo96.png' or \ self.path == '/logo128.png' or \ self.path == '/logo144.png' or \ self.path == '/logo150.png' or \ self.path == '/logo192.png' or \ self.path == '/logo256.png' or \ self.path == '/logo512.png' or \ self.path == '/apple-touch-icon.png': mediaFilename = \ self.server.base_dir + '/img' + self.path if os.path.isfile(mediaFilename): if self._etag_exists(mediaFilename): # The file has not changed self._304() return tries = 0 mediaBinary = None while tries < 5: try: with open(mediaFilename, 'rb') as avFile: mediaBinary = avFile.read() break except Exception as ex: print('ERROR: manifest logo ' + str(tries) + ' ' + str(ex)) time.sleep(1) tries += 1 if mediaBinary: mimeType = media_file_mime_type(mediaFilename) self._set_headers_etag(mediaFilename, mimeType, mediaBinary, cookie, self.server.domain_full, False, None) self._write(mediaBinary) fitness_performance(GETstartTime, self.server.fitness, '_GET', 'manifest logo shown', self.server.debug) return self._404() return fitness_performance(GETstartTime, 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': screenFilename = \ self.server.base_dir + '/img' + self.path if os.path.isfile(screenFilename): if self._etag_exists(screenFilename): # The file has not changed self._304() return tries = 0 mediaBinary = None while tries < 5: try: with open(screenFilename, 'rb') as avFile: mediaBinary = avFile.read() break except Exception as ex: print('ERROR: manifest screenshot ' + str(tries) + ' ' + str(ex)) time.sleep(1) tries += 1 if mediaBinary: mimeType = media_file_mime_type(screenFilename) self._set_headers_etag(screenFilename, mimeType, mediaBinary, cookie, self.server.domain_full, False, None) self._write(mediaBinary) fitness_performance(GETstartTime, self.server.fitness, '_GET', 'show screenshot', self.server.debug) return self._404() return fitness_performance(GETstartTime, 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'))): iconFilename = \ self.server.base_dir + '/accounts' + self.path if os.path.isfile(iconFilename): if self._etag_exists(iconFilename): # The file has not changed self._304() return tries = 0 mediaBinary = None while tries < 5: try: with open(iconFilename, 'rb') as avFile: mediaBinary = avFile.read() break except Exception as ex: print('ERROR: login screen image ' + str(tries) + ' ' + str(ex)) time.sleep(1) tries += 1 if mediaBinary: mimeTypeStr = media_file_mime_type(iconFilename) self._set_headers_etag(iconFilename, mimeTypeStr, mediaBinary, cookie, self.server.domain_full, False, None) self._write(mediaBinary) fitness_performance(GETstartTime, self.server.fitness, '_GET', 'login screen logo', self.server.debug) return self._404() return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'login screen logo done', self.server.debug) # QR code for account handle if usersInPath and \ self.path.endswith('/qrcode.png'): if self._show_q_rcode(calling_domain, self.path, self.server.base_dir, self.server.domain, self.server.port, GETstartTime): return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'account qrcode done', self.server.debug) # search screen banner image if usersInPath: if self.path.endswith('/search_banner.png'): if self._search_screen_banner(calling_domain, self.path, self.server.base_dir, self.server.domain, self.server.port, GETstartTime): return if self.path.endswith('/left_col_image.png'): if self._column_image('left', calling_domain, self.path, self.server.base_dir, self.server.domain, self.server.port, GETstartTime): return if self.path.endswith('/right_col_image.png'): if self._column_image('right', calling_domain, self.path, self.server.base_dir, self.server.domain, self.server.port, GETstartTime): return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'search screen banner done', self.server.debug) if self.path.startswith('/defaultprofilebackground'): self._show_default_profile_background(calling_domain, self.path, self.server.base_dir, self.server.theme_name, GETstartTime) return # show a background image on the login or person options page if '-background.' in self.path: if self._show_background_image(calling_domain, self.path, self.server.base_dir, GETstartTime): return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'background shown done', self.server.debug) # emoji images if '/emoji/' in self.path: self._show_emoji(calling_domain, self.path, self.server.base_dir, GETstartTime) return fitness_performance(GETstartTime, 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(calling_domain, self.path, self.server.base_dir, GETstartTime) 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, GETstartTime) return fitness_performance(GETstartTime, 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(calling_domain, self.path, self.server.base_dir, GETstartTime): return fitness_performance(GETstartTime, 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(calling_domain, self.path, self.server.base_dir, GETstartTime) 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(calling_domain, self.path, self.server.base_dir, GETstartTime) return fitness_performance(GETstartTime, 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(refererDomain, self.path, self.server.base_dir, GETstartTime) return fitness_performance(GETstartTime, 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(refererDomain, self.path, self.server.base_dir, self.server.domain, GETstartTime): return fitness_performance(GETstartTime, 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_timeGET = int(time.time() * 1000) if self.server.GETbusy: if curr_timeGET - self.server.lastGET < 500: if self.server.debug: print('DEBUG: GET Busy') self.send_response(429) self.end_headers() return self.server.GETbusy = True self.server.lastGET = curr_timeGET # returns after this point should set GETbusy to False fitness_performance(GETstartTime, 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.GETbusy = False return # get webfinger endpoint for a person if self._webfinger(calling_domain): fitness_performance(GETstartTime, self.server.fitness, '_GET', 'webfinger called', self.server.debug) self.server.GETbusy = False return fitness_performance(GETstartTime, 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.css_cache, self.server.translate, self.server.base_dir, self.server.http_prefix, self.server.domain_full, self.server.system_language, True).encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', 'login shown', self.server.debug) self.server.GETbusy = 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(GETstartTime, self.server.fitness, '_GET', 'news front page shown', self.server.debug) self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'login shown done', self.server.debug) # the newswire screen on mobile if htmlGET 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.GETbusy = False return timelinePath = \ '/users/' + nickname + '/' + self.server.defaultTimeline 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 defaultTimeline = self.server.defaultTimeline accessKeys = self.server.accessKeys if self.server.keyShortcuts.get(nickname): accessKeys = self.server.keyShortcuts[nickname] msg = \ html_newswire_mobile(self.server.css_cache, 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, timelinePath, show_publish_as_icon, authorized, rss_icon_at_top, icons_as_buttons, defaultTimeline, self.server.theme_name, accessKeys).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) self.server.GETbusy = False return if htmlGET 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.GETbusy = False return accessKeys = self.server.accessKeys if self.server.keyShortcuts.get(nickname): accessKeys = self.server.keyShortcuts[nickname] timelinePath = \ '/users/' + nickname + '/' + self.server.defaultTimeline icons_as_buttons = self.server.icons_as_buttons defaultTimeline = self.server.defaultTimeline sharedItemsDomains = \ self.server.shared_items_federated_domains msg = \ html_links_mobile(self.server.css_cache, self.server.base_dir, nickname, self.server.domain_full, self.server.http_prefix, self.server.translate, timelinePath, authorized, self.server.rss_icon_at_top, icons_as_buttons, defaultTimeline, self.server.theme_name, accessKeys, sharedItemsDomains).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) self.server.GETbusy = 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, GETstartTime) self.server.GETbusy = 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, GETstartTime) self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'hashtag search done', self.server.debug) # show or hide buttons in the web interface if htmlGET and usersInPath and \ self.path.endswith('/minimal') and \ authorized: nickname = self.path.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] notMin = not is_minimal(self.server.base_dir, self.server.domain, nickname) set_minimal(self.server.base_dir, self.server.domain, nickname, notMin) if not (self.server.media_instance or self.server.blogs_instance): self.path = '/users/' + nickname + '/inbox' else: if self.server.blogs_instance: self.path = '/users/' + nickname + '/tlblogs' elif self.server.media_instance: self.path = '/users/' + nickname + '/tlmedia' else: self.path = '/users/' + nickname + '/tlfeatures' # search for a fediverse address, shared item or emoji # from the web interface by selecting search icon if htmlGET and usersInPath: 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] accessKeys = self.server.accessKeys if self.server.keyShortcuts.get(nickname): accessKeys = self.server.keyShortcuts[nickname] # show the search screen msg = html_search(self.server.css_cache, self.server.translate, self.server.base_dir, self.path, self.server.domain, self.server.defaultTimeline, self.server.theme_name, self.server.text_mode_banner, accessKeys).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', 'search screen shown', self.server.debug) self.server.GETbusy = False return # show a hashtag category from the search screen if htmlGET and '/category/' in self.path: msg = html_search_hashtag_category(self.server.css_cache, 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(GETstartTime, self.server.fitness, '_GET', 'hashtag category screen shown', self.server.debug) self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'search screen shown done', self.server.debug) # Show the calendar for a user if htmlGET and usersInPath: if '/calendar' in self.path: nickname = self.path.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] accessKeys = self.server.accessKeys if self.server.keyShortcuts.get(nickname): accessKeys = self.server.keyShortcuts[nickname] # show the calendar screen msg = html_calendar(self.server.person_cache, self.server.css_cache, self.server.translate, self.server.base_dir, self.path, self.server.http_prefix, self.server.domain_full, self.server.text_mode_banner, accessKeys).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', 'calendar shown', self.server.debug) self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'calendar shown done', self.server.debug) # Show confirmation for deleting a calendar event if htmlGET and usersInPath: 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, GETstartTime): self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'calendar delete shown done', self.server.debug) # search for emoji by name if htmlGET and usersInPath: if self.path.endswith('/searchemoji'): # show the search screen msg = \ html_search_emoji_text_entry(self.server.css_cache, 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(GETstartTime, self.server.fitness, '_GET', 'emoji search shown', self.server.debug) self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'emoji search shown done', self.server.debug) repeatPrivate = False if htmlGET and '?repeatprivate=' in self.path: repeatPrivate = True self.path = self.path.replace('?repeatprivate=', '?repeat=') # announce/repeat button was pressed if authorized and htmlGET and '?repeat=' in self.path: self._announce_button(calling_domain, self.path, self.server.base_dir, cookie, self.server.proxy_type, self.server.http_prefix, self.server.domain, self.server.domain_full, self.server.port, self.server.onion_domain, self.server.i2p_domain, GETstartTime, repeatPrivate, self.server.debug) self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'show announce done', self.server.debug) if authorized and htmlGET and '?unrepeatprivate=' in self.path: self.path = self.path.replace('?unrepeatprivate=', '?unrepeat=') # undo an announce/repeat from the web interface if authorized and htmlGET and '?unrepeat=' in self.path: self._undo_announce_button(calling_domain, self.path, self.server.base_dir, cookie, self.server.proxy_type, self.server.http_prefix, self.server.domain, self.server.domain_full, self.server.port, self.server.onion_domain, self.server.i2p_domain, GETstartTime, repeatPrivate, self.server.debug, self.server.recent_posts_cache) self.server.GETbusy = False return fitness_performance(GETstartTime, 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, self.server.domain_full, self.server.port, self.server.onion_domain, self.server.i2p_domain, GETstartTime, self.server.proxy_type, self.server.debug, self.server.newswire) self.server.GETbusy = 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, self.server.domain_full, self.server.port, self.server.onion_domain, self.server.i2p_domain, GETstartTime, self.server.proxy_type, self.server.debug, self.server.newswire) self.server.GETbusy = 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, GETstartTime, self.server.proxy_type, self.server.debug) self.server.GETbusy = False return fitness_performance(GETstartTime, 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, GETstartTime, self.server.proxy_type, self.server.debug) self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'follow deny done', self.server.debug) # like from the web interface icon if authorized and htmlGET 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug) self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'like button done', self.server.debug) # undo a like from the web interface icon if authorized and htmlGET 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug) self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'unlike button done', self.server.debug) # emoji reaction from the web interface icon if authorized and htmlGET 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug) self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'emoji reaction button done', self.server.debug) # undo an emoji reaction from the web interface icon if authorized and htmlGET 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug) self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'unreaction button done', self.server.debug) # bookmark from the web interface icon if authorized and htmlGET 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug) self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'bookmark shown done', self.server.debug) # emoji recation from the web interface bottom icon if authorized and htmlGET 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.domain_full, self.server.port, self.server.onion_domain, self.server.i2p_domain, GETstartTime, self.server.proxy_type, cookie, self.server.debug) self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'bookmark shown done', self.server.debug) # undo a bookmark from the web interface icon if authorized and htmlGET 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug) self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'unbookmark shown done', self.server.debug) # delete button is pressed on a post if authorized and htmlGET and '?delete=' in self.path: self._delete_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, GETstartTime, self.server.proxy_type, cookie, self.server.debug) self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'delete shown done', self.server.debug) # The mute button is pressed if authorized and htmlGET 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug) self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'post muted done', self.server.debug) # unmute a post from the web interface icon if authorized and htmlGET 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug) self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'unmute activated done', self.server.debug) # reply from the web interface icon inReplyToUrl = None # replyWithDM = False replyToList = [] replyPageNumber = 1 replyCategory = '' shareDescription = None conversationId = None # replytoActor = None if htmlGET: if '?conversationId=' in self.path: conversationId = self.path.split('?conversationId=')[1] if '?' in conversationId: conversationId = conversationId.split('?')[0] # public reply if '?replyto=' in self.path: inReplyToUrl = self.path.split('?replyto=')[1] if '?' in inReplyToUrl: mentionsList = inReplyToUrl.split('?') for m in mentionsList: if m.startswith('mention='): replyHandle = m.replace('mention=', '') if replyHandle not in replyToList: replyToList.append(replyHandle) if m.startswith('page='): replyPageStr = m.replace('page=', '') if replyPageStr.isdigit(): replyPageNumber = int(replyPageStr) # if m.startswith('actor='): # replytoActor = m.replace('actor=', '') inReplyToUrl = mentionsList[0] self.path = self.path.split('?replyto=')[0] + '/newpost' if self.server.debug: print('DEBUG: replyto path ' + self.path) # reply to followers if '?replyfollowers=' in self.path: inReplyToUrl = self.path.split('?replyfollowers=')[1] if '?' in inReplyToUrl: mentionsList = inReplyToUrl.split('?') for m in mentionsList: if m.startswith('mention='): replyHandle = m.replace('mention=', '') if m.replace('mention=', '') not in replyToList: replyToList.append(replyHandle) if m.startswith('page='): replyPageStr = m.replace('page=', '') if replyPageStr.isdigit(): replyPageNumber = int(replyPageStr) # if m.startswith('actor='): # replytoActor = m.replace('actor=', '') inReplyToUrl = mentionsList[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 if '?replydm=' in self.path: inReplyToUrl = self.path.split('?replydm=')[1] inReplyToUrl = urllib.parse.unquote_plus(inReplyToUrl) if '?' in inReplyToUrl: # multiple parameters mentionsList = inReplyToUrl.split('?') for m in mentionsList: if m.startswith('mention='): replyHandle = m.replace('mention=', '') inReplyToUrl = replyHandle if replyHandle not in replyToList: replyToList.append(replyHandle) elif m.startswith('page='): replyPageStr = m.replace('page=', '') if replyPageStr.isdigit(): replyPageNumber = int(replyPageStr) elif m.startswith('category='): replyCategory = m.replace('category=', '') elif m.startswith('sharedesc:'): # get the title for the shared item shareDescription = \ m.replace('sharedesc:', '').strip() shareDescription = \ shareDescription.replace('_', ' ') else: # single parameter if inReplyToUrl.startswith('mention='): replyHandle = inReplyToUrl.replace('mention=', '') inReplyToUrl = replyHandle if replyHandle not in replyToList: replyToList.append(replyHandle) elif inReplyToUrl.startswith('sharedesc:'): # get the title for the shared item shareDescription = \ inReplyToUrl.replace('sharedesc:', '').strip() shareDescription = \ shareDescription.replace('_', ' ') self.path = self.path.split('?replydm=')[0] + '/newdm' if self.server.debug: print('DEBUG: replydm path ' + self.path) # Edit a blog post if authorized and \ '/users/' in self.path and \ '?editblogpost=' in self.path and \ ';actor=' in self.path: messageId = self.path.split('?editblogpost=')[1] if ';' in messageId: messageId = messageId.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 nickname == actor: postUrl = \ local_actor_url(self.server.http_prefix, nickname, self.server.domain_full) + \ '/statuses/' + messageId msg = html_edit_blog(self.server.media_instance, self.server.translate, self.server.base_dir, self.server.http_prefix, self.path, replyPageNumber, nickname, self.server.domain, postUrl, 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.GETbusy = False return # list of known crawlers accessing nodeinfo or masto API if self._show_known_crawlers(calling_domain, self.path, self.server.base_dir, self.server.knownCrawlers): self.server.GETbusy = 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.GETbusy = 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.GETbusy = 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.GETbusy = False return # edit news post if self._edit_news_post(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.GETbusy = False return if self._show_new_post(calling_domain, self.path, self.server.media_instance, self.server.translate, self.server.base_dir, self.server.http_prefix, inReplyToUrl, replyToList, shareDescription, replyPageNumber, replyCategory, self.server.domain, self.server.domain_full, GETstartTime, cookie, noDropDown, conversationId): self.server.GETbusy = False return fitness_performance(GETstartTime, 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(authorized, 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug): self.server.GETbusy = False return fitness_performance(GETstartTime, 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, 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug): self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'post replies done', self.server.debug) # roles on profile screen if self.path.endswith('/roles') and usersInPath: if self._show_roles(authorized, 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug): self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'show roles done', self.server.debug) # show skills on the profile page if self.path.endswith('/skills') and usersInPath: if self._show_skills(authorized, 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug): self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'show skills done', self.server.debug) if '?notifypost=' in self.path and usersInPath and authorized: if self._show_notify_post(authorized, 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug): self.server.GETbusy = False return # get an individual post from the path # /users/nickname/statuses/number if '/statuses/' in self.path and usersInPath: if self._show_individual_post(authorized, 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug): self.server.GETbusy = False return fitness_performance(GETstartTime, 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, 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug, self.server.recent_posts_cache, self.server.session, self.server.defaultTimeline, 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): self.server.GETbusy = False return fitness_performance(GETstartTime, 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_d_ms(authorized, 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug): self.server.GETbusy = False return fitness_performance(GETstartTime, 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, 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug): self.server.GETbusy = False return fitness_performance(GETstartTime, 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, 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug): self.server.GETbusy = False return fitness_performance(GETstartTime, 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, 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug): self.server.GETbusy = False return fitness_performance(GETstartTime, 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, 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug): self.server.GETbusy = 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, 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug): self.server.GETbusy = False return fitness_performance(GETstartTime, 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.domain_full, self.server.port, self.server.onion_domain, self.server.i2p_domain, GETstartTime, self.server.proxy_type, cookie, self.server.debug): self.server.GETbusy = 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.domain_full, self.server.port, self.server.onion_domain, self.server.i2p_domain, GETstartTime, self.server.proxy_type, cookie, self.server.debug): self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'show shares 2 done', self.server.debug) # block a domain from html_account_info if authorized and usersInPath 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.GETbusy = False return blockDomain = self.path.split('/accountinfo?blockdomain=')[1] searchHandle = blockDomain.split('?handle=')[1] searchHandle = urllib.parse.unquote_plus(searchHandle) blockDomain = blockDomain.split('?handle=')[0] blockDomain = urllib.parse.unquote_plus(blockDomain.strip()) if '?' in blockDomain: blockDomain = blockDomain.split('?')[0] add_global_block(self.server.base_dir, '*', blockDomain) msg = \ html_account_info(self.server.css_cache, self.server.translate, self.server.base_dir, self.server.http_prefix, nickname, self.server.domain, self.server.port, searchHandle, self.server.debug, self.server.system_language, self.server.signing_priv_key_pem) msg = msg.encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) self.server.GETbusy = False return # unblock a domain from html_account_info if authorized and usersInPath 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.GETbusy = False return blockDomain = self.path.split('/accountinfo?unblockdomain=')[1] searchHandle = blockDomain.split('?handle=')[1] searchHandle = urllib.parse.unquote_plus(searchHandle) blockDomain = blockDomain.split('?handle=')[0] blockDomain = urllib.parse.unquote_plus(blockDomain.strip()) remove_global_block(self.server.base_dir, '*', blockDomain) msg = \ html_account_info(self.server.css_cache, self.server.translate, self.server.base_dir, self.server.http_prefix, nickname, self.server.domain, self.server.port, searchHandle, self.server.debug, self.server.system_language, self.server.signing_priv_key_pem) msg = msg.encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, calling_domain) self._write(msg) self.server.GETbusy = 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, 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug): self.server.GETbusy = False return fitness_performance(GETstartTime, 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, 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug): self.server.GETbusy = False return fitness_performance(GETstartTime, 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, 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug): self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'show moderation done', self.server.debug) if self._show_shares_feed(authorized, 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug, 'shares'): self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'show profile 2 done', self.server.debug) if self._show_following_feed(authorized, 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug): self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'show profile 3 done', self.server.debug) if self._show_followers_feed(authorized, 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug): self.server.GETbusy = False return fitness_performance(GETstartTime, self.server.fitness, '_GET', 'show profile 4 done', self.server.debug) # look up a person if self._show_person_profile(authorized, 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, GETstartTime, self.server.proxy_type, cookie, self.server.debug): self.server.GETbusy = False return fitness_performance(GETstartTime, 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.GETbusy = False return if not self._secure_mode(): if self.server.debug: print('WARN: Unauthorized GET') self._404() self.server.GETbusy = False return fitness_performance(GETstartTime, 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 File: content = File.read() except OSError: print('EX: unable to read file ' + filename) if content: contentJson = json.loads(content) msg = json.dumps(contentJson, ensure_ascii=False).encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) fitness_performance(GETstartTime, self.server.fitness, '_GET', 'arbitrary json', self.server.debug) else: if self.server.debug: print('DEBUG: GET Unknown file') self._404() self.server.GETbusy = False fitness_performance(GETstartTime, self.server.fitness, '_GET', 'end benchmarks', 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 != self.server.domain and \ calling_domain != self.server.domain_full and \ calling_domain != self.server.onion_domain: print('HEAD domain blocked: ' + calling_domain) self._400() return else: if calling_domain != self.server.domain and \ calling_domain != self.server.domain_full: print('HEAD domain blocked: ' + calling_domain) self._400() return checkPath = self.path etag = None fileLength = -1 if '/media/' in self.path: if is_image_file(self.path) or \ path_is_video(self.path) or \ path_is_audio(self.path): mediaStr = self.path.split('/media/')[1] mediaFilename = \ self.server.base_dir + '/media/' + mediaStr if os.path.isfile(mediaFilename): checkPath = mediaFilename fileLength = os.path.getsize(mediaFilename) mediaTagFilename = mediaFilename + '.etag' if os.path.isfile(mediaTagFilename): try: with open(mediaTagFilename, 'r') as etagFile: etag = etagFile.read() except OSError: print('EX: do_HEAD unable to read ' + mediaTagFilename) else: mediaBinary = None try: with open(mediaFilename, 'rb') as avFile: mediaBinary = avFile.read() except OSError: print('EX: unable to read media binary ' + mediaFilename) if mediaBinary: etag = md5(mediaBinary).hexdigest() # nosec try: with open(mediaTagFilename, 'w+') as etagFile: etagFile.write(etag) except OSError: print('EX: do_HEAD unable to write ' + mediaTagFilename) mediaFileType = media_file_mime_type(checkPath) self._set_headers_head(mediaFileType, fileLength, etag, calling_domain, False) def _receive_new_post_process(self, postType: str, path: str, headers: {}, length: int, postBytes, boundary: str, calling_domain: str, cookie: str, authorized: bool, content_license_url: 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 nicknameStr = path.split('/users/')[1] if '?' in nicknameStr: nicknameStr = nicknameStr.split('?')[0] if '/' in nicknameStr: nickname = nicknameStr.split('/')[0] else: nickname = nicknameStr if self.server.debug: print('DEBUG: POST nickname ' + str(nickname)) if not nickname: print('WARN: no nickname found when receiving ' + postType + ' path ' + path) return -1 length = int(headers['Content-Length']) if length > self.server.max_post_length: print('POST size too large') return -1 boundary = headers['Content-Type'].split('boundary=')[1] if ';' in boundary: boundary = boundary.split(';')[0] # Note: we don't use cgi here because it's due to be deprecated # in Python 3.8/3.10 # Instead we use the multipart mime parser from the email module if self.server.debug: print('DEBUG: extracting media from POST') mediaBytes, postBytes = \ extract_media_in_form_post(postBytes, boundary, 'attachpic') if self.server.debug: if mediaBytes: print('DEBUG: media was found. ' + str(len(mediaBytes)) + ' 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 filenameBase = \ acct_dir(self.server.base_dir, nickname, self.server.domain) + '/upload.temp' filename, attachmentMediaType = \ save_media_in_form_post(mediaBytes, self.server.debug, filenameBase) 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_imageFilename = filename.replace('.temp', '') print('Removing metadata from ' + post_imageFilename) 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_imageFilename, city, content_license_url) if os.path.isfile(post_imageFilename): print('POST media saved to ' + post_imageFilename) else: print('ERROR: POST media could not be saved to ' + post_imageFilename) else: if os.path.isfile(filename): newFilename = filename.replace('.temp', '') os.rename(filename, newFilename) filename = newFilename fields = \ extract_text_fields_in_post(postBytes, 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? citationsButtonPress = False if postType == 'newblog' and fields.get('submitCitations'): if fields['submitCitations'] == \ self.server.translate['Citations']: citationsButtonPress = True if not citationsButtonPress: # 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 submitText = self.server.translate['Submit'] customSubmitText = \ get_config_param(self.server.base_dir, 'customSubmitText') if customSubmitText: submitText = customSubmitText if fields.get('submitPost'): if fields['submitPost'] != submitText: 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('location'): fields['location'] = None if not citationsButtonPress: # 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 lastUsedFilename = \ acct_dir(self.server.base_dir, nickname, self.server.domain) + '/.lastUsed' try: with open(lastUsedFilename, 'w+') as lastUsedFile: lastUsedFile.write(str(int(time.time()))) except OSError: print('EX: _receive_new_post_process unable to write ' + lastUsedFilename) mentionsStr = '' if fields.get('mentions'): mentionsStr = fields['mentions'].strip() + ' ' if not fields.get('commentsEnabled'): commentsEnabled = False else: commentsEnabled = True if postType == 'newpost': if not fields.get('pinToProfile'): pinToProfile = False else: pinToProfile = 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) conversationId = None if fields.get('conversationId'): conversationId = fields['conversationId'] message_json = \ create_public_post(self.server.base_dir, nickname, self.server.domain, self.server.port, self.server.http_prefix, mentionsStr + fields['message'], False, False, False, commentsEnabled, filename, attachmentMediaType, fields['imageDescription'], city, fields['replyTo'], fields['replyTo'], fields['subject'], fields['schedulePost'], fields['eventDate'], fields['eventTime'], fields['location'], False, self.server.system_language, conversationId, self.server.low_bandwidth, self.server.content_license_url) if message_json: if fields['schedulePost']: return 1 if pinToProfile: sys_language = self.server.system_language contentStr = \ get_base_content_from_post(message_json, sys_language) followersOnly = False pin_post(self.server.base_dir, nickname, self.server.domain, contentStr, followersOnly) return 1 if self._post_to_outbox(message_json, self.server.project_version, nickname): 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 else: return -1 elif postType == 'newblog': # citations button on newblog screen if citationsButtonPress: message_json = \ html_citations(self.server.base_dir, nickname, self.server.domain, self.server.http_prefix, self.server.defaultTimeline, self.server.translate, self.server.newswire, self.server.css_cache, fields['subject'], fields['message'], filename, attachmentMediaType, fields['imageDescription'], self.server.theme_name) if message_json: message_json = message_json.encode('utf-8') message_jsonLen = len(message_json) self._set_headers('text/html', message_jsonLen, 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 followersOnly = False saveToFile = False client_to_server = False city = None conversationId = None if fields.get('conversationId'): conversationId = fields['conversationId'] message_json = \ create_blog_post(self.server.base_dir, nickname, self.server.domain, self.server.port, self.server.http_prefix, fields['message'], followersOnly, saveToFile, client_to_server, commentsEnabled, filename, attachmentMediaType, fields['imageDescription'], city, fields['replyTo'], fields['replyTo'], fields['subject'], fields['schedulePost'], fields['eventDate'], fields['eventTime'], fields['location'], self.server.system_language, conversationId, self.server.low_bandwidth, self.server.content_license_url) if message_json: if fields['schedulePost']: return 1 if self._post_to_outbox(message_json, self.server.project_version, nickname): 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 else: return -1 elif postType == '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: cachedFilename = \ acct_dir(self.server.base_dir, nickname, self.server.domain) + \ '/postcache/' + \ fields['postUrl'].replace('/', '#') + '.html' if os.path.isfile(cachedFilename): print('Edited blog post, removing cached html') try: os.remove(cachedFilename) except OSError: print('EX: _receive_new_post_process ' + 'unable to delete ' + cachedFilename) # 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 = [] hashtagsDict = {} mentionedRecipients = [] fields['message'] = \ add_html_tags(self.server.base_dir, self.server.http_prefix, nickname, self.server.domain, fields['message'], mentionedRecipients, hashtagsDict, True) # replace emoji with unicode tags = [] for tagName, tag in hashtagsDict.items(): tags.append(tag) # get list of tags fields['message'] = \ replace_emoji_from_tags(self.server.session, self.server.base_dir, fields['message'], tags, 'content', self.server.debug) post_json_object['object']['content'] = \ fields['message'] contentMap = post_json_object['object']['contentMap'] contentMap[self.server.system_language] = \ fields['message'] imgDescription = '' if fields.get('imageDescription'): imgDescription = 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, attachmentMediaType, imgDescription, 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 postType == 'newunlisted': city = get_spoofed_city(self.server.city, self.server.base_dir, nickname, self.server.domain) followersOnly = False saveToFile = False client_to_server = False conversationId = None if fields.get('conversationId'): conversationId = fields['conversationId'] message_json = \ create_unlisted_post(self.server.base_dir, nickname, self.server.domain, self.server.port, self.server.http_prefix, mentionsStr + fields['message'], followersOnly, saveToFile, client_to_server, commentsEnabled, filename, attachmentMediaType, fields['imageDescription'], city, fields['replyTo'], fields['replyTo'], fields['subject'], fields['schedulePost'], fields['eventDate'], fields['eventTime'], fields['location'], self.server.system_language, conversationId, self.server.low_bandwidth, self.server.content_license_url) if message_json: if fields['schedulePost']: return 1 if self._post_to_outbox(message_json, self.server.project_version, nickname): populate_replies(self.server.base_dir, self.server.http_prefix, self.server.domain, message_json, self.server.max_replies, self.server.debug) return 1 else: return -1 elif postType == 'newfollowers': city = get_spoofed_city(self.server.city, self.server.base_dir, nickname, self.server.domain) followersOnly = True saveToFile = False client_to_server = False conversationId = None if fields.get('conversationId'): conversationId = fields['conversationId'] message_json = \ create_followers_only_post(self.server.base_dir, nickname, self.server.domain, self.server.port, self.server.http_prefix, mentionsStr + fields['message'], followersOnly, saveToFile, client_to_server, commentsEnabled, filename, attachmentMediaType, fields['imageDescription'], city, fields['replyTo'], fields['replyTo'], fields['subject'], fields['schedulePost'], fields['eventDate'], fields['eventTime'], fields['location'], self.server.system_language, conversationId, self.server.low_bandwidth, self.server.content_license_url) if message_json: if fields['schedulePost']: return 1 if self._post_to_outbox(message_json, self.server.project_version, nickname): populate_replies(self.server.base_dir, self.server.http_prefix, self.server.domain, message_json, self.server.max_replies, self.server.debug) return 1 else: return -1 elif postType == 'newdm': message_json = None print('A DM was posted') if '@' in mentionsStr: city = get_spoofed_city(self.server.city, self.server.base_dir, nickname, self.server.domain) followersOnly = True saveToFile = False client_to_server = False conversationId = None if fields.get('conversationId'): conversationId = fields['conversationId'] content_license_url = self.server.content_license_url message_json = \ create_direct_message_post(self.server.base_dir, nickname, self.server.domain, self.server.port, self.server.http_prefix, mentionsStr + fields['message'], followersOnly, saveToFile, client_to_server, commentsEnabled, filename, attachmentMediaType, fields['imageDescription'], city, fields['replyTo'], fields['replyTo'], fields['subject'], True, fields['schedulePost'], fields['eventDate'], fields['eventTime'], fields['location'], self.server.system_language, conversationId, self.server.low_bandwidth, content_license_url) if message_json: if fields['schedulePost']: return 1 print('Sending new DM to ' + str(message_json['object']['to'])) if self._post_to_outbox(message_json, self.server.project_version, nickname): populate_replies(self.server.base_dir, self.server.http_prefix, self.server.domain, message_json, self.server.max_replies, self.server.debug) return 1 else: return -1 elif postType == 'newreminder': message_json = None handle = nickname + '@' + self.server.domain_full print('A reminder was posted for ' + handle) if '@' + handle not in mentionsStr: mentionsStr = '@' + handle + ' ' + mentionsStr city = get_spoofed_city(self.server.city, self.server.base_dir, nickname, self.server.domain) followersOnly = True saveToFile = False client_to_server = False commentsEnabled = False conversationId = None message_json = \ create_direct_message_post(self.server.base_dir, nickname, self.server.domain, self.server.port, self.server.http_prefix, mentionsStr + fields['message'], followersOnly, saveToFile, client_to_server, commentsEnabled, filename, attachmentMediaType, fields['imageDescription'], city, None, None, fields['subject'], True, fields['schedulePost'], fields['eventDate'], fields['eventTime'], fields['location'], self.server.system_language, conversationId, self.server.low_bandwidth, self.server.content_license_url) if message_json: if fields['schedulePost']: return 1 print('DEBUG: new reminder to ' + str(message_json['object']['to'])) if self._post_to_outbox(message_json, self.server.project_version, nickname): return 1 else: return -1 elif postType == 'newreport': if attachmentMediaType: if attachmentMediaType != '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) message_json = \ create_report_post(self.server.base_dir, nickname, self.server.domain, self.server.port, self.server.http_prefix, mentionsStr + fields['message'], True, False, False, True, filename, attachmentMediaType, fields['imageDescription'], city, self.server.debug, fields['subject'], self.server.system_language, self.server.low_bandwidth, self.server.content_license_url) if message_json: if self._post_to_outbox(message_json, self.server.project_version, nickname): return 1 else: return -1 elif postType == 'newquestion': if not fields.get('duration'): return -1 if not fields.get('message'): return -1 # questionStr = fields['message'] qOptions = [] for questionCtr in range(8): if fields.get('questionOption' + str(questionCtr)): qOptions.append(fields['questionOption' + str(questionCtr)]) if not qOptions: return -1 city = get_spoofed_city(self.server.city, self.server.base_dir, nickname, self.server.domain) intDuration = int(fields['duration']) message_json = \ create_question_post(self.server.base_dir, nickname, self.server.domain, self.server.port, self.server.http_prefix, fields['message'], qOptions, False, False, False, commentsEnabled, filename, attachmentMediaType, fields['imageDescription'], city, fields['subject'], intDuration, self.server.system_language, self.server.low_bandwidth, self.server.content_license_url) if message_json: if self.server.debug: print('DEBUG: new Question') if self._post_to_outbox(message_json, self.server.project_version, nickname): return 1 return -1 elif postType == 'newshare' or postType == 'newwanted': if not fields.get('itemQty'): print(postType + ' no itemQty') return -1 if not fields.get('itemType'): print(postType + ' no itemType') return -1 if 'itemPrice' not in fields: print(postType + ' no itemPrice') return -1 if 'itemCurrency' not in fields: print(postType + ' no itemCurrency') return -1 if not fields.get('category'): print(postType + ' no category') return -1 if not fields.get('duration'): print(postType + ' no duratio') return -1 if attachmentMediaType: if attachmentMediaType != 'image': print('Attached media is not an image') return -1 durationStr = fields['duration'] if durationStr: if ' ' not in durationStr: durationStr = durationStr + ' days' city = get_spoofed_city(self.server.city, self.server.base_dir, nickname, self.server.domain) itemQty = 1 if fields['itemQty']: if is_float(fields['itemQty']): itemQty = float(fields['itemQty']) itemPrice = "0.00" itemCurrency = "EUR" if fields['itemPrice']: itemPrice, itemCurrency = \ get_price_from_string(fields['itemPrice']) if fields['itemCurrency']: itemCurrency = fields['itemCurrency'] if postType == 'newshare': print('Adding shared item') sharesFileType = 'shares' else: print('Adding wanted item') sharesFileType = 'wanted' add_share(self.server.base_dir, self.server.http_prefix, nickname, self.server.domain, self.server.port, fields['subject'], fields['message'], filename, itemQty, fields['itemType'], fields['category'], fields['location'], durationStr, self.server.debug, city, itemPrice, itemCurrency, self.server.system_language, self.server.translate, sharesFileType, 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.postToNickname = nickname return 1 return -1 def _receive_new_post(self, postType: str, path: str, calling_domain: str, cookie: str, authorized: bool, content_license_url: str) -> int: """A new post has been created This creates a thread to send the new post """ pageNumber = 1 if '/users/' not in path: print('Not receiving new post for ' + path + ' because /users/ not in path') return None if '?' + postType + '?' not in path: print('Not receiving new post for ' + path + ' because ?' + postType + '? not in path') return None print('New post begins: ' + postType + ' ' + path) if '?page=' in path: pageNumberStr = path.split('?page=')[1] if '?' in pageNumberStr: pageNumberStr = pageNumberStr.split('?')[0] if '#' in pageNumberStr: pageNumberStr = pageNumberStr.split('#')[0] if pageNumberStr.isdigit(): pageNumber = int(pageNumberStr) 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') waitCtr = 0 np_thread = self.server.new_post_thread[new_post_thread_name] while np_thread.is_alive() and waitCtr < 8: time.sleep(1) waitCtr += 1 if waitCtr >= 8: print('Killing previous new post thread for ' + new_post_thread_name) np_thread.kill() # make a copy of self.headers headers = {} headersWithoutCookie = {} for dictEntryName, headerLine in self.headers.items(): headers[dictEntryName] = headerLine if dictEntryName.lower() != 'cookie': headersWithoutCookie[dictEntryName] = headerLine print('New post headers: ' + str(headersWithoutCookie)) 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: postBytes = self.rfile.read(length) except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: POST postBytes ' + 'connection reset by peer') else: print('WARN: POST postBytes socket error') return None except ValueError as ex: print('ERROR: POST postBytes rfile.read failed, ' + str(ex)) return None # second length check from the bytes received # since Content-Length could be untruthful length = len(postBytes) 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(postType, path, headers, length, postBytes, boundary, calling_domain, cookie, authorized, content_license_url) return pageNumber def _crypto_ap_iread_handle(self): """Reads handle """ messageBytes = None maxDeviceIdLength = 2048 length = int(self.headers['Content-length']) if length >= maxDeviceIdLength: print('WARN: handle post to crypto API is too long ' + str(length) + ' bytes') return {} try: messageBytes = self.rfile.read(length) except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: handle POST messageBytes ' + 'connection reset by peer') else: print('WARN: handle POST messageBytes socket error') return {} except ValueError as ex: print('ERROR: handle POST messageBytes rfile.read failed ' + str(ex)) return {} lenMessage = len(messageBytes) if lenMessage > 2048: print('WARN: handle post to crypto API is too long ' + str(lenMessage) + ' bytes') return {} handle = messageBytes.decode("utf-8") if not handle: return None if '@' not in handle: return None if '[' in handle: return json.loads(messageBytes) 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 """ messageBytes = None maxCryptoMessageLength = 10240 length = int(self.headers['Content-length']) if length >= maxCryptoMessageLength: print('WARN: post to crypto API is too long ' + str(length) + ' bytes') return {} try: messageBytes = self.rfile.read(length) except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: POST messageBytes ' + 'connection reset by peer') else: print('WARN: POST messageBytes socket error') return {} except ValueError as ex: print('ERROR: POST messageBytes rfile.read failed, ' + str(ex)) return {} lenMessage = len(messageBytes) if lenMessage > 10240: print('WARN: post to crypto API is too long ' + str(lenMessage) + ' bytes') return {} return json.loads(messageBytes) def _crypto_api_query(self, calling_domain: str) -> bool: handle = self._crypto_ap_iread_handle() if not handle: return False if isinstance(handle, str): personDir = self.server.base_dir + '/accounts/' + handle if not os.path.isdir(personDir + '/devices'): return False devicesList = [] for subdir, dirs, files in os.walk(personDir + '/devices'): for f in files: deviceFilename = os.path.join(personDir + '/devices', f) if not os.path.isfile(deviceFilename): continue contentJson = load_json(deviceFilename) if contentJson: devicesList.append(contentJson) break # return the list of devices for this handle msg = \ json.dumps(devicesList, ensure_ascii=False).encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, None, calling_domain, False) self._write(msg) return True return False def _crypto_api(self, path: str, authorized: bool) -> 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.authorizedNickname: self._400() return deviceKeys = self._crypto_ap_iread_json() if not deviceKeys: self._400() return if isinstance(deviceKeys, dict): if not e2e_evalid_device(deviceKeys): self._400() return fingerprintKey = \ deviceKeys['fingerprintKey']['publicKeyBase64'] e2e_eadd_device(self.server.base_dir, self.authorizedNickname, self.server.domain, deviceKeys['deviceId'], deviceKeys['name'], deviceKeys['claim'], fingerprintKey, deviceKeys['identityKey']['publicKeyBase64'], deviceKeys['fingerprintKey']['type'], deviceKeys['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(): 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): POSTstartTime = time.time() if not self._establish_session("POST"): fitness_performance(POSTstartTime, self.server.fitness, '_POST', 'create_session', self.server.debug) self._404() return if self.server.debug: print('DEBUG: POST to ' + self.server.base_dir + ' path: ' + self.path + ' busy: ' + str(self.server.POSTbusy)) 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 != self.server.domain and \ calling_domain != self.server.domain_full and \ calling_domain != self.server.onion_domain: print('POST domain blocked: ' + calling_domain) self._400() return elif self.server.i2p_domain: if calling_domain != self.server.domain and \ calling_domain != self.server.domain_full and \ calling_domain != self.server.i2p_domain: print('POST domain blocked: ' + calling_domain) self._400() return else: if calling_domain != self.server.domain and \ calling_domain != self.server.domain_full: print('POST domain blocked: ' + calling_domain) self._400() return curr_timePOST = int(time.time() * 1000) if self.server.POSTbusy: if curr_timePOST - self.server.lastPOST < 500: self.send_response(429) self.end_headers() return self.server.POSTbusy = True self.server.lastPOST = curr_timePOST uaStr = self._get_user_agent() if self._blocked_user_agent(calling_domain, uaStr): self._400() self.server.POSTbusy = False return if not self.headers.get('Content-type'): print('Content-type header missing') self._400() self.server.POSTbusy = False return # returns after this point should set POSTbusy 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.POSTbusy = 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) self.server.POSTbusy = False return # if this is a POST to the outbox then check authentication self.outboxAuthenticated = False self.postToNickname = None fitness_performance(POSTstartTime, self.server.fitness, '_POST', 'start', self.server.debug) # login screen if self.path.startswith('/login'): self._show_login_screen(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) self.server.POSTbusy = False return fitness_performance(POSTstartTime, self.server.fitness, '_POST', '_login_screen', self.server.debug) if authorized and self.path.endswith('/sethashtagcategory'): self._set_hashtag_category(calling_domain, cookie, authorized, 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.defaultTimeline, self.server.allow_local_network_access) self.server.POSTbusy = 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, authorized, 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) self.server.POSTbusy = False return if authorized and self.path.endswith('/linksdata'): self._links_update(calling_domain, cookie, authorized, 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.defaultTimeline, self.server.allow_local_network_access) self.server.POSTbusy = False return if authorized and self.path.endswith('/newswiredata'): self._newswire_update(calling_domain, cookie, authorized, 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.defaultTimeline) self.server.POSTbusy = False return if authorized and self.path.endswith('/citationsdata'): self._citations_update(calling_domain, cookie, authorized, 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.defaultTimeline, self.server.newswire) self.server.POSTbusy = False return if authorized and self.path.endswith('/newseditdata'): self._news_post_edit(calling_domain, cookie, authorized, 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.defaultTimeline) self.server.POSTbusy = False return fitness_performance(POSTstartTime, self.server.fitness, '_POST', '_news_post_edit', self.server.debug) usersInPath = False if '/users/' in self.path: usersInPath = True # moderator action buttons if authorized and usersInPath 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.domain_full, self.server.port, self.server.onion_domain, self.server.i2p_domain, self.server.debug) self.server.POSTbusy = False return fitness_performance(POSTstartTime, self.server.fitness, '_POST', '_moderator_actions', self.server.debug) searchForEmoji = False if self.path.endswith('/searchhandleemoji'): searchForEmoji = True self.path = self.path.replace('/searchhandleemoji', '/searchhandle') if self.server.debug: print('DEBUG: searching for emoji') print('authorized: ' + str(authorized)) fitness_performance(POSTstartTime, self.server.fitness, '_POST', 'searchhandleemoji', self.server.debug) # a search was made if ((authorized or searchForEmoji) 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, searchForEmoji, self.server.onion_domain, self.server.i2p_domain, POSTstartTime, {}, self.server.debug) self.server.POSTbusy = False return fitness_performance(POSTstartTime, 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.POSTbusy = False return else: # a vote/question/poll is posted if self.path.endswith('/question') or \ '/question?page=' in self.path: self._receive_vote(calling_domain, cookie, authorized, 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.POSTbusy = 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, self.server.domain_full, self.server.onion_domain, self.server.i2p_domain, self.server.debug) self.server.POSTbusy = 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, self.server.domain_full, self.server.onion_domain, self.server.i2p_domain, self.server.debug) self.server.POSTbusy = False return fitness_performance(POSTstartTime, 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.POSTbusy = False return if self.path.endswith('/rmpost'): self._receive_remove_post(calling_domain, cookie, authorized, 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.POSTbusy = False return fitness_performance(POSTstartTime, 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, authorized, 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.POSTbusy = False return fitness_performance(POSTstartTime, 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, authorized, 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.POSTbusy = False return fitness_performance(POSTstartTime, 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, authorized, 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.POSTbusy = False return fitness_performance(POSTstartTime, 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, authorized, 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.POSTbusy = False return fitness_performance(POSTstartTime, 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) self.server.POSTbusy = False return # Change the key shortcuts if usersInPath and \ self.path.endswith('/changeAccessKeys'): nickname = self.path.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] if not self.server.keyShortcuts.get(nickname): accessKeys = self.server.accessKeys self.server.keyShortcuts[nickname] = accessKeys.copy() accessKeys = self.server.keyShortcuts[nickname] self._key_shortcuts(self.path, calling_domain, cookie, self.server.base_dir, self.server.http_prefix, nickname, self.server.domain, self.server.domain_full, self.server.port, self.server.onion_domain, self.server.i2p_domain, self.server.debug, accessKeys, self.server.defaultTimeline) self.server.POSTbusy = False return # theme designer submit/cancel button if usersInPath and \ self.path.endswith('/changeThemeSettings'): nickname = self.path.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] if not self.server.keyShortcuts.get(nickname): accessKeys = self.server.accessKeys self.server.keyShortcuts[nickname] = accessKeys.copy() accessKeys = self.server.keyShortcuts[nickname] allow_local_network_access = \ self.server.allow_local_network_access self._theme_designer_edit(self.path, calling_domain, cookie, self.server.base_dir, self.server.http_prefix, nickname, self.server.domain, self.server.domain_full, self.server.port, self.server.onion_domain, self.server.i2p_domain, self.server.debug, accessKeys, self.server.defaultTimeline, self.server.theme_name, allow_local_network_access, self.server.system_language) self.server.POSTbusy = 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: siDomainsStr = \ get_config_param(self.server.base_dir, 'shared_items_federated_domains') if siDomainsStr: if self.server.debug: print('Loading shared items federated domains list') siDomainsList = siDomainsStr.split(',') domainsList = self.server.shared_items_federated_domains for siDomain in siDomainsList: domainsList.append(siDomain.strip()) originDomain = self.headers.get('Origin') if originDomain != self.server.domain_full and \ originDomain != self.server.onion_domain and \ originDomain != self.server.i2p_domain and \ originDomain in self.server.shared_items_federated_domains: if self.server.debug: print('DEBUG: ' + 'POST updating shared item federation ' + 'token for ' + originDomain + ' to ' + self.server.domain_full) sharedItemTokens = self.server.sharedItemFederationTokens sharesToken = self.headers['SharesCatalog'] self.server.sharedItemFederationTokens = \ update_shared_item_federation_token(self.server.base_dir, originDomain, sharesToken, self.server.debug, sharedItemTokens) elif self.server.debug: fed_domains = self.server.shared_items_federated_domains if originDomain not in fed_domains: print('originDomain is not in federated domains list ' + originDomain) else: print('originDomain is not a different instance. ' + originDomain + ' ' + self.server.domain_full + ' ' + str(fed_domains)) fitness_performance(POSTstartTime, self.server.fitness, '_POST', 'SharesCatalog', self.server.debug) # receive different types of post created by html_new_post newPostEndpoints = get_new_post_endpoints() for currPostType in newPostEndpoints: if not authorized: if self.server.debug: print('POST was not authorized') break postRedirect = self.server.defaultTimeline if currPostType == 'newshare': postRedirect = 'tlshares' elif currPostType == 'newwanted': postRedirect = 'tlwanted' pageNumber = \ self._receive_new_post(currPostType, self.path, calling_domain, cookie, authorized, self.server.content_license_url) if pageNumber: print(currPostType + ' 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: actorPathStr = \ local_actor_url('http', nickname, self.server.onion_domain) + \ '/' + postRedirect + \ '?page=' + str(pageNumber) self._redirect_headers(actorPathStr, cookie, calling_domain) elif (calling_domain.endswith('.i2p') and self.server.i2p_domain): actorPathStr = \ local_actor_url('http', nickname, self.server.i2p_domain) + \ '/' + postRedirect + \ '?page=' + str(pageNumber) self._redirect_headers(actorPathStr, cookie, calling_domain) else: actorPathStr = \ local_actor_url(self.server.http_prefix, nickname, self.server.domain_full) + \ '/' + postRedirect + '?page=' + str(pageNumber) self._redirect_headers(actorPathStr, cookie, calling_domain) self.server.POSTbusy = False return fitness_performance(POSTstartTime, 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 usersInPath: if authorized: self.outboxAuthenticated = True pathUsersSection = self.path.split('/users/')[1] self.postToNickname = pathUsersSection.split('/')[0] if not self.outboxAuthenticated: self.send_response(405) self.end_headers() self.server.POSTbusy = False return fitness_performance(POSTstartTime, 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.POSTbusy = False return fitness_performance(POSTstartTime, self.server.fitness, '_POST', 'check path', self.server.debug) # 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 self.headers['Content-type'].startswith('image/') and \ not self.headers['Content-type'].startswith('video/') and \ not self.headers['Content-type'].startswith('audio/'): if length > self.server.maxMessageLength: print('Maximum message length exceeded ' + str(length)) self._400() self.server.POSTbusy = False return else: if length > self.server.maxMediaSize: print('Maximum media size exceeded ' + str(length)) self._400() self.server.POSTbusy = False return # receive images to the outbox if self.headers['Content-type'].startswith('image/') and \ usersInPath: self._receive_image(length, calling_domain, cookie, authorized, 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.POSTbusy = False return # refuse to receive non-json content content_typeStr = self.headers['Content-type'] if not content_typeStr.startswith('application/json') and \ not content_typeStr.startswith('application/activity+json') and \ not content_typeStr.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: unknownPost = self.rfile.read(length).decode('utf-8') except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: POST unknownPost ' + 'connection reset by peer') else: print('WARN: POST unknownPost socket error') self.send_response(400) self.end_headers() self.server.POSTbusy = False return except ValueError as ex: print('ERROR: POST unknownPost rfile.read failed, ' + str(ex)) self.send_response(400) self.end_headers() self.server.POSTbusy = False return print(str(unknownPost)) self._400() self.server.POSTbusy = False return if self.server.debug: print('DEBUG: Reading message') fitness_performance(POSTstartTime, self.server.fitness, '_POST', 'check content type', self.server.debug) # check content length before reading bytes if self.path == '/sharedInbox' or self.path == '/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.POSTbusy = False return try: messageBytes = self.rfile.read(length) except SocketError as ex: if ex.errno == errno.ECONNRESET: print('WARN: POST messageBytes ' + 'connection reset by peer') else: print('WARN: POST messageBytes socket error') self.send_response(400) self.end_headers() self.server.POSTbusy = False return except ValueError as ex: print('ERROR: POST messageBytes rfile.read failed, ' + str(ex)) self.send_response(400) self.end_headers() self.server.POSTbusy = False return # check content length after reading bytes if self.path == '/sharedInbox' or self.path == '/inbox': lenMessage = len(messageBytes) if lenMessage > 10240: print('WARN: post to shared inbox is too long ' + str(lenMessage) + ' bytes') self._400() self.server.POSTbusy = False return if contains_invalid_chars(messageBytes.decode("utf-8")): self._400() self.server.POSTbusy = False return # convert the raw bytes to json message_json = json.loads(messageBytes) fitness_performance(POSTstartTime, self.server.fitness, '_POST', 'load json', self.server.debug) # https://www.w3.org/TR/activitypub/#object-without-create if self.outboxAuthenticated: if self._post_to_outbox(message_json, self.server.project_version, None): if message_json.get('id'): locnStr = remove_id_ending(message_json['id']) self.headers['Location'] = locnStr self.send_response(201) self.end_headers() self.server.POSTbusy = False return else: if self.server.debug: print('Failed to post to outbox') self.send_response(403) self.end_headers() self.server.POSTbusy = False return fitness_performance(POSTstartTime, 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.POSTbusy = 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.POSTbusy = False return fitness_performance(POSTstartTime, self.server.fitness, '_POST', 'inbox_message_has_params', self.server.debug) headerSignature = self._getheader_signature_input() if headerSignature: if 'keyId=' not in headerSignature: 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.POSTbusy = False return fitness_performance(POSTstartTime, 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.POSTbusy = False return fitness_performance(POSTstartTime, self.server.fitness, '_POST', 'inbox_permitted_message', self.server.debug) if self.server.debug: print('DEBUG: POST saving to inbox queue') if usersInPath: pathUsersSection = self.path.split('/users/')[1] if '/' not in pathUsersSection: if self.server.debug: print('DEBUG: This is not a users endpoint') else: self.postToNickname = pathUsersSection.split('/')[0] if self.postToNickname: queueStatus = \ self._update_inbox_queue(self.postToNickname, message_json, messageBytes) if queueStatus >= 0 and queueStatus <= 3: self.server.POSTbusy = False return if self.server.debug: print('_update_inbox_queue exited ' + 'without doing anything') else: if self.server.debug: print('self.postToNickname is None') self.send_response(403) self.end_headers() self.server.POSTbusy = False return else: if self.path == '/sharedInbox' or self.path == '/inbox': if self.server.debug: print('DEBUG: POST to shared inbox') queueStatus = \ self._update_inbox_queue('inbox', message_json, messageBytes) if queueStatus >= 0 and queueStatus <= 3: self.server.POSTbusy = False return self._200() self.server.POSTbusy = 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 = sys.exc_info()[:2] if cls is ConnectionResetError: if e.errno != errno.ECONNRESET: print('ERROR: (EpicyonServer) ' + str(cls) + ", " + str(e)) pass elif cls is BrokenPipeError: pass else: print('ERROR: (EpicyonServer) ' + str(cls) + ", " + str(e)) return HTTPServer.handle_error(self, request, client_address) def run_posts_queue(base_dir: str, send_threads: [], debug: bool, timeoutMins: int) -> None: """Manages the threads used to send posts """ while True: time.sleep(1) remove_dormant_threads(base_dir, send_threads, debug, timeoutMins) def run_shares_expire(versionNumber: 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('Starting posts queue watchdog') postsQueueOriginal = httpd.thrPostsQueue.clone(run_posts_queue) httpd.thrPostsQueue.start() while True: time.sleep(20) if httpd.thrPostsQueue.is_alive(): continue httpd.thrPostsQueue.kill() httpd.thrPostsQueue = postsQueueOriginal.clone(run_posts_queue) httpd.thrPostsQueue.start() 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('Starting shares expiry watchdog') sharesExpireOriginal = httpd.thrSharesExpire.clone(run_shares_expire) httpd.thrSharesExpire.start() while True: time.sleep(20) if httpd.thrSharesExpire.is_alive(): continue httpd.thrSharesExpire.kill() httpd.thrSharesExpire = sharesExpireOriginal.clone(run_shares_expire) httpd.thrSharesExpire.start() print('Restarting shares expiry...') def load_tokens(base_dir: str, tokensDict: {}, tokens_lookup: {}) -> None: for subdir, dirs, files in os.walk(base_dir + '/accounts'): for handle in dirs: if '@' in handle: tokenFilename = base_dir + '/accounts/' + handle + '/.token' if not os.path.isfile(tokenFilename): continue nickname = handle.split('@')[0] token = None try: with open(tokenFilename, 'r') as fp: token = fp.read() except Exception as ex: print('WARN: Unable to read token for ' + nickname + ' ' + str(ex)) if not token: continue tokensDict[nickname] = token tokens_lookup[token] = nickname break def run_daemon(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_postsPerSource: int, show_published_date_only: bool, voting_time_mins: int, positive_voting: bool, newswire_votes_threshold: int, news_instance: bool, blogs_instance: bool, media_instance: bool, max_recent_posts: int, enable_shared_inbox: bool, registration: bool, language: str, project_version: str, instance_id: str, client_to_server: bool, base_dir: str, domain: str, onion_domain: str, i2p_domain: str, yt_replace_domain: str, twitter_replacement_domain: str, port: int = 80, proxy_port: int = 80, http_prefix: str = 'https', fed_list: [] = [], max_mentions: int = 10, max_emoji: int = 10, secure_mode: bool = False, proxy_type: str = None, max_replies: int = 64, domain_max_posts_per_day: int = 8640, account_max_posts_per_day: int = 864, allow_deletion: bool = False, debug: bool = False, unit_test: bool = False, instance_only_skills_search: bool = False, send_threads: [] = [], manual_follower_approval: bool = True) -> None: if len(domain) == 0: domain = 'localhost' if '.' not in domain: if domain != 'localhost': print('Invalid domain: ' + domain) return if unit_test: serverAddress = (domain, proxy_port) pubHandler = partial(PubServerUnitTest) else: serverAddress = ('', proxy_port) pubHandler = partial(PubServer) if not os.path.isdir(base_dir + '/accounts'): print('Creating accounts directory') os.mkdir(base_dir + '/accounts') try: httpd = EpicyonServer(serverAddress, pubHandler) except Exception as ex: if ex.errno == 98: print('ERROR: HTTP server address is already in use. ' + str(serverAddress)) return False print('ERROR: HTTP server failed to start. ' + str(ex)) print('serverAddress: ' + str(serverAddress)) return False # scan the theme directory for any svg files containing scripts assert not scan_themes_for_scripts(base_dir) # license for content of the instance if not content_license_url: content_license_url = 'https://creativecommons.org/licenses/by/4.0' httpd.content_license_url = content_license_url # fitness metrics fitness_filename = base_dir + '/accounts/fitness.json' httpd.fitness = {} if os.path.isfile(fitness_filename): httpd.fitness = load_json(fitness_filename) # 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.accessKeys = { 'Page up': ',', 'Page down': '.', 'submitButton': 'y', 'followButton': 'f', 'blockButton': 'b', 'infoButton': 'i', 'snoozeButton': 's', 'reportButton': '[', 'viewButton': 'v', 'enterPetname': 'p', 'enterNotes': 'n', 'menuTimeline': 't', 'menuEdit': 'e', 'menuThemeDesigner': 'z', 'menuProfile': 'p', 'menuInbox': 'i', 'menuSearch': '/', 'menuNewPost': 'n', 'menuCalendar': 'c', 'menuDM': 'd', 'menuReplies': 'r', 'menuOutbox': 's', 'menuBookmarks': 'q', 'menuShares': 'h', 'menuWanted': 'w', 'menuBlogs': 'b', 'menuNewswire': 'u', 'menuLinks': 'l', 'menuMedia': 'm', 'menuModeration': 'o', 'menuFollowing': 'f', 'menuFollowers': 'g', 'menuRoles': 'o', 'menuSkills': 'a', 'menuLogout': 'x', 'menuKeys': 'k', 'Public': 'p', 'Reminder': 'r' } # how many hours after a post was publushed can a reply be made default_reply_interval_hrs = 9999999 httpd.default_reply_interval_hrs = default_reply_interval_hrs httpd.keyShortcuts = {} load_access_keys_for_accounts(base_dir, httpd.keyShortcuts, httpd.accessKeys) # 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 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 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_postsPerSource = max_newswire_postsPerSource # Show only the date at the bottom of posts, and not the time httpd.show_published_date_only = show_published_date_only # maximum number of news articles to mirror httpd.max_mirrored_articles = max_mirrored_articles # maximum number of posts in the news timeline/outbox httpd.max_news_posts = max_news_posts # The maximum number of tags per post which can be # attached to RSS feeds pulled in via the newswire httpd.maxTags = 32 # maximum number of followers per account httpd.max_followers = max_followers # whether to show an icon for publish on the # newswire, or a 'Publish' button httpd.show_publish_as_icon = show_publish_as_icon # Whether to show the timeline header containing inbox, outbox # calendar, etc as the full width of the screen or not httpd.full_width_tl_button_header = full_width_tl_button_header # whether to show icons in the header (eg calendar) as buttons httpd.icons_as_buttons = icons_as_buttons # whether to show the RSS icon at the top or the bottom of the timeline httpd.rss_icon_at_top = rss_icon_at_top # Whether to show the newswire publish button at the top, # above the header image httpd.publish_button_at_top = publish_button_at_top # maximum size of individual RSS feed items, in K httpd.max_feed_item_size_kb = max_feed_item_size_kb # maximum size of a hashtag category, in K httpd.maxCategoriesFeedItemSizeKb = 1024 # how many months does a followed account need to be unseen # for it to be considered dormant? httpd.dormant_months = dormant_months # maximum number of likes to display on a post httpd.max_like_count = max_like_count if httpd.max_like_count < 0: httpd.max_like_count = 0 elif httpd.max_like_count > 16: httpd.max_like_count = 16 httpd.followingItemsPerPage = 12 if registration == 'open': httpd.registration = True else: httpd.registration = False httpd.enable_shared_inbox = enable_shared_inbox httpd.outboxThread = {} httpd.outbox_thread_index = {} httpd.new_post_thread = {} httpd.project_version = project_version httpd.secure_mode = secure_mode # max POST size of 30M httpd.max_post_length = 1024 * 1024 * 30 httpd.maxMediaSize = httpd.max_post_length # Maximum text length is 64K - enough for a blog post httpd.maxMessageLength = 64000 # Maximum overall number of posts per box httpd.maxPostsInBox = 32000 httpd.domain = domain httpd.port = port httpd.domain_full = get_full_domain(domain, port) save_domain_qrcode(base_dir, http_prefix, httpd.domain_full) 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_last_update = 0 httpd.lastGET = 0 httpd.lastPOST = 0 httpd.GETbusy = False httpd.POSTbusy = 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.blockedCache = [] httpd.blockedCacheLastUpdated = 0 httpd.blockedCacheUpdateSecs = 120 httpd.blockedCacheLastUpdated = \ update_blocked_cache(base_dir, httpd.blockedCache, httpd.blockedCacheLastUpdated, httpd.blockedCacheUpdateSecs) # cache to store css files httpd.css_cache = {} # get the list of custom emoji, for use by the mastodon api httpd.customEmoji = \ 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, "lists_enabled", "Murdoch press") # dict of known web crawlers accessing nodeinfo or the masto API # and how many times they have been seen httpd.knownCrawlers = {} knownCrawlersFilename = base_dir + '/accounts/knownCrawlers.json' if os.path.isfile(knownCrawlersFilename): httpd.knownCrawlers = load_json(knownCrawlersFilename) # when was the last crawler seen? httpd.lastKnownCrawler = 0 if lists_enabled: httpd.lists_enabled = lists_enabled else: httpd.lists_enabled = get_config_param(base_dir, "lists_enabled") 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.defaultTimeline = 'inbox' if media_instance: httpd.defaultTimeline = 'tlmedia' if blogs_instance: httpd.defaultTimeline = 'tlblogs' if news_instance: httpd.defaultTimeline = '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('Creating fitness thread') httpd.thrFitness = \ thread_with_trace(target=fitness_thread, args=(base_dir, httpd.fitness), daemon=True) httpd.thrFitness.start() print('Creating cache expiry thread') httpd.thrCache = \ thread_with_trace(target=expire_cache, args=(base_dir, httpd.person_cache, httpd.http_prefix, archive_dir, httpd.maxPostsInBox), daemon=True) httpd.thrCache.start() # number of mins after which sending posts or updates will expire httpd.send_threads_timeout_mins = send_threads_timeout_mins print('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: httpd.thrPostsWatchdog = \ thread_with_trace(target=run_posts_watchdog, args=(project_version, httpd), daemon=True) httpd.thrPostsWatchdog.start() else: httpd.thrPostsQueue.start() print('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: httpd.thrSharesExpireWatchdog = \ thread_with_trace(target=run_shares_expire_watchdog, args=(project_version, httpd), daemon=True) httpd.thrSharesExpireWatchdog.start() else: httpd.thrSharesExpire.start() httpd.recent_posts_cache = {} 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.sharedItemFederationTokens = \ generate_shared_item_federation_tokens(fed_domains, base_dir) httpd.sharedItemFederationTokens = \ create_shared_item_federation_token(base_dir, httpd.domain_full, False, httpd.sharedItemFederationTokens) # load peertube instances from file into a list httpd.peertube_instances = [] load_peertube_instances(base_dir, httpd.peertube_instances) create_initial_last_seen(base_dir, http_prefix) print('Creating inbox queue') httpd.thrInboxQueue = \ thread_with_trace(target=run_inbox_queue, args=(httpd.recent_posts_cache, httpd.max_recent_posts, project_version, base_dir, http_prefix, httpd.send_threads, httpd.postLog, httpd.cached_webfingers, httpd.person_cache, httpd.inbox_queue, domain, onion_domain, i2p_domain, port, proxy_type, httpd.federation_list, max_replies, domain_max_posts_per_day, account_max_posts_per_day, allow_deletion, debug, max_mentions, max_emoji, httpd.translate, unit_test, httpd.yt_replace_domain, httpd.twitter_replacement_domain, httpd.show_published_date_only, httpd.max_followers, httpd.allow_local_network_access, httpd.peertube_instances, verify_all_signatures, httpd.theme_name, httpd.system_language, httpd.max_like_count, httpd.signing_priv_key_pem, httpd.default_reply_interval_hrs, httpd.cw_lists), daemon=True) print('Creating scheduled post thread') httpd.thrPostSchedule = \ thread_with_trace(target=run_post_schedule, args=(base_dir, httpd, 20), daemon=True) print('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('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.restartInboxQueueInProgress = False httpd.restartInboxQueue = 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) if not unit_test: print('Creating inbox queue watchdog') httpd.thrWatchdog = \ thread_with_trace(target=run_inbox_queue_watchdog, args=(project_version, httpd), daemon=True) httpd.thrWatchdog.start() print('Creating scheduled post watchdog') httpd.thrWatchdogSchedule = \ thread_with_trace(target=run_post_schedule_watchdog, args=(project_version, httpd), daemon=True) httpd.thrWatchdogSchedule.start() print('Creating newswire watchdog') httpd.thrNewswireWatchdog = \ thread_with_trace(target=run_newswire_watchdog, args=(project_version, httpd), daemon=True) httpd.thrNewswireWatchdog.start() print('Creating federated shares watchdog') httpd.thrFederatedSharesWatchdog = \ thread_with_trace(target=run_federated_shares_watchdog, args=(project_version, httpd), daemon=True) httpd.thrFederatedSharesWatchdog.start() else: print('Starting inbox queue') httpd.thrInboxQueue.start() print('Starting scheduled posts daemon') httpd.thrPostSchedule.start() print('Starting federated shares daemon') httpd.thrFederatedSharesDaemon.start() 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()