epicyon/lxmf.py

187 lines
6.1 KiB
Python
Raw Normal View History

2026-02-28 17:05:18 +00:00
__filename__ = "lxmf.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.7.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@libreserver.org"
__status__ = "Production"
__module_group__ = "Profile Metadata"
2026-03-02 15:43:52 +00:00
import os
import pyqrcode
2026-02-28 17:05:18 +00:00
from utils import get_attachment_property_value
2026-03-02 15:43:52 +00:00
from utils import acct_dir
from utils import load_json
from utils import string_contains
2026-02-28 17:05:18 +00:00
VALID_LXMF_CHARS = set('0123456789abcdefghijklmnopqrstuvwxyz')
2026-02-28 17:12:03 +00:00
def _is_valid_lxmf_address(lxmf_address: str) -> bool:
"""Is the given LXMF address valid?
"""
if len(lxmf_address) != 32:
return False
if lxmf_address.lower() != lxmf_address:
return False
if not set(lxmf_address).issubset(VALID_LXMF_CHARS):
return False
return True
2026-03-02 17:00:52 +00:00
def save_lxmf_qrcode(base_dir: str,
nickname: str, domain: str,
scale: int = 6) -> bool:
2026-03-02 15:43:52 +00:00
"""Saves a qrcode image for the handle of the person
This helps to transfer onion or i2p handles to a mobile device
"""
qrcode_filename = acct_dir(base_dir, nickname, domain) + '/qrcode_lxmf.png'
if os.path.isfile(qrcode_filename):
return False
actor_filename = \
acct_dir(base_dir, nickname, domain) + '.json'
if not os.path.isfile(actor_filename):
return False
actor_json = load_json(actor_filename)
if not actor_json:
return False
lxmf_address = get_lxmf_address(actor_json)
if not lxmf_address:
return False
url = pyqrcode.create(lxmf_address)
try:
url.png(qrcode_filename, scale)
return True
except ModuleNotFoundError:
print('EX: save_lxmf_qrcode pyqrcode png module not found')
return False
2026-02-28 17:05:18 +00:00
def get_lxmf_address(actor_json: {}) -> str:
"""Returns lxmf address for the given actor
"""
if not actor_json.get('attachment'):
return ''
if not isinstance(actor_json['attachment'], list):
return ''
for property_value in actor_json['attachment']:
if not isinstance(property_value, dict):
print("WARN: actor attachment is not dict: " + str(property_value))
continue
name_value = None
if property_value.get('name'):
name_value = property_value['name']
elif property_value.get('schema:name'):
name_value = property_value['schema:name']
if not name_value:
continue
name_value_lower = name_value.lower()
if not string_contains(name_value_lower,
('lxmf', 'reticulum', 'nomadnet')):
2026-02-28 17:05:18 +00:00
continue
if not property_value.get('type'):
continue
prop_value_name, _ = \
get_attachment_property_value(property_value)
if not prop_value_name:
continue
if not property_value['type'].endswith('PropertyValue'):
continue
2026-02-28 17:23:55 +00:00
lxmf_address = property_value[prop_value_name].strip()
# remove any prefix
if lxmf_address.startswith('lxmf://'):
lxmf_address = lxmf_address.replace('lxmf://', '')
elif lxmf_address.startswith('lxmf:'):
lxmf_address = lxmf_address.replace('lxmf:', '')
if not _is_valid_lxmf_address(lxmf_address):
2026-02-28 17:05:18 +00:00
continue
2026-02-28 17:23:55 +00:00
return lxmf_address
2026-02-28 17:05:18 +00:00
return ''
2026-03-02 15:43:52 +00:00
def set_lxmf_address(base_dir: str, nickname: str, domain: str,
actor_json: {}, lxmf_address: str,
qrcode_scale: int) -> None:
2026-02-28 17:05:18 +00:00
"""Sets an lxmf address for the given actor
"""
2026-03-02 15:43:52 +00:00
if not lxmf_address:
qrcode_filename = \
acct_dir(base_dir, nickname, domain) + '/qrcode_lxmf.png'
if os.path.isfile(qrcode_filename):
try:
os.remove(qrcode_filename)
except OSError:
print('EX: cannot remove lxmf qrcode ' + qrcode_filename)
2026-02-28 17:05:18 +00:00
lxmf_address = lxmf_address.strip()
2026-02-28 17:23:55 +00:00
# remove any prefix
if lxmf_address.startswith('lxmf://'):
lxmf_address = lxmf_address.replace('lxmf://', '')
elif lxmf_address.startswith('lxmf:'):
lxmf_address = lxmf_address.replace('lxmf:', '')
2026-02-28 17:05:18 +00:00
is_lxmfaddress = _is_valid_lxmf_address(lxmf_address)
if not actor_json.get('attachment'):
actor_json['attachment']: list[dict] = []
# remove any existing value
property_found = None
for property_value in actor_json['attachment']:
if not isinstance(property_value, dict):
print("WARN: actor attachment is not dict: " + str(property_value))
continue
name_value = None
if property_value.get('name'):
name_value = property_value['name']
elif property_value.get('schema:name'):
name_value = property_value['schema:name']
if not name_value:
continue
if not property_value.get('type'):
continue
if not name_value.lower().startswith('lxmf'):
continue
property_found = property_value
break
if property_found:
actor_json['attachment'].remove(property_found)
if not is_lxmfaddress:
return
for property_value in actor_json['attachment']:
if not isinstance(property_value, dict):
print("WARN: actor attachment is not dict: " + str(property_value))
continue
name_value = None
if property_value.get('name'):
name_value = property_value['name']
elif property_value.get('schema:name'):
name_value = property_value['schema:name']
if not name_value:
continue
if not property_value.get('type'):
continue
if not name_value.lower().startswith('lxmf'):
continue
if not property_value['type'].endswith('PropertyValue'):
continue
prop_value_name, _ = \
get_attachment_property_value(property_value)
if not prop_value_name:
continue
property_value[prop_value_name] = lxmf_address
return
new_lxmf_address = {
"name": "LXMF",
"type": "PropertyValue",
"value": lxmf_address
}
actor_json['attachment'].append(new_lxmf_address)
2026-03-02 17:00:52 +00:00
save_lxmf_qrcode(base_dir, nickname, domain, qrcode_scale)