diff --git a/daemon_post.py b/daemon_post.py index 75d0496fe..194387197 100644 --- a/daemon_post.py +++ b/daemon_post.py @@ -16,14 +16,8 @@ from socket import error as SocketError from utils import dangerous_markup from utils import binary_is_image from utils import get_image_extension_from_mime_type -from utils import remove_post_from_cache -from utils import get_cached_post_filename -from utils import text_in_file from utils import load_json from utils import save_json -from utils import locate_post -from utils import get_full_domain -from utils import get_domain_from_actor from utils import is_editor from utils import get_config_param from utils import decoded_host @@ -42,7 +36,6 @@ from session import get_session_for_domain from session import establish_session from fitnessFunctions import fitness_performance from shares import update_shared_item_federation_token -from inbox import populate_replies from inbox import inbox_message_has_params from inbox import inbox_permitted_message from httpsig import getheader_signature_input @@ -61,9 +54,6 @@ from daemon_utils import is_authorized from theme import reset_theme_designer_settings from theme import set_theme from theme import set_theme_from_designer -from languages import get_understood_languages -from city import get_spoofed_city -from posts import create_direct_message_post from daemon_post_login import post_login_screen from daemon_post_receive import receive_new_post from daemon_post_profile import profile_edit @@ -81,6 +71,7 @@ from daemon_post_remove import remove_reading_status from daemon_post_remove import remove_share from daemon_post_remove import remove_wanted from daemon_post_remove import receive_remove_post +from daemon_post_question import receive_vote # maximum number of posts in a hashtag feed MAX_POSTS_IN_HASHTAG_FEED = 6 @@ -369,28 +360,28 @@ def daemon_http_post(self) -> None: '/question?page=' in self.path or \ '/question?firstpost=' in self.path or \ '/question?lastpost=' in self.path: - _receive_vote(self, calling_domain, cookie, - self.path, - self.server.http_prefix, - self.server.domain, - self.server.domain_full, - self.server.port, - self.server.onion_domain, - self.server.i2p_domain, - curr_session, - proxy_type, - self.server.base_dir, - self.server.city, - self.server.person_cache, - self.server.debug, - self.server.system_language, - self.server.low_bandwidth, - self.server.dm_license_url, - self.server.content_license_url, - self.server.translate, - self.server.max_replies, - self.server.project_version, - self.server.recent_posts_cache) + receive_vote(self, calling_domain, cookie, + self.path, + self.server.http_prefix, + self.server.domain, + self.server.domain_full, + self.server.port, + self.server.onion_domain, + self.server.i2p_domain, + curr_session, + proxy_type, + self.server.base_dir, + self.server.city, + self.server.person_cache, + self.server.debug, + self.server.system_language, + self.server.low_bandwidth, + self.server.dm_license_url, + self.server.content_license_url, + self.server.translate, + self.server.max_replies, + self.server.project_version, + self.server.recent_posts_cache) self.server.postreq_busy = False return @@ -1280,144 +1271,6 @@ def _theme_designer_edit(self, calling_domain: str, cookie: str, return -def _send_reply_to_question(self, base_dir: str, - http_prefix: str, - nickname: str, domain: str, - domain_full: str, - port: int, - message_id: str, - answer: str, - curr_session, proxy_type: str, - city_name: str, - person_cache: {}, - debug: bool, - system_language: str, - low_bandwidth: bool, - dm_license_url: str, - content_license_url: str, - translate: {}, - max_replies: int, - project_version: str, - recent_posts_cache: {}) -> None: - """Sends a reply to a question - """ - votes_filename = \ - acct_dir(base_dir, nickname, domain) + \ - '/questions.txt' - - if os.path.isfile(votes_filename): - # have we already voted on this? - if text_in_file(message_id, votes_filename): - print('Already voted on message ' + message_id) - return - - print('Voting on message ' + message_id) - print('Vote for: ' + answer) - comments_enabled = True - attach_image_filename = None - media_type = None - image_description = None - video_transcript = None - in_reply_to = message_id - in_reply_to_atom_uri = message_id - subject = None - schedule_post = False - event_date = None - event_time = None - event_end_time = None - location = None - conversation_id = None - buy_url = '' - chat_url = '' - city = get_spoofed_city(city_name, base_dir, nickname, domain) - languages_understood = \ - get_understood_languages(base_dir, http_prefix, - nickname, domain_full, - person_cache) - reply_to_nickname = get_nickname_from_actor(in_reply_to) - reply_to_domain, reply_to_port = get_domain_from_actor(in_reply_to) - message_json = None - if reply_to_nickname and reply_to_domain: - reply_to_domain_full = \ - get_full_domain(reply_to_domain, reply_to_port) - mentions_str = '@' + reply_to_nickname + '@' + reply_to_domain_full - - message_json = \ - create_direct_message_post(base_dir, nickname, domain, - port, http_prefix, - mentions_str + ' ' + answer, - False, False, - comments_enabled, - attach_image_filename, - media_type, image_description, - video_transcript, city, - in_reply_to, in_reply_to_atom_uri, - subject, debug, - schedule_post, - event_date, event_time, - event_end_time, - location, - system_language, - conversation_id, - low_bandwidth, - dm_license_url, - content_license_url, '', - languages_understood, False, - translate, buy_url, - chat_url, - self.server.auto_cw_cache) - if message_json: - # NOTE: content and contentMap are not required, but we will keep - # them in there so that the post does not get filtered out by - # inbox processing. - # name field contains the answer - message_json['object']['name'] = answer - if post_to_outbox(self, message_json, - project_version, nickname, - curr_session, proxy_type): - post_filename = \ - locate_post(base_dir, nickname, domain, message_id) - if post_filename: - post_json_object = load_json(post_filename) - if post_json_object: - populate_replies(base_dir, - http_prefix, - domain_full, - post_json_object, - max_replies, - debug) - # record the vote - try: - with open(votes_filename, 'a+', - encoding='utf-8') as votes_file: - votes_file.write(message_id + '\n') - except OSError: - print('EX: unable to write vote ' + - votes_filename) - - # ensure that the cached post is removed if it exists, - # so that it then will be recreated - cached_post_filename = \ - get_cached_post_filename(base_dir, - nickname, domain, - post_json_object) - if cached_post_filename: - if os.path.isfile(cached_post_filename): - try: - os.remove(cached_post_filename) - except OSError: - print('EX: _send_reply_to_question ' + - 'unable to delete ' + - cached_post_filename) - # remove from memory cache - remove_post_from_cache(post_json_object, - recent_posts_cache) - else: - print('ERROR: unable to post vote to outbox') - else: - print('ERROR: unable to create vote') - - def _receive_image(self, length: int, path: str, base_dir: str, domain: str, debug: bool) -> None: """Receives an image via POST @@ -1771,140 +1624,3 @@ def _set_hashtag_category2(self, calling_domain: str, cookie: str, redirect_headers(self, tag_screen_str, cookie, calling_domain) self.server.postreq_busy = False - - -def _receive_vote(self, calling_domain: str, cookie: str, - path: str, http_prefix: str, - domain: str, domain_full: str, port: int, - onion_domain: str, i2p_domain: str, - curr_session, proxy_type: str, - base_dir: str, city: str, - person_cache: {}, debug: bool, - system_language: str, - low_bandwidth: bool, - dm_license_url: str, - content_license_url: str, - translate: {}, max_replies: int, - project_version: str, - recent_posts_cache: {}) -> None: - """Receive a vote on a question via POST - """ - first_post_id = '' - if '?firstpost=' in path: - first_post_id = path.split('?firstpost=')[1] - path = path.split('?firstpost=')[0] - if ';firstpost=' in path: - first_post_id = path.split(';firstpost=')[1] - path = path.split(';firstpost=')[0] - if first_post_id: - if '?' in first_post_id: - first_post_id = first_post_id.split('?')[0] - if ';' in first_post_id: - first_post_id = first_post_id.split(';')[0] - first_post_id = first_post_id.replace('/', '--') - first_post_id = ';firstpost=' + first_post_id.replace('#', '--') - - last_post_id = '' - if '?lastpost=' in path: - last_post_id = path.split('?lastpost=')[1] - path = path.split('?lastpost=')[0] - if ';lastpost=' in path: - last_post_id = path.split(';lastpost=')[1] - path = path.split(';lastpost=')[0] - if last_post_id: - if '?' in last_post_id: - last_post_id = last_post_id.split('?')[0] - if ';' in last_post_id: - last_post_id = last_post_id.split(';')[0] - last_post_id = last_post_id.replace('/', '--') - last_post_id = ';lastpost=' + last_post_id.replace('#', '--') - - page_number = 1 - if '?page=' in path: - page_number_str = path.split('?page=')[1] - if '#' in page_number_str: - page_number_str = page_number_str.split('#')[0] - if len(page_number_str) > 5: - page_number_str = "1" - if page_number_str.isdigit(): - page_number = int(page_number_str) - - # the actor who votes - users_path = path.replace('/question', '') - actor = http_prefix + '://' + domain_full + users_path - nickname = get_nickname_from_actor(actor) - if not nickname: - if calling_domain.endswith('.onion') and onion_domain: - actor = 'http://' + onion_domain + users_path - elif (calling_domain.endswith('.i2p') and i2p_domain): - actor = 'http://' + i2p_domain + users_path - actor_path_str = \ - actor + '/' + self.server.default_timeline + \ - '?page=' + str(page_number) - redirect_headers(self, actor_path_str, - cookie, calling_domain) - self.server.postreq_busy = False - return - - # get the parameters - length = int(self.headers['Content-length']) - - try: - question_params = self.rfile.read(length).decode('utf-8') - except SocketError as ex: - if ex.errno == errno.ECONNRESET: - print('EX: POST question_params connection was reset') - else: - print('EX: POST question_params socket error') - self.send_response(400) - self.end_headers() - self.server.postreq_busy = False - return - except ValueError as ex: - print('EX: POST question_params rfile.read failed, ' + str(ex)) - self.send_response(400) - self.end_headers() - self.server.postreq_busy = False - return - - question_params = question_params.replace('+', ' ') - question_params = question_params.replace('%3F', '') - question_params = \ - urllib.parse.unquote_plus(question_params.strip()) - - # post being voted on - message_id = None - if 'messageId=' in question_params: - message_id = question_params.split('messageId=')[1] - if '&' in message_id: - message_id = message_id.split('&')[0] - - answer = None - if 'answer=' in question_params: - answer = question_params.split('answer=')[1] - if '&' in answer: - answer = answer.split('&')[0] - - _send_reply_to_question(self, base_dir, http_prefix, - nickname, domain, domain_full, port, - message_id, answer, - curr_session, proxy_type, city, - person_cache, debug, - system_language, - low_bandwidth, - dm_license_url, - content_license_url, - translate, max_replies, - project_version, - recent_posts_cache) - if calling_domain.endswith('.onion') and onion_domain: - actor = 'http://' + onion_domain + users_path - elif (calling_domain.endswith('.i2p') and i2p_domain): - actor = 'http://' + i2p_domain + users_path - actor_path_str = \ - actor + '/' + self.server.default_timeline + \ - '?page=' + str(page_number) + first_post_id + last_post_id - redirect_headers(self, actor_path_str, cookie, - calling_domain) - self.server.postreq_busy = False - return diff --git a/daemon_post_question.py b/daemon_post_question.py new file mode 100644 index 000000000..1a4f26313 --- /dev/null +++ b/daemon_post_question.py @@ -0,0 +1,303 @@ +__filename__ = "daemon_post_question.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "1.5.0" +__maintainer__ = "Bob Mottram" +__email__ = "bob@libreserver.org" +__status__ = "Production" +__module_group__ = "Core" + +import os +import errno +import urllib.parse +from socket import error as SocketError +from utils import remove_post_from_cache +from utils import get_cached_post_filename +from utils import load_json +from utils import locate_post +from utils import get_full_domain +from utils import get_domain_from_actor +from utils import text_in_file +from utils import get_nickname_from_actor +from utils import acct_dir +from httpheaders import redirect_headers +from city import get_spoofed_city +from languages import get_understood_languages +from posts import create_direct_message_post +from daemon_utils import post_to_outbox +from inbox import populate_replies + + +def receive_vote(self, calling_domain: str, cookie: str, + path: str, http_prefix: str, + domain: str, domain_full: str, port: int, + onion_domain: str, i2p_domain: str, + curr_session, proxy_type: str, + base_dir: str, city: str, + person_cache: {}, debug: bool, + system_language: str, + low_bandwidth: bool, + dm_license_url: str, + content_license_url: str, + translate: {}, max_replies: int, + project_version: str, + recent_posts_cache: {}) -> None: + """Receive a vote on a question via POST + """ + first_post_id = '' + if '?firstpost=' in path: + first_post_id = path.split('?firstpost=')[1] + path = path.split('?firstpost=')[0] + if ';firstpost=' in path: + first_post_id = path.split(';firstpost=')[1] + path = path.split(';firstpost=')[0] + if first_post_id: + if '?' in first_post_id: + first_post_id = first_post_id.split('?')[0] + if ';' in first_post_id: + first_post_id = first_post_id.split(';')[0] + first_post_id = first_post_id.replace('/', '--') + first_post_id = ';firstpost=' + first_post_id.replace('#', '--') + + last_post_id = '' + if '?lastpost=' in path: + last_post_id = path.split('?lastpost=')[1] + path = path.split('?lastpost=')[0] + if ';lastpost=' in path: + last_post_id = path.split(';lastpost=')[1] + path = path.split(';lastpost=')[0] + if last_post_id: + if '?' in last_post_id: + last_post_id = last_post_id.split('?')[0] + if ';' in last_post_id: + last_post_id = last_post_id.split(';')[0] + last_post_id = last_post_id.replace('/', '--') + last_post_id = ';lastpost=' + last_post_id.replace('#', '--') + + page_number = 1 + if '?page=' in path: + page_number_str = path.split('?page=')[1] + if '#' in page_number_str: + page_number_str = page_number_str.split('#')[0] + if len(page_number_str) > 5: + page_number_str = "1" + if page_number_str.isdigit(): + page_number = int(page_number_str) + + # the actor who votes + users_path = path.replace('/question', '') + actor = http_prefix + '://' + domain_full + users_path + nickname = get_nickname_from_actor(actor) + if not nickname: + if calling_domain.endswith('.onion') and onion_domain: + actor = 'http://' + onion_domain + users_path + elif (calling_domain.endswith('.i2p') and i2p_domain): + actor = 'http://' + i2p_domain + users_path + actor_path_str = \ + actor + '/' + self.server.default_timeline + \ + '?page=' + str(page_number) + redirect_headers(self, actor_path_str, + cookie, calling_domain) + self.server.postreq_busy = False + return + + # get the parameters + length = int(self.headers['Content-length']) + + try: + question_params = self.rfile.read(length).decode('utf-8') + except SocketError as ex: + if ex.errno == errno.ECONNRESET: + print('EX: POST question_params connection was reset') + else: + print('EX: POST question_params socket error') + self.send_response(400) + self.end_headers() + self.server.postreq_busy = False + return + except ValueError as ex: + print('EX: POST question_params rfile.read failed, ' + str(ex)) + self.send_response(400) + self.end_headers() + self.server.postreq_busy = False + return + + question_params = question_params.replace('+', ' ') + question_params = question_params.replace('%3F', '') + question_params = \ + urllib.parse.unquote_plus(question_params.strip()) + + # post being voted on + message_id = None + if 'messageId=' in question_params: + message_id = question_params.split('messageId=')[1] + if '&' in message_id: + message_id = message_id.split('&')[0] + + answer = None + if 'answer=' in question_params: + answer = question_params.split('answer=')[1] + if '&' in answer: + answer = answer.split('&')[0] + + _send_reply_to_question(self, base_dir, http_prefix, + nickname, domain, domain_full, port, + message_id, answer, + curr_session, proxy_type, city, + person_cache, debug, + system_language, + low_bandwidth, + dm_license_url, + content_license_url, + translate, max_replies, + project_version, + recent_posts_cache) + if calling_domain.endswith('.onion') and onion_domain: + actor = 'http://' + onion_domain + users_path + elif (calling_domain.endswith('.i2p') and i2p_domain): + actor = 'http://' + i2p_domain + users_path + actor_path_str = \ + actor + '/' + self.server.default_timeline + \ + '?page=' + str(page_number) + first_post_id + last_post_id + redirect_headers(self, actor_path_str, cookie, + calling_domain) + self.server.postreq_busy = False + return + + +def _send_reply_to_question(self, base_dir: str, + http_prefix: str, + nickname: str, domain: str, + domain_full: str, + port: int, + message_id: str, + answer: str, + curr_session, proxy_type: str, + city_name: str, + person_cache: {}, + debug: bool, + system_language: str, + low_bandwidth: bool, + dm_license_url: str, + content_license_url: str, + translate: {}, + max_replies: int, + project_version: str, + recent_posts_cache: {}) -> None: + """Sends a reply to a question + """ + votes_filename = \ + acct_dir(base_dir, nickname, domain) + \ + '/questions.txt' + + if os.path.isfile(votes_filename): + # have we already voted on this? + if text_in_file(message_id, votes_filename): + print('Already voted on message ' + message_id) + return + + print('Voting on message ' + message_id) + print('Vote for: ' + answer) + comments_enabled = True + attach_image_filename = None + media_type = None + image_description = None + video_transcript = None + in_reply_to = message_id + in_reply_to_atom_uri = message_id + subject = None + schedule_post = False + event_date = None + event_time = None + event_end_time = None + location = None + conversation_id = None + buy_url = '' + chat_url = '' + city = get_spoofed_city(city_name, base_dir, nickname, domain) + languages_understood = \ + get_understood_languages(base_dir, http_prefix, + nickname, domain_full, + person_cache) + reply_to_nickname = get_nickname_from_actor(in_reply_to) + reply_to_domain, reply_to_port = get_domain_from_actor(in_reply_to) + message_json = None + if reply_to_nickname and reply_to_domain: + reply_to_domain_full = \ + get_full_domain(reply_to_domain, reply_to_port) + mentions_str = '@' + reply_to_nickname + '@' + reply_to_domain_full + + message_json = \ + create_direct_message_post(base_dir, nickname, domain, + port, http_prefix, + mentions_str + ' ' + answer, + False, False, + comments_enabled, + attach_image_filename, + media_type, image_description, + video_transcript, city, + in_reply_to, in_reply_to_atom_uri, + subject, debug, + schedule_post, + event_date, event_time, + event_end_time, + location, + system_language, + conversation_id, + low_bandwidth, + dm_license_url, + content_license_url, '', + languages_understood, False, + translate, buy_url, + chat_url, + self.server.auto_cw_cache) + if message_json: + # NOTE: content and contentMap are not required, but we will keep + # them in there so that the post does not get filtered out by + # inbox processing. + # name field contains the answer + message_json['object']['name'] = answer + if post_to_outbox(self, message_json, + project_version, nickname, + curr_session, proxy_type): + post_filename = \ + locate_post(base_dir, nickname, domain, message_id) + if post_filename: + post_json_object = load_json(post_filename) + if post_json_object: + populate_replies(base_dir, + http_prefix, + domain_full, + post_json_object, + max_replies, + debug) + # record the vote + try: + with open(votes_filename, 'a+', + encoding='utf-8') as votes_file: + votes_file.write(message_id + '\n') + except OSError: + print('EX: unable to write vote ' + + votes_filename) + + # ensure that the cached post is removed if it exists, + # so that it then will be recreated + cached_post_filename = \ + get_cached_post_filename(base_dir, + nickname, domain, + post_json_object) + if cached_post_filename: + if os.path.isfile(cached_post_filename): + try: + os.remove(cached_post_filename) + except OSError: + print('EX: _send_reply_to_question ' + + 'unable to delete ' + + cached_post_filename) + # remove from memory cache + remove_post_from_cache(post_json_object, + recent_posts_cache) + else: + print('ERROR: unable to post vote to outbox') + else: + print('ERROR: unable to create vote')