__filename__ = "webapp_profile.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.3.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@libreserver.org"
__status__ = "Production"
__module_group__ = "Web Interface"
import os
from pprint import pprint
from webfinger import webfinger_handle
from utils import standardize_text
from utils import get_display_name
from utils import is_group_account
from utils import has_object_dict
from utils import get_occupation_name
from utils import get_locked_account
from utils import get_full_domain
from utils import is_artist
from utils import is_dormant
from utils import get_nickname_from_actor
from utils import get_domain_from_actor
from utils import is_system_account
from utils import remove_html
from utils import load_json
from utils import get_config_param
from utils import get_image_formats
from utils import acct_dir
from utils import get_supported_languages
from utils import local_actor_url
from utils import get_reply_interval_hours
from utils import get_account_timezone
from utils import remove_eol
from languages import get_actor_languages
from skills import get_skills
from theme import get_themes_list
from person import person_box_json
from person import get_actor_json
from person import get_person_avatar_url
from posts import get_post_expiry_keep_dms
from posts import get_post_expiry_days
from posts import get_person_box
from posts import is_moderator
from posts import parse_user_feed
from posts import is_create_inside_announce
from donate import get_donation_url
from donate import get_website
from donate import get_gemini_link
from xmpp import get_xmpp_address
from matrix import get_matrix_address
from ssb import get_ssb_address
from pgp import get_email_address
from pgp import get_pgp_fingerprint
from pgp import get_pgp_pub_key
from enigma import get_enigma_pub_key
from tox import get_tox_address
from briar import get_briar_address
from cwtch import get_cwtch_address
from filters import is_filtered
from follow import is_follower_of_person
from follow import get_follower_domains
from webapp_frontscreen import html_front_screen
from webapp_utils import edit_number_field
from webapp_utils import html_keyboard_navigation
from webapp_utils import html_hide_from_screen_reader
from webapp_utils import scheduled_posts_exist
from webapp_utils import html_header_with_external_style
from webapp_utils import html_header_with_person_markup
from webapp_utils import html_footer
from webapp_utils import add_emoji_to_display_name
from webapp_utils import get_profile_background_file
from webapp_utils import html_post_separator
from webapp_utils import edit_check_box
from webapp_utils import edit_text_field
from webapp_utils import edit_text_area
from webapp_utils import begin_edit_section
from webapp_utils import end_edit_section
from blog import get_blog_address
from webapp_post import individual_post_as_html
from webapp_timeline import html_individual_share
from webapp_timeline import page_number_buttons
from blocking import get_cw_list_variable
from blocking import is_blocked
from content import bold_reading_string
from roles import is_devops
from session import site_is_verified
THEME_FORMATS = '.zip, .gz'
def _valid_profile_preview_post(post_json_object: {},
person_url: str) -> (bool, {}):
"""Returns true if the given post should appear on a person/group profile
after searching for a handle
"""
is_announced_feed_item = False
if is_create_inside_announce(post_json_object):
is_announced_feed_item = True
post_json_object = post_json_object['object']
if not post_json_object.get('type'):
return False, None
if post_json_object['type'] == 'Create':
if not has_object_dict(post_json_object):
return False, None
if post_json_object['type'] != 'Create' and \
post_json_object['type'] != 'Announce':
if post_json_object['type'] != 'Note' and \
post_json_object['type'] != 'Page':
return False, None
if not post_json_object.get('to'):
return False, None
if not post_json_object.get('id'):
return False, None
# wrap in create
cc_list = []
if post_json_object.get('cc'):
cc_list = post_json_object['cc']
new_post_json_object = {
'object': post_json_object,
'to': post_json_object['to'],
'cc': cc_list,
'id': post_json_object['id'],
'actor': person_url,
'type': 'Create'
}
post_json_object = new_post_json_object
if not post_json_object.get('actor'):
return False, None
if not is_announced_feed_item:
if has_object_dict(post_json_object):
if post_json_object['actor'] != person_url and \
post_json_object['object']['type'] != 'Page':
return False, None
return True, post_json_object
def html_profile_after_search(recent_posts_cache: {}, max_recent_posts: int,
translate: {},
base_dir: str, path: str, http_prefix: str,
nickname: str, domain: str, port: int,
profile_handle: str,
session, cached_webfingers: {}, person_cache: {},
debug: bool, project_version: str,
yt_replace_domain: str,
twitter_replacement_domain: str,
show_published_date_only: bool,
default_timeline: str,
peertube_instances: [],
allow_local_network_access: bool,
theme_name: str,
access_keys: {},
system_language: str,
max_like_count: int,
signing_priv_key_pem: str,
cw_lists: {}, lists_enabled: str,
timezone: str,
onion_domain: str, i2p_domain: str,
bold_reading: bool, dogwhistles: {}) -> str:
"""Show a profile page after a search for a fediverse address
"""
http = False
gnunet = False
ipfs = False
ipns = False
if http_prefix == 'http':
http = True
elif http_prefix == 'gnunet':
gnunet = True
elif http_prefix == 'ipfs':
ipfs = True
elif http_prefix == 'ipns':
ipns = True
from_domain = domain
if onion_domain:
if '.onion/' in profile_handle or profile_handle.endswith('.onion'):
from_domain = onion_domain
http = True
if i2p_domain:
if '.i2p/' in profile_handle or profile_handle.endswith('.i2p'):
from_domain = i2p_domain
http = True
profile_json, as_header = \
get_actor_json(from_domain, profile_handle, http,
gnunet, ipfs, ipns, debug, False,
signing_priv_key_pem, session)
if not profile_json:
return None
if not profile_json.get('id'):
return None
person_url = profile_json['id']
search_domain, search_port = get_domain_from_actor(person_url)
if not search_domain:
return None
search_nickname = get_nickname_from_actor(person_url)
if not search_nickname:
return None
search_domain_full = get_full_domain(search_domain, search_port)
profile_str = ''
css_filename = base_dir + '/epicyon-profile.css'
if os.path.isfile(base_dir + '/epicyon.css'):
css_filename = base_dir + '/epicyon.css'
is_group = False
if profile_json.get('type'):
if profile_json['type'] == 'Group':
is_group = True
avatar_url = ''
if profile_json.get('icon'):
if profile_json['icon'].get('url'):
avatar_url = profile_json['icon']['url']
if not avatar_url:
avatar_url = get_person_avatar_url(base_dir, person_url, person_cache)
display_name = search_nickname
if profile_json.get('name'):
display_name = profile_json['name']
locked_account = get_locked_account(profile_json)
if locked_account:
display_name += '🔒'
moved_to = ''
if profile_json.get('movedTo'):
moved_to = profile_json['movedTo']
if '"' in moved_to:
moved_to = moved_to.split('"')[1]
display_name += ' ⌂'
follows_you = \
is_follower_of_person(base_dir,
nickname, domain,
search_nickname,
search_domain_full)
profile_description = ''
if profile_json.get('summary'):
profile_description = profile_json['summary']
outbox_url = None
if not profile_json.get('outbox'):
if debug:
pprint(profile_json)
print('DEBUG: No outbox found')
return None
outbox_url = profile_json['outbox']
# profileBackgroundImage = ''
# if profile_json.get('image'):
# if profile_json['image'].get('url'):
# profileBackgroundImage = profile_json['image']['url']
# url to return to
back_url = path
if not back_url.endswith('/inbox'):
back_url += '/inbox'
profile_description_short = profile_description
if '\n' in profile_description:
if len(profile_description.split('\n')) > 2:
profile_description_short = ''
else:
if '
' in profile_description:
if len(profile_description.split('
')) > 2:
profile_description_short = ''
# keep the profile description short
if len(profile_description_short) > 2048:
profile_description_short = ''
# remove formatting from profile description used on title
avatar_description = ''
if profile_json.get('summary'):
if isinstance(profile_json['summary'], str):
avatar_description = \
profile_json['summary'].replace('
', '\n')
avatar_description = avatar_description.replace('
', '') avatar_description = avatar_description.replace('
', '') if '<' in avatar_description: avatar_description = remove_html(avatar_description) image_url = '' if profile_json.get('image'): if profile_json['image'].get('url'): image_url = profile_json['image']['url'] also_known_as = None if profile_json.get('alsoKnownAs'): also_known_as = profile_json['alsoKnownAs'] joined_date = None if profile_json.get('published'): if 'T' in profile_json['published']: joined_date = profile_json['published'] profile_str = \ _get_profile_header_after_search(nickname, default_timeline, search_nickname, search_domain_full, translate, display_name, follows_you, profile_description_short, avatar_url, image_url, moved_to, profile_json['id'], also_known_as, access_keys, joined_date) domain_full = get_full_domain(domain, port) follow_is_permitted = True if not profile_json.get('followers'): # no followers collection specified within actor follow_is_permitted = False elif search_nickname == 'news' and search_domain_full == domain_full: # currently the news actor is not something you can follow follow_is_permitted = False elif search_nickname == nickname and search_domain_full == domain_full: # don't follow yourself! follow_is_permitted = False blocked = \ is_blocked(base_dir, nickname, domain, search_nickname, search_domain) if follow_is_permitted: follow_str = 'Follow' if is_group: follow_str = 'Join' profile_str += \ '' + translate['Website'] + ': ' + \ '' + \ website_url + '
\n' if gemini_link: donate_section += \ '' + 'Gemini' + ': ' + \ gemini_link + '
\n' if email_address: donate_section += \ '' + translate['Email'] + ': ' + \ email_address + '
\n' if blog_address: if site_is_verified(session, base_dir, http_prefix, nickname, domain, blog_address, False, debug): donate_section += \ 'Blog: ' + \ blog_address + '
\n' if xmpp_address: donate_section += \ '' + translate['XMPP'] + ': ' + xmpp_address + '
\n' if matrix_address: donate_section += \ '' + translate['Matrix'] + ': ' + matrix_address + '
\n' if ssb_address: donate_section += \ 'SSB:
\n' if tox_address: donate_section += \ 'Tox:
\n' if briar_address: if briar_address.startswith('briar://'): donate_section += \ '\n' else: donate_section += \ 'briar://
\n' if cwtch_address: donate_section += \ 'Cwtch:
\n' if enigma_pub_key: donate_section += \ 'Enigma:
\n' if pgp_fingerprint: donate_section += \ 'PGP: ' + \
pgp_fingerprint.replace('\n', '
') + '
' + \
pgp_pub_key.replace('\n', '
') + '
', '') avatar_description = avatar_description.replace('
', '') moved_to = '' if profile_json.get('movedTo'): moved_to = profile_json['movedTo'] if '"' in moved_to: moved_to = moved_to.split('"')[1] also_known_as = None if profile_json.get('alsoKnownAs'): also_known_as = profile_json['alsoKnownAs'] joined_date = None if profile_json.get('published'): if 'T' in profile_json['published']: joined_date = profile_json['published'] occupation_name = None if profile_json.get('hasOccupation'): occupation_name = get_occupation_name(profile_json) avatar_url = profile_json['icon']['url'] # use alternate path for local avatars to avoid any caching issues if '://' + domain_full + '/system/accounts/avatars/' in avatar_url: avatar_url = \ avatar_url.replace('://' + domain_full + '/system/accounts/avatars/', '://' + domain_full + '/users/') # get pinned post content account_dir = acct_dir(base_dir, nickname, domain) pinned_filename = account_dir + '/pinToProfile.txt' pinned_content = None if os.path.isfile(pinned_filename): with open(pinned_filename, 'r', encoding='utf-8') as pin_file: pinned_content = pin_file.read() profile_header_str = \ _get_profile_header(base_dir, http_prefix, nickname, domain, domain_full, translate, default_timeline, display_name, profile_description_short, login_button, avatar_url, theme, moved_to, also_known_as, pinned_content, access_keys, joined_date, occupation_name) # keyboard navigation user_path_str = '/users/' + nickname deft = default_timeline is_group = False followers_str = translate['Followers'] if is_group_account(base_dir, nickname, domain): is_group = True followers_str = translate['Members'] menu_timeline = \ html_hide_from_screen_reader('🏠') + ' ' + \ translate['Switch to timeline view'] menu_edit = \ html_hide_from_screen_reader('✍') + ' ' + translate['Edit'] menu_followers = \ html_hide_from_screen_reader('👪') + ' ' + followers_str menu_logout = \ html_hide_from_screen_reader('❎') + ' ' + translate['Logout'] nav_links = { menu_timeline: user_path_str + '/' + deft, menu_edit: user_path_str + '/editprofile', menu_followers: user_path_str + '/followers#timeline', menu_logout: '/logout' } if not is_group: menu_following = \ html_hide_from_screen_reader('👥') + ' ' + translate['Following'] nav_links[menu_following] = user_path_str + '/following#timeline' menu_roles = \ html_hide_from_screen_reader('🤚') + ' ' + translate['Roles'] nav_links[menu_roles] = user_path_str + '/roles#timeline' menu_skills = \ html_hide_from_screen_reader('🛠') + ' ' + translate['Skills'] nav_links[menu_skills] = user_path_str + '/skills#timeline' if is_artist(base_dir, nickname): menu_theme_designer = \ html_hide_from_screen_reader('🎨') + ' ' + \ translate['Theme Designer'] nav_links[menu_theme_designer] = user_path_str + '/themedesigner' nav_access_keys = {} for variable_name, key in access_keys.items(): if not locals().get(variable_name): continue nav_access_keys[locals()[variable_name]] = key profile_str = html_keyboard_navigation(text_mode_banner, nav_links, nav_access_keys) profile_str += profile_header_str + donate_section profile_str += '@' + nickname + '@' + domain + ' has no roles assigned
\n' else: profile_str = '