slidge 0.1.0rc1__py3-none-any.whl → 0.1.2__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (164) hide show
  1. slidge/__init__.py +54 -31
  2. slidge/__main__.py +51 -5
  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 +121 -39
  16. slidge/core/config.py +116 -11
  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 +795 -0
  29. slidge/core/gateway/vcard_temp.py +130 -0
  30. slidge/core/mixins/__init__.py +9 -1
  31. slidge/core/mixins/attachment.py +506 -0
  32. slidge/core/mixins/avatar.py +167 -0
  33. slidge/core/mixins/base.py +6 -19
  34. slidge/core/mixins/disco.py +66 -15
  35. slidge/core/mixins/lock.py +31 -0
  36. slidge/core/mixins/message.py +254 -252
  37. slidge/core/mixins/message_maker.py +154 -0
  38. slidge/core/mixins/presence.py +128 -31
  39. slidge/core/mixins/recipient.py +43 -0
  40. slidge/core/pubsub.py +275 -116
  41. slidge/core/session.py +586 -518
  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_0050 → slixfix/link_preview}/__init__.py +4 -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 +1 -2
  54. slidge/slixfix/xep_0077/stanza.py +104 -0
  55. slidge/{util → slixfix}/xep_0100/gateway.py +17 -12
  56. slidge/slixfix/xep_0153/__init__.py +10 -0
  57. slidge/slixfix/xep_0153/stanza.py +25 -0
  58. slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
  59. slidge/slixfix/xep_0264/__init__.py +5 -0
  60. slidge/slixfix/xep_0264/stanza.py +36 -0
  61. slidge/slixfix/xep_0264/thumbnail.py +23 -0
  62. slidge/slixfix/xep_0292/__init__.py +5 -0
  63. slidge/slixfix/xep_0292/vcard4.py +100 -0
  64. slidge/slixfix/xep_0313/__init__.py +12 -0
  65. slidge/slixfix/xep_0313/mam.py +262 -0
  66. slidge/slixfix/xep_0313/stanza.py +359 -0
  67. slidge/slixfix/xep_0317/__init__.py +5 -0
  68. slidge/slixfix/xep_0317/hats.py +17 -0
  69. slidge/slixfix/xep_0317/stanza.py +28 -0
  70. slidge/{util → slixfix}/xep_0356_old/privilege.py +9 -7
  71. slidge/slixfix/xep_0424/__init__.py +9 -0
  72. slidge/slixfix/xep_0424/retraction.py +77 -0
  73. slidge/slixfix/xep_0424/stanza.py +28 -0
  74. slidge/slixfix/xep_0490/__init__.py +8 -0
  75. slidge/slixfix/xep_0490/mds.py +47 -0
  76. slidge/slixfix/xep_0490/stanza.py +17 -0
  77. slidge/util/__init__.py +4 -6
  78. slidge/util/archive_msg.py +61 -0
  79. slidge/util/conf.py +25 -4
  80. slidge/util/db.py +23 -69
  81. slidge/util/schema.sql +126 -0
  82. slidge/util/sql.py +508 -0
  83. slidge/util/test.py +136 -86
  84. slidge/util/types.py +155 -14
  85. slidge/util/util.py +225 -51
  86. slidge-0.1.2.dist-info/METADATA +111 -0
  87. slidge-0.1.2.dist-info/RECORD +96 -0
  88. {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/WHEEL +1 -1
  89. slidge/core/adhoc.py +0 -492
  90. slidge/core/chat_command.py +0 -197
  91. slidge/core/contact.py +0 -441
  92. slidge/core/disco.py +0 -59
  93. slidge/core/gateway.py +0 -899
  94. slidge/core/muc/__init__.py +0 -3
  95. slidge/core/muc/bookmarks.py +0 -74
  96. slidge/core/muc/participant.py +0 -152
  97. slidge/core/muc/room.py +0 -348
  98. slidge/plugins/discord/__init__.py +0 -121
  99. slidge/plugins/discord/client.py +0 -121
  100. slidge/plugins/discord/session.py +0 -172
  101. slidge/plugins/dummy.py +0 -334
  102. slidge/plugins/facebook.py +0 -591
  103. slidge/plugins/hackernews.py +0 -209
  104. slidge/plugins/mattermost/__init__.py +0 -1
  105. slidge/plugins/mattermost/api.py +0 -288
  106. slidge/plugins/mattermost/gateway.py +0 -417
  107. slidge/plugins/mattermost/websocket.py +0 -248
  108. slidge/plugins/signal/__init__.py +0 -4
  109. slidge/plugins/signal/config.py +0 -4
  110. slidge/plugins/signal/contact.py +0 -104
  111. slidge/plugins/signal/gateway.py +0 -379
  112. slidge/plugins/signal/group.py +0 -76
  113. slidge/plugins/signal/session.py +0 -515
  114. slidge/plugins/signal/txt.py +0 -13
  115. slidge/plugins/signal/util.py +0 -32
  116. slidge/plugins/skype.py +0 -310
  117. slidge/plugins/steam.py +0 -400
  118. slidge/plugins/telegram/__init__.py +0 -6
  119. slidge/plugins/telegram/client.py +0 -325
  120. slidge/plugins/telegram/config.py +0 -21
  121. slidge/plugins/telegram/contact.py +0 -154
  122. slidge/plugins/telegram/gateway.py +0 -182
  123. slidge/plugins/telegram/group.py +0 -184
  124. slidge/plugins/telegram/session.py +0 -275
  125. slidge/plugins/telegram/util.py +0 -153
  126. slidge/plugins/whatsapp/__init__.py +0 -6
  127. slidge/plugins/whatsapp/config.py +0 -17
  128. slidge/plugins/whatsapp/contact.py +0 -33
  129. slidge/plugins/whatsapp/event.go +0 -455
  130. slidge/plugins/whatsapp/gateway.go +0 -156
  131. slidge/plugins/whatsapp/gateway.py +0 -69
  132. slidge/plugins/whatsapp/go.mod +0 -17
  133. slidge/plugins/whatsapp/go.sum +0 -22
  134. slidge/plugins/whatsapp/session.go +0 -371
  135. slidge/plugins/whatsapp/session.py +0 -370
  136. slidge/util/xep_0030/__init__.py +0 -13
  137. slidge/util/xep_0030/disco.py +0 -811
  138. slidge/util/xep_0030/stanza/__init__.py +0 -7
  139. slidge/util/xep_0030/stanza/info.py +0 -270
  140. slidge/util/xep_0030/stanza/items.py +0 -147
  141. slidge/util/xep_0030/static.py +0 -467
  142. slidge/util/xep_0050/adhoc.py +0 -631
  143. slidge/util/xep_0050/stanza.py +0 -180
  144. slidge/util/xep_0077/stanza.py +0 -71
  145. slidge/util/xep_0292/__init__.py +0 -1
  146. slidge/util/xep_0292/stanza.py +0 -167
  147. slidge/util/xep_0292/vcard4.py +0 -74
  148. slidge/util/xep_0356/__init__.py +0 -7
  149. slidge/util/xep_0356/permissions.py +0 -35
  150. slidge/util/xep_0356/privilege.py +0 -160
  151. slidge/util/xep_0356/stanza.py +0 -44
  152. slidge/util/xep_0461/__init__.py +0 -6
  153. slidge/util/xep_0461/reply.py +0 -48
  154. slidge/util/xep_0461/stanza.py +0 -80
  155. slidge-0.1.0rc1.dist-info/METADATA +0 -171
  156. slidge-0.1.0rc1.dist-info/RECORD +0 -99
  157. /slidge/{plugins/__init__.py → py.typed} +0 -0
  158. /slidge/{util → slixfix}/xep_0077/__init__.py +0 -0
  159. /slidge/{util → slixfix}/xep_0100/__init__.py +0 -0
  160. /slidge/{util → slixfix}/xep_0100/stanza.py +0 -0
  161. /slidge/{util → slixfix}/xep_0356_old/__init__.py +0 -0
  162. /slidge/{util → slixfix}/xep_0356_old/stanza.py +0 -0
  163. {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/LICENSE +0 -0
  164. {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/entry_points.txt +0 -0
@@ -1,591 +0,0 @@
1
- import asyncio
2
- import io
3
- import json
4
- import logging
5
- import random
6
- import shelve
7
- import zlib
8
- from collections import OrderedDict, defaultdict
9
- from dataclasses import dataclass
10
- from mimetypes import guess_type
11
- from typing import Optional, Union
12
-
13
- import aiohttp
14
- import maufbapi.types.graphql
15
- from maufbapi import AndroidAPI, AndroidMQTT, AndroidState
16
- from maufbapi.mqtt.subscription import RealtimeTopic
17
- from maufbapi.proxy import ProxyHandler
18
- from maufbapi.thrift import ThriftObject
19
- from maufbapi.types import mqtt as mqtt_t
20
- from maufbapi.types.graphql import Participant, ParticipantNode, Thread
21
- from maufbapi.types.graphql.responses import FriendshipStatus
22
- from slixmpp import JID
23
- from slixmpp.exceptions import XMPPError
24
-
25
- from slidge import *
26
- from slidge.core.adhoc import RegistrationType, TwoFactorNotRequired
27
-
28
-
29
- class Config:
30
- CHATS_TO_FETCH = 20
31
- CHATS_TO_FETCH__DOC = (
32
- "The number of most recent chats to fetch on startup. "
33
- "Getting all chats might hit rate limiting and possibly account lock. "
34
- "Please report if you try with high values and don't hit any problem!"
35
- )
36
-
37
-
38
- class Gateway(BaseGateway):
39
- REGISTRATION_INSTRUCTIONS = "Enter facebook credentials"
40
- REGISTRATION_FIELDS = [
41
- FormField(var="email", label="Email", required=True),
42
- FormField(var="password", label="Password", required=True, private=True),
43
- ]
44
- REGISTRATION_MULTISTEP = True
45
- REGISTRATION_TYPE = RegistrationType.TWO_FACTOR_CODE
46
-
47
- ROSTER_GROUP = "Facebook"
48
-
49
- COMPONENT_NAME = "Facebook (slidge)"
50
- COMPONENT_TYPE = "facebook"
51
- COMPONENT_AVATAR = "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6c/Facebook_Messenger_logo_2018.svg/480px-Facebook_Messenger_logo_2018.svg.png"
52
-
53
- SEARCH_TITLE = "Search in your facebook friends"
54
- SEARCH_INSTRUCTIONS = "Enter something that can be used to search for one of your friends, eg, a first name"
55
- SEARCH_FIELDS = [FormField(var="query", label="Term(s)")]
56
-
57
- def __init__(self):
58
- super().__init__()
59
- self._pending_reg = dict[str, AndroidAPI]()
60
-
61
- async def validate(
62
- self, user_jid: JID, registration_form: dict[str, Optional[str]]
63
- ):
64
- s = AndroidState()
65
- x = ProxyHandler(None)
66
- api = AndroidAPI(state=s, proxy_handler=x)
67
- s.generate(random.randbytes(30)) # type: ignore
68
- await api.mobile_config_sessionless()
69
- try:
70
- await api.login(
71
- email=registration_form["email"], password=registration_form["password"]
72
- )
73
- except maufbapi.http.errors.TwoFactorRequired:
74
- self._pending_reg[user_jid.bare] = api
75
- except maufbapi.http.errors.OAuthException as e:
76
- raise XMPPError("not-authorized", text=str(e))
77
- else:
78
- save_state(user_jid.bare, api.state)
79
- raise TwoFactorNotRequired
80
-
81
- async def validate_two_factor_code(self, user: GatewayUser, code):
82
- api = self._pending_reg.pop(user.bare_jid)
83
- try:
84
- await api.login_2fa(email=user.registration_form["email"], code=code)
85
- except maufbapi.http.errors as e:
86
- raise XMPPError("not-authorized", text=str(e))
87
- save_state(user.bare_jid, api.state)
88
-
89
-
90
- def get_shelf_path(user_bare_jid):
91
- return str(global_config.HOME_DIR / user_bare_jid)
92
-
93
-
94
- def save_state(user_bare_jid: str, state: AndroidState):
95
- shelf_path = get_shelf_path(user_bare_jid)
96
- with shelve.open(shelf_path) as shelf:
97
- shelf["state"] = state
98
-
99
-
100
- class Contact(LegacyContact["Session", str]):
101
- # legacy_id = facebook username, as in facebook.com/name.surname123
102
- REACTIONS_SINGLE_EMOJI = True
103
-
104
- def __init__(self, *a, **k):
105
- super(Contact, self).__init__(*a, **k)
106
- self._fb_id: Optional[int] = None
107
-
108
- async def fb_id(self):
109
- if self._fb_id is None:
110
- results = await self.session.api.search(
111
- self.legacy_id, entity_types=["user"]
112
- )
113
- for search_result in results.search_results.edges:
114
- result = search_result.node
115
- if (
116
- isinstance(result, Participant)
117
- and result.username == self.legacy_id
118
- ):
119
- self._fb_id = int(result.id)
120
- break
121
- else:
122
- raise XMPPError(
123
- "not-found", text=f"Cannot find the facebook ID of {self.legacy_id}"
124
- )
125
- self.session.contacts.by_fb_id_dict[self._fb_id] = self
126
- return self._fb_id
127
-
128
- async def populate_from_participant(
129
- self, participant: ParticipantNode, update_avatar=True
130
- ):
131
- if self.legacy_id != participant.messaging_actor.username:
132
- raise RuntimeError(
133
- "Attempted to populate a contact with a non-corresponding participant"
134
- )
135
- self.name = participant.messaging_actor.name
136
- self._fb_id = int(participant.id)
137
- if self.avatar is None or update_avatar:
138
- self.avatar = participant.messaging_actor.profile_pic_large.uri
139
-
140
-
141
- class Roster(LegacyRoster["Session", Contact, str]):
142
- def __init__(self, *a, **k):
143
- super().__init__(*a, **k)
144
- self.by_fb_id_dict: dict[int, Contact] = {}
145
-
146
- async def by_fb_id(self, fb_id: int) -> "Contact":
147
- contact = self.by_fb_id_dict.get(fb_id)
148
- if contact is None:
149
- thread = (await self.session.api.fetch_thread_info(fb_id))[0]
150
- return await self.by_thread(thread)
151
- return contact
152
-
153
- async def by_thread_key(self, t: mqtt_t.ThreadKey):
154
- if is_group_thread(t):
155
- raise ValueError("Thread seems to be a group thread")
156
- return await self.by_fb_id(t.other_user_id)
157
-
158
- async def by_thread(self, t: Thread):
159
- if t.is_group_thread:
160
- raise RuntimeError("Tried to populate a user from a group chat")
161
-
162
- if len(t.all_participants.nodes) != 2:
163
- raise RuntimeError(
164
- "Tried is not a group chat but doesn't have 2 participants ‽"
165
- )
166
-
167
- for participant in t.all_participants.nodes:
168
- if participant.id != self.session.me.id:
169
- break
170
- else:
171
- raise RuntimeError(
172
- "Couldn't find friend in thread participants", t.all_participants
173
- )
174
-
175
- contact = await self.by_legacy_id(participant.messaging_actor.username)
176
- await contact.populate_from_participant(participant)
177
- self.by_fb_id_dict[int(participant.id)] = contact
178
- return contact
179
-
180
-
181
- class Session(
182
- BaseSession[
183
- Gateway, str, Roster, Contact, LegacyBookmarks, LegacyMUC, LegacyParticipant
184
- ]
185
- ):
186
- mqtt: AndroidMQTT
187
- api: AndroidAPI
188
- me: maufbapi.types.graphql.OwnInfo
189
-
190
- def __init__(self, user):
191
- super().__init__(user)
192
-
193
- # keys = "offline thread ID"
194
- self.ack_futures = dict[int, asyncio.Future[FacebookMessage]]()
195
-
196
- # keys = "facebook message id"
197
- self.reaction_futures = dict[str, asyncio.Future]()
198
- self.unsend_futures = dict[str, asyncio.Future]()
199
-
200
- # keys = "contact ID"
201
- self.sent_messages = defaultdict[int, Messages](Messages)
202
- self.received_messages = defaultdict[int, Messages](Messages)
203
-
204
- async def login(self):
205
- shelf: shelve.Shelf[AndroidState]
206
- with shelve.open(get_shelf_path(self.user.bare_jid)) as shelf:
207
- s = shelf["state"]
208
- x = ProxyHandler(None)
209
- self.api = AndroidAPI(state=s, proxy_handler=x)
210
- self.mqtt = AndroidMQTT(self.api.state, proxy_handler=self.api.proxy_handler)
211
- self.me = await self.api.get_self()
212
- self.me.id = int(self.me.id) # bug in maufbapi?
213
- await self.add_friends()
214
- self.mqtt.seq_id_update_callback = lambda i: setattr(self.mqtt, "seq_id", i)
215
- self.mqtt.add_event_handler(mqtt_t.Message, self.on_fb_message)
216
- self.mqtt.add_event_handler(mqtt_t.ExtendedMessage, self.on_fb_message)
217
- self.mqtt.add_event_handler(mqtt_t.ReadReceipt, self.on_fb_message_read)
218
- self.mqtt.add_event_handler(mqtt_t.TypingNotification, self.on_fb_typing)
219
- self.mqtt.add_event_handler(mqtt_t.OwnReadReceipt, self.on_fb_user_read)
220
- self.mqtt.add_event_handler(mqtt_t.Reaction, self.on_fb_reaction)
221
- self.mqtt.add_event_handler(mqtt_t.UnsendMessage, self.on_fb_unsend)
222
-
223
- self.mqtt.add_event_handler(mqtt_t.NameChange, self.on_fb_event)
224
- self.mqtt.add_event_handler(mqtt_t.AvatarChange, self.on_fb_event)
225
- self.mqtt.add_event_handler(mqtt_t.Presence, self.on_fb_event)
226
- self.mqtt.add_event_handler(mqtt_t.AddMember, self.on_fb_event)
227
- self.mqtt.add_event_handler(mqtt_t.RemoveMember, self.on_fb_event)
228
- self.mqtt.add_event_handler(mqtt_t.ThreadChange, self.on_fb_event)
229
- self.mqtt.add_event_handler(mqtt_t.MessageSyncError, self.on_fb_event)
230
- self.mqtt.add_event_handler(mqtt_t.ForcedFetch, self.on_fb_event)
231
- # self.mqtt.add_event_handler(Connect, self.on_connect)
232
- # self.mqtt.add_event_handler(Disconnect, self.on_disconnect)
233
- self.xmpp.loop.create_task(self.mqtt.listen(self.mqtt.seq_id))
234
- return f"Connected as '{self.me.name} <{self.me.email}>'"
235
-
236
- async def add_friends(self):
237
- thread_list = await self.api.fetch_thread_list(
238
- msg_count=0, thread_count=Config.CHATS_TO_FETCH
239
- )
240
- self.mqtt.seq_id = int(thread_list.sync_sequence_id)
241
- self.log.debug("SEQ ID: %s", self.mqtt.seq_id)
242
- self.log.debug("Thread list: %s", thread_list)
243
- self.log.debug("Thread list page info: %s", thread_list.page_info)
244
- for t in thread_list.nodes:
245
- if t.is_group_thread:
246
- log.debug("Skipping group: %s", t)
247
- continue
248
- c = await self.contacts.by_thread(t)
249
- await c.add_to_roster()
250
- c.online()
251
-
252
- async def logout(self):
253
- pass
254
-
255
- async def send_text(
256
- self, text: str, chat: Contact, *, reply_to_msg_id=None, **kwargs
257
- ) -> str:
258
- resp: mqtt_t.SendMessageResponse = await self.mqtt.send_message(
259
- target=(fb_id := await chat.fb_id()),
260
- message=text,
261
- is_group=False,
262
- reply_to=reply_to_msg_id,
263
- )
264
- fut = self.ack_futures[
265
- resp.offline_threading_id
266
- ] = self.xmpp.loop.create_future()
267
- log.debug("Send message response: %s", resp)
268
- if not resp.success:
269
- raise XMPPError(resp.error_message)
270
- fb_msg = await fut
271
- self.sent_messages[fb_id].add(fb_msg)
272
- return fb_msg.mid
273
-
274
- async def send_file(self, url: str, chat: Contact, reply_to_msg_id=None, **kwargs):
275
- async with aiohttp.ClientSession() as s:
276
- async with s.get(url) as r:
277
- data = await r.read()
278
- oti = self.mqtt.generate_offline_threading_id()
279
- fut = self.ack_futures[oti] = self.xmpp.loop.create_future()
280
- resp = await self.api.send_media(
281
- data=data,
282
- file_name=url.split("/")[-1],
283
- mimetype=guess_type(url)[0] or "application/octet-stream",
284
- offline_threading_id=oti,
285
- chat_id=await chat.fb_id(),
286
- is_group=False,
287
- reply_to=reply_to_msg_id,
288
- )
289
- ack = await fut
290
- log.debug("Upload ack: %s", ack)
291
- return resp.media_id
292
-
293
- async def active(self, c: Contact):
294
- pass
295
-
296
- async def inactive(self, c: Contact):
297
- pass
298
-
299
- async def composing(self, c: Contact):
300
- await self.mqtt.set_typing(target=await c.fb_id())
301
-
302
- async def paused(self, c: Contact):
303
- await self.mqtt.set_typing(target=await c.fb_id(), typing=False)
304
-
305
- async def displayed(self, legacy_msg_id: str, c: Contact):
306
- fb_id = await c.fb_id()
307
- try:
308
- t = self.received_messages[fb_id].by_mid[legacy_msg_id].timestamp_ms
309
- except KeyError:
310
- log.debug("Cannot find the timestamp of %s", legacy_msg_id)
311
- else:
312
- await self.mqtt.mark_read(target=fb_id, read_to=t, is_group=False)
313
-
314
- async def on_fb_message(self, evt: Union[mqtt_t.Message, mqtt_t.ExtendedMessage]):
315
- if isinstance(evt, mqtt_t.ExtendedMessage):
316
- reply_to = evt.reply_to_message.metadata.id
317
- msg = evt.message
318
- else:
319
- reply_to = None
320
- msg = evt
321
- meta = msg.metadata
322
- if is_group_thread(thread_key := meta.thread):
323
- return
324
- contact = await self.contacts.by_thread_key(thread_key)
325
-
326
- if not contact.added_to_roster:
327
- await contact.add_to_roster()
328
-
329
- log.debug("Facebook message: %s", evt)
330
- fb_msg = FacebookMessage(mid=meta.id, timestamp_ms=meta.timestamp)
331
- if meta.sender == self.me.id:
332
- try:
333
- fut = self.ack_futures.pop(meta.offline_threading_id)
334
- except KeyError:
335
- log.debug("Received carbon %s - %s", meta.id, msg.text)
336
- contact.send_text(body=msg.text, legacy_id=meta.id, carbon=True)
337
- log.debug("Sent carbon")
338
- self.sent_messages[thread_key.other_user_id].add(fb_msg)
339
- else:
340
- log.debug("Received echo of %s", meta.offline_threading_id)
341
- fut.set_result(fb_msg)
342
- else:
343
- self.received_messages[thread_key.other_user_id].add(fb_msg)
344
-
345
- text = msg.text
346
- msg_id = meta.id
347
- if not (attachments := msg.attachments):
348
- if text:
349
- contact.send_text(
350
- text, legacy_msg_id=msg_id, reply_to_msg_id=reply_to
351
- )
352
- return
353
-
354
- last_attachment_i = len(attachments) - 1
355
- async with aiohttp.ClientSession() as c:
356
- for i, a in enumerate(attachments):
357
- last = i == last_attachment_i
358
- try:
359
- url = (
360
- ((v := a.video_info) and v.download_url)
361
- or ((au := a.audio_info) and au.url)
362
- or a.image_info.uri_map.get(0)
363
- )
364
- except AttributeError:
365
- log.warning("Unhandled attachment: %s", a)
366
- contact.send_text(
367
- "/me sent an attachment that slidge does not support"
368
- )
369
- continue
370
- if url is None:
371
- if last:
372
- contact.send_text(
373
- text, legacy_msg_id=msg_id, reply_to_msg_id=reply_to
374
- )
375
- continue
376
- async with c.get(url) as r:
377
- await contact.send_file(
378
- filename=a.file_name,
379
- content_type=a.mime_type,
380
- input_file=io.BytesIO(await r.read()),
381
- caption=text if last else None,
382
- legacy_msg_id=msg_id if last else None,
383
- reply_to_msg_id=reply_to if last else None,
384
- )
385
-
386
- async def on_fb_message_read(self, receipt: mqtt_t.ReadReceipt):
387
- log.debug("Facebook read: %s", receipt)
388
- try:
389
- mid = self.sent_messages[receipt.user_id].pop_up_to(receipt.read_to).mid
390
- except KeyError:
391
- log.debug("Cannot find MID of %s", receipt.read_to)
392
- else:
393
- contact = await self.contacts.by_thread_key(receipt.thread)
394
- contact.displayed(mid)
395
-
396
- async def on_fb_typing(self, notification: mqtt_t.TypingNotification):
397
- log.debug("Facebook typing: %s", notification)
398
- c = await self.contacts.by_fb_id(notification.user_id)
399
- if notification.typing_status:
400
- c.composing()
401
- else:
402
- c.paused()
403
-
404
- async def on_fb_user_read(self, receipt: mqtt_t.OwnReadReceipt):
405
- log.debug("Facebook own read: %s", receipt)
406
- when = receipt.read_to
407
- for thread in receipt.threads:
408
- c = await self.contacts.by_fb_id(thread.other_user_id)
409
- try:
410
- mid = self.received_messages[await c.fb_id()].pop_up_to(when).mid
411
- except KeyError:
412
- log.debug("Cannot find mid of %s", when)
413
- continue
414
- c.displayed(mid, carbon=True)
415
-
416
- async def on_fb_reaction(self, reaction: mqtt_t.Reaction):
417
- self.log.debug("Reaction: %s", reaction)
418
- if is_group_thread(tk := reaction.thread):
419
- return
420
- contact = await self.contacts.by_thread_key(tk)
421
- mid = reaction.message_id
422
- if reaction.reaction_sender_id == self.me.id:
423
- try:
424
- f = self.reaction_futures.pop(mid)
425
- except KeyError:
426
- contact.react(mid, reaction.reaction or "", carbon=True)
427
- else:
428
- f.set_result(None)
429
- else:
430
- contact.react(reaction.message_id, reaction.reaction or "")
431
-
432
- async def on_fb_unsend(self, unsend: mqtt_t.UnsendMessage):
433
- self.log.debug("Unsend: %s", unsend)
434
- if is_group_thread(tk := unsend.thread):
435
- return
436
- contact = await self.contacts.by_thread_key(tk)
437
- mid = unsend.message_id
438
- if unsend.user_id == self.me.id:
439
- try:
440
- f = self.unsend_futures.pop(mid)
441
- except KeyError:
442
- contact.retract(mid, carbon=True)
443
- else:
444
- f.set_result(None)
445
- else:
446
- contact.retract(unsend.message_id)
447
-
448
- async def correct(self, text: str, legacy_msg_id: str, c: Contact):
449
- await self.api.unsend(legacy_msg_id)
450
- return await self.send_text(text, c)
451
-
452
- async def react(self, legacy_msg_id: str, emojis: list[str], c: Contact):
453
- # only reaction per msg on facebook, but this is handled by slidge core
454
- if len(emojis) == 0:
455
- emoji = None
456
- else:
457
- emoji = emojis[-1]
458
- f = self.reaction_futures[legacy_msg_id] = self.xmpp.loop.create_future()
459
- await self.api.react(legacy_msg_id, emoji)
460
- await f
461
-
462
- async def retract(self, legacy_msg_id: str, c: Contact):
463
- f = self.unsend_futures[legacy_msg_id] = self.xmpp.loop.create_future()
464
- await self.api.unsend(legacy_msg_id)
465
- await f
466
-
467
- async def search(self, form_values: dict[str, str]) -> SearchResult:
468
- results = await self.api.search(form_values["query"], entity_types=["user"])
469
- log.debug("Search results: %s", results)
470
- items = []
471
- for search_result in results.search_results.edges:
472
- result = search_result.node
473
- if isinstance(result, Participant):
474
- is_friend = (
475
- friend := result.friendship_status
476
- ) is not None and friend == FriendshipStatus.ARE_FRIENDS
477
- items.append(
478
- {
479
- "name": result.name + " (friend)"
480
- if is_friend
481
- else " (not friend)",
482
- "jid": f"{result.username}@{self.xmpp.boundjid.bare}",
483
- }
484
- )
485
-
486
- return SearchResult(
487
- fields=[
488
- FormField(var="name", label="Name"),
489
- FormField(var="jid", label="JID", type="jid-single"),
490
- ],
491
- items=items,
492
- )
493
-
494
- @staticmethod
495
- async def on_fb_event(evt):
496
- log.debug("Facebook event: %s", evt)
497
-
498
-
499
- @dataclass
500
- class FacebookMessage:
501
- mid: str
502
- timestamp_ms: int
503
-
504
-
505
- class Messages:
506
- def __init__(self):
507
- self.by_mid: OrderedDict[str, FacebookMessage] = OrderedDict()
508
- self.by_timestamp_ms: OrderedDict[int, FacebookMessage] = OrderedDict()
509
-
510
- def __len__(self):
511
- return len(self.by_mid)
512
-
513
- def add(self, m: FacebookMessage):
514
- self.by_mid[m.mid] = m
515
- self.by_timestamp_ms[m.timestamp_ms] = m
516
-
517
- def pop_up_to(self, approx_t: int) -> FacebookMessage:
518
- i = 0
519
- for i, t in enumerate(self.by_timestamp_ms.keys()):
520
- if t > approx_t:
521
- i -= 1
522
- break
523
- for j, t in enumerate(list(self.by_timestamp_ms.keys())):
524
- msg = self.by_timestamp_ms.pop(t)
525
- self.by_mid.pop(msg.mid)
526
- if j == i:
527
- return msg
528
- else:
529
- raise KeyError(approx_t)
530
-
531
-
532
- def is_group_thread(t: mqtt_t.ThreadKey):
533
- return t.other_user_id is None and t.thread_fbid is not None
534
-
535
-
536
- # Monkeypatch
537
- # TODO: remove me when https://github.com/mautrix/facebook/pull/270 is merged
538
- # and a new maufbapi is released
539
-
540
-
541
- REQUEST_TIMEOUT = 60
542
-
543
-
544
- def publish(
545
- self,
546
- topic,
547
- payload,
548
- prefix: bytes = b"",
549
- compress: bool = True,
550
- ) -> asyncio.Future:
551
- if isinstance(payload, dict):
552
- payload = json.dumps(payload)
553
- if isinstance(payload, str):
554
- payload = payload.encode("utf-8")
555
- if isinstance(payload, ThriftObject):
556
- payload = payload.to_thrift()
557
- if compress:
558
- payload = zlib.compress(prefix + payload, level=9)
559
- elif prefix:
560
- payload = prefix + payload
561
- info = self._client.publish(
562
- topic.encoded if isinstance(topic, RealtimeTopic) else topic, payload, qos=1
563
- )
564
- fut = self._loop.create_future()
565
- timeout_handle = self._loop.call_later(REQUEST_TIMEOUT, self._cancel_later, fut)
566
- fut.add_done_callback(lambda _: timeout_handle.cancel())
567
- self._publish_waiters[info.mid] = fut
568
- return fut
569
-
570
-
571
- async def request(
572
- self,
573
- topic: RealtimeTopic,
574
- response: RealtimeTopic,
575
- payload,
576
- prefix: bytes = b"",
577
- ):
578
- async with self._response_waiter_locks[response]:
579
- fut = self._loop.create_future()
580
- self._response_waiters[response] = fut
581
- await self.publish(topic, payload, prefix)
582
- timeout_handle = self._loop.call_later(REQUEST_TIMEOUT, self._cancel_later, fut)
583
- fut.add_done_callback(lambda _: timeout_handle.cancel())
584
- return await fut
585
-
586
-
587
- AndroidMQTT.publish = publish
588
- AndroidMQTT.request = request
589
-
590
-
591
- log = logging.getLogger(__name__)