Bob Mottram 2022-12-28 10:44:18 +00:00
commit f217f4ad48
8 changed files with 293 additions and 261 deletions

View File

@ -12,6 +12,10 @@ from utils import has_object_dict
from utils import acct_dir
from utils import remove_id_ending
from utils import text_in_file
from utils import locate_post
from utils import load_json
from keys import get_instance_actor_key
from session import get_json
def _get_conversation_filename(base_dir: str, nickname: str, domain: str,
@ -98,3 +102,88 @@ def unmute_conversation(base_dir: str, nickname: str, domain: str,
except OSError:
print('EX: unmute_conversation unable to delete ' +
conversation_filename + '.muted')
def download_conversation_posts(session, http_prefix: str, base_dir: str,
nickname: str, domain: str,
post_id: str, debug: bool) -> []:
"""Downloads all posts for a conversation and returns a list of the
json objects
"""
if '://' not in post_id:
return []
profile_str = 'https://www.w3.org/ns/activitystreams'
as_header = {
'Accept': 'application/ld+json; profile="' + profile_str + '"'
}
conversation_view = []
signing_priv_key_pem = get_instance_actor_key(base_dir, domain)
post_id = remove_id_ending(post_id)
post_filename = \
locate_post(base_dir, nickname, domain, post_id)
if post_filename:
post_json = load_json(post_filename)
else:
post_json = get_json(signing_priv_key_pem, session, post_id,
as_header, None, debug, __version__,
http_prefix, domain)
if debug:
if not post_json:
print(post_id + ' returned no json')
while post_json:
if not isinstance(post_json, dict):
break
if not has_object_dict(post_json):
if not post_json.get('attributedTo'):
print(str(post_json))
if debug:
print(post_id + ' has no attributedTo')
break
if not isinstance(post_json['attributedTo'], str):
break
if not post_json.get('published'):
if debug:
print(post_id + ' has no published date')
break
if not post_json.get('to'):
if debug:
print(post_id + ' has no "to" list')
break
if not isinstance(post_json['to'], list):
break
if 'cc' not in post_json:
if debug:
print(post_id + ' has no "cc" list')
break
if not isinstance(post_json['cc'], list):
break
wrapped_post = {
"@context": "https://www.w3.org/ns/activitystreams",
'id': post_id + '/activity',
'type': 'Create',
'actor': post_json['attributedTo'],
'published': post_json['published'],
'to': post_json['to'],
'cc': post_json['cc'],
'object': post_json
}
post_json = wrapped_post
conversation_view = [post_json] + conversation_view
if not post_json['object'].get('inReplyTo'):
if debug:
print(post_id + ' is not a reply')
break
post_id = post_json['object']['inReplyTo']
post_id = remove_id_ending(post_id)
post_filename = \
locate_post(base_dir, nickname, domain, post_id)
if post_filename:
post_json = load_json(post_filename)
else:
post_json = get_json(signing_priv_key_pem, session, post_id,
as_header, None, debug, __version__,
http_prefix, domain)
if debug:
if not post_json:
print(post_id + ' returned no json')
return conversation_view

View File

@ -83,6 +83,7 @@ from person import remove_account
from person import can_remove_post
from person import person_snooze
from person import person_unsnooze
from keys import get_instance_actor_key
from posts import get_max_profile_posts
from posts import set_max_profile_posts
from posts import get_post_expiry_keep_dms
@ -91,7 +92,6 @@ from posts import get_post_expiry_days
from posts import set_post_expiry_days
from posts import get_original_post_from_announce_url
from posts import save_post_to_box
from posts import get_instance_actor_key
from posts import remove_post_interactions
from posts import outbox_message_create_wrap
from posts import get_pinned_post_as_json
@ -221,7 +221,7 @@ 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_conversation_thread
from webapp_conversation import html_conversation_view
from webapp_post import html_emoji_reaction_picker
from webapp_post import html_post_replies
from webapp_post import html_individual_post
@ -11834,7 +11834,7 @@ class PubServer(BaseHTTPRequestHandler):
if self.server.bold_reading.get(nickname):
bold_reading = True
conv_str = \
html_conversation_thread(post_id, self.server.translate,
html_conversation_view(post_id, self.server.translate,
base_dir,
http_prefix,
nickname,

View File

@ -26,9 +26,9 @@ from roles import set_role
from webfinger import webfinger_handle
from bookmarks import send_bookmark_via_server
from bookmarks import send_undo_bookmark_via_server
from posts import download_conversation_posts
from conversation import download_conversation_posts
from keys import get_instance_actor_key
from posts import set_post_expiry_days
from posts import get_instance_actor_key
from posts import send_mute_via_server
from posts import send_undo_mute_via_server
from posts import c2s_box_json

64
keys.py 100644
View File

@ -0,0 +1,64 @@
__filename__ = "keys.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.3.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@libreserver.org"
__status__ = "Production"
__module_group__ = "ActivityPub"
import os
def _get_local_private_key(base_dir: str, nickname: str, domain: str) -> str:
"""Returns the private key for a local account
"""
if not domain or not nickname:
return None
handle = nickname + '@' + domain
key_filename = base_dir + '/keys/private/' + handle.lower() + '.key'
if not os.path.isfile(key_filename):
return None
with open(key_filename, 'r', encoding='utf-8') as pem_file:
return pem_file.read()
return None
def _get_local_public_key(base_dir: str, nickname: str, domain: str) -> str:
"""Returns the public key for a local account
"""
if not domain or not nickname:
return None
handle = nickname + '@' + domain
key_filename = base_dir + '/keys/public/' + handle.lower() + '.key'
if not os.path.isfile(key_filename):
return None
with open(key_filename, 'r', encoding='utf-8') as pem_file:
return pem_file.read()
return None
def get_instance_actor_key(base_dir: str, domain: str) -> str:
"""Returns the private key for the instance actor used for
signing GET posts
"""
return _get_local_private_key(base_dir, 'inbox', domain)
def get_person_key(nickname: str, domain: str, base_dir: str,
key_type: str = 'public', debug: bool = False):
"""Returns the public or private key of a person
"""
if key_type == 'private':
key_pem = _get_local_private_key(base_dir, nickname, domain)
else:
key_pem = _get_local_public_key(base_dir, nickname, domain)
if not key_pem:
if debug:
print('DEBUG: ' + key_type + ' key file not found')
return ''
if len(key_pem) < 20:
if debug:
print('DEBUG: private key was too short: ' + key_pem)
return ''
return key_pem

142
posts.py
View File

@ -101,6 +101,7 @@ from petnames import resolve_petnames
from video import convert_video_to_note
from context import get_individual_post_context
from maps import geocoords_from_map_link
from keys import get_person_key
def is_moderator(base_dir: str, nickname: str) -> bool:
@ -151,60 +152,6 @@ def no_of_followers_on_domain(base_dir: str, handle: str,
return ctr
def _get_local_private_key(base_dir: str, nickname: str, domain: str) -> str:
"""Returns the private key for a local account
"""
if not domain or not nickname:
return None
handle = nickname + '@' + domain
key_filename = base_dir + '/keys/private/' + handle.lower() + '.key'
if not os.path.isfile(key_filename):
return None
with open(key_filename, 'r', encoding='utf-8') as pem_file:
return pem_file.read()
return None
def get_instance_actor_key(base_dir: str, domain: str) -> str:
"""Returns the private key for the instance actor used for
signing GET posts
"""
return _get_local_private_key(base_dir, 'inbox', domain)
def _get_local_public_key(base_dir: str, nickname: str, domain: str) -> str:
"""Returns the public key for a local account
"""
if not domain or not nickname:
return None
handle = nickname + '@' + domain
key_filename = base_dir + '/keys/public/' + handle.lower() + '.key'
if not os.path.isfile(key_filename):
return None
with open(key_filename, 'r', encoding='utf-8') as pem_file:
return pem_file.read()
return None
def _get_person_key(nickname: str, domain: str, base_dir: str,
key_type: str = 'public', debug: bool = False):
"""Returns the public or private key of a person
"""
if key_type == 'private':
key_pem = _get_local_private_key(base_dir, nickname, domain)
else:
key_pem = _get_local_public_key(base_dir, nickname, domain)
if not key_pem:
if debug:
print('DEBUG: ' + key_type + ' key file not found')
return ''
if len(key_pem) < 20:
if debug:
print('DEBUG: private key was too short: ' + key_pem)
return ''
return key_pem
def _clean_html(raw_html: str) -> str:
# text=BeautifulSoup(raw_html, 'html.parser').get_text()
text = raw_html
@ -2532,7 +2479,7 @@ def send_post(signing_priv_key_pem: str, project_version: str,
translate)
# get the senders private key
private_key_pem = _get_person_key(nickname, domain, base_dir, 'private')
private_key_pem = get_person_key(nickname, domain, base_dir, 'private')
if len(private_key_pem) == 0:
return 6
@ -2937,7 +2884,7 @@ def send_signed_json(post_json_object: {}, session, base_dir: str,
if account_domain == i2p_domain:
account_domain = curr_domain
private_key_pem = \
_get_person_key(nickname, account_domain, base_dir, 'private', debug)
get_person_key(nickname, account_domain, base_dir, 'private', debug)
if len(private_key_pem) == 0:
if debug:
print('DEBUG: Private key not found for ' +
@ -6094,86 +6041,3 @@ def set_max_profile_posts(base_dir: str, nickname: str, domain: str,
max_posts_filename)
return False
return True
def download_conversation_posts(session, http_prefix: str, base_dir: str,
nickname: str, domain: str,
post_id: str, debug: bool) -> []:
"""Downloads all posts for a conversation and returns a list of the
json objects
"""
if '://' not in post_id:
return []
profile_str = 'https://www.w3.org/ns/activitystreams'
as_header = {
'Accept': 'application/ld+json; profile="' + profile_str + '"'
}
conversation_thread = []
signing_priv_key_pem = get_instance_actor_key(base_dir, domain)
post_id = remove_id_ending(post_id)
post_filename = \
locate_post(base_dir, nickname, domain, post_id)
if post_filename:
post_json = load_json(post_filename)
else:
post_json = get_json(signing_priv_key_pem, session, post_id,
as_header, None, debug, __version__,
http_prefix, domain)
if debug:
if not post_json:
print(post_id + ' returned no json')
while post_json:
if not has_object_dict(post_json):
if not post_json.get('attributedTo'):
print(str(post_json))
if debug:
print(post_id + ' has no attributedTo')
break
if not isinstance(post_json['attributedTo'], str):
break
if not post_json.get('published'):
if debug:
print(post_id + ' has no published date')
break
if not post_json.get('to'):
if debug:
print(post_id + ' has no "to" list')
break
if not isinstance(post_json['to'], list):
break
if 'cc' not in post_json:
if debug:
print(post_id + ' has no "cc" list')
break
if not isinstance(post_json['cc'], list):
break
wrapped_post = {
"@context": "https://www.w3.org/ns/activitystreams",
'id': post_id + '/activity',
'type': 'Create',
'actor': post_json['attributedTo'],
'published': post_json['published'],
'to': post_json['to'],
'cc': post_json['cc'],
'object': post_json
}
post_json = wrapped_post
conversation_thread = [post_json] + conversation_thread
if not post_json['object'].get('inReplyTo'):
if debug:
print(post_id + ' is not a reply')
break
post_id = post_json['object']['inReplyTo']
post_id = remove_id_ending(post_id)
post_filename = \
locate_post(base_dir, nickname, domain, post_id)
if post_filename:
post_json = load_json(post_filename)
else:
post_json = get_json(signing_priv_key_pem, session, post_id,
as_header, None, debug, __version__,
http_prefix, domain)
if debug:
if not post_json:
print(post_id + ' returned no json')
return conversation_thread

View File

@ -1400,7 +1400,7 @@ def test_post_message_between_servers(base_dir: str) -> None:
assert 'यह एक परीक्षण है' in received_json['object']['content']
print('Check that message received from Alice contains an attachment')
assert received_json['object']['attachment']
assert len(received_json['object']['attachment']) == 1
assert len(received_json['object']['attachment']) == 2
attached = received_json['object']['attachment'][0]
pprint(attached)
assert attached.get('type')

View File

@ -0,0 +1,104 @@
__filename__ = "conversation.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.3.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@libreserver.org"
__status__ = "Production"
__module_group__ = "Timeline"
import os
from conversation import download_conversation_posts
from utils import get_config_param
from webapp_utils import html_header_with_external_style
from webapp_utils import html_post_separator
from webapp_utils import html_footer
from webapp_post import individual_post_as_html
def html_conversation_view(post_id: str,
translate: {}, base_dir: str,
http_prefix: str,
nickname: str, domain: str,
project_version: str,
recent_posts_cache: {},
max_recent_posts: int,
session,
cached_webfingers,
person_cache: {},
port: int,
yt_replace_domain: str,
twitter_replacement_domain: str,
show_published_date_only: bool,
peertube_instances: [],
allow_local_network_access: bool,
theme_name: str,
system_language: str,
max_like_count: int,
signing_priv_key_pem: str,
cw_lists: {},
lists_enabled: str,
timezone: str, bold_reading: bool,
dogwhistles: {}, access_keys: {},
min_images_for_accounts: [],
debug: bool) -> str:
"""Show a page containing a conversation thread
"""
conv_posts = \
download_conversation_posts(session, http_prefix, base_dir,
nickname, domain,
post_id, debug)
if not conv_posts:
return None
css_filename = base_dir + '/epicyon-profile.css'
if os.path.isfile(base_dir + '/epicyon.css'):
css_filename = base_dir + '/epicyon.css'
instance_title = \
get_config_param(base_dir, 'instanceTitle')
conv_str = \
html_header_with_external_style(css_filename, instance_title, None)
separator_str = html_post_separator(base_dir, None)
text_mode_separator = '<div class="transparent"><hr></div>'
minimize_all_images = False
if nickname in min_images_for_accounts:
minimize_all_images = True
for post_json_object in conv_posts:
show_individual_post_icons = False
allow_deletion = False
post_str = \
individual_post_as_html(signing_priv_key_pem,
True, recent_posts_cache,
max_recent_posts,
translate, None,
base_dir, session, cached_webfingers,
person_cache,
nickname, domain, port,
post_json_object,
None, True, allow_deletion,
http_prefix, project_version,
'search',
yt_replace_domain,
twitter_replacement_domain,
show_published_date_only,
peertube_instances,
allow_local_network_access,
theme_name, system_language,
max_like_count,
show_individual_post_icons,
show_individual_post_icons,
False, False, False, False,
cw_lists, lists_enabled,
timezone, False, bold_reading,
dogwhistles,
minimize_all_images)
if post_str:
conv_str += text_mode_separator + separator_str + post_str
conv_str += text_mode_separator + html_footer()
return conv_str

View File

@ -21,7 +21,6 @@ from announce import no_of_announces
from like import liked_by_person
from like import no_of_likes
from follow import is_following_actor
from posts import download_conversation_posts
from posts import post_is_muted
from posts import get_person_box
from posts import download_announce
@ -93,7 +92,6 @@ from webapp_utils import get_post_attachments_as_html
from webapp_utils import html_header_with_external_style
from webapp_utils import html_footer
from webapp_utils import get_broken_link_substitute
from webapp_utils import html_post_separator
from webapp_media import add_embedded_elements
from webapp_question import insert_question
from devices import e2e_edecrypt_message_from_device
@ -2922,90 +2920,3 @@ def html_emoji_reaction_picker(recent_posts_cache: {}, max_recent_posts: int,
'</header>\n'
return header_str + reacted_to_post_str + emoji_picks_str + html_footer()
def html_conversation_thread(post_id: str,
translate: {}, base_dir: str,
http_prefix: str,
nickname: str, domain: str,
project_version: str,
recent_posts_cache: {},
max_recent_posts: int,
session,
cached_webfingers,
person_cache: {},
port: int,
yt_replace_domain: str,
twitter_replacement_domain: str,
show_published_date_only: bool,
peertube_instances: [],
allow_local_network_access: bool,
theme_name: str,
system_language: str,
max_like_count: int,
signing_priv_key_pem: str,
cw_lists: {},
lists_enabled: str,
timezone: str, bold_reading: bool,
dogwhistles: {}, access_keys: {},
min_images_for_accounts: [],
debug: bool) -> str:
"""Show a page containing a conversation thread
"""
conv_posts = \
download_conversation_posts(session, http_prefix, base_dir,
nickname, domain,
post_id, debug)
if not conv_posts:
return None
css_filename = base_dir + '/epicyon-profile.css'
if os.path.isfile(base_dir + '/epicyon.css'):
css_filename = base_dir + '/epicyon.css'
instance_title = \
get_config_param(base_dir, 'instanceTitle')
conv_str = \
html_header_with_external_style(css_filename, instance_title, None)
separator_str = html_post_separator(base_dir, None)
text_mode_separator = '<div class="transparent"><hr></div>'
minimize_all_images = False
if nickname in min_images_for_accounts:
minimize_all_images = True
for post_json_object in conv_posts:
show_individual_post_icons = False
allow_deletion = False
post_str = \
individual_post_as_html(signing_priv_key_pem,
True, recent_posts_cache,
max_recent_posts,
translate, None,
base_dir, session, cached_webfingers,
person_cache,
nickname, domain, port,
post_json_object,
None, True, allow_deletion,
http_prefix, project_version,
'search',
yt_replace_domain,
twitter_replacement_domain,
show_published_date_only,
peertube_instances,
allow_local_network_access,
theme_name, system_language,
max_like_count,
show_individual_post_icons,
show_individual_post_icons,
False, False, False, False,
cw_lists, lists_enabled,
timezone, False, bold_reading,
dogwhistles,
minimize_all_images)
if post_str:
conv_str += text_mode_separator + separator_str + post_str
conv_str += text_mode_separator + html_footer()
return conv_str