slidge 0.1.0b2__py3-none-any.whl → 0.1.1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (155) hide show
  1. slidge/__init__.py +55 -31
  2. slidge/__main__.py +118 -116
  3. slidge/command/__init__.py +28 -0
  4. slidge/command/adhoc.py +258 -0
  5. slidge/command/admin.py +193 -0
  6. slidge/command/base.py +441 -0
  7. slidge/command/categories.py +3 -0
  8. slidge/command/chat_command.py +288 -0
  9. slidge/command/register.py +179 -0
  10. slidge/command/user.py +250 -0
  11. slidge/contact/__init__.py +8 -0
  12. slidge/contact/contact.py +452 -0
  13. slidge/contact/roster.py +192 -0
  14. slidge/core/__init__.py +2 -0
  15. slidge/core/cache.py +183 -0
  16. slidge/core/config.py +216 -0
  17. slidge/core/gateway/__init__.py +3 -0
  18. slidge/core/gateway/base.py +895 -0
  19. slidge/core/gateway/caps.py +63 -0
  20. slidge/core/gateway/delivery_receipt.py +52 -0
  21. slidge/core/gateway/disco.py +80 -0
  22. slidge/core/gateway/mam.py +75 -0
  23. slidge/core/gateway/muc_admin.py +35 -0
  24. slidge/core/gateway/ping.py +66 -0
  25. slidge/core/gateway/presence.py +95 -0
  26. slidge/core/gateway/registration.py +53 -0
  27. slidge/core/gateway/search.py +102 -0
  28. slidge/core/gateway/session_dispatcher.py +789 -0
  29. slidge/core/gateway/vcard_temp.py +130 -0
  30. slidge/core/mixins/__init__.py +19 -0
  31. slidge/core/mixins/attachment.py +506 -0
  32. slidge/core/mixins/avatar.py +167 -0
  33. slidge/core/mixins/base.py +31 -0
  34. slidge/core/mixins/disco.py +130 -0
  35. slidge/core/mixins/lock.py +31 -0
  36. slidge/core/mixins/message.py +398 -0
  37. slidge/core/mixins/message_maker.py +154 -0
  38. slidge/core/mixins/presence.py +217 -0
  39. slidge/core/mixins/recipient.py +43 -0
  40. slidge/core/pubsub.py +282 -116
  41. slidge/core/session.py +595 -372
  42. slidge/group/__init__.py +10 -0
  43. slidge/group/archive.py +125 -0
  44. slidge/group/bookmarks.py +163 -0
  45. slidge/group/participant.py +458 -0
  46. slidge/group/room.py +1103 -0
  47. slidge/migration.py +18 -0
  48. slidge/slixfix/__init__.py +68 -0
  49. slidge/{util/xep_0084 → slixfix/link_preview}/__init__.py +3 -5
  50. slidge/slixfix/link_preview/link_preview.py +17 -0
  51. slidge/slixfix/link_preview/stanza.py +99 -0
  52. slidge/slixfix/roster.py +60 -0
  53. slidge/{util → slixfix}/xep_0077/register.py +14 -2
  54. slidge/slixfix/xep_0077/stanza.py +104 -0
  55. slidge/{util → slixfix}/xep_0100/gateway.py +25 -15
  56. slidge/slixfix/xep_0100/stanza.py +9 -0
  57. slidge/slixfix/xep_0153/__init__.py +10 -0
  58. slidge/slixfix/xep_0153/stanza.py +25 -0
  59. slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
  60. slidge/slixfix/xep_0264/__init__.py +5 -0
  61. slidge/slixfix/xep_0264/stanza.py +36 -0
  62. slidge/slixfix/xep_0264/thumbnail.py +23 -0
  63. slidge/slixfix/xep_0292/__init__.py +5 -0
  64. slidge/slixfix/xep_0292/vcard4.py +100 -0
  65. slidge/slixfix/xep_0313/__init__.py +12 -0
  66. slidge/slixfix/xep_0313/mam.py +262 -0
  67. slidge/slixfix/xep_0313/stanza.py +359 -0
  68. slidge/slixfix/xep_0317/__init__.py +5 -0
  69. slidge/slixfix/xep_0317/hats.py +17 -0
  70. slidge/slixfix/xep_0317/stanza.py +28 -0
  71. slidge/{util → slixfix}/xep_0356_old/privilege.py +9 -7
  72. slidge/slixfix/xep_0424/__init__.py +9 -0
  73. slidge/slixfix/xep_0424/retraction.py +77 -0
  74. slidge/slixfix/xep_0424/stanza.py +28 -0
  75. slidge/slixfix/xep_0490/__init__.py +8 -0
  76. slidge/slixfix/xep_0490/mds.py +47 -0
  77. slidge/slixfix/xep_0490/stanza.py +17 -0
  78. slidge/util/__init__.py +4 -6
  79. slidge/util/archive_msg.py +61 -0
  80. slidge/util/conf.py +206 -0
  81. slidge/util/db.py +57 -76
  82. slidge/util/schema.sql +126 -0
  83. slidge/util/sql.py +508 -0
  84. slidge/util/test.py +215 -25
  85. slidge/util/types.py +177 -4
  86. slidge/util/util.py +225 -59
  87. slidge-0.1.1.dist-info/METADATA +110 -0
  88. slidge-0.1.1.dist-info/RECORD +96 -0
  89. {slidge-0.1.0b2.dist-info → slidge-0.1.1.dist-info}/WHEEL +1 -1
  90. slidge/core/contact.py +0 -891
  91. slidge/core/gateway.py +0 -916
  92. slidge/plugins/discord/__init__.py +0 -90
  93. slidge/plugins/discord/client.py +0 -108
  94. slidge/plugins/discord/session.py +0 -162
  95. slidge/plugins/dummy.py +0 -203
  96. slidge/plugins/facebook.py +0 -493
  97. slidge/plugins/hackernews.py +0 -213
  98. slidge/plugins/mattermost/__init__.py +0 -1
  99. slidge/plugins/mattermost/api.py +0 -280
  100. slidge/plugins/mattermost/gateway.py +0 -365
  101. slidge/plugins/mattermost/websocket.py +0 -252
  102. slidge/plugins/signal/__init__.py +0 -3
  103. slidge/plugins/signal/contact.py +0 -106
  104. slidge/plugins/signal/gateway.py +0 -282
  105. slidge/plugins/signal/session.py +0 -448
  106. slidge/plugins/signal/txt.py +0 -53
  107. slidge/plugins/skype.py +0 -325
  108. slidge/plugins/steam.py +0 -310
  109. slidge/plugins/telegram/__init__.py +0 -5
  110. slidge/plugins/telegram/client.py +0 -228
  111. slidge/plugins/telegram/config.py +0 -12
  112. slidge/plugins/telegram/contact.py +0 -176
  113. slidge/plugins/telegram/gateway.py +0 -150
  114. slidge/plugins/telegram/session.py +0 -256
  115. slidge/util/xep_0030/__init__.py +0 -13
  116. slidge/util/xep_0030/disco.py +0 -811
  117. slidge/util/xep_0030/stanza/__init__.py +0 -7
  118. slidge/util/xep_0030/stanza/info.py +0 -270
  119. slidge/util/xep_0030/stanza/items.py +0 -147
  120. slidge/util/xep_0030/static.py +0 -467
  121. slidge/util/xep_0055/__init__.py +0 -5
  122. slidge/util/xep_0055/search.py +0 -75
  123. slidge/util/xep_0055/stanza.py +0 -10
  124. slidge/util/xep_0077/stanza.py +0 -71
  125. slidge/util/xep_0084/avatar.py +0 -137
  126. slidge/util/xep_0084/stanza.py +0 -104
  127. slidge/util/xep_0115/__init__.py +0 -12
  128. slidge/util/xep_0115/caps.py +0 -379
  129. slidge/util/xep_0115/stanza.py +0 -16
  130. slidge/util/xep_0115/static.py +0 -137
  131. slidge/util/xep_0292/__init__.py +0 -1
  132. slidge/util/xep_0292/stanza.py +0 -167
  133. slidge/util/xep_0292/vcard4.py +0 -75
  134. slidge/util/xep_0333/__init__.py +0 -10
  135. slidge/util/xep_0333/markers.py +0 -96
  136. slidge/util/xep_0333/stanza.py +0 -34
  137. slidge/util/xep_0356/__init__.py +0 -7
  138. slidge/util/xep_0356/permissions.py +0 -35
  139. slidge/util/xep_0356/privilege.py +0 -160
  140. slidge/util/xep_0356/stanza.py +0 -44
  141. slidge/util/xep_0363/__init__.py +0 -16
  142. slidge/util/xep_0363/http_upload.py +0 -215
  143. slidge/util/xep_0363/stanza.py +0 -46
  144. slidge/util/xep_0461/__init__.py +0 -6
  145. slidge/util/xep_0461/reply.py +0 -48
  146. slidge/util/xep_0461/stanza.py +0 -47
  147. slidge-0.1.0b2.dist-info/METADATA +0 -171
  148. slidge-0.1.0b2.dist-info/RECORD +0 -81
  149. /slidge/{plugins/__init__.py → py.typed} +0 -0
  150. /slidge/{util → slixfix}/xep_0077/__init__.py +0 -0
  151. /slidge/{util → slixfix}/xep_0100/__init__.py +0 -0
  152. /slidge/{util → slixfix}/xep_0356_old/__init__.py +0 -0
  153. /slidge/{util → slixfix}/xep_0356_old/stanza.py +0 -0
  154. {slidge-0.1.0b2.dist-info → slidge-0.1.1.dist-info}/LICENSE +0 -0
  155. {slidge-0.1.0b2.dist-info → slidge-0.1.1.dist-info}/entry_points.txt +0 -0
@@ -1,252 +0,0 @@
1
- import asyncio
2
- import json
3
- import logging
4
- import pprint
5
- import ssl
6
- import time
7
- from dataclasses import dataclass
8
- from enum import Enum
9
-
10
- import aiohttp
11
-
12
-
13
- class EventType(str, Enum):
14
- AddedToTeam = "added_to_team"
15
- AuthenticationChallenge = "authentication_challenge"
16
- ChannelConverted = "channel_converted"
17
- ChannelCreated = "channel_created"
18
- ChannelDeleted = "channel_deleted"
19
- ChannelMemberUpdated = "channel_member_updated"
20
- ChannelUpdated = "channel_updated"
21
- ChannelViewed = "channel_viewed"
22
- ConfigChanged = "config_changed"
23
- DeleteTeam = "delete_team"
24
- DirectAdded = "direct_added"
25
- EmojiAdded = "emoji_added"
26
- EphemeralMessage = "ephemeral_message"
27
- GroupAdded = "group_added"
28
- Hello = "hello"
29
- LeaveTeam = "leave_team"
30
- LicenseChanged = "license_changed"
31
- MemberroleUpdated = "memberrole_updated"
32
- NewUser = "new_user"
33
- PluginDisabled = "plugin_disabled"
34
- PluginEnabled = "plugin_enabled"
35
- PluginStatusesChanged = "plugin_statuses_changed"
36
- PostDeleted = "post_deleted"
37
- PostEdited = "post_edited"
38
- PostUnread = "post_unread"
39
- Posted = "posted"
40
- PreferenceChanged = "preference_changed"
41
- PreferencesChanged = "preferences_changed"
42
- PreferencesDeleted = "preferences_deleted"
43
- ReactionAdded = "reaction_added"
44
- ReactionRemoved = "reaction_removed"
45
- Response = "response"
46
- RoleUpdated = "role_updated"
47
- StatusChange = "status_change"
48
- Typing = "typing"
49
- UpdateTeam = "update_team"
50
- UserAdded = "user_added"
51
- UserRemoved = "user_removed"
52
- UserRoleUpdated = "user_role_updated"
53
- UserUpdated = "user_updated"
54
- DialogOpened = "dialog_opened"
55
- ThreadUpdated = "thread_updated"
56
- ThreadFollowChanged = "thread_follow_changed"
57
- ThreadReadChanged = "thread_read_changed"
58
-
59
- # not in the https://api.mattermost.com
60
- SidebarCategoryUpdated = "sidebar_category_updated"
61
-
62
- Unknown = "__unknown__"
63
-
64
-
65
- @dataclass
66
- class MattermostEvent:
67
- type: EventType
68
- data: dict
69
- broadcast: dict
70
- left: dict
71
-
72
- def __str__(self):
73
- return (
74
- f"<{self.type}:"
75
- f" \ndata: {pprint.pformat(self.data)}"
76
- f" \nbroadcast: {pprint.pformat(self.broadcast)}"
77
- f" \nleft: {pprint.pformat(self.left)}"
78
- f">"
79
- )
80
-
81
-
82
- class Websocket:
83
- def __init__(self, url, token):
84
- self.token = token
85
- self.url = url
86
-
87
- self._alive = False
88
- self._last_msg = 0
89
-
90
- self.ssl_verify = True
91
- self.keep_alive = True
92
- self.keep_alive_delay = 30
93
- self.websocket: asyncio.Future[
94
- aiohttp.ClientWebSocketResponse
95
- ] = asyncio.get_event_loop().create_future()
96
- self._futures: dict[int, asyncio.Future[dict]] = {}
97
- self._seq_cursor = 0
98
-
99
- async def connect(self, event_handler):
100
- """
101
- Connect to the websocket and authenticate it.
102
- When the authentication has finished, start the loop listening for messages,
103
- sending a ping to the server to keep the connection alive.
104
- :param event_handler: Every websocket event will be passed there. Takes one argument.
105
- :type event_handler: Function(message)
106
- :return:
107
- """
108
- context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
109
- if not self.ssl_verify:
110
- context.verify_mode = ssl.CERT_NONE
111
-
112
- url = self.url
113
- self._alive = True
114
-
115
- while True:
116
- try:
117
- kw_args = {}
118
- async with aiohttp.ClientSession() as session:
119
- async with session.ws_connect(
120
- url,
121
- ssl=context,
122
- **kw_args,
123
- ) as websocket:
124
- self.websocket.set_result(websocket)
125
- await self._authenticate_websocket(websocket)
126
- while self._alive:
127
- try:
128
- await self._start_loop(websocket, event_handler)
129
- except aiohttp.ClientError:
130
- break
131
- if (not self.keep_alive) or (not self._alive):
132
- break
133
- except Exception as e:
134
- log.exception(
135
- f"Failed to establish websocket connection: {type(e)} thrown"
136
- )
137
- await asyncio.sleep(self.keep_alive_delay)
138
-
139
- async def _start_loop(self, websocket, event_handler):
140
- """
141
- We will listen for websockets events, sending a heartbeats on a timer.
142
- If we don't the webserver would close the idle connection,
143
- forcing us to reconnect.
144
- """
145
- log.debug("Starting websocket loop")
146
- keep_alive = asyncio.create_task(self._do_heartbeats(websocket))
147
- log.debug("Waiting for messages on websocket")
148
- while self._alive:
149
- message = await websocket.receive_str()
150
- d = json.loads(message)
151
- self._last_msg = time.time()
152
- if (seq := d.get("seq_reply")) is None:
153
- await handle_event(d, event_handler)
154
- else:
155
- try:
156
- self._futures.pop(seq).set_result(d)
157
- except KeyError:
158
- log.warning("Ignoring %s", d)
159
- log.debug("cancelling heartbeat task")
160
- keep_alive.cancel()
161
- try:
162
- await keep_alive
163
- except asyncio.CancelledError:
164
- pass
165
-
166
- async def _do_heartbeats(self, websocket):
167
- """
168
- This is a little complicated, but we only need to pong the websocket if
169
- we haven't received a message inside the timeout window.
170
- Since messages can be received, while we are waiting we need to check
171
- after sleep.
172
- """
173
- timeout = 30
174
- while True:
175
- since_last_msg = time.time() - self._last_msg
176
- next_timeout = (
177
- timeout - since_last_msg if since_last_msg <= timeout else timeout
178
- )
179
- await asyncio.sleep(next_timeout)
180
- if time.time() - self._last_msg >= timeout:
181
- log.debug("sending heartbeat...")
182
- await websocket.pong()
183
- self._last_msg = time.time()
184
-
185
- def disconnect(self):
186
- """Sets `self._alive` to False so the loop in `self._start_loop` will finish."""
187
- log.info("Disconnecting websocket")
188
- self._alive = False
189
-
190
- async def _authenticate_websocket(self, websocket):
191
- """
192
- Sends an authentication challenge over a websocket.
193
- This is not needed when we just send the cookie we got on login
194
- when connecting to the websocket.
195
- """
196
- log.debug("Authenticating websocket")
197
- json_data = json.dumps(
198
- {
199
- "seq": 1,
200
- "action": "authentication_challenge",
201
- "data": {"token": self.token},
202
- }
203
- )
204
- await websocket.send_str(json_data)
205
- while True:
206
- message = await websocket.receive_str()
207
- status = json.loads(message)
208
- log.debug(status)
209
- if ("event" in status and status["event"] == "hello") and (
210
- "seq" in status and status["seq"] == 0
211
- ):
212
- log.info("Websocket authentication OK")
213
- return True
214
- log.error("Websocket authentication failed")
215
-
216
- async def user_typing(self, channel_id):
217
- seq = self._seq_cursor
218
- self._seq_cursor += 1
219
- f = self._futures[seq] = asyncio.get_event_loop().create_future()
220
- payload = json.dumps(
221
- {
222
- "seq": seq,
223
- "action": "user_typing",
224
- "data": {"channel_id": channel_id},
225
- }
226
- )
227
- log.debug("Sending %s", payload)
228
- await (await self.websocket).send_str(payload)
229
- r = await f
230
- log.debug("Confirmation %s", r)
231
-
232
-
233
- async def handle_event(d, event_handler):
234
- if "event" in d:
235
- raw_data = d.pop("data")
236
- data = {}
237
-
238
- for k, v in raw_data.items():
239
- try:
240
- data[k] = json.loads(v)
241
- except (json.JSONDecodeError, TypeError):
242
- data[k] = v
243
-
244
- try:
245
- event = EventType(d.pop("event"))
246
- except ValueError:
247
- event = EventType.Unknown
248
- bro = d.pop("broadcast")
249
- await event_handler(MattermostEvent(event, data, bro, d))
250
-
251
-
252
- log = logging.getLogger(__name__)
@@ -1,3 +0,0 @@
1
- from .contact import Contact, Roster
2
- from .gateway import Gateway
3
- from .session import Session
@@ -1,106 +0,0 @@
1
- import functools
2
- import logging
3
- from datetime import datetime
4
- from mimetypes import guess_extension
5
- from pathlib import Path
6
- from typing import TYPE_CHECKING, Optional
7
-
8
- import aiosignald.exc as sigexc
9
- import aiosignald.generated as sigapi
10
- from slixmpp.exceptions import XMPPError
11
-
12
- from slidge import *
13
-
14
- if TYPE_CHECKING:
15
- from .session import Session
16
-
17
-
18
- class Contact(LegacyContact["Session"]):
19
- CORRECTION = False
20
-
21
- def __init__(self, *a, **k):
22
- super().__init__(*a, **k)
23
- # keys = msg timestamp; vals = single character emoji
24
- self.user_reactions = dict[int, str]()
25
-
26
- @functools.cached_property
27
- def signal_address(self):
28
- return sigapi.JsonAddressv1(uuid=self.legacy_id)
29
-
30
- async def get_identities(self):
31
- s = await self.session.signal
32
- log.debug("%s, %s", type(self.session.phone), type(self.signal_address))
33
- try:
34
- r = await s.get_identities(
35
- account=self.session.phone,
36
- address=self.signal_address,
37
- )
38
- except sigexc.UnregisteredUserError:
39
- raise XMPPError("not-found")
40
- identities = r.identities
41
- self.session.send_gateway_message(str(identities))
42
-
43
- async def send_attachments(
44
- self,
45
- attachments: list[sigapi.JsonAttachmentv1],
46
- /,
47
- legacy_msg_id: int,
48
- reply_to_msg_id: int,
49
- when: Optional[datetime] = None,
50
- ):
51
- for attachment in attachments:
52
- filename = get_filename(attachment)
53
- with open(attachment.storedFilename, "rb") as f:
54
- await self.send_file(
55
- filename=filename,
56
- input_file=f,
57
- content_type=attachment.contentType,
58
- legacy_msg_id=legacy_msg_id,
59
- reply_to_msg_id=reply_to_msg_id,
60
- when=when,
61
- )
62
-
63
- async def update_info(self, profile: Optional[sigapi.Profilev1] = None):
64
- if profile is None:
65
- profile = await (await self.session.signal).get_profile(
66
- account=self.session.phone, address=self.signal_address
67
- )
68
- nick = profile.name or profile.profile_name
69
- if nick is not None:
70
- nick = nick.replace("\u0000", "")
71
- self.name = nick
72
- if profile.avatar is not None:
73
- self.avatar = Path(profile.avatar)
74
-
75
- address = await (await self.session.signal).resolve_address(
76
- account=self.session.phone,
77
- partial=sigapi.JsonAddressv1(uuid=self.legacy_id),
78
- )
79
-
80
- self.set_vcard(full_name=nick, phone=address.number, note=profile.about)
81
-
82
- async def update_and_add(self):
83
- await self.update_info()
84
- await self.add_to_roster()
85
-
86
-
87
- def get_filename(attachment: sigapi.JsonAttachmentv1):
88
- if f := attachment.customFilename:
89
- return f
90
- else:
91
- filename = attachment.id or "unnamed"
92
- ext = guess_extension(attachment.contentType)
93
- if ext is not None:
94
- filename += ext
95
- return filename
96
-
97
-
98
- class Roster(LegacyRoster[Contact, "Session"]):
99
- def by_json_address(self, address: sigapi.JsonAddressv1):
100
- c = self.by_legacy_id(address.uuid)
101
- if not c.added_to_roster:
102
- self.session.xmpp.loop.create_task(c.update_and_add())
103
- return c
104
-
105
-
106
- log = logging.getLogger(__name__)
@@ -1,282 +0,0 @@
1
- import asyncio
2
- import functools
3
- import logging
4
- from argparse import ArgumentParser
5
- from datetime import datetime
6
- from typing import TYPE_CHECKING, Any, Optional
7
-
8
- import aiosignald.exc as sigexc
9
- import aiosignald.generated as sigapi
10
- from aiosignald import SignaldAPI
11
- from slixmpp import JID, Iq, Message
12
- from slixmpp.exceptions import XMPPError
13
- from slixmpp.plugins.xep_0004 import Form
14
-
15
- from slidge import *
16
- from slidge.util import is_valid_phone_number
17
-
18
- if TYPE_CHECKING:
19
- from .session import Session
20
-
21
- from . import txt
22
-
23
-
24
- class Gateway(BaseGateway):
25
- COMPONENT_NAME = "Signal (slidge)"
26
- COMPONENT_TYPE = "signal"
27
- COMPONENT_AVATAR = (
28
- "https://upload.wikimedia.org/wikipedia/commons/5/56/Logo_Signal..png"
29
- )
30
- REGISTRATION_INSTRUCTIONS = txt.REGISTRATION_INSTRUCTIONS
31
- REGISTRATION_FIELDS = txt.REGISTRATION_FIELDS
32
-
33
- ROSTER_GROUP = "Signal"
34
-
35
- SEARCH_FIELDS = [
36
- FormField(var="phone", label="Phone number", required=True),
37
- ]
38
-
39
- signal: asyncio.Future["Signal"]
40
- signal_socket: str
41
- sessions_by_phone: dict[str, "Session"] = {}
42
-
43
- CHAT_COMMANDS = {
44
- "add_device": "_chat_command_add_device",
45
- "get_identities": "_chat_command_get_identities",
46
- }
47
-
48
- def __init__(self, args):
49
- super(Gateway, self).__init__(args)
50
- self.signal: asyncio.Future[Signal] = self.loop.create_future()
51
-
52
- def config(self, argv: list[str]):
53
- args = get_parser().parse_args(argv)
54
- self.signal_socket = socket = args.socket
55
- self.loop.create_task(self.connect_signal(socket))
56
-
57
- async def connect_signal(self, socket: str):
58
- """
59
- Establish connection to the signald socker
60
- """
61
- log.debug("Connecting to signald...")
62
- _, signal = await self.loop.create_unix_connection(
63
- functools.partial(Signal, self), socket
64
- )
65
- self.signal.set_result(signal)
66
- await signal.on_con_lost
67
- log.error("Signald UNIX socket connection lost!")
68
- raise RuntimeError("Signald socket connection lost")
69
-
70
- def add_adhoc_commands(self):
71
- self["xep_0050"].add_command(
72
- node="linked_devices",
73
- name="Get linked devices",
74
- handler=self._handle_linked_devices,
75
- )
76
- self["xep_0050"].add_command(
77
- node="add_device",
78
- name="Link a new device",
79
- handler=self._handle_add_device1,
80
- )
81
-
82
- async def _handle_linked_devices(self, iq: Iq, adhoc_session: dict[str, Any]):
83
- user = user_store.get_by_stanza(iq)
84
- if user is None:
85
- raise XMPPError("subscription-required")
86
-
87
- devices = await (await self.signal).get_linked_devices(
88
- account=user.registration_form["phone"]
89
- )
90
-
91
- # TODO: uncomment this when https://dev.gajim.org/gajim/gajim/-/issues/10857 is fixed
92
- # There are probably other clients that handle this just fine and this would make more sense
93
- # to use this, but I think targeting gajim compatibility when there are easy workarounds
94
- # is OK
95
- # form = self["xep_0004"].make_form("result", "Linked devices")
96
- # form.add_reported("id", label="ID", type="fixed")
97
- # form.add_reported("name", label="Name", type="fixed")
98
- # form.add_reported("created", label="Created", type="fixed")
99
- # form.add_reported("last_seen", label="Last seen", type="fixed")
100
- # for d in devices.devices:
101
- # form.add_item(
102
- # {
103
- # "name": d.name,
104
- # "id": str(d.id),
105
- # "created": datetime.fromtimestamp(d.created / 1000).isoformat(),
106
- # "last_seen": datetime.fromtimestamp(d.lastSeen / 1000).isoformat(),
107
- # }
108
- # )
109
- #
110
- # adhoc_session["payload"] = form
111
- adhoc_session["notes"] = [
112
- (
113
- "info",
114
- f"Name: {d.name} / "
115
- f"ID: {d.id} / "
116
- f"Created: {datetime.fromtimestamp(d.created / 1000).isoformat()} / "
117
- f"Last seen: {datetime.fromtimestamp(d.lastSeen / 1000).isoformat()}",
118
- )
119
- for d in devices.devices
120
- ]
121
- adhoc_session["has_next"] = False
122
-
123
- return adhoc_session
124
-
125
- async def _handle_add_device1(self, iq: Iq, adhoc_session: dict[str, Any]):
126
- user = user_store.get_by_stanza(iq)
127
- if user is None:
128
- raise XMPPError("subscription-required")
129
-
130
- form = self["xep_0004"].make_form(
131
- "form", "Link a new device to your signal account"
132
- )
133
- form.add_field(
134
- var="uri",
135
- ftype="text-single",
136
- label="Linking URI. Use a QR code reader app to get it from official signal clients.",
137
- required=True,
138
- )
139
-
140
- adhoc_session["payload"] = form
141
- adhoc_session["has_next"] = True
142
- adhoc_session["next"] = self._handle_add_device2
143
-
144
- return adhoc_session
145
-
146
- async def _handle_add_device2(self, stanza: Form, adhoc_session: dict[str, Any]):
147
- user = user_store.get_by_jid(adhoc_session["from"])
148
- if user is None:
149
- raise XMPPError("subscription-required")
150
-
151
- values = stanza.get_values()
152
- uri = values.get("uri")
153
-
154
- await (await self.signal).add_device(
155
- account=user.registration_form["phone"], uri=uri
156
- )
157
-
158
- adhoc_session["notes"] = [
159
- (
160
- "info",
161
- "Your new device is now correctly linked to your signal account",
162
- )
163
- ]
164
- adhoc_session["has_next"] = False
165
-
166
- return adhoc_session
167
-
168
- @staticmethod
169
- async def _chat_command_add_device(
170
- *args, msg: Message, session: Optional["Session"] = None
171
- ):
172
- if session is None:
173
- msg.reply("I don't know you, so don't talk to me").send()
174
- return
175
- if len(args) == 0:
176
- uri = await session.input("URI?")
177
- elif len(args) > 1:
178
- msg.reply("Syntax error! Use 'add_device [LINKING_URI]'").send()
179
- return
180
- else:
181
- uri = args[0]
182
- await session.add_device(uri)
183
-
184
- @staticmethod
185
- async def _chat_command_get_identities(
186
- *args, msg: Message, session: Optional["Session"] = None
187
- ):
188
- if session is None:
189
- msg.reply("I don't know you, so don't talk to me").send()
190
- return
191
- if len(args) == 0:
192
- uuid = await session.input("UUID?")
193
- elif len(args) > 1:
194
- msg.reply("Syntax error! Use 'get_identities [UUID]'").send()
195
- return
196
- else:
197
- uuid = args[0]
198
- await session.contacts.by_legacy_id(uuid).get_identities()
199
-
200
- async def validate(
201
- self, user_jid: JID, registration_form: dict[str, Optional[str]]
202
- ):
203
- phone = registration_form.get("phone")
204
- if not is_valid_phone_number(phone):
205
- raise ValueError("Not a valid phone number")
206
- for u in user_store.get_all():
207
- if u.registration_form.get("phone") == phone:
208
- raise XMPPError(
209
- "not-allowed",
210
- text="Someone is already using this phone number on this server.\n",
211
- )
212
- if registration_form.get("device") == "primary" and not registration_form.get(
213
- "name"
214
- ):
215
- raise ValueError(txt.NAME_REQUIRED)
216
-
217
- async def unregister(self, user: GatewayUser):
218
- try:
219
- await (await self.signal).delete_account(
220
- account=user.registration_form.get("phone"), server=False
221
- )
222
- except sigexc.NoSuchAccountError:
223
- # if user unregisters before completing the registration process,
224
- # NoSuchAccountError is raised by signald
225
- pass
226
-
227
- log.info("Removed user: %s", user)
228
-
229
-
230
- # noinspection PyPep8Naming,GrazieInspection
231
- class Signal(SignaldAPI):
232
- """
233
- Extends :class:`.SignaldAPI` with handlers for events we are interested in.
234
- """
235
-
236
- def __init__(self, xmpp: Gateway):
237
- super().__init__()
238
- self.sessions_by_phone = xmpp.sessions_by_phone
239
-
240
- async def handle_WebSocketConnectionState(
241
- self, state: sigapi.WebSocketConnectionStatev1, payload
242
- ):
243
- """
244
- We should not care much about this since
245
-
246
- :param state:
247
- :param payload:
248
- """
249
- session = self.sessions_by_phone[payload["account"]]
250
- await session.on_websocket_connection_state(state)
251
-
252
- async def handle_ListenerState(self, state: sigapi.ListenerStatev1, payload):
253
- """
254
- Deprecated in signald and replaced by WebSocketConnectionState
255
- Just here to avoid cluttering logs with unhandled events warnings
256
-
257
- :param state:
258
- :param payload:
259
- """
260
- pass
261
-
262
- async def handle_IncomingMessage(self, msg: sigapi.IncomingMessagev1, _payload):
263
- """
264
- Dispatch a signald message to the proper session.
265
-
266
- Can be a lot of other things than an actual message, still need to figure
267
- things out to cover all cases.
268
-
269
- :param msg: the data!
270
- :param _payload:
271
- """
272
- session = self.sessions_by_phone[msg.account]
273
- await session.on_signal_message(msg)
274
-
275
-
276
- def get_parser():
277
- parser = ArgumentParser()
278
- parser.add_argument("--socket", default="/signald/signald.sock")
279
- return parser
280
-
281
-
282
- log = logging.getLogger(__name__)