user.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. # mautrix-signal - A Matrix-Signal puppeting bridge
  2. # Copyright (C) 2021 Tulir Asokan
  3. #
  4. # This program is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU Affero General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU Affero General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU Affero General Public License
  15. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. from __future__ import annotations
  17. from typing import TYPE_CHECKING, AsyncGenerator, cast
  18. from asyncio.tasks import sleep
  19. from datetime import datetime
  20. from uuid import UUID
  21. import asyncio
  22. from mausignald.errors import AuthorizationFailedError, ProfileUnavailableError
  23. from mausignald.types import (
  24. Account,
  25. Address,
  26. GroupV2,
  27. MessageResendSuccessEvent,
  28. Profile,
  29. WebsocketConnectionState,
  30. WebsocketConnectionStateChangeEvent,
  31. )
  32. from mautrix.appservice import AppService
  33. from mautrix.bridge import AutologinError, BaseUser, async_getter_lock
  34. from mautrix.types import EventType, RoomID, UserID
  35. from mautrix.util import background_task
  36. from mautrix.util.bridge_state import BridgeState, BridgeStateEvent
  37. from mautrix.util.message_send_checkpoint import MessageSendCheckpointStatus
  38. from mautrix.util.opt_prometheus import Gauge
  39. from . import portal as po, puppet as pu
  40. from .config import Config
  41. from .db import Message as DBMessage, User as DBUser
  42. if TYPE_CHECKING:
  43. from .__main__ import SignalBridge
  44. METRIC_CONNECTED = Gauge("bridge_connected", "Bridge users connected to Signal")
  45. METRIC_LOGGED_IN = Gauge("bridge_logged_in", "Bridge users logged into Signal")
  46. BridgeState.human_readable_errors.update(
  47. {
  48. "logged-out": "You're not logged into Signal",
  49. "signal-not-connected": None,
  50. }
  51. )
  52. class User(DBUser, BaseUser):
  53. by_mxid: dict[UserID, User] = {}
  54. by_username: dict[str, User] = {}
  55. by_uuid: dict[UUID, User] = {}
  56. config: Config
  57. az: AppService
  58. loop: asyncio.AbstractEventLoop
  59. bridge: "SignalBridge"
  60. relay_whitelisted: bool
  61. is_admin: bool
  62. permission_level: str
  63. _sync_lock: asyncio.Lock
  64. _notice_room_lock: asyncio.Lock
  65. _connected: bool
  66. _state_id: str | None
  67. _websocket_connection_state: BridgeStateEvent | None
  68. _latest_non_transient_bridge_state: datetime | None
  69. challenge_token: str | None
  70. def __init__(
  71. self,
  72. mxid: UserID,
  73. username: str | None = None,
  74. uuid: UUID | None = None,
  75. notice_room: RoomID | None = None,
  76. ) -> None:
  77. super().__init__(mxid=mxid, username=username, uuid=uuid, notice_room=notice_room)
  78. BaseUser.__init__(self)
  79. self._notice_room_lock = asyncio.Lock()
  80. self._sync_lock = asyncio.Lock()
  81. self._connected = False
  82. self._state_id = self.username
  83. self._websocket_connection_state = None
  84. self._latest_non_transient_bridge_state = None
  85. self.challenge_token = None
  86. perms = self.config.get_permissions(mxid)
  87. self.relay_whitelisted, self.is_whitelisted, self.is_admin, self.permission_level = perms
  88. @classmethod
  89. def init_cls(cls, bridge: "SignalBridge") -> None:
  90. cls.bridge = bridge
  91. cls.config = bridge.config
  92. cls.az = bridge.az
  93. cls.loop = bridge.loop
  94. @property
  95. def address(self) -> Address | None:
  96. if not self.username:
  97. return None
  98. return Address(uuid=self.uuid, number=self.username)
  99. async def is_logged_in(self) -> bool:
  100. return bool(self.username)
  101. async def needs_relay(self, portal: po.Portal) -> bool:
  102. return not await self.is_logged_in() or (
  103. portal.is_direct and portal.receiver != self.username
  104. )
  105. async def logout(self) -> None:
  106. if not self.username:
  107. return
  108. username = self.username
  109. if self.uuid and self.by_uuid.get(self.uuid) == self:
  110. del self.by_uuid[self.uuid]
  111. if self.username and self.by_username.get(self.username) == self:
  112. del self.by_username[self.username]
  113. self.username = None
  114. self.uuid = None
  115. await self.update()
  116. await self.bridge.signal.unsubscribe(username)
  117. # Wait a while for signald to finish disconnecting
  118. await asyncio.sleep(1)
  119. await self.bridge.signal.delete_account(username)
  120. self._track_metric(METRIC_LOGGED_IN, False)
  121. await self.push_bridge_state(BridgeStateEvent.LOGGED_OUT, remote_id=username)
  122. async def fill_bridge_state(self, state: BridgeState) -> None:
  123. await super().fill_bridge_state(state)
  124. if not state.remote_id:
  125. state.remote_id = self._state_id
  126. if self.address:
  127. puppet = await self.get_puppet()
  128. state.remote_name = puppet.name or self.username
  129. async def get_bridge_states(self) -> list[BridgeState]:
  130. if not self.username:
  131. return []
  132. state = BridgeState(state_event=BridgeStateEvent.UNKNOWN_ERROR)
  133. if self.bridge.signal.is_connected and self._connected:
  134. state.state_event = BridgeStateEvent.CONNECTED
  135. else:
  136. state.state_event = BridgeStateEvent.TRANSIENT_DISCONNECT
  137. return [state]
  138. async def handle_auth_failure(self, e: Exception) -> None:
  139. if isinstance(e, AuthorizationFailedError):
  140. self.username = None
  141. await self.push_bridge_state(BridgeStateEvent.BAD_CREDENTIALS, error=str(e))
  142. async def get_puppet(self) -> pu.Puppet | None:
  143. if not self.address:
  144. return None
  145. return await pu.Puppet.get_by_address(self.address)
  146. async def get_portal_with(self, puppet: pu.Puppet, create: bool = True) -> po.Portal | None:
  147. if not self.username:
  148. return None
  149. return await po.Portal.get_by_chat_id(puppet.uuid, receiver=self.username, create=create)
  150. async def on_signin(self, account: Account) -> None:
  151. self.username = account.account_id
  152. self._state_id = account.account_id
  153. self.uuid = account.address.uuid
  154. self._add_to_cache()
  155. await self.update()
  156. self.log.debug(f"Subscribing to {self.username} / {self.uuid}")
  157. if await self.bridge.signal.subscribe(self.username):
  158. background_task.create(self.sync(is_startup=True))
  159. self._track_metric(METRIC_LOGGED_IN, True)
  160. self.log.debug("Successfully subscribed")
  161. else:
  162. self.log.warning("Failed to subscribe")
  163. self.username = None
  164. def on_websocket_connection_state_change(
  165. self, evt: WebsocketConnectionStateChangeEvent
  166. ) -> None:
  167. if evt.state == WebsocketConnectionState.CONNECTED:
  168. self.log.info(f"Connected to Signal (ws: {evt.socket})")
  169. self._track_metric(METRIC_CONNECTED, True)
  170. self._track_metric(METRIC_LOGGED_IN, True)
  171. self._connected = True
  172. else:
  173. if evt.exception:
  174. self.log.error(
  175. f"New {evt.socket} websocket state from signald {evt.state} "
  176. f"with error {evt.exception}"
  177. )
  178. else:
  179. self.log.warning(f"New {evt.socket} websocket state from signald {evt.state}")
  180. self._track_metric(METRIC_CONNECTED, False)
  181. self._connected = False
  182. bridge_state = {
  183. # Signald disconnected
  184. WebsocketConnectionState.SOCKET_DISCONNECTED: BridgeStateEvent.TRANSIENT_DISCONNECT,
  185. # Websocket state reported by signald
  186. WebsocketConnectionState.DISCONNECTED: (
  187. None
  188. if self._websocket_connection_state == BridgeStateEvent.BAD_CREDENTIALS
  189. else BridgeStateEvent.TRANSIENT_DISCONNECT
  190. ),
  191. WebsocketConnectionState.CONNECTING: BridgeStateEvent.CONNECTING,
  192. WebsocketConnectionState.CONNECTED: BridgeStateEvent.CONNECTED,
  193. WebsocketConnectionState.RECONNECTING: BridgeStateEvent.TRANSIENT_DISCONNECT,
  194. WebsocketConnectionState.DISCONNECTING: BridgeStateEvent.TRANSIENT_DISCONNECT,
  195. WebsocketConnectionState.AUTHENTICATION_FAILED: BridgeStateEvent.BAD_CREDENTIALS,
  196. WebsocketConnectionState.FAILED: BridgeStateEvent.TRANSIENT_DISCONNECT,
  197. }.get(evt.state)
  198. if bridge_state is None:
  199. self.log.info(f"Websocket state {evt.state} seen, not reporting new bridge state")
  200. return
  201. now = datetime.now()
  202. if bridge_state in (BridgeStateEvent.TRANSIENT_DISCONNECT, BridgeStateEvent.CONNECTING):
  203. self.log.debug(
  204. f"New bridge state {bridge_state} is likely transient. Waiting 15 seconds to send."
  205. )
  206. async def wait_report_bridge_state():
  207. # Wait for 15 seconds (that should be enough for the bridge to get connected)
  208. # before sending a TRANSIENT_DISCONNECT/CONNECTING.
  209. await sleep(15)
  210. if (
  211. self._latest_non_transient_bridge_state
  212. and now > self._latest_non_transient_bridge_state
  213. ):
  214. background_task.create(self.push_bridge_state(bridge_state))
  215. self._websocket_connection_state = bridge_state
  216. # Wait for another minute. If the bridge stays in TRANSIENT_DISCONNECT/CONNECTING
  217. # for that long, something terrible has happened (signald failed to restart, the
  218. # internet broke, etc.)
  219. await sleep(60)
  220. if (
  221. self._latest_non_transient_bridge_state
  222. and now > self._latest_non_transient_bridge_state
  223. ):
  224. background_task.create(
  225. self.push_bridge_state(
  226. BridgeStateEvent.UNKNOWN_ERROR,
  227. message="Failed to restore connection to Signal",
  228. )
  229. )
  230. self._websocket_connection_state = BridgeStateEvent.UNKNOWN_ERROR
  231. else:
  232. self.log.info(
  233. f"New state since last {bridge_state} push, "
  234. "not transitioning to UNKNOWN_ERROR."
  235. )
  236. background_task.create(wait_report_bridge_state())
  237. elif self._websocket_connection_state == bridge_state:
  238. self.log.info("Websocket state unchanged, not reporting new bridge state")
  239. self._latest_non_transient_bridge_state = now
  240. else:
  241. if bridge_state == BridgeStateEvent.BAD_CREDENTIALS:
  242. self.username = None
  243. background_task.create(self.push_bridge_state(bridge_state))
  244. self._latest_non_transient_bridge_state = now
  245. self._websocket_connection_state = bridge_state
  246. async def on_message_resend_success(self, evt: MessageResendSuccessEvent):
  247. # These messages mean we need to resend the message to that user.
  248. my_uuid = self.address.uuid
  249. self.log.debug(f"Successfully resent message {my_uuid}/{evt.timestamp}")
  250. message = await DBMessage.find_by_sender_timestamp(my_uuid, evt.timestamp)
  251. if not message:
  252. self.log.warning("Couldn't find message that was resent")
  253. return
  254. self.log.debug(f"Successfully resent {message.mxid} in {message.mx_room}")
  255. self.send_remote_checkpoint(
  256. status=MessageSendCheckpointStatus.SUCCESS,
  257. event_id=message.mxid,
  258. room_id=message.mx_room,
  259. event_type=EventType.ROOM_MESSAGE,
  260. )
  261. async def _sync_puppet(self) -> None:
  262. puppet = await self.get_puppet()
  263. if not puppet:
  264. self.log.warning(f"Didn't find puppet for own address {self.address}")
  265. return
  266. if puppet.uuid and not self.uuid:
  267. self.uuid = puppet.uuid
  268. self.by_uuid[self.uuid] = self
  269. if puppet.custom_mxid != self.mxid and puppet.can_auto_login(self.mxid):
  270. self.log.info("Automatically enabling custom puppet")
  271. try:
  272. await puppet.switch_mxid(access_token="auto", mxid=self.mxid)
  273. except AutologinError as e:
  274. self.log.warning(f"Failed to enable custom puppet: {e}")
  275. async def sync(self, is_startup: bool = False) -> None:
  276. await self.sync_puppet()
  277. await self.sync_contacts(is_startup=is_startup)
  278. await self.sync_groups()
  279. self.log.debug("Sync complete")
  280. async def sync_puppet(self) -> None:
  281. try:
  282. async with self._sync_lock:
  283. await self._sync_puppet()
  284. except Exception:
  285. self.log.exception("Error while syncing own puppet")
  286. async def sync_contacts(self, is_startup: bool = False) -> None:
  287. try:
  288. async with self._sync_lock:
  289. await self._sync_contacts(is_startup)
  290. except Exception as e:
  291. self.log.exception("Error while syncing contacts")
  292. await self.handle_auth_failure(e)
  293. async def sync_groups(self) -> None:
  294. try:
  295. async with self._sync_lock:
  296. await self._sync_groups()
  297. except Exception as e:
  298. self.log.exception("Error while syncing groups")
  299. await self.handle_auth_failure(e)
  300. async def sync_contact(
  301. self, contact: Profile | Address, create_portals: bool = False, use_cache: bool = True
  302. ) -> None:
  303. self.log.trace("Syncing contact %s", contact)
  304. try:
  305. if isinstance(contact, Address):
  306. address = contact
  307. try:
  308. profile = await self.bridge.signal.get_profile(
  309. self.username, address, use_cache=use_cache
  310. )
  311. except ProfileUnavailableError:
  312. self.log.debug(f"Profile of {address} was not available when syncing")
  313. profile = None
  314. if profile and profile.name:
  315. self.log.trace("Got profile for %s: %s", address, profile)
  316. else:
  317. address = contact.address
  318. profile = contact
  319. puppet = await pu.Puppet.get_by_address(address, resolve_via=self.username)
  320. if not puppet:
  321. self.log.debug(f"Didn't find puppet for {address} while syncing contact")
  322. return
  323. await puppet.update_info(profile or address, self)
  324. if create_portals:
  325. portal = await po.Portal.get_by_chat_id(
  326. puppet.uuid, receiver=self.username, create=True
  327. )
  328. await portal.create_matrix_room(self, profile or address)
  329. except Exception as e:
  330. await self.handle_auth_failure(e)
  331. raise
  332. async def _sync_group_v2(self, group: GroupV2, create_portals: bool) -> None:
  333. self.log.trace("Syncing group %s", group.id)
  334. portal = await po.Portal.get_by_chat_id(group.id, create=True)
  335. if create_portals:
  336. await portal.create_matrix_room(self, group)
  337. elif portal.mxid:
  338. await portal.update_matrix_room(self, group)
  339. async def _hacky_duplicate_contact_check(
  340. self, contacts: list[Profile], is_startup: bool
  341. ) -> None:
  342. name_map: dict[str, list[Profile]] = {}
  343. for contact in contacts:
  344. if contact.contact_name:
  345. name_map.setdefault(contact.contact_name, []).append(contact)
  346. duplicates = {name: profiles for name, profiles in name_map.items() if len(profiles) > 1}
  347. if duplicates:
  348. self.log.warning(f"Found duplicate contact names, potential name mixup: {duplicates}")
  349. if is_startup:
  350. self.log.debug("Requesting contact sync to resolve potential name mixup")
  351. await self.bridge.signal.request_sync(self.username)
  352. else:
  353. self.log.debug("No duplicate contact names found")
  354. async def _sync_contacts(self, is_startup: bool) -> None:
  355. create_contact_portal = self.config["bridge.autocreate_contact_portal"]
  356. contacts = await self.bridge.signal.list_contacts(self.username)
  357. if self.config["bridge.hacky_contact_name_mixup_detection"]:
  358. await self._hacky_duplicate_contact_check(contacts, is_startup=is_startup)
  359. for contact in contacts:
  360. try:
  361. await self.sync_contact(contact, create_contact_portal)
  362. except Exception:
  363. self.log.exception(f"Failed to sync contact {contact.address}")
  364. async def _sync_groups(self) -> None:
  365. create_group_portal = self.config["bridge.autocreate_group_portal"]
  366. for group in await self.bridge.signal.list_groups(self.username):
  367. try:
  368. await self._sync_group_v2(group, create_group_portal)
  369. except Exception:
  370. self.log.exception(f"Failed to sync group {group.id}")
  371. # region Database getters
  372. def _add_to_cache(self) -> None:
  373. self.by_mxid[self.mxid] = self
  374. if self.username:
  375. self.by_username[self.username] = self
  376. if self.uuid:
  377. self.by_uuid[self.uuid] = self
  378. @classmethod
  379. @async_getter_lock
  380. async def get_by_mxid(cls, mxid: UserID, /, *, create: bool = True) -> User | None:
  381. # Never allow ghosts to be users
  382. if pu.Puppet.get_id_from_mxid(mxid):
  383. return None
  384. try:
  385. return cls.by_mxid[mxid]
  386. except KeyError:
  387. pass
  388. user = cast(cls, await super().get_by_mxid(mxid))
  389. if user is not None:
  390. user._add_to_cache()
  391. return user
  392. if create:
  393. user = cls(mxid)
  394. await user.insert()
  395. user._add_to_cache()
  396. return user
  397. return None
  398. @classmethod
  399. @async_getter_lock
  400. async def get_by_username(cls, username: str, /) -> User | None:
  401. try:
  402. return cls.by_username[username]
  403. except KeyError:
  404. pass
  405. user = cast(cls, await super().get_by_username(username))
  406. if user is not None:
  407. user._add_to_cache()
  408. return user
  409. return None
  410. @classmethod
  411. @async_getter_lock
  412. async def get_by_uuid(cls, uuid: UUID, /) -> User | None:
  413. try:
  414. return cls.by_uuid[uuid]
  415. except KeyError:
  416. pass
  417. user = cast(cls, await super().get_by_uuid(uuid))
  418. if user is not None:
  419. user._add_to_cache()
  420. return user
  421. return None
  422. @classmethod
  423. async def get_by_address(cls, address: Address) -> User | None:
  424. if address.uuid:
  425. return await cls.get_by_uuid(address.uuid)
  426. elif address.number:
  427. return await cls.get_by_username(address.number)
  428. else:
  429. raise ValueError("Given address is blank")
  430. @classmethod
  431. async def all_logged_in(cls) -> AsyncGenerator[User, None]:
  432. users = await super().all_logged_in()
  433. user: cls
  434. for user in users:
  435. try:
  436. yield cls.by_mxid[user.mxid]
  437. except KeyError:
  438. user._add_to_cache()
  439. yield user
  440. # endregion