puppet.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. # mautrix-signal - A Matrix-Signal puppeting bridge
  2. # Copyright (C) 2020 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 (Optional, Dict, AsyncIterable, Awaitable, AsyncGenerator, Union,
  17. TYPE_CHECKING, cast)
  18. from uuid import UUID
  19. import asyncio
  20. from mausignald.types import Address, Contact, Profile
  21. from mautrix.bridge import BasePuppet
  22. from mautrix.appservice import IntentAPI
  23. from mautrix.types import UserID, SyncToken, RoomID
  24. from mautrix.util.simple_template import SimpleTemplate
  25. from .db import Puppet as DBPuppet
  26. from .config import Config
  27. from . import portal as p
  28. if TYPE_CHECKING:
  29. from .__main__ import SignalBridge
  30. try:
  31. import phonenumbers
  32. except ImportError:
  33. phonenumbers = None
  34. class Puppet(DBPuppet, BasePuppet):
  35. by_uuid: Dict[UUID, 'Puppet'] = {}
  36. by_number: Dict[str, 'Puppet'] = {}
  37. by_custom_mxid: Dict[UserID, 'Puppet'] = {}
  38. hs_domain: str
  39. mxid_template: SimpleTemplate[str]
  40. config: Config
  41. default_mxid_intent: IntentAPI
  42. default_mxid: UserID
  43. _uuid_lock: asyncio.Lock
  44. _update_info_lock: asyncio.Lock
  45. def __init__(self, uuid: Optional[UUID], number: Optional[str],
  46. name: Optional[str] = None, uuid_registered: bool = False,
  47. number_registered: bool = False, custom_mxid: Optional[UserID] = None,
  48. access_token: Optional[str] = None, next_batch: Optional[SyncToken] = None
  49. ) -> None:
  50. super().__init__(uuid=uuid, number=number, name=name, uuid_registered=uuid_registered,
  51. number_registered=number_registered, custom_mxid=custom_mxid,
  52. access_token=access_token, next_batch=next_batch)
  53. self.log = self.log.getChild(str(uuid) or number)
  54. self.default_mxid = self.get_mxid_from_id(self.address)
  55. self.default_mxid_intent = self.az.intent.user(self.default_mxid)
  56. self.intent = self._fresh_intent()
  57. self._uuid_lock = asyncio.Lock()
  58. self._update_info_lock = asyncio.Lock()
  59. @classmethod
  60. def init_cls(cls, bridge: 'SignalBridge') -> AsyncIterable[Awaitable[None]]:
  61. cls.config = bridge.config
  62. cls.loop = bridge.loop
  63. cls.mx = bridge.matrix
  64. cls.az = bridge.az
  65. cls.hs_domain = cls.config["homeserver.domain"]
  66. cls.mxid_template = SimpleTemplate(cls.config["bridge.username_template"], "userid",
  67. prefix="@", suffix=f":{cls.hs_domain}", type=str)
  68. cls.sync_with_custom_puppets = cls.config["bridge.sync_with_custom_puppets"]
  69. secret = cls.config["bridge.login_shared_secret"]
  70. cls.login_shared_secret = secret.encode("utf-8") if secret else None
  71. cls.login_device_name = "Signal Bridge"
  72. return (puppet.try_start() async for puppet in cls.all_with_custom_mxid())
  73. def intent_for(self, portal: 'p.Portal') -> IntentAPI:
  74. if portal.chat_id == self.uuid:
  75. return self.default_mxid_intent
  76. return self.intent
  77. @property
  78. def is_registered(self) -> bool:
  79. return (self.uuid is not None and self.uuid_registered) or self.number_registered
  80. @is_registered.setter
  81. def is_registered(self, value: bool) -> None:
  82. if self.uuid is not None:
  83. self.uuid_registered = value
  84. else:
  85. self.number_registered = value
  86. @property
  87. def address(self) -> Address:
  88. return Address(uuid=self.uuid, number=self.number)
  89. async def handle_uuid_receive(self, uuid: UUID) -> None:
  90. async with self._uuid_lock:
  91. if self.uuid:
  92. # Received UUID was handled while this call was waiting
  93. return
  94. await self._handle_uuid_receive(uuid)
  95. async def _handle_uuid_receive(self, uuid: UUID) -> None:
  96. self.log.debug(f"Found UUID for user: {uuid}")
  97. await self._set_uuid(uuid)
  98. self.by_uuid[self.uuid] = self
  99. prev_intent = self.default_mxid_intent
  100. self.default_mxid = self.get_mxid_from_id(self.address)
  101. self.default_mxid_intent = self.az.intent.user(self.default_mxid)
  102. self.intent = self._fresh_intent()
  103. self.log = self.log.getChild(str(uuid))
  104. self.log.debug(f"Migrating memberships {prev_intent.mxid} -> {self.default_mxid_intent}")
  105. for room_id in await prev_intent.get_joined_rooms():
  106. await prev_intent.invite_user(room_id, self.default_mxid)
  107. await self.default_mxid_intent.join_room_by_id(room_id)
  108. await prev_intent.leave_room(room_id)
  109. async def update_info(self, info: Union[Profile, Contact]) -> None:
  110. if isinstance(info, Contact):
  111. if info.address.uuid and not self.uuid:
  112. await self.handle_uuid_receive(info.address.uuid)
  113. if not self.config["bridge.allow_contact_list_name_updates"] and self.name is not None:
  114. return
  115. async with self._update_info_lock:
  116. update = False
  117. update = await self._update_name(info.name) or update
  118. if update:
  119. await self.update()
  120. @staticmethod
  121. def fmt_phone(number: str) -> str:
  122. if phonenumbers is None:
  123. return number
  124. parsed = phonenumbers.parse(number)
  125. fmt = phonenumbers.PhoneNumberFormat.INTERNATIONAL
  126. return phonenumbers.format_number(parsed, fmt)
  127. @classmethod
  128. def _get_displayname(cls, address: Address, name: Optional[str]) -> str:
  129. names = name.split("\x00") if name else []
  130. data = {
  131. "first_name": names[0] if len(names) > 0 else "",
  132. "last_name": names[-1] if len(names) > 1 else "",
  133. "full_name": " ".join(names),
  134. "phone": cls.fmt_phone(address.number),
  135. "uuid": str(address.uuid) if address.uuid else None,
  136. }
  137. for pref in cls.config["bridge.displayname_preference"]:
  138. value = data.get(pref.replace(" ", "_"))
  139. if value:
  140. data["displayname"] = value
  141. break
  142. return cls.config["bridge.displayname_template"].format(**data)
  143. async def _update_name(self, name: Optional[str]) -> bool:
  144. name = self._get_displayname(self.address, name)
  145. if name != self.name:
  146. self.name = name
  147. await self.default_mxid_intent.set_displayname(self.name)
  148. self.loop.create_task(self._update_portal_names())
  149. return True
  150. return False
  151. async def _update_portal_names(self) -> None:
  152. async for portal in p.Portal.find_private_chats_with(self.uuid):
  153. await portal.update_puppet_name(self.name)
  154. async def default_puppet_should_leave_room(self, room_id: RoomID) -> bool:
  155. portal = await p.Portal.get_by_mxid(room_id)
  156. return portal and portal.chat_id != self.uuid
  157. # region Database getters
  158. def _add_to_cache(self) -> None:
  159. if self.uuid:
  160. self.by_uuid[self.uuid] = self
  161. if self.number:
  162. self.by_number[self.number] = self
  163. if self.custom_mxid:
  164. self.by_custom_mxid[self.custom_mxid] = self
  165. async def save(self) -> None:
  166. await self.update()
  167. @classmethod
  168. async def get_by_mxid(cls, mxid: UserID, create: bool = True) -> Optional['Puppet']:
  169. address = cls.get_id_from_mxid(mxid)
  170. if not address:
  171. return None
  172. return await cls.get_by_address(address, create)
  173. @classmethod
  174. async def get_by_custom_mxid(cls, mxid: UserID) -> Optional['Puppet']:
  175. try:
  176. return cls.by_custom_mxid[mxid]
  177. except KeyError:
  178. pass
  179. puppet = cast(cls, await super().get_by_custom_mxid(mxid))
  180. if puppet:
  181. puppet._add_to_cache()
  182. return puppet
  183. return None
  184. @classmethod
  185. def get_id_from_mxid(cls, mxid: UserID) -> Optional[Address]:
  186. identifier = cls.mxid_template.parse(mxid)
  187. if not identifier:
  188. return None
  189. if identifier.startswith("phone_"):
  190. return Address(number="+" + identifier[len("phone_"):])
  191. else:
  192. try:
  193. return Address(uuid=UUID(identifier.upper()))
  194. except ValueError:
  195. return None
  196. @classmethod
  197. def get_mxid_from_id(cls, address: Address) -> UserID:
  198. if address.uuid:
  199. identifier = str(address.uuid).lower()
  200. elif address.number:
  201. identifier = f"phone_{address.number.lstrip('+')}"
  202. else:
  203. raise ValueError("Empty address")
  204. return UserID(cls.mxid_template.format_full(identifier))
  205. @classmethod
  206. async def get_by_address(cls, address: Address, create: bool = True) -> Optional['Puppet']:
  207. puppet = await cls._get_by_address(address, create)
  208. if puppet and address.uuid and not puppet.uuid:
  209. # We found a UUID for this user, store it ASAP
  210. await puppet.handle_uuid_receive(address.uuid)
  211. return puppet
  212. @classmethod
  213. async def _get_by_address(cls, address: Address, create: bool = True) -> Optional['Puppet']:
  214. if not address.is_valid:
  215. raise ValueError("Empty address")
  216. if address.uuid:
  217. try:
  218. return cls.by_uuid[address.uuid]
  219. except KeyError:
  220. pass
  221. if address.number:
  222. try:
  223. return cls.by_number[address.number]
  224. except KeyError:
  225. pass
  226. puppet = cast(cls, await super().get_by_address(address))
  227. if puppet is not None:
  228. puppet._add_to_cache()
  229. return puppet
  230. if create:
  231. puppet = cls(address.uuid, address.number)
  232. await puppet.insert()
  233. puppet._add_to_cache()
  234. return puppet
  235. return None
  236. @classmethod
  237. async def all_with_custom_mxid(cls) -> AsyncGenerator['Puppet', None]:
  238. puppets = await super().all_with_custom_mxid()
  239. puppet: cls
  240. for index, puppet in enumerate(puppets):
  241. try:
  242. yield cls.by_uuid[puppet.uuid]
  243. except KeyError:
  244. try:
  245. yield cls.by_number[puppet.number]
  246. except KeyError:
  247. puppet._add_to_cache()
  248. yield puppet
  249. # endregion