mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-04-28 04:06:09 +00:00
Update Contacts and Identity Management Features
- Updated the identities API to include message counts for current identities. - Modified the contacts API to return total contact counts for pagination. - Refactored database queries to use asynchronous calls for improved performance. - Added a new method to count contacts based on search criteria. - Simplified the get_interfaces method in CommunityInterfacesManager.
This commit is contained in:
+108
-22
@@ -4395,9 +4395,15 @@ class ReticulumMeshChat:
|
||||
@routes.get("/api/v1/identities")
|
||||
async def identities_list(request):
|
||||
try:
|
||||
identities = self.list_identities()
|
||||
if self.database:
|
||||
for item in identities:
|
||||
if item.get("is_current"):
|
||||
item["message_count"] = self.database.messages.count_lxmf_messages()
|
||||
break
|
||||
return web.json_response(
|
||||
{
|
||||
"identities": self.list_identities(),
|
||||
"identities": identities,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -5757,6 +5763,7 @@ class ReticulumMeshChat:
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
total_count = self.database.contacts.get_contacts_count(search=search)
|
||||
|
||||
contacts = []
|
||||
for row in contacts_rows:
|
||||
@@ -5778,7 +5785,7 @@ class ReticulumMeshChat:
|
||||
d["remote_telephony_hash"] = tele_hash
|
||||
contacts.append(d)
|
||||
|
||||
return web.json_response(contacts)
|
||||
return web.json_response({"contacts": contacts, "total_count": total_count})
|
||||
|
||||
@routes.post("/api/v1/telephone/contacts")
|
||||
async def telephone_contacts_post(request):
|
||||
@@ -5908,35 +5915,32 @@ class ReticulumMeshChat:
|
||||
|
||||
blocked_identity_hashes = None
|
||||
if not include_blocked:
|
||||
blocked = self.database.misc.get_blocked_destinations()
|
||||
blocked = await asyncio.to_thread(
|
||||
self.database.misc.get_blocked_destinations
|
||||
)
|
||||
blocked_identity_hashes = [b["destination_hash"] for b in blocked]
|
||||
|
||||
# fetch announces from database
|
||||
# If we don't have a search query, we can paginate at the database level
|
||||
# which is much faster than fetching thousands of records and then paginating in Python.
|
||||
db_limit = limit if not search_query else None
|
||||
db_offset = offset if not search_query else 0
|
||||
|
||||
results = self.announce_manager.get_filtered_announces(
|
||||
results = await asyncio.to_thread(
|
||||
self.announce_manager.get_filtered_announces,
|
||||
aspect=aspect,
|
||||
identity_hash=identity_hash,
|
||||
destination_hash=destination_hash,
|
||||
query=None, # We filter in Python to support name search
|
||||
query=None,
|
||||
blocked_identity_hashes=blocked_identity_hashes,
|
||||
limit=db_limit,
|
||||
offset=db_offset,
|
||||
)
|
||||
|
||||
# fetch total count if we paginated in DB
|
||||
total_count = 0
|
||||
if not search_query:
|
||||
# Get the count from the database for the same filters
|
||||
# We should probably add a get_filtered_announces_count method to announce_manager
|
||||
if db_limit is None:
|
||||
total_count = len(results)
|
||||
else:
|
||||
# We need the total count for pagination to work in the frontend
|
||||
total_count = self.announce_manager.get_filtered_announces_count(
|
||||
total_count = await asyncio.to_thread(
|
||||
self.announce_manager.get_filtered_announces_count,
|
||||
aspect=aspect,
|
||||
identity_hash=identity_hash,
|
||||
destination_hash=destination_hash,
|
||||
@@ -5950,7 +5954,11 @@ class ReticulumMeshChat:
|
||||
other_user_hashes = [r["destination_hash"] for r in results]
|
||||
user_icons = {}
|
||||
if other_user_hashes:
|
||||
db_icons = self.database.misc.get_user_icons(other_user_hashes)
|
||||
|
||||
def _fetch_icons():
|
||||
return self.database.misc.get_user_icons(other_user_hashes)
|
||||
|
||||
db_icons = await asyncio.to_thread(_fetch_icons)
|
||||
for icon in db_icons:
|
||||
user_icons[icon["destination_hash"]] = {
|
||||
"icon_name": icon["icon_name"],
|
||||
@@ -5962,10 +5970,14 @@ class ReticulumMeshChat:
|
||||
custom_names = {}
|
||||
lxmf_names_for_telephony = {}
|
||||
if other_user_hashes:
|
||||
db_custom_names = self.database.provider.fetchall(
|
||||
f"SELECT destination_hash, display_name FROM custom_destination_display_names WHERE destination_hash IN ({','.join(['?'] * len(other_user_hashes))})", # noqa: S608
|
||||
other_user_hashes,
|
||||
)
|
||||
|
||||
def _fetch_custom_names():
|
||||
return self.database.provider.fetchall(
|
||||
f"SELECT destination_hash, display_name FROM custom_destination_display_names WHERE destination_hash IN ({','.join(['?'] * len(other_user_hashes))})", # noqa: S608
|
||||
other_user_hashes,
|
||||
)
|
||||
|
||||
db_custom_names = await asyncio.to_thread(_fetch_custom_names)
|
||||
for row in db_custom_names:
|
||||
custom_names[row["destination_hash"]] = row["display_name"]
|
||||
|
||||
@@ -5981,10 +5993,14 @@ class ReticulumMeshChat:
|
||||
),
|
||||
)
|
||||
if identity_hashes:
|
||||
lxmf_results = self.database.announces.provider.fetchall(
|
||||
f"SELECT identity_hash, app_data FROM announces WHERE aspect = 'lxmf.delivery' AND identity_hash IN ({','.join(['?'] * len(identity_hashes))})", # noqa: S608
|
||||
identity_hashes,
|
||||
)
|
||||
|
||||
def _fetch_lxmf_names():
|
||||
return self.database.announces.provider.fetchall(
|
||||
f"SELECT identity_hash, app_data FROM announces WHERE aspect = 'lxmf.delivery' AND identity_hash IN ({','.join(['?'] * len(identity_hashes))})", # noqa: S608
|
||||
identity_hashes,
|
||||
)
|
||||
|
||||
lxmf_results = await asyncio.to_thread(_fetch_lxmf_names)
|
||||
for row in lxmf_results:
|
||||
lxmf_names_for_telephony[row["identity_hash"]] = (
|
||||
parse_lxmf_display_name(row["app_data"])
|
||||
@@ -10138,6 +10154,75 @@ class ReticulumMeshChat:
|
||||
duplicate_signal = "duplicate_lxm"
|
||||
|
||||
try:
|
||||
# Columba-style contact sharing URI:
|
||||
# lxma://<destination_hash_hex>:<public_key_hex>
|
||||
if uri.lower().startswith("lxma://"):
|
||||
lxma_payload = uri[7:]
|
||||
if ":" not in lxma_payload:
|
||||
raise ValueError(
|
||||
"Invalid LXMA URI format, expected lxma://<destination_hash>:<public_key>",
|
||||
)
|
||||
|
||||
destination_hash_hex, public_key_hex = lxma_payload.split(":", 1)
|
||||
destination_hash_hex = destination_hash_hex.strip().lower()
|
||||
public_key_hex = public_key_hex.strip().lower()
|
||||
|
||||
if len(destination_hash_hex) != 32:
|
||||
raise ValueError(
|
||||
"Invalid LXMA destination hash length, expected 32 hex characters",
|
||||
)
|
||||
if len(public_key_hex) not in (64, 128):
|
||||
raise ValueError(
|
||||
"Invalid LXMA public key length, expected 64 or 128 hex characters",
|
||||
)
|
||||
|
||||
bytes.fromhex(destination_hash_hex)
|
||||
raw_bytes = bytes.fromhex(public_key_hex)
|
||||
public_key_bytes = (
|
||||
raw_bytes[:32] if len(raw_bytes) >= 32 else raw_bytes
|
||||
)
|
||||
|
||||
identity = RNS.Identity(create_keys=False)
|
||||
if not identity.load_public_key(public_key_bytes):
|
||||
if len(raw_bytes) == 64:
|
||||
raise ValueError("Invalid LXMA public key")
|
||||
public_key_bytes = raw_bytes
|
||||
if not identity.load_public_key(public_key_bytes):
|
||||
raise ValueError("Invalid LXMA public key")
|
||||
|
||||
remote_identity_hash = identity.hash.hex()
|
||||
existing_contact = (
|
||||
self.database.contacts.get_contact_by_identity_hash(
|
||||
remote_identity_hash,
|
||||
)
|
||||
)
|
||||
contact_name = (
|
||||
existing_contact["name"]
|
||||
if existing_contact and existing_contact.get("name")
|
||||
else f"Contact {destination_hash_hex[:8]}"
|
||||
)
|
||||
|
||||
self.database.contacts.add_contact(
|
||||
contact_name,
|
||||
remote_identity_hash,
|
||||
lxmf_address=destination_hash_hex,
|
||||
)
|
||||
|
||||
AsyncUtils.run_async(
|
||||
client.send_str(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "lxm.ingest_uri.result",
|
||||
"status": "success",
|
||||
"message": f"Contact imported from LXMA URI ({destination_hash_hex})",
|
||||
"ingest_type": "lxma_contact",
|
||||
"destination_hash": destination_hash_hex,
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
# ensure uri starts with lxmf:// or lxm://
|
||||
if not uri.lower().startswith(
|
||||
LXMF.LXMessage.URI_SCHEMA + "://",
|
||||
@@ -10364,6 +10449,7 @@ class ReticulumMeshChat:
|
||||
return {
|
||||
"display_name": ctx.config.display_name.get(),
|
||||
"identity_hash": ctx.identity.hash.hex(),
|
||||
"identity_public_key": ctx.identity.get_public_key().hex(),
|
||||
"lxmf_address_hash": ctx.local_lxmf_destination.hexhash,
|
||||
"telephone_address_hash": ctx.telephone_manager.telephone.destination.hexhash
|
||||
if ctx.telephone_manager.telephone
|
||||
|
||||
@@ -19,7 +19,4 @@ class CommunityInterfacesManager:
|
||||
]
|
||||
|
||||
async def get_interfaces(self) -> list[dict[str, Any]]:
|
||||
return [
|
||||
{**iface, "online": None, "last_check": 0}
|
||||
for iface in self.interfaces
|
||||
]
|
||||
return [{**iface, "online": None, "last_check": 0} for iface in self.interfaces]
|
||||
|
||||
@@ -61,6 +61,19 @@ class ContactsDAO:
|
||||
(limit, offset),
|
||||
)
|
||||
|
||||
def get_contacts_count(self, search=None):
|
||||
if search:
|
||||
row = self.provider.fetchone(
|
||||
"""
|
||||
SELECT COUNT(*) as n FROM contacts
|
||||
WHERE name LIKE ? OR remote_identity_hash LIKE ? OR lxmf_address LIKE ? OR lxst_address LIKE ?
|
||||
""",
|
||||
(f"%{search}%", f"%{search}%", f"%{search}%", f"%{search}%"),
|
||||
)
|
||||
else:
|
||||
row = self.provider.fetchone("SELECT COUNT(*) as n FROM contacts")
|
||||
return row["n"] if row else 0
|
||||
|
||||
def get_contact(self, contact_id):
|
||||
return self.provider.fetchone(
|
||||
"SELECT * FROM contacts WHERE id = ?",
|
||||
|
||||
Reference in New Issue
Block a user