toDus-API 1.3.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- todus/__init__.py +52 -0
- todus/client/__init__.py +277 -0
- todus/client/auth.py +89 -0
- todus/client/base.py +183 -0
- todus/client/file.py +190 -0
- todus/client/message.py +202 -0
- todus/client/profile.py +56 -0
- todus/constants.py +10 -0
- todus/errors.py +41 -0
- todus/group.py +370 -0
- todus/parser.py +415 -0
- todus/stanza.py +54 -0
- todus/stanzas/__init__.py +1 -0
- todus/stanzas/group.py +175 -0
- todus/stanzas/presence.py +37 -0
- todus/stanzas/private.py +203 -0
- todus/stanzas/utils.py +122 -0
- todus/types.py +54 -0
- todus/util.py +177 -0
- todus_api-1.3.3.dist-info/METADATA +299 -0
- todus_api-1.3.3.dist-info/RECORD +24 -0
- todus_api-1.3.3.dist-info/WHEEL +5 -0
- todus_api-1.3.3.dist-info/licenses/LICENSE +21 -0
- todus_api-1.3.3.dist-info/top_level.txt +1 -0
todus/parser.py
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"""Parser de stanzas XMPP/ToDus."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from . import util
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _attr(stanza: str, name: str) -> str:
|
|
8
|
+
"""Extrae atributo de stanza. Soporta comillas simples y dobles."""
|
|
9
|
+
for quote in ("'", '"'):
|
|
10
|
+
pattern = rf"\b{name}={quote}([^{quote}]*){quote}"
|
|
11
|
+
match = re.search(pattern, stanza)
|
|
12
|
+
if match:
|
|
13
|
+
return match.group(1)
|
|
14
|
+
return ""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_todus_message(stanza: str) -> dict:
|
|
18
|
+
"""Parsea stanza <m> de ToDus."""
|
|
19
|
+
# Extraer tag de apertura <m ...> para leer atributos propios del mensaje
|
|
20
|
+
m_open_match = re.search(r"<m\b[^>]*>", stanza)
|
|
21
|
+
m_tag = m_open_match.group(0) if m_open_match else ""
|
|
22
|
+
|
|
23
|
+
result = {
|
|
24
|
+
"from": _attr(m_tag, "f"),
|
|
25
|
+
"to": _attr(m_tag, "o"),
|
|
26
|
+
"id": _attr(m_tag, "i"),
|
|
27
|
+
"type": _attr(m_tag, "t"),
|
|
28
|
+
"original_id": _attr(m_tag, "mi"),
|
|
29
|
+
"body": "",
|
|
30
|
+
"url": "",
|
|
31
|
+
"file_name": "",
|
|
32
|
+
"file_size": 0,
|
|
33
|
+
"file_id": "",
|
|
34
|
+
"file_hash": "",
|
|
35
|
+
"message_file_id": "",
|
|
36
|
+
"contact_id": "",
|
|
37
|
+
"contact_name": "",
|
|
38
|
+
"contact_phone": "",
|
|
39
|
+
"sticker_id": "",
|
|
40
|
+
"sticker_name": "",
|
|
41
|
+
"sticker_pack": "",
|
|
42
|
+
"sticker_hash": "",
|
|
43
|
+
"video_id": "",
|
|
44
|
+
"video_url": "",
|
|
45
|
+
"video_name": "",
|
|
46
|
+
"video_size": 0,
|
|
47
|
+
"video_duration": 0,
|
|
48
|
+
"video_width": 0,
|
|
49
|
+
"video_height": 0,
|
|
50
|
+
"video_thumbnail": "",
|
|
51
|
+
"image_width": 0,
|
|
52
|
+
"image_height": 0,
|
|
53
|
+
"image_thumbnail": "",
|
|
54
|
+
"has_key": "<k" in stanza,
|
|
55
|
+
"offline_ts": "",
|
|
56
|
+
"edited": "",
|
|
57
|
+
"deleted": "",
|
|
58
|
+
"chat_state": "",
|
|
59
|
+
"receipt": "",
|
|
60
|
+
"receipt_type": "",
|
|
61
|
+
"location_id": "",
|
|
62
|
+
"location_lat": 0.0,
|
|
63
|
+
"location_lon": 0.0,
|
|
64
|
+
"location_zoom": 0.0,
|
|
65
|
+
"location_text": "",
|
|
66
|
+
"event_id": "",
|
|
67
|
+
"event_title": "",
|
|
68
|
+
"event_start": 0,
|
|
69
|
+
"event_end": 0,
|
|
70
|
+
"event_all_day": False,
|
|
71
|
+
"event_ics": "",
|
|
72
|
+
"has_format": False,
|
|
73
|
+
"buttons": [],
|
|
74
|
+
"raw": stanza,
|
|
75
|
+
"is_group": False,
|
|
76
|
+
"group_id": "",
|
|
77
|
+
"sender_phone": "",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Detectar si es mensaje de grupo
|
|
81
|
+
msg_type = _attr(m_tag, "t")
|
|
82
|
+
result["is_group"] = msg_type == "gc"
|
|
83
|
+
|
|
84
|
+
if result["is_group"]:
|
|
85
|
+
# Extraer ID del grupo del JID 'from' o 'to'
|
|
86
|
+
from_jid = result.get("from", "")
|
|
87
|
+
if "@muclight.im.todus.cu" in from_jid:
|
|
88
|
+
parts = from_jid.split("/", 1)
|
|
89
|
+
result["group_id"] = parts[0].split("@")[0] if parts else ""
|
|
90
|
+
if len(parts) > 1:
|
|
91
|
+
result["sender_phone"] = parts[1].split("@")[0]
|
|
92
|
+
|
|
93
|
+
# Body
|
|
94
|
+
match = re.search(r"<b>(.*?)</b>", stanza, re.DOTALL)
|
|
95
|
+
if match:
|
|
96
|
+
result["body"] = util.unescape_xml(match.group(1)).strip()
|
|
97
|
+
if "<linkInfo" in stanza:
|
|
98
|
+
result["has_format"] = True
|
|
99
|
+
|
|
100
|
+
# Botones interactivos
|
|
101
|
+
button_pattern = r"<button\s+btn_t='([^']*)'\s+btn_cmd='([^']*)'\s+btn_msg_c='([^']*)'\s+btn_size='([^']*)'/?>"
|
|
102
|
+
for btn_match in re.finditer(button_pattern, stanza):
|
|
103
|
+
result["buttons"].append({
|
|
104
|
+
"text": util.unescape_xml(btn_match.group(1)),
|
|
105
|
+
"command": btn_match.group(2),
|
|
106
|
+
"data": util.unescape_xml(btn_match.group(3)),
|
|
107
|
+
"size": btn_match.group(4)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
# URL de archivo (formato antiguo <u>)
|
|
111
|
+
match = re.search(r"<u>(.*?)</u>", stanza, re.DOTALL)
|
|
112
|
+
if match:
|
|
113
|
+
result["url"] = util.unescape_xml(match.group(1)).strip()
|
|
114
|
+
|
|
115
|
+
# Archivo adjunto (formato nuevo <file>)
|
|
116
|
+
file_match = re.search(r"<file\b[^>]*>", stanza)
|
|
117
|
+
if file_match:
|
|
118
|
+
file_tag = file_match.group(0)
|
|
119
|
+
result["file_id"] = _attr(file_tag, "i")
|
|
120
|
+
result["message_file_id"] = _attr(file_tag, "mi")
|
|
121
|
+
result["file_name"] = util.unescape_xml(_attr(file_tag, "n"))
|
|
122
|
+
result["url"] = _attr(file_tag, "url")
|
|
123
|
+
try:
|
|
124
|
+
result["file_size"] = int(_attr(file_tag, "s"))
|
|
125
|
+
except ValueError:
|
|
126
|
+
result["file_size"] = 0
|
|
127
|
+
result["file_hash"] = _attr(file_tag, "h")
|
|
128
|
+
|
|
129
|
+
# Imagen adjunta (formato </tr>)
|
|
130
|
+
image_match = re.search(r"<image\b[^>]*>", stanza)
|
|
131
|
+
if image_match:
|
|
132
|
+
image_tag = image_match.group(0)
|
|
133
|
+
result["file_id"] = _attr(image_tag, "i")
|
|
134
|
+
result["message_file_id"] = _attr(image_tag, "mi")
|
|
135
|
+
result["file_name"] = util.unescape_xml(_attr(image_tag, "n")) or "image.jpg"
|
|
136
|
+
result["url"] = _attr(image_tag, "url")
|
|
137
|
+
try:
|
|
138
|
+
result["file_size"] = int(_attr(image_tag, "s"))
|
|
139
|
+
except ValueError:
|
|
140
|
+
result["file_size"] = 0
|
|
141
|
+
result["file_hash"] = _attr(image_tag, "h")
|
|
142
|
+
try:
|
|
143
|
+
result["image_width"] = int(_attr(image_tag, "w"))
|
|
144
|
+
except ValueError:
|
|
145
|
+
result["image_width"] = 0
|
|
146
|
+
try:
|
|
147
|
+
result["image_height"] = int(_attr(image_tag, "he"))
|
|
148
|
+
except ValueError:
|
|
149
|
+
result["image_height"] = 0
|
|
150
|
+
result["image_thumbnail"] = _attr(image_tag, "tnail")
|
|
151
|
+
|
|
152
|
+
# Contacto adjunto (formato <contact>)
|
|
153
|
+
contact_match = re.search(r"<contact\b[^>]*>", stanza)
|
|
154
|
+
if contact_match:
|
|
155
|
+
contact_tag = contact_match.group(0)
|
|
156
|
+
result["contact_id"] = _attr(contact_tag, "i")
|
|
157
|
+
result["contact_name"] = util.unescape_xml(_attr(contact_tag, "n"))
|
|
158
|
+
result["contact_phone"] = _attr(contact_tag, "num")
|
|
159
|
+
result["message_file_id"] = _attr(contact_tag, "mi")
|
|
160
|
+
|
|
161
|
+
# Sticker adjunto (formato <sticker>)
|
|
162
|
+
sticker_match = re.search(r"<sticker\b[^>]*>", stanza)
|
|
163
|
+
if sticker_match:
|
|
164
|
+
sticker_tag = sticker_match.group(0)
|
|
165
|
+
result["sticker_id"] = _attr(sticker_tag, "i")
|
|
166
|
+
result["sticker_name"] = util.unescape_xml(_attr(sticker_tag, "n"))
|
|
167
|
+
result["sticker_pack"] = util.unescape_xml(_attr(sticker_tag, "f"))
|
|
168
|
+
result["sticker_hash"] = _attr(sticker_tag, "h")
|
|
169
|
+
result["message_file_id"] = _attr(sticker_tag, "mi")
|
|
170
|
+
|
|
171
|
+
# Video adjunto (formato <video>)
|
|
172
|
+
video_match = re.search(r"<video\b[^>]*>", stanza)
|
|
173
|
+
if video_match:
|
|
174
|
+
video_tag = video_match.group(0)
|
|
175
|
+
result["video_id"] = _attr(video_tag, "i")
|
|
176
|
+
result["message_file_id"] = _attr(video_tag, "mi")
|
|
177
|
+
result["video_name"] = util.unescape_xml(_attr(video_tag, "n"))
|
|
178
|
+
result["video_url"] = _attr(video_tag, "url")
|
|
179
|
+
try:
|
|
180
|
+
result["video_size"] = int(_attr(video_tag, "s"))
|
|
181
|
+
except ValueError:
|
|
182
|
+
result["video_size"] = 0
|
|
183
|
+
try:
|
|
184
|
+
result["video_duration"] = int(_attr(video_tag, "d"))
|
|
185
|
+
except ValueError:
|
|
186
|
+
result["video_duration"] = 0
|
|
187
|
+
try:
|
|
188
|
+
result["video_width"] = int(_attr(video_tag, "w"))
|
|
189
|
+
except ValueError:
|
|
190
|
+
result["video_width"] = 0
|
|
191
|
+
try:
|
|
192
|
+
result["video_height"] = int(_attr(video_tag, "he"))
|
|
193
|
+
except ValueError:
|
|
194
|
+
result["video_height"] = 0
|
|
195
|
+
result["video_thumbnail"] = _attr(video_tag, "tnail")
|
|
196
|
+
result["file_hash"] = _attr(video_tag, "h")
|
|
197
|
+
|
|
198
|
+
# Offline timestamp
|
|
199
|
+
match = re.search(r"<todus_offline\s+ts='([^']+)'", stanza)
|
|
200
|
+
if not match:
|
|
201
|
+
match = re.search(r'<todus_offline\s+ts="([^"]+)"', stanza)
|
|
202
|
+
if match:
|
|
203
|
+
result["offline_ts"] = match.group(1)
|
|
204
|
+
|
|
205
|
+
# Edited
|
|
206
|
+
edited_match = re.search(r"<edited\b[^>]*>", stanza)
|
|
207
|
+
if edited_match:
|
|
208
|
+
edited_tag = edited_match.group(0)
|
|
209
|
+
result["edited"] = _attr(edited_tag, "i")
|
|
210
|
+
|
|
211
|
+
# Deleted
|
|
212
|
+
deleted_match = re.search(r"<deleted\b[^>]*>", stanza)
|
|
213
|
+
if deleted_match:
|
|
214
|
+
deleted_tag = deleted_match.group(0)
|
|
215
|
+
result["deleted"] = _attr(deleted_tag, "mi") or _attr(deleted_tag, "i")
|
|
216
|
+
|
|
217
|
+
# Ubicación adjunta (location)
|
|
218
|
+
location_match = re.search(r"<location\b[^>]*>", stanza)
|
|
219
|
+
if location_match:
|
|
220
|
+
loc_tag = location_match.group(0)
|
|
221
|
+
result["location_id"] = _attr(loc_tag, "i")
|
|
222
|
+
result["message_file_id"] = _attr(loc_tag, "mi")
|
|
223
|
+
try:
|
|
224
|
+
result["location_lat"] = float(_attr(loc_tag, "lat"))
|
|
225
|
+
except ValueError:
|
|
226
|
+
result["location_lat"] = 0.0
|
|
227
|
+
try:
|
|
228
|
+
result["location_lon"] = float(_attr(loc_tag, "lon"))
|
|
229
|
+
except ValueError:
|
|
230
|
+
result["location_lon"] = 0.0
|
|
231
|
+
try:
|
|
232
|
+
result["location_zoom"] = float(_attr(loc_tag, "z"))
|
|
233
|
+
except ValueError:
|
|
234
|
+
result["location_zoom"] = 0.0
|
|
235
|
+
result["location_text"] = util.unescape_xml(_attr(loc_tag, "t"))
|
|
236
|
+
|
|
237
|
+
# Evento adjunto (event)
|
|
238
|
+
event_match = re.search(r"<event\b[^>]*>", stanza)
|
|
239
|
+
if event_match:
|
|
240
|
+
event_tag = event_match.group(0)
|
|
241
|
+
result["event_id"] = _attr(event_tag, "i")
|
|
242
|
+
result["message_file_id"] = _attr(event_tag, "mi")
|
|
243
|
+
result["event_title"] = util.unescape_xml(_attr(event_tag, "ti"))
|
|
244
|
+
try:
|
|
245
|
+
result["event_start"] = int(_attr(event_tag, "s"))
|
|
246
|
+
except ValueError:
|
|
247
|
+
result["event_start"] = 0
|
|
248
|
+
try:
|
|
249
|
+
result["event_end"] = int(_attr(event_tag, "e"))
|
|
250
|
+
except ValueError:
|
|
251
|
+
result["event_end"] = 0
|
|
252
|
+
result["event_all_day"] = _attr(event_tag, "ad").lower() == "true"
|
|
253
|
+
|
|
254
|
+
ics_match = re.search(r"<ics>(.*?)</ics>", stanza, re.DOTALL)
|
|
255
|
+
if ics_match:
|
|
256
|
+
result["event_ics"] = ics_match.group(1).strip()
|
|
257
|
+
|
|
258
|
+
# Estado de chat (csp/csc)
|
|
259
|
+
if "<csp xmlns='uc1'/>" in stanza:
|
|
260
|
+
result["chat_state"] = "composing"
|
|
261
|
+
elif "<csc xmlns='uc1'/>" in stanza:
|
|
262
|
+
result["chat_state"] = "paused"
|
|
263
|
+
|
|
264
|
+
# Recibos de entrega (dd) o lectura (rd)
|
|
265
|
+
receipt_match = re.search(r"<dd\b[^>]*>", stanza)
|
|
266
|
+
if receipt_match:
|
|
267
|
+
receipt_tag = receipt_match.group(0)
|
|
268
|
+
result["receipt"] = _attr(receipt_tag, "i")
|
|
269
|
+
result["receipt_type"] = "delivered"
|
|
270
|
+
else:
|
|
271
|
+
read_match = re.search(r"<rd\b[^>]*>", stanza)
|
|
272
|
+
if read_match:
|
|
273
|
+
read_tag = read_match.group(0)
|
|
274
|
+
result["receipt"] = _attr(read_tag, "i")
|
|
275
|
+
result["receipt_type"] = "read"
|
|
276
|
+
|
|
277
|
+
return result
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def parse_presence(stanza: str) -> dict:
|
|
281
|
+
"""Parsea stanza <p> de presencia."""
|
|
282
|
+
result = {
|
|
283
|
+
"from": _attr(stanza, "f"),
|
|
284
|
+
"to": _attr(stanza, "o"),
|
|
285
|
+
"id": _attr(stanza, "i"),
|
|
286
|
+
"status": "",
|
|
287
|
+
"show": "",
|
|
288
|
+
"priority": "",
|
|
289
|
+
"raw": stanza,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
match = re.search(r"<status>(.*?)</status>", stanza, re.DOTALL)
|
|
293
|
+
if match:
|
|
294
|
+
result["status"] = util.unescape_xml(match.group(1))
|
|
295
|
+
|
|
296
|
+
match = re.search(r"<show>(.*?)</show>", stanza, re.DOTALL)
|
|
297
|
+
if match:
|
|
298
|
+
result["show"] = match.group(1)
|
|
299
|
+
|
|
300
|
+
match = re.search(r"<priority>(\d+)</priority>", stanza)
|
|
301
|
+
if match:
|
|
302
|
+
result["priority"] = int(match.group(1))
|
|
303
|
+
|
|
304
|
+
return result
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def parse_iq(stanza: str) -> dict:
|
|
308
|
+
"""Parsea stanza IQ."""
|
|
309
|
+
result = {
|
|
310
|
+
"from": _attr(stanza, "f"),
|
|
311
|
+
"to": _attr(stanza, "o"),
|
|
312
|
+
"id": _attr(stanza, "i"),
|
|
313
|
+
"type": _attr(stanza, "t"),
|
|
314
|
+
"error": "",
|
|
315
|
+
"raw": stanza,
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if "<error" in stanza:
|
|
319
|
+
match = re.search(r"<error[^>]*>(.*?)</error>", stanza, re.DOTALL)
|
|
320
|
+
if match:
|
|
321
|
+
result["error"] = match.group(1)
|
|
322
|
+
|
|
323
|
+
# Upload URLs
|
|
324
|
+
if _attr(stanza, "put"):
|
|
325
|
+
result["upload_url"] = _attr(stanza, "put").replace("amp;", "")
|
|
326
|
+
result["download_url"] = _attr(stanza, "get").replace("amp;", "")
|
|
327
|
+
|
|
328
|
+
# Download URL
|
|
329
|
+
if _attr(stanza, "du"):
|
|
330
|
+
result["real_url"] = _attr(stanza, "du").replace("amp;", "")
|
|
331
|
+
|
|
332
|
+
return result
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def parse_tdack(stanza: str) -> dict:
|
|
336
|
+
"""Parsea stanza <tdack> de ToDus (acknowledgment)."""
|
|
337
|
+
return {
|
|
338
|
+
"type": "tdack",
|
|
339
|
+
"message_id": _attr(stanza, "mi"),
|
|
340
|
+
"raw": stanza,
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def extract_all_stanzas(xml: str) -> dict:
|
|
345
|
+
"""Extrae todas las stanzas de un chunk XML."""
|
|
346
|
+
return {
|
|
347
|
+
"messages": re.findall(r"<m\b.*?</m>", xml, re.DOTALL),
|
|
348
|
+
"presences": re.findall(r"<p\b.*?</p>", xml, re.DOTALL),
|
|
349
|
+
"iqs": re.findall(r"<iq\b.*?</iq>", xml, re.DOTALL),
|
|
350
|
+
"tdacks": re.findall(r"<tdack\b[^>]*?/>", xml),
|
|
351
|
+
"streams": re.findall(r"<\?xml[^?]*\?><stream:stream[^>]*/?>", xml),
|
|
352
|
+
"unknown": [],
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class IncrementalParser:
|
|
357
|
+
"""Parser incremental que maneja stanzas fragmentadas por TCP."""
|
|
358
|
+
|
|
359
|
+
def __init__(self):
|
|
360
|
+
self._buffer = ""
|
|
361
|
+
|
|
362
|
+
def feed(self, chunk: str) -> list[dict]:
|
|
363
|
+
"""Alimenta con nuevo chunk y retorna stanzas completas parseadas."""
|
|
364
|
+
if not chunk:
|
|
365
|
+
return []
|
|
366
|
+
|
|
367
|
+
self._buffer += chunk
|
|
368
|
+
stanzas = []
|
|
369
|
+
seen_raw = set()
|
|
370
|
+
|
|
371
|
+
patterns = [
|
|
372
|
+
(r"<m\b.*?</m>", parse_todus_message),
|
|
373
|
+
(r"<p\b.*?</p>", parse_presence),
|
|
374
|
+
(r"<iq\b.*?</iq>", parse_iq),
|
|
375
|
+
(r"<tdack\b[^>]*?/>", parse_tdack),
|
|
376
|
+
]
|
|
377
|
+
|
|
378
|
+
for pattern, parser_fn in patterns:
|
|
379
|
+
for match in re.finditer(pattern, self._buffer, re.DOTALL):
|
|
380
|
+
stanza_str = match.group(0)
|
|
381
|
+
if stanza_str in seen_raw:
|
|
382
|
+
continue
|
|
383
|
+
seen_raw.add(stanza_str)
|
|
384
|
+
try:
|
|
385
|
+
parsed = parser_fn(stanza_str)
|
|
386
|
+
stanzas.append(parsed)
|
|
387
|
+
except Exception:
|
|
388
|
+
pass
|
|
389
|
+
|
|
390
|
+
# Limpiar buffer hasta el final de la última stanza procesada
|
|
391
|
+
if stanzas:
|
|
392
|
+
last_end = 0
|
|
393
|
+
for s in stanzas:
|
|
394
|
+
raw = s.get("raw", "")
|
|
395
|
+
idx = self._buffer.find(raw)
|
|
396
|
+
if idx >= 0:
|
|
397
|
+
end = idx + len(raw)
|
|
398
|
+
if end > last_end:
|
|
399
|
+
last_end = end
|
|
400
|
+
self._buffer = self._buffer[last_end:]
|
|
401
|
+
|
|
402
|
+
# Evitar crecimiento infinito de basura no-XML
|
|
403
|
+
if len(self._buffer) > 20000:
|
|
404
|
+
if "<" not in self._buffer:
|
|
405
|
+
self._buffer = ""
|
|
406
|
+
else:
|
|
407
|
+
last_tag = self._buffer.rfind("<")
|
|
408
|
+
if last_tag > 0:
|
|
409
|
+
self._buffer = self._buffer[last_tag:]
|
|
410
|
+
|
|
411
|
+
return stanzas
|
|
412
|
+
|
|
413
|
+
def reset(self):
|
|
414
|
+
"""Limpia el buffer."""
|
|
415
|
+
self._buffer = ""
|
todus/stanza.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Constructor de stanzas XMPP/ToDus (re-exportación unificada desde subdirectorio)."""
|
|
2
|
+
|
|
3
|
+
from .stanzas.private import (
|
|
4
|
+
_generate_msg_id,
|
|
5
|
+
message,
|
|
6
|
+
edit_message,
|
|
7
|
+
file_message,
|
|
8
|
+
image_message,
|
|
9
|
+
image_message_simple,
|
|
10
|
+
button_message,
|
|
11
|
+
contact_message,
|
|
12
|
+
sticker_message,
|
|
13
|
+
video_message,
|
|
14
|
+
delete_message,
|
|
15
|
+
location_message,
|
|
16
|
+
event_message,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from .stanzas.group import (
|
|
20
|
+
group_message,
|
|
21
|
+
group_file_message,
|
|
22
|
+
group_image_message,
|
|
23
|
+
group_video_message,
|
|
24
|
+
group_sticker_message,
|
|
25
|
+
group_contact_message,
|
|
26
|
+
group_edit_message,
|
|
27
|
+
group_delete_message,
|
|
28
|
+
group_location_message,
|
|
29
|
+
group_event_message,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
from .stanzas.presence import (
|
|
33
|
+
presence,
|
|
34
|
+
muc_presence,
|
|
35
|
+
muc_unavailable,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
from .stanzas.utils import (
|
|
39
|
+
iq,
|
|
40
|
+
ping,
|
|
41
|
+
chat_state,
|
|
42
|
+
receipt,
|
|
43
|
+
read_receipt,
|
|
44
|
+
ack,
|
|
45
|
+
keepalive,
|
|
46
|
+
stream_open,
|
|
47
|
+
stream_restart,
|
|
48
|
+
stream_close,
|
|
49
|
+
sasl_auth,
|
|
50
|
+
bind,
|
|
51
|
+
mam_query,
|
|
52
|
+
upload_query,
|
|
53
|
+
download_query,
|
|
54
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# todus stanzas builders package
|
todus/stanzas/group.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Generadores de stanzas XML para chat de grupos en ToDus."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from .. import util
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _generate_msg_id() -> str:
|
|
8
|
+
"""Genera msg_id en formato hex 32 chars como usa ToDus oficial."""
|
|
9
|
+
return hashlib.md5(util.generate_token(16).encode()).hexdigest()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def group_message(to: str, body: str, msg_id: str = "") -> str:
|
|
13
|
+
"""Mensaje de texto para grupo MUC Light."""
|
|
14
|
+
mid = msg_id or _generate_msg_id()
|
|
15
|
+
body_esc = util.escape_xml(body)
|
|
16
|
+
return (
|
|
17
|
+
f"<m xml:lang='en' o='{to}' t='gc' i='{mid}' xmlns='jc'>"
|
|
18
|
+
f"<k xmlns='x8'/>"
|
|
19
|
+
f"<b>{body_esc}</b>"
|
|
20
|
+
f"</m>"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def group_file_message(to: str, url: str, file_name: str, file_size: int,
|
|
25
|
+
caption: str = "", msg_id: str = "") -> str:
|
|
26
|
+
"""Archivo para grupo MUC Light."""
|
|
27
|
+
mid = msg_id or _generate_msg_id()
|
|
28
|
+
fid = _generate_msg_id()
|
|
29
|
+
name_esc = util.escape_xml(file_name)
|
|
30
|
+
url_esc = util.escape_xml(url)
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
f"<m xml:lang='en' o='{to}' t='gc' i='{mid}' xmlns='jc'>"
|
|
34
|
+
f"<file xmlns='file:n' i='{fid}' mi='{mid}' n='{name_esc}' "
|
|
35
|
+
f"url='{url_esc}' s='{file_size}' h=''/>"
|
|
36
|
+
f"<k xmlns='x8'/>"
|
|
37
|
+
f"<b/>"
|
|
38
|
+
f"</m>"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def group_image_message(to: str, url: str, file_name: str, file_size: int,
|
|
43
|
+
width: int, height: int, thumbnail: str = "",
|
|
44
|
+
caption: str = "", msg_id: str = "") -> str:
|
|
45
|
+
"""Imagen para grupo MUC Light."""
|
|
46
|
+
mid = msg_id or _generate_msg_id()
|
|
47
|
+
fid = _generate_msg_id()
|
|
48
|
+
name_esc = util.escape_xml(file_name)
|
|
49
|
+
url_esc = util.escape_xml(url)
|
|
50
|
+
tnail = thumbnail if thumbnail else "U6688O?Hr=xu^-w2sp-;,^VZnm-;_3xHMyt5"
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
f"<m xml:lang='en' o='{to}' t='gc' i='{mid}' xmlns='jc'>"
|
|
54
|
+
f"<image xmlns='image:n' i='{fid}' mi='{mid}' url='{url_esc}' "
|
|
55
|
+
f"n='{name_esc}' s='{file_size}' h='' w='{width}' he='{height}' "
|
|
56
|
+
f"tnail='{tnail}'/>"
|
|
57
|
+
f"<k xmlns='x8'/>"
|
|
58
|
+
f"<b/>"
|
|
59
|
+
f"</m>"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def group_video_message(to: str, url: str, video_id: str, file_name: str,
|
|
64
|
+
file_size: int, duration: int, width: int, height: int,
|
|
65
|
+
thumbnail: str, caption: str = "", msg_id: str = "") -> str:
|
|
66
|
+
"""Video para grupo MUC Light."""
|
|
67
|
+
mid = msg_id or _generate_msg_id()
|
|
68
|
+
name_esc = util.escape_xml(file_name)
|
|
69
|
+
url_esc = util.escape_xml(url)
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
f"<m xml:lang='en' o='{to}' t='gc' i='{mid}' xmlns='jc'>"
|
|
73
|
+
f"<video xmlns='video:n' i='{video_id}' mi='{mid}' url='{url_esc}' "
|
|
74
|
+
f"s='{file_size}' h='' d='{duration}' n='{name_esc}' "
|
|
75
|
+
f"w='{width}' he='{height}' tnail='{thumbnail}'/>"
|
|
76
|
+
f"<k xmlns='x8'/>"
|
|
77
|
+
f"<b/>"
|
|
78
|
+
f"</m>"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def group_sticker_message(to: str, sticker_id: str, sticker_name: str,
|
|
83
|
+
sticker_pack: str, sticker_hash: str,
|
|
84
|
+
msg_id: str = "") -> str:
|
|
85
|
+
"""Sticker para grupo MUC Light."""
|
|
86
|
+
mid = msg_id or _generate_msg_id()
|
|
87
|
+
name_esc = util.escape_xml(sticker_name)
|
|
88
|
+
pack_esc = util.escape_xml(sticker_pack)
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
f"<m xml:lang='en' o='{to}' t='gc' i='{mid}' xmlns='jc'>"
|
|
92
|
+
f"<sticker xmlns='sticker:n' i='{sticker_id}' mi='{mid}' "
|
|
93
|
+
f"n='{name_esc}' f='{pack_esc}' url='' s='0' h='{sticker_hash}' json=''/>"
|
|
94
|
+
f"<k xmlns='x8'/>"
|
|
95
|
+
f"<b/>"
|
|
96
|
+
f"</m>"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def group_contact_message(to: str, contact_id: str, contact_name: str,
|
|
101
|
+
contact_phone: str, msg_id: str = "") -> str:
|
|
102
|
+
"""Contacto para grupo MUC Light."""
|
|
103
|
+
mid = msg_id or _generate_msg_id()
|
|
104
|
+
name_esc = util.escape_xml(contact_name)
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
f"<m xml:lang='en' o='{to}' t='gc' i='{mid}' xmlns='jc'>"
|
|
108
|
+
f"<contact xmlns='contact:n' i='{contact_id}' mi='{mid}' "
|
|
109
|
+
f"n='{name_esc}' num='{contact_phone}'/>"
|
|
110
|
+
f"<k xmlns='x8'/>"
|
|
111
|
+
f"<b/>"
|
|
112
|
+
f"</m>"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def group_edit_message(to: str, new_body: str, original_msg_id: str,
|
|
117
|
+
edit_id: str = "") -> str:
|
|
118
|
+
"""Editar mensaje en grupo."""
|
|
119
|
+
eid = edit_id or _generate_msg_id()
|
|
120
|
+
body_esc = util.escape_xml(new_body)
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
f"<m xml:lang='en' o='{to}' t='gc' i='{original_msg_id}' xmlns='jc'>"
|
|
124
|
+
f"<edited xmlns='edited:n' i='{eid}' mi='{original_msg_id}'/>"
|
|
125
|
+
f"<k xmlns='x8'/>"
|
|
126
|
+
f"<b>{body_esc}</b>"
|
|
127
|
+
f"</m>"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def group_delete_message(to: str, message_id: str, msg_id: str = "", body: str = "", media_xml: str = "") -> str:
|
|
132
|
+
"""Eliminar mensaje en grupo."""
|
|
133
|
+
mid = msg_id or message_id
|
|
134
|
+
did = _generate_msg_id()
|
|
135
|
+
body_xml = f"<b>{util.escape_xml(body)}</b>" if body or not media_xml else "<b/>"
|
|
136
|
+
return (
|
|
137
|
+
f"<m xml:lang='en' o='{to}' t='gc' i='{mid}' xmlns='jc'>"
|
|
138
|
+
f"{media_xml}"
|
|
139
|
+
f"<deleted xmlns='deleted:n' i='{did}' mi='{message_id}'/>"
|
|
140
|
+
f"<k xmlns='x8'/>"
|
|
141
|
+
f"{body_xml}"
|
|
142
|
+
f"</m>"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def group_location_message(to: str, lat: float, lon: float, zoom: float = 11.0, text: str = "", msg_id: str = "") -> str:
|
|
147
|
+
"""Stanza con ubicación adjunta para grupo MUC Light."""
|
|
148
|
+
mid = msg_id or _generate_msg_id()
|
|
149
|
+
lid = _generate_msg_id()
|
|
150
|
+
text_esc = util.escape_xml(text)
|
|
151
|
+
return (
|
|
152
|
+
f"<m xml:lang='en' o='{to}' t='gc' i='{mid}' xmlns='jc'>"
|
|
153
|
+
f"<location xmlns='location:n' i='{lid}' mi='{mid}' lat='{lat}' lon='{lon}' z='{zoom}' t='{text_esc}'/>"
|
|
154
|
+
f"<k xmlns='x8'/>"
|
|
155
|
+
f"<b/>"
|
|
156
|
+
f"</m>"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def group_event_message(to: str, event_id: str, title: str, start: int, end: int, all_day: bool, ics_data: str, msg_id: str = "") -> str:
|
|
161
|
+
"""Stanza con evento/calendario adjunto para grupo MUC Light."""
|
|
162
|
+
mid = msg_id or _generate_msg_id()
|
|
163
|
+
eid = event_id or _generate_msg_id()
|
|
164
|
+
ad_str = "true" if all_day else "false"
|
|
165
|
+
title_esc = util.escape_xml(title)
|
|
166
|
+
ics_esc = util.escape_xml(ics_data)
|
|
167
|
+
return (
|
|
168
|
+
f"<m xml:lang='en' o='{to}' t='gc' i='{mid}' xmlns='jc'>"
|
|
169
|
+
f"<event xmlns='event:n' i='{eid}' mi='{mid}' ti='{title_esc}' s='{start}' e='{end}' ad='{ad_str}'>"
|
|
170
|
+
f"<ics>{ics_esc}</ics>"
|
|
171
|
+
f"</event>"
|
|
172
|
+
f"<k xmlns='x8'/>"
|
|
173
|
+
f"<b/>"
|
|
174
|
+
f"</m>"
|
|
175
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Generadores de stanzas XML de presencia para ToDus."""
|
|
2
|
+
|
|
3
|
+
from .. import util
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def presence(status: str = "Online", priority: int = 5, show: str = "", caps: bool = True) -> str:
|
|
7
|
+
"""Presencia estándar para chat privado."""
|
|
8
|
+
cap = ""
|
|
9
|
+
if caps:
|
|
10
|
+
cap = (
|
|
11
|
+
"<c ver='foVtX1ZDcopvf5CM63LcnVayPRs=' "
|
|
12
|
+
"node='http://www.process-one.net/en/ejabberd/' "
|
|
13
|
+
"hash='sha-1' xmlns='http://jabber.org/protocol/caps'/>"
|
|
14
|
+
)
|
|
15
|
+
show_tag = f"<show>{show}</show>" if show else ""
|
|
16
|
+
return (
|
|
17
|
+
f"<presence xmlns='jc'>"
|
|
18
|
+
f"<status>{util.escape_xml(status)}</status>"
|
|
19
|
+
f"{show_tag}"
|
|
20
|
+
f"<priority>{priority}</priority>"
|
|
21
|
+
f"{cap}"
|
|
22
|
+
f"</presence>"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def muc_presence(group_jid: str, nickname: str) -> str:
|
|
27
|
+
"""Presencia para unirse a grupo MUC Light."""
|
|
28
|
+
return (
|
|
29
|
+
f"<presence xmlns='jc' to='{group_jid}/{nickname}'>"
|
|
30
|
+
f"<x xmlns='http://jabber.org/protocol/muc'/>"
|
|
31
|
+
f"</presence>"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def muc_unavailable(group_jid: str) -> str:
|
|
36
|
+
"""Presencia para salir de grupo MUC Light."""
|
|
37
|
+
return f"<presence xmlns='jc' to='{group_jid}' type='unavailable'/>"
|