user.py 15 KB

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