2023-03-17 21:13:43 +00:00
|
|
|
__filename__ = "followerSync.py"
|
|
|
|
__author__ = "Bob Mottram"
|
|
|
|
__license__ = "AGPL3+"
|
|
|
|
__version__ = "1.4.0"
|
|
|
|
__maintainer__ = "Bob Mottram"
|
|
|
|
__email__ = "bob@libreserver.org"
|
|
|
|
__status__ = "Production"
|
|
|
|
__module_group__ = "ActivityPub"
|
|
|
|
|
|
|
|
import os
|
2023-03-17 22:26:12 +00:00
|
|
|
import hashlib
|
2023-03-17 21:13:43 +00:00
|
|
|
from hashlib import sha256
|
|
|
|
from utils import acct_dir
|
|
|
|
from utils import get_user_paths
|
|
|
|
|
|
|
|
|
2023-03-18 21:06:10 +00:00
|
|
|
def remove_followers_sync(followers_sync_cache: {},
|
|
|
|
nickname: str,
|
|
|
|
follower_domain: str) -> None:
|
|
|
|
"""Remove an entry within the followers synchronization cache,
|
|
|
|
so that it will subsequently be regenerated
|
|
|
|
"""
|
|
|
|
foll_sync_key = nickname + ':' + follower_domain
|
|
|
|
if not followers_sync_cache.get(foll_sync_key):
|
|
|
|
return
|
|
|
|
del followers_sync_cache[foll_sync_key]
|
|
|
|
|
|
|
|
|
2023-03-17 21:13:43 +00:00
|
|
|
def _get_followers_for_domain(base_dir: str,
|
|
|
|
nickname: str, domain: str,
|
|
|
|
search_domain: str) -> []:
|
|
|
|
"""Returns the followers for a given domain
|
|
|
|
this is used for followers synchronization
|
|
|
|
"""
|
|
|
|
followers_filename = \
|
|
|
|
acct_dir(base_dir, nickname, domain) + '/followers.txt'
|
|
|
|
if not os.path.isfile(followers_filename):
|
|
|
|
return []
|
|
|
|
lines = []
|
|
|
|
foll_text = ''
|
|
|
|
try:
|
|
|
|
with open(followers_filename, 'r', encoding='utf-8') as fp_foll:
|
|
|
|
foll_text = fp_foll.read()
|
|
|
|
except OSError:
|
|
|
|
print('EX: get_followers_for_domain unable to read followers ' +
|
|
|
|
followers_filename)
|
|
|
|
if search_domain not in foll_text:
|
|
|
|
return []
|
|
|
|
lines = foll_text.splitlines()
|
|
|
|
result = []
|
|
|
|
for line_str in lines:
|
|
|
|
if search_domain not in line_str:
|
|
|
|
continue
|
|
|
|
if line_str.endswith('@' + search_domain):
|
|
|
|
nick = line_str.split('@')[0]
|
|
|
|
paths_list = get_user_paths()
|
|
|
|
found = False
|
|
|
|
for prefix in ('https', 'http'):
|
|
|
|
if found:
|
|
|
|
break
|
|
|
|
for possible_path in paths_list:
|
|
|
|
url = prefix + '://' + search_domain + \
|
|
|
|
possible_path + nick
|
|
|
|
filename = base_dir + '/cache/actors/' + \
|
|
|
|
url.replace('/', '#') + '.json'
|
|
|
|
if not os.path.isfile(filename):
|
|
|
|
continue
|
|
|
|
if url not in result:
|
|
|
|
result.append(url)
|
|
|
|
found = True
|
|
|
|
break
|
|
|
|
elif '://' + search_domain in line_str:
|
|
|
|
result.append(line_str)
|
|
|
|
result.sort()
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
def _get_followers_sync_json(base_dir: str,
|
|
|
|
nickname: str, domain: str,
|
|
|
|
http_prefix: str, domain_full: str,
|
|
|
|
search_domain: str) -> {}:
|
|
|
|
"""Returns a response for followers synchronization
|
|
|
|
See https://github.com/mastodon/mastodon/pull/14510
|
|
|
|
https://codeberg.org/fediverse/fep/src/branch/main/feps/fep-8fcf.md
|
|
|
|
"""
|
|
|
|
sync_list = \
|
|
|
|
_get_followers_for_domain(base_dir,
|
|
|
|
nickname, domain,
|
|
|
|
search_domain)
|
|
|
|
id_str = http_prefix + '://' + domain_full + \
|
|
|
|
'/users/' + nickname + '/followers?domain=' + search_domain
|
|
|
|
sync_json = {
|
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
|
|
'id': id_str,
|
|
|
|
'orderedItems': sync_list,
|
|
|
|
'type': 'OrderedCollection'
|
|
|
|
}
|
|
|
|
return sync_json
|
|
|
|
|
|
|
|
|
2023-03-17 22:26:12 +00:00
|
|
|
def get_followers_sync_hash(sync_json: {}) -> str:
|
2023-03-17 21:13:43 +00:00
|
|
|
"""Returns a hash used within the Collection-Synchronization http header
|
|
|
|
See https://github.com/mastodon/mastodon/pull/14510
|
|
|
|
https://codeberg.org/fediverse/fep/src/branch/main/feps/fep-8fcf.md
|
|
|
|
"""
|
|
|
|
if not sync_json:
|
|
|
|
return None
|
|
|
|
sync_hash = None
|
|
|
|
for actor in sync_json['orderedItems']:
|
2023-03-17 22:26:12 +00:00
|
|
|
curr_sync_hash = sha256(actor.encode('utf-8'))
|
|
|
|
sync_hash_hex = curr_sync_hash.hexdigest()
|
|
|
|
sync_hash_int = int(sync_hash_hex, 16)
|
2023-03-17 21:13:43 +00:00
|
|
|
if sync_hash:
|
2023-03-17 22:26:12 +00:00
|
|
|
sync_hash = sync_hash ^ sync_hash_int
|
2023-03-17 21:13:43 +00:00
|
|
|
else:
|
2023-03-17 22:26:12 +00:00
|
|
|
sync_hash = sync_hash_int
|
|
|
|
if sync_hash is None:
|
|
|
|
return None
|
|
|
|
sync_hash_int = sync_hash
|
|
|
|
sync_hash = hashlib.sha256()
|
|
|
|
sync_hash_bytes = sync_hash_int.to_bytes(32, 'big')
|
|
|
|
return sync_hash_bytes.hex()
|
2023-03-17 21:13:43 +00:00
|
|
|
|
|
|
|
|
|
|
|
def update_followers_sync_cache(base_dir: str,
|
|
|
|
nickname: str, domain: str,
|
|
|
|
http_prefix: str, domain_full: str,
|
|
|
|
calling_domain: str,
|
|
|
|
sync_cache: {}) -> ({}, str):
|
|
|
|
"""Updates the followers synchronization cache
|
|
|
|
See https://github.com/mastodon/mastodon/pull/14510
|
|
|
|
https://codeberg.org/fediverse/fep/src/branch/main/feps/fep-8fcf.md
|
|
|
|
"""
|
|
|
|
foll_sync_key = nickname + ':' + calling_domain
|
|
|
|
if sync_cache.get(foll_sync_key):
|
|
|
|
sync_hash = sync_cache[foll_sync_key]['hash']
|
|
|
|
sync_json = sync_cache[foll_sync_key]['response']
|
|
|
|
else:
|
|
|
|
sync_json = \
|
|
|
|
_get_followers_sync_json(base_dir,
|
|
|
|
nickname, domain,
|
|
|
|
http_prefix,
|
|
|
|
domain_full,
|
|
|
|
calling_domain)
|
2023-03-17 22:26:12 +00:00
|
|
|
sync_hash = get_followers_sync_hash(sync_json)
|
2023-03-17 21:13:43 +00:00
|
|
|
if sync_hash:
|
|
|
|
sync_cache[foll_sync_key] = {
|
|
|
|
"hash": sync_hash,
|
|
|
|
"response": sync_json
|
|
|
|
}
|
|
|
|
return sync_json, sync_hash
|