user.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  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 ResponseError
  23. from mausignald.types import (
  24. Account,
  25. Address,
  26. Group,
  27. GroupV2,
  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 RoomID, UserID
  35. from mautrix.util.bridge_state import BridgeState, BridgeStateEvent
  36. from mautrix.util.opt_prometheus import Gauge
  37. from . import portal as po, puppet as pu
  38. from .config import Config
  39. from .db import User as DBUser
  40. if TYPE_CHECKING:
  41. from .__main__ import SignalBridge
  42. METRIC_CONNECTED = Gauge("bridge_connected", "Bridge users connected to Signal")
  43. METRIC_LOGGED_IN = Gauge("bridge_logged_in", "Bridge users logged into Signal")
  44. BridgeState.human_readable_errors.update(
  45. {
  46. "logged-out": "You're not logged into Signal",
  47. "signal-not-connected": None,
  48. }
  49. )
  50. class User(DBUser, BaseUser):
  51. by_mxid: dict[UserID, User] = {}
  52. by_username: dict[str, User] = {}
  53. by_uuid: dict[UUID, User] = {}
  54. config: Config
  55. az: AppService
  56. loop: asyncio.AbstractEventLoop
  57. bridge: "SignalBridge"
  58. relay_whitelisted: bool
  59. is_admin: bool
  60. permission_level: str
  61. _sync_lock: asyncio.Lock
  62. _notice_room_lock: asyncio.Lock
  63. _connected: bool
  64. _websocket_connection_state: BridgeStateEvent | None
  65. _latest_non_transient_disconnect_state: datetime | None
  66. def __init__(
  67. self,
  68. mxid: UserID,
  69. username: str | None = None,
  70. uuid: UUID | None = None,
  71. notice_room: RoomID | None = None,
  72. ) -> None:
  73. super().__init__(mxid=mxid, username=username, uuid=uuid, notice_room=notice_room)
  74. BaseUser.__init__(self)
  75. self._notice_room_lock = asyncio.Lock()
  76. self._sync_lock = asyncio.Lock()
  77. self._connected = False
  78. self._websocket_connection_state = None
  79. perms = self.config.get_permissions(mxid)
  80. self.relay_whitelisted, self.is_whitelisted, self.is_admin, self.permission_level = perms
  81. @classmethod
  82. def init_cls(cls, bridge: "SignalBridge") -> None:
  83. cls.bridge = bridge
  84. cls.config = bridge.config
  85. cls.az = bridge.az
  86. cls.loop = bridge.loop
  87. @property
  88. def address(self) -> Address | None:
  89. if not self.username:
  90. return None
  91. return Address(uuid=self.uuid, number=self.username)
  92. async def is_logged_in(self) -> bool:
  93. return bool(self.username)
  94. async def needs_relay(self, portal: po.Portal) -> bool:
  95. return not await self.is_logged_in() or (
  96. portal.is_direct and portal.receiver != self.username
  97. )
  98. async def logout(self) -> None:
  99. if not self.username:
  100. return
  101. username = self.username
  102. if self.uuid and self.by_uuid.get(self.uuid) == self:
  103. del self.by_uuid[self.uuid]
  104. if self.username and self.by_username.get(self.username) == self:
  105. del self.by_username[self.username]
  106. self.username = None
  107. self.uuid = None
  108. await self.update()
  109. await self.bridge.signal.unsubscribe(username)
  110. # Wait a while for signald to finish disconnecting
  111. await asyncio.sleep(1)
  112. await self.bridge.signal.delete_account(username)
  113. self._track_metric(METRIC_LOGGED_IN, False)
  114. await self.push_bridge_state(BridgeStateEvent.LOGGED_OUT, remote_id=username)
  115. async def fill_bridge_state(self, state: BridgeState) -> None:
  116. await super().fill_bridge_state(state)
  117. if not state.remote_id:
  118. state.remote_id = self.username
  119. if self.address:
  120. puppet = await self.get_puppet()
  121. state.remote_name = puppet.name or self.username
  122. async def get_bridge_states(self) -> list[BridgeState]:
  123. if not self.username:
  124. return []
  125. state = BridgeState(state_event=BridgeStateEvent.UNKNOWN_ERROR)
  126. if self.bridge.signal.is_connected and self._connected:
  127. state.state_event = BridgeStateEvent.CONNECTED
  128. else:
  129. state.state_event = BridgeStateEvent.TRANSIENT_DISCONNECT
  130. return [state]
  131. async def handle_auth_failure(self, e: Exception) -> None:
  132. auth_failed = (
  133. "org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException"
  134. )
  135. if isinstance(e, ResponseError) and auth_failed in e.data.get("exceptions", []):
  136. await self.push_bridge_state(BridgeStateEvent.BAD_CREDENTIALS, error=str(e))
  137. async def get_puppet(self) -> pu.Puppet | None:
  138. if not self.address:
  139. return None
  140. return await pu.Puppet.get_by_address(self.address)
  141. async def on_signin(self, account: Account) -> None:
  142. self.username = account.account_id
  143. self.uuid = account.address.uuid
  144. self._add_to_cache()
  145. await self.update()
  146. await self.bridge.signal.subscribe(self.username)
  147. asyncio.create_task(self.sync())
  148. self._track_metric(METRIC_LOGGED_IN, True)
  149. def on_websocket_connection_state_change(
  150. self, evt: WebsocketConnectionStateChangeEvent
  151. ) -> None:
  152. if evt.state == WebsocketConnectionState.CONNECTED:
  153. self.log.info("Connected to Signal")
  154. self._track_metric(METRIC_CONNECTED, True)
  155. self._track_metric(METRIC_LOGGED_IN, True)
  156. self._connected = True
  157. else:
  158. self.log.warning(
  159. f"New websocket state from signald: {evt.state}. Error: {evt.exception}"
  160. )
  161. self._track_metric(METRIC_CONNECTED, False)
  162. self._connected = False
  163. bridge_state = {
  164. # Signald disconnected
  165. WebsocketConnectionState.SOCKET_DISCONNECTED: BridgeStateEvent.TRANSIENT_DISCONNECT,
  166. # Websocket state reported by signald
  167. WebsocketConnectionState.DISCONNECTED: (
  168. None
  169. if self._websocket_connection_state == BridgeStateEvent.BAD_CREDENTIALS
  170. else BridgeStateEvent.TRANSIENT_DISCONNECT
  171. ),
  172. WebsocketConnectionState.CONNECTING: BridgeStateEvent.CONNECTING,
  173. WebsocketConnectionState.CONNECTED: BridgeStateEvent.CONNECTED,
  174. WebsocketConnectionState.RECONNECTING: BridgeStateEvent.TRANSIENT_DISCONNECT,
  175. WebsocketConnectionState.DISCONNECTING: BridgeStateEvent.TRANSIENT_DISCONNECT,
  176. WebsocketConnectionState.AUTHENTICATION_FAILED: BridgeStateEvent.BAD_CREDENTIALS,
  177. WebsocketConnectionState.FAILED: BridgeStateEvent.TRANSIENT_DISCONNECT,
  178. }.get(evt.state)
  179. if bridge_state is None:
  180. self.log.info(f"Websocket state {evt.state} seen. Will not report new Bridge State")
  181. return
  182. now = datetime.now()
  183. if bridge_state == BridgeStateEvent.TRANSIENT_DISCONNECT:
  184. async def wait_report_transient_disconnect():
  185. # Wait for 10 seconds (that should be enough for the bridge to get connected)
  186. # before sending a TRANSIENT_DISCONNECT.
  187. # self._latest_non_transient_disconnect_state will only be None if the bridge is
  188. # still starting.
  189. if self._latest_non_transient_disconnect_state is None:
  190. await sleep(15)
  191. if self._latest_non_transient_disconnect_state is None:
  192. asyncio.create_task(self.push_bridge_state(bridge_state))
  193. # Wait for another minute. If the bridge stays in TRANSIENT_DISCONNECT for that
  194. # long, something terrible has happened (signald failed to restart, the internet
  195. # broke, etc.)
  196. await sleep(60)
  197. if (
  198. self._latest_non_transient_disconnect_state
  199. and now > self._latest_non_transient_disconnect_state
  200. ):
  201. asyncio.create_task(
  202. self.push_bridge_state(
  203. BridgeStateEvent.UNKNOWN_ERROR,
  204. message="Failed to restore connection to Signal",
  205. )
  206. )
  207. else:
  208. self.log.info(
  209. "New state since last TRANSIENT_DISCONNECT push. "
  210. "Not transitioning to UNKNOWN_ERROR."
  211. )
  212. asyncio.create_task(wait_report_transient_disconnect())
  213. else:
  214. asyncio.create_task(self.push_bridge_state(bridge_state))
  215. self._latest_non_transient_disconnect_state = now
  216. self._websocket_connection_state = bridge_state
  217. async def _sync_puppet(self) -> None:
  218. puppet = await pu.Puppet.get_by_address(self.address)
  219. if puppet.uuid and not self.uuid:
  220. self.uuid = puppet.uuid
  221. self.by_uuid[self.uuid] = self
  222. if puppet.custom_mxid != self.mxid and puppet.can_auto_login(self.mxid):
  223. self.log.info("Automatically enabling custom puppet")
  224. try:
  225. await puppet.switch_mxid(access_token="auto", mxid=self.mxid)
  226. except AutologinError as e:
  227. self.log.warning(f"Failed to enable custom puppet: {e}")
  228. async def sync(self) -> None:
  229. await self.sync_puppet()
  230. await self.sync_contacts()
  231. await self.sync_groups()
  232. async def sync_puppet(self) -> None:
  233. try:
  234. async with self._sync_lock:
  235. await self._sync_puppet()
  236. except Exception:
  237. self.log.exception("Error while syncing own puppet")
  238. async def sync_contacts(self) -> None:
  239. try:
  240. async with self._sync_lock:
  241. await self._sync_contacts()
  242. except Exception as e:
  243. self.log.exception("Error while syncing contacts")
  244. await self.handle_auth_failure(e)
  245. async def sync_groups(self) -> None:
  246. try:
  247. async with self._sync_lock:
  248. await self._sync_groups()
  249. except Exception as e:
  250. self.log.exception("Error while syncing groups")
  251. await self.handle_auth_failure(e)
  252. async def sync_contact(self, contact: Profile | Address, create_portals: bool = False) -> None:
  253. self.log.trace("Syncing contact %s", contact)
  254. try:
  255. if isinstance(contact, Address):
  256. address = contact
  257. profile = await self.bridge.signal.get_profile(
  258. self.username, address, use_cache=True
  259. )
  260. if profile and profile.name:
  261. self.log.trace("Got profile for %s: %s", address, profile)
  262. else:
  263. address = contact.address
  264. profile = contact
  265. puppet = await pu.Puppet.get_by_address(address)
  266. await puppet.update_info(profile)
  267. if create_portals:
  268. portal = await po.Portal.get_by_chat_id(
  269. puppet.address, receiver=self.username, create=True
  270. )
  271. await portal.create_matrix_room(self, profile)
  272. except Exception as e:
  273. await self.handle_auth_failure(e)
  274. raise
  275. async def _sync_group(self, group: Group, create_portals: bool) -> None:
  276. self.log.trace("Syncing group %s", group)
  277. portal = await po.Portal.get_by_chat_id(group.group_id, create=True)
  278. if create_portals:
  279. await portal.create_matrix_room(self, group)
  280. elif portal.mxid:
  281. await portal.update_matrix_room(self, group)
  282. async def _sync_group_v2(self, group: GroupV2, create_portals: bool) -> None:
  283. self.log.trace("Syncing group %s", group.id)
  284. portal = await po.Portal.get_by_chat_id(group.id, create=True)
  285. if create_portals:
  286. await portal.create_matrix_room(self, group)
  287. elif portal.mxid:
  288. await portal.update_matrix_room(self, group)
  289. async def _sync_contacts(self) -> None:
  290. create_contact_portal = self.config["bridge.autocreate_contact_portal"]
  291. for contact in await self.bridge.signal.list_contacts(self.username):
  292. try:
  293. await self.sync_contact(contact, create_contact_portal)
  294. except Exception:
  295. self.log.exception(f"Failed to sync contact {contact.address}")
  296. async def _sync_groups(self) -> None:
  297. create_group_portal = self.config["bridge.autocreate_group_portal"]
  298. for group in await self.bridge.signal.list_groups(self.username):
  299. group_id = group.group_id if isinstance(group, Group) else group.id
  300. try:
  301. if isinstance(group, Group):
  302. await self._sync_group(group, create_group_portal)
  303. elif isinstance(group, GroupV2):
  304. await self._sync_group_v2(group, create_group_portal)
  305. else:
  306. self.log.warning("Unknown return type in list_groups: %s", type(group))
  307. except Exception:
  308. self.log.exception(f"Failed to sync group {group_id}")
  309. # region Database getters
  310. def _add_to_cache(self) -> None:
  311. self.by_mxid[self.mxid] = self
  312. if self.username:
  313. self.by_username[self.username] = self
  314. if self.uuid:
  315. self.by_uuid[self.uuid] = self
  316. @classmethod
  317. @async_getter_lock
  318. async def get_by_mxid(cls, mxid: UserID, create: bool = True) -> User | None:
  319. # Never allow ghosts to be users
  320. if pu.Puppet.get_id_from_mxid(mxid):
  321. return None
  322. try:
  323. return cls.by_mxid[mxid]
  324. except KeyError:
  325. pass
  326. user = cast(cls, await super().get_by_mxid(mxid))
  327. if user is not None:
  328. user._add_to_cache()
  329. return user
  330. if create:
  331. user = cls(mxid)
  332. await user.insert()
  333. user._add_to_cache()
  334. return user
  335. return None
  336. @classmethod
  337. @async_getter_lock
  338. async def get_by_username(cls, username: str) -> User | None:
  339. try:
  340. return cls.by_username[username]
  341. except KeyError:
  342. pass
  343. user = cast(cls, await super().get_by_username(username))
  344. if user is not None:
  345. user._add_to_cache()
  346. return user
  347. return None
  348. @classmethod
  349. @async_getter_lock
  350. async def get_by_uuid(cls, uuid: UUID) -> User | None:
  351. try:
  352. return cls.by_uuid[uuid]
  353. except KeyError:
  354. pass
  355. user = cast(cls, await super().get_by_uuid(uuid))
  356. if user is not None:
  357. user._add_to_cache()
  358. return user
  359. return None
  360. @classmethod
  361. async def get_by_address(cls, address: Address) -> User | None:
  362. if address.uuid:
  363. return await cls.get_by_uuid(address.uuid)
  364. elif address.number:
  365. return await cls.get_by_username(address.number)
  366. else:
  367. raise ValueError("Given address is blank")
  368. @classmethod
  369. async def all_logged_in(cls) -> AsyncGenerator[User, None]:
  370. users = await super().all_logged_in()
  371. user: cls
  372. for user in users:
  373. try:
  374. yield cls.by_mxid[user.mxid]
  375. except KeyError:
  376. user._add_to_cache()
  377. yield user
  378. # endregion