user.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  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 typing import Union, Dict, Optional, AsyncGenerator, TYPE_CHECKING, cast
  17. from uuid import UUID
  18. import asyncio
  19. from mausignald.types import Account, Address, Profile, Group, GroupV2, ListenEvent, ListenAction
  20. from mautrix.bridge import BaseUser, BridgeState, AutologinError, async_getter_lock
  21. from mautrix.types import UserID, RoomID
  22. from mautrix.appservice import AppService
  23. from mautrix.util.opt_prometheus import Gauge
  24. from .db import User as DBUser
  25. from .config import Config
  26. from . import puppet as pu, portal as po
  27. if TYPE_CHECKING:
  28. from .__main__ import SignalBridge
  29. METRIC_CONNECTED = Gauge('bridge_connected', 'Bridge users connected to Signal')
  30. METRIC_LOGGED_IN = Gauge('bridge_logged_in', 'Bridge users logged into Signal')
  31. BridgeState.human_readable_errors.update({
  32. "logged-out": "You're not logged into Signal",
  33. "signal-not-connected": None,
  34. })
  35. class User(DBUser, BaseUser):
  36. by_mxid: Dict[UserID, 'User'] = {}
  37. by_username: Dict[str, 'User'] = {}
  38. by_uuid: Dict[UUID, 'User'] = {}
  39. config: Config
  40. az: AppService
  41. loop: asyncio.AbstractEventLoop
  42. bridge: 'SignalBridge'
  43. relay_whitelisted: bool
  44. is_admin: bool
  45. permission_level: str
  46. _sync_lock: asyncio.Lock
  47. _notice_room_lock: asyncio.Lock
  48. _connected: bool
  49. def __init__(self, mxid: UserID, username: Optional[str] = None, uuid: Optional[UUID] = None,
  50. notice_room: Optional[RoomID] = None) -> None:
  51. super().__init__(mxid=mxid, username=username, uuid=uuid, notice_room=notice_room)
  52. BaseUser.__init__(self)
  53. self._notice_room_lock = asyncio.Lock()
  54. self._sync_lock = asyncio.Lock()
  55. self._connected = False
  56. perms = self.config.get_permissions(mxid)
  57. self.relay_whitelisted, self.is_whitelisted, self.is_admin, self.permission_level = perms
  58. @classmethod
  59. def init_cls(cls, bridge: 'SignalBridge') -> None:
  60. cls.bridge = bridge
  61. cls.config = bridge.config
  62. cls.az = bridge.az
  63. cls.loop = bridge.loop
  64. @property
  65. def address(self) -> Optional[Address]:
  66. if not self.username:
  67. return None
  68. return Address(uuid=self.uuid, number=self.username)
  69. async def is_logged_in(self) -> bool:
  70. return bool(self.username)
  71. async def logout(self) -> None:
  72. if not self.username:
  73. return
  74. username = self.username
  75. if self.uuid and self.by_uuid.get(self.uuid) == self:
  76. del self.by_uuid[self.uuid]
  77. if self.username and self.by_username.get(self.username) == self:
  78. del self.by_username[self.username]
  79. self.username = None
  80. self.uuid = None
  81. await self.update()
  82. await self.bridge.signal.unsubscribe(username)
  83. # Wait a while for signald to finish disconnecting
  84. await asyncio.sleep(1)
  85. await self.bridge.signal.delete_account(username)
  86. self._track_metric(METRIC_LOGGED_IN, False)
  87. await self.push_bridge_state(ok=False, error="logged-out", remote_id=username)
  88. async def fill_bridge_state(self, state: BridgeState) -> None:
  89. await super().fill_bridge_state(state)
  90. if not state.remote_id:
  91. state.remote_id = self.username
  92. if self.address:
  93. puppet = await self.get_puppet()
  94. state.remote_name = puppet.name or self.username
  95. async def get_bridge_state(self) -> BridgeState:
  96. if not self.username:
  97. return BridgeState(ok=False, error="logged-out")
  98. elif not self._connected:
  99. return BridgeState(ok=False, error="signal-not-connected")
  100. return BridgeState(ok=True)
  101. async def get_puppet(self) -> Optional['pu.Puppet']:
  102. if not self.address:
  103. return None
  104. return await pu.Puppet.get_by_address(self.address)
  105. async def on_signin(self, account: Account) -> None:
  106. self.username = account.account_id
  107. self.uuid = account.address.uuid
  108. self._add_to_cache()
  109. await self.update()
  110. await self.bridge.signal.subscribe(self.username)
  111. asyncio.create_task(self.sync())
  112. self._track_metric(METRIC_LOGGED_IN, True)
  113. def on_listen(self, evt: ListenEvent) -> None:
  114. if evt.action == ListenAction.STARTED:
  115. self.log.info("Connected to Signal")
  116. self._track_metric(METRIC_CONNECTED, True)
  117. self._track_metric(METRIC_LOGGED_IN, True)
  118. self._connected = True
  119. asyncio.create_task(self.push_bridge_state(ok=True))
  120. elif evt.action == ListenAction.STOPPED:
  121. if evt.exception:
  122. self.log.warning(f"Disconnected from Signal: {evt.exception}")
  123. else:
  124. self.log.info("Disconnected from Signal")
  125. self._track_metric(METRIC_CONNECTED, False)
  126. asyncio.create_task(self.push_bridge_state(ok=False, error="signal-not-connected"))
  127. self._connected = False
  128. else:
  129. self.log.warning(f"Unrecognized listen action {evt.action}")
  130. async def _sync_puppet(self) -> None:
  131. puppet = await pu.Puppet.get_by_address(self.address)
  132. if puppet.uuid and not self.uuid:
  133. self.uuid = puppet.uuid
  134. self.by_uuid[self.uuid] = self
  135. if puppet.custom_mxid != self.mxid and puppet.can_auto_login(self.mxid):
  136. self.log.info(f"Automatically enabling custom puppet")
  137. try:
  138. await puppet.switch_mxid(access_token="auto", mxid=self.mxid)
  139. except AutologinError as e:
  140. self.log.warning(f"Failed to enable custom puppet: {e}")
  141. async def sync(self) -> None:
  142. await self.sync_puppet()
  143. await self.sync_contacts()
  144. await self.sync_groups()
  145. async def sync_puppet(self) -> None:
  146. try:
  147. async with self._sync_lock:
  148. await self._sync_puppet()
  149. except Exception:
  150. self.log.exception("Error while syncing own puppet")
  151. async def sync_contacts(self) -> None:
  152. try:
  153. async with self._sync_lock:
  154. await self._sync_contacts()
  155. except Exception:
  156. self.log.exception("Error while syncing contacts")
  157. async def sync_groups(self) -> None:
  158. try:
  159. async with self._sync_lock:
  160. await self._sync_groups()
  161. except Exception:
  162. self.log.exception("Error while syncing groups")
  163. async def sync_contact(self, contact: Union[Profile, Address], create_portals: bool = False
  164. ) -> None:
  165. self.log.trace("Syncing contact %s", contact)
  166. if isinstance(contact, Address):
  167. address = contact
  168. profile = await self.bridge.signal.get_profile(self.username, address)
  169. if profile and profile.name:
  170. self.log.trace("Got profile for %s: %s", address, profile)
  171. else:
  172. address = contact.address
  173. profile = contact
  174. puppet = await pu.Puppet.get_by_address(address)
  175. await puppet.update_info(profile)
  176. if create_portals:
  177. portal = await po.Portal.get_by_chat_id(puppet.address, receiver=self.username,
  178. create=True)
  179. await portal.create_matrix_room(self, profile)
  180. async def _sync_group(self, group: Group, create_portals: bool) -> None:
  181. self.log.trace("Syncing group %s", group)
  182. portal = await po.Portal.get_by_chat_id(group.group_id, create=True)
  183. if create_portals:
  184. await portal.create_matrix_room(self, group)
  185. elif portal.mxid:
  186. await portal.update_matrix_room(self, group)
  187. async def _sync_group_v2(self, group: GroupV2, create_portals: bool) -> None:
  188. self.log.trace("Syncing group %s", group.id)
  189. portal = await po.Portal.get_by_chat_id(group.id, create=True)
  190. if create_portals:
  191. await portal.create_matrix_room(self, group)
  192. elif portal.mxid:
  193. await portal.update_matrix_room(self, group)
  194. async def _sync_contacts(self) -> None:
  195. create_contact_portal = self.config["bridge.autocreate_contact_portal"]
  196. for contact in await self.bridge.signal.list_contacts(self.username):
  197. try:
  198. await self.sync_contact(contact, create_contact_portal)
  199. except Exception:
  200. self.log.exception(f"Failed to sync contact {contact.address}")
  201. async def _sync_groups(self) -> None:
  202. create_group_portal = self.config["bridge.autocreate_group_portal"]
  203. for group in await self.bridge.signal.list_groups(self.username):
  204. group_id = group.group_id if isinstance(group, Group) else group.id
  205. try:
  206. if isinstance(group, Group):
  207. await self._sync_group(group, create_group_portal)
  208. elif isinstance(group, GroupV2):
  209. await self._sync_group_v2(group, create_group_portal)
  210. else:
  211. self.log.warning("Unknown return type in list_groups: %s", type(group))
  212. except Exception:
  213. self.log.exception(f"Failed to sync group {group_id}")
  214. # region Database getters
  215. def _add_to_cache(self) -> None:
  216. self.by_mxid[self.mxid] = self
  217. if self.username:
  218. self.by_username[self.username] = self
  219. if self.uuid:
  220. self.by_uuid[self.uuid] = self
  221. @classmethod
  222. @async_getter_lock
  223. async def get_by_mxid(cls, mxid: UserID, create: bool = True) -> Optional['User']:
  224. # Never allow ghosts to be users
  225. if pu.Puppet.get_id_from_mxid(mxid):
  226. return None
  227. try:
  228. return cls.by_mxid[mxid]
  229. except KeyError:
  230. pass
  231. user = cast(cls, await super().get_by_mxid(mxid))
  232. if user is not None:
  233. user._add_to_cache()
  234. return user
  235. if create:
  236. user = cls(mxid)
  237. await user.insert()
  238. user._add_to_cache()
  239. return user
  240. return None
  241. @classmethod
  242. @async_getter_lock
  243. async def get_by_username(cls, username: str) -> Optional['User']:
  244. try:
  245. return cls.by_username[username]
  246. except KeyError:
  247. pass
  248. user = cast(cls, await super().get_by_username(username))
  249. if user is not None:
  250. user._add_to_cache()
  251. return user
  252. return None
  253. @classmethod
  254. @async_getter_lock
  255. async def get_by_uuid(cls, uuid: UUID) -> Optional['User']:
  256. try:
  257. return cls.by_uuid[uuid]
  258. except KeyError:
  259. pass
  260. user = cast(cls, await super().get_by_uuid(uuid))
  261. if user is not None:
  262. user._add_to_cache()
  263. return user
  264. return None
  265. @classmethod
  266. async def get_by_address(cls, address: Address) -> Optional['User']:
  267. if address.uuid:
  268. return await cls.get_by_uuid(address.uuid)
  269. elif address.number:
  270. return await cls.get_by_username(address.number)
  271. else:
  272. raise ValueError("Given address is blank")
  273. @classmethod
  274. async def all_logged_in(cls) -> AsyncGenerator['User', None]:
  275. users = await super().all_logged_in()
  276. user: cls
  277. for user in users:
  278. try:
  279. yield cls.by_mxid[user.mxid]
  280. except KeyError:
  281. user._add_to_cache()
  282. yield user
  283. # endregion