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/client/file.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import time
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Callable
|
|
6
|
+
import requests
|
|
7
|
+
from .. import util, stanza
|
|
8
|
+
from ..types import FileType
|
|
9
|
+
from ..errors import ConnectionLostError, TokenExpiredError, UploadError
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("todus")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _ProgressReader:
|
|
15
|
+
def __init__(self, data: bytes, progress_callback: Callable[[int, int], None]) -> None:
|
|
16
|
+
self.data = data
|
|
17
|
+
self.total = len(data)
|
|
18
|
+
self.offset = 0
|
|
19
|
+
self.progress_callback = progress_callback
|
|
20
|
+
|
|
21
|
+
def read(self, size: int = -1) -> bytes:
|
|
22
|
+
if self.offset >= self.total:
|
|
23
|
+
return b""
|
|
24
|
+
|
|
25
|
+
if size is None or size < 0:
|
|
26
|
+
chunk = self.data[self.offset:]
|
|
27
|
+
self.offset = self.total
|
|
28
|
+
else:
|
|
29
|
+
end = min(self.offset + size, self.total)
|
|
30
|
+
chunk = self.data[self.offset:end]
|
|
31
|
+
self.offset = end
|
|
32
|
+
|
|
33
|
+
if chunk and self.progress_callback:
|
|
34
|
+
self.progress_callback(self.offset, self.total)
|
|
35
|
+
|
|
36
|
+
return chunk
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ToDusFileMixin:
|
|
40
|
+
"""Mixin que contiene los métodos de subida y descarga de archivos de ToDus."""
|
|
41
|
+
|
|
42
|
+
# --- Archivos ---
|
|
43
|
+
|
|
44
|
+
def reserve_upload_url(self, token: str, size: int, file_type: FileType, file_name: str = "") -> tuple[str, str]:
|
|
45
|
+
phone, authstr = self._authstr_from_token(token)
|
|
46
|
+
sid = util.generate_token(5)
|
|
47
|
+
up_url = down_url = ""
|
|
48
|
+
|
|
49
|
+
# Sanitizar nombre del archivo
|
|
50
|
+
sanitized_name = util.sanitize_filename(file_name, int(file_type))
|
|
51
|
+
|
|
52
|
+
with self._xmpp_session(token) as sock:
|
|
53
|
+
sock.send(stanza.upload_query(sid, size, int(file_type), file_name=sanitized_name).encode())
|
|
54
|
+
while True:
|
|
55
|
+
response = self._recv_all(sock)
|
|
56
|
+
if response is None:
|
|
57
|
+
raise ConnectionLostError()
|
|
58
|
+
if response == "":
|
|
59
|
+
continue
|
|
60
|
+
if "i='" + sid + "-3'" in response and "put='" in response:
|
|
61
|
+
put_match = re.search(r"put=['\"]([^'\"]+)['\"]", response)
|
|
62
|
+
get_match = re.search(r"get=['\"]([^'\"]+)['\"]", response)
|
|
63
|
+
if put_match and get_match:
|
|
64
|
+
up_url = put_match.group(1).replace("amp;", "")
|
|
65
|
+
down_url = get_match.group(1)
|
|
66
|
+
break
|
|
67
|
+
if "<not-authorized/>" in response:
|
|
68
|
+
raise TokenExpiredError()
|
|
69
|
+
|
|
70
|
+
return up_url, down_url
|
|
71
|
+
|
|
72
|
+
def get_real_download_url(self, token: str, url: str) -> str:
|
|
73
|
+
_, authstr = self._authstr_from_token(token)
|
|
74
|
+
sid = util.generate_token(5)
|
|
75
|
+
|
|
76
|
+
with self._xmpp_session(token) as sock:
|
|
77
|
+
sock.send(stanza.download_query(sid, url).encode())
|
|
78
|
+
while True:
|
|
79
|
+
response = self._recv_all(sock)
|
|
80
|
+
if response is None:
|
|
81
|
+
raise ConnectionLostError()
|
|
82
|
+
if response == "":
|
|
83
|
+
continue
|
|
84
|
+
if "i='" + sid + "-2'" in response and "du='" in response:
|
|
85
|
+
match = re.match(".*du='(.*)' stat.*", response)
|
|
86
|
+
if match:
|
|
87
|
+
return match.group(1).replace("amp;", "")
|
|
88
|
+
break
|
|
89
|
+
if "<not-authorized/>" in response:
|
|
90
|
+
raise TokenExpiredError()
|
|
91
|
+
|
|
92
|
+
return ""
|
|
93
|
+
|
|
94
|
+
def upload_file(self, token: str, data: bytes, file_type: FileType = FileType.FILE, progress_callback: Callable[[int, int], None] = None, file_name: str = "") -> str:
|
|
95
|
+
up_url, down_url = self.reserve_upload_url(token, len(data), file_type, file_name=file_name)
|
|
96
|
+
upload_data = _ProgressReader(data, progress_callback) if progress_callback else data
|
|
97
|
+
resp = requests.put(
|
|
98
|
+
up_url,
|
|
99
|
+
data=upload_data,
|
|
100
|
+
headers={"Content-Length": str(len(data))},
|
|
101
|
+
timeout=60,
|
|
102
|
+
)
|
|
103
|
+
resp.raise_for_status()
|
|
104
|
+
if progress_callback:
|
|
105
|
+
progress_callback(len(data), len(data))
|
|
106
|
+
return down_url
|
|
107
|
+
|
|
108
|
+
def download_file(self, token: str, url: str, path: str) -> int:
|
|
109
|
+
real_url = self.get_real_download_url(token, url)
|
|
110
|
+
headers = {
|
|
111
|
+
"User-Agent": "ToDus " + self.version_name + " HTTP-Download",
|
|
112
|
+
"Authorization": "Bearer " + token,
|
|
113
|
+
}
|
|
114
|
+
temp_path = path + ".part"
|
|
115
|
+
size = -1
|
|
116
|
+
with open(temp_path, "ab") as f:
|
|
117
|
+
pos = f.tell()
|
|
118
|
+
while pos < size or size == -1:
|
|
119
|
+
if pos:
|
|
120
|
+
headers["Range"] = "bytes=" + str(pos) + "-"
|
|
121
|
+
try:
|
|
122
|
+
with self.session.get(real_url, headers=headers, stream=True, timeout=60) as resp:
|
|
123
|
+
resp.raise_for_status()
|
|
124
|
+
size = pos + int(resp.headers.get("Content-Length", 0))
|
|
125
|
+
for chunk in resp.iter_content(chunk_size=8192):
|
|
126
|
+
f.write(chunk)
|
|
127
|
+
except Exception:
|
|
128
|
+
time.sleep(5)
|
|
129
|
+
pos = f.tell()
|
|
130
|
+
os.rename(temp_path, path)
|
|
131
|
+
return size
|
|
132
|
+
|
|
133
|
+
def download_file_to_folder(self, token: str, url: str, folder: str, filename: str = "") -> tuple[int, str]:
|
|
134
|
+
headers = {
|
|
135
|
+
"User-Agent": "ToDus " + self.version_name + " HTTP-Download",
|
|
136
|
+
"Authorization": "Bearer " + token,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
os.makedirs(folder, exist_ok=True)
|
|
140
|
+
|
|
141
|
+
if not filename:
|
|
142
|
+
filename = os.path.basename(url.split("?")[0]) or "download"
|
|
143
|
+
|
|
144
|
+
final_path = os.path.join(folder, filename)
|
|
145
|
+
temp_path = final_path + ".part"
|
|
146
|
+
|
|
147
|
+
if os.path.exists(temp_path):
|
|
148
|
+
os.remove(temp_path)
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
test_resp = self.session.head(url, headers=headers, timeout=15, allow_redirects=True)
|
|
152
|
+
if test_resp.status_code in (200, 206, 401, 403, 302, 301):
|
|
153
|
+
real_url = url
|
|
154
|
+
else:
|
|
155
|
+
real_url = self.get_real_download_url(token, url)
|
|
156
|
+
if not real_url:
|
|
157
|
+
raise UploadError("No se pudo resolver URL de descarga")
|
|
158
|
+
except Exception:
|
|
159
|
+
real_url = self.get_real_download_url(token, url)
|
|
160
|
+
if not real_url:
|
|
161
|
+
raise UploadError("No se pudo obtener URL de descarga")
|
|
162
|
+
|
|
163
|
+
size = -1
|
|
164
|
+
downloaded = 0
|
|
165
|
+
start_time = time.time()
|
|
166
|
+
last_progress = 0
|
|
167
|
+
|
|
168
|
+
with open(temp_path, "wb") as f:
|
|
169
|
+
with self.session.get(real_url, headers=headers, stream=True, timeout=300) as resp:
|
|
170
|
+
if resp.status_code not in (200, 206):
|
|
171
|
+
raise UploadError(f"HTTP {resp.status_code}: {resp.text[:100]}")
|
|
172
|
+
|
|
173
|
+
size = int(resp.headers.get("Content-Length", 0))
|
|
174
|
+
|
|
175
|
+
for chunk in resp.iter_content(chunk_size=8192):
|
|
176
|
+
if chunk:
|
|
177
|
+
f.write(chunk)
|
|
178
|
+
downloaded += len(chunk)
|
|
179
|
+
if downloaded - last_progress >= (500 * 1024):
|
|
180
|
+
elapsed = time.time() - start_time
|
|
181
|
+
speed = downloaded / elapsed if elapsed > 0 else 0
|
|
182
|
+
logger.info("Descargando %s / %s @ %s/s",
|
|
183
|
+
util.format_size(downloaded),
|
|
184
|
+
util.format_size(size),
|
|
185
|
+
util.format_size(int(speed)))
|
|
186
|
+
last_progress = downloaded
|
|
187
|
+
|
|
188
|
+
os.rename(temp_path, final_path)
|
|
189
|
+
logger.info("Descarga completa: %s", util.format_size(downloaded))
|
|
190
|
+
return downloaded, final_path
|
todus/client/message.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
import socket
|
|
5
|
+
import hashlib
|
|
6
|
+
from typing import Callable
|
|
7
|
+
from .. import util, stanza, constants
|
|
8
|
+
from ..errors import TokenExpiredError, ConnectionLostError
|
|
9
|
+
from ..types import FileType
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("todus")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ToDusMessageMixin:
|
|
15
|
+
"""Mixin que contiene la lógica de mensajería (envío y recepción) de ToDus."""
|
|
16
|
+
|
|
17
|
+
# --- Mensajeria Privada ---
|
|
18
|
+
|
|
19
|
+
def send_message(self, token: str, to_jid: str, body: str) -> str:
|
|
20
|
+
"""Envía mensaje de texto privado. Retorna el msg_id generado."""
|
|
21
|
+
mid = util.generate_token(8)
|
|
22
|
+
msg = stanza.message(to_jid, body, msg_id=mid)
|
|
23
|
+
with self._xmpp_session(token) as sock:
|
|
24
|
+
sock.send(msg.encode())
|
|
25
|
+
return mid
|
|
26
|
+
|
|
27
|
+
def edit_message(self, token: str, to_jid: str, new_body: str, original_msg_id: str) -> str:
|
|
28
|
+
"""Edita un mensaje privado."""
|
|
29
|
+
edit_id = util.generate_token(8)
|
|
30
|
+
msg = stanza.edit_message(to_jid, new_body, original_msg_id, edit_id=edit_id)
|
|
31
|
+
with self._xmpp_session(token) as sock:
|
|
32
|
+
sock.send(msg.encode())
|
|
33
|
+
return edit_id
|
|
34
|
+
|
|
35
|
+
def send_file_message(self, token: str, to_jid: str, url: str, file_type: FileType, caption: str = "", file_name: str = "", file_size: int = 0) -> str:
|
|
36
|
+
mid = util.generate_token(8)
|
|
37
|
+
msg = stanza.file_message(to_jid, url, int(file_type), caption, msg_id=mid, file_name=file_name, file_size=file_size)
|
|
38
|
+
with self._xmpp_session(token) as sock:
|
|
39
|
+
sock.send(msg.encode())
|
|
40
|
+
return mid
|
|
41
|
+
|
|
42
|
+
def send_image_message(self, token: str, to_jid: str, url: str, file_name: str, file_size: int, width: int = 0, height: int = 0, thumbnail: str = "", caption: str = "") -> str:
|
|
43
|
+
"""Envía mensaje privado con imagen adjunta."""
|
|
44
|
+
mid = util.generate_token(8)
|
|
45
|
+
msg = stanza.image_message(to_jid, url, file_name, file_size, width, height, thumbnail, caption, msg_id=mid)
|
|
46
|
+
with self._xmpp_session(token) as sock:
|
|
47
|
+
sock.send(msg.encode())
|
|
48
|
+
return mid
|
|
49
|
+
|
|
50
|
+
def send_image_message_simple(self, token: str, to_jid: str, url: str, file_name: str, file_size: int, msg_id: str = "") -> str:
|
|
51
|
+
"""Envía mensaje privado con imagen SIN metadata."""
|
|
52
|
+
mid = msg_id or util.generate_token(8)
|
|
53
|
+
msg = stanza.image_message_simple(to_jid, url, file_name, file_size, msg_id=mid)
|
|
54
|
+
with self._xmpp_session(token) as sock:
|
|
55
|
+
sock.send(msg.encode())
|
|
56
|
+
return mid
|
|
57
|
+
|
|
58
|
+
def send_button_message(self, token: str, to_jid: str, text: str, buttons: list[dict]) -> str:
|
|
59
|
+
"""Envía mensaje con botones interactivos."""
|
|
60
|
+
mid = util.generate_token(8)
|
|
61
|
+
msg = stanza.button_message(to_jid, text, buttons, msg_id=mid)
|
|
62
|
+
with self._xmpp_session(token) as sock:
|
|
63
|
+
sock.send(msg.encode())
|
|
64
|
+
return mid
|
|
65
|
+
|
|
66
|
+
def send_contact_message(self, token: str, to_jid: str, contact_id: str, contact_name: str, contact_phone: str) -> str:
|
|
67
|
+
mid = util.generate_token(8)
|
|
68
|
+
msg = stanza.contact_message(to_jid, contact_id, contact_name, contact_phone, msg_id=mid)
|
|
69
|
+
with self._xmpp_session(token) as sock:
|
|
70
|
+
sock.send(msg.encode())
|
|
71
|
+
return mid
|
|
72
|
+
|
|
73
|
+
def send_sticker_message(self, token: str, to_jid: str, sticker_id: str, sticker_name: str, sticker_pack: str, sticker_hash: str) -> str:
|
|
74
|
+
mid = util.generate_token(8)
|
|
75
|
+
msg = stanza.sticker_message(to_jid, sticker_id, sticker_name, sticker_pack, sticker_hash, msg_id=mid)
|
|
76
|
+
with self._xmpp_session(token) as sock:
|
|
77
|
+
sock.send(msg.encode())
|
|
78
|
+
return mid
|
|
79
|
+
|
|
80
|
+
def send_video_message(self, token: str, to_jid: str, url: str, video_id: str, file_name: str, file_size: int, duration: int, width: int, height: int, thumbnail: str, info_text: str = "") -> str:
|
|
81
|
+
mid = hashlib.md5(util.generate_token(16).encode()).hexdigest()
|
|
82
|
+
msg = stanza.video_message(to_jid, url, video_id, file_name, file_size, duration, width, height, thumbnail, msg_id=mid, info_text=info_text)
|
|
83
|
+
with self._xmpp_session(token) as sock:
|
|
84
|
+
sock.send(msg.encode())
|
|
85
|
+
return mid
|
|
86
|
+
|
|
87
|
+
def send_location_message(self, token: str, to_jid: str, lat: float, lon: float, zoom: float = 11.0, text: str = "") -> str:
|
|
88
|
+
"""Envía un mensaje con ubicación adjunta."""
|
|
89
|
+
mid = util.generate_token(8)
|
|
90
|
+
msg = stanza.location_message(to_jid, lat, lon, zoom, text, msg_id=mid)
|
|
91
|
+
with self._xmpp_session(token) as sock:
|
|
92
|
+
sock.send(msg.encode())
|
|
93
|
+
return mid
|
|
94
|
+
|
|
95
|
+
def send_event_message(self, token: str, to_jid: str, title: str, start: int, end: int, all_day: bool, ics_data: str, event_id: str = "") -> str:
|
|
96
|
+
"""Envía un mensaje con evento/calendario adjunto."""
|
|
97
|
+
mid = util.generate_token(8)
|
|
98
|
+
msg = stanza.event_message(to_jid, event_id, title, start, end, all_day, ics_data, msg_id=mid)
|
|
99
|
+
with self._xmpp_session(token) as sock:
|
|
100
|
+
sock.send(msg.encode())
|
|
101
|
+
return mid
|
|
102
|
+
|
|
103
|
+
def send_chat_state(self, token: str, to_jid: str, state: str) -> None:
|
|
104
|
+
st = stanza.chat_state(to_jid, state)
|
|
105
|
+
with self._xmpp_session(token) as sock:
|
|
106
|
+
sock.send(st.encode())
|
|
107
|
+
|
|
108
|
+
def delete_message(self, token: str, to_jid: str, message_id: str, msg_type: str = "c", body: str = "", media_xml: str = "") -> str:
|
|
109
|
+
"""Elimina un mensaje propio."""
|
|
110
|
+
msg = stanza.delete_message(to_jid, message_id, msg_id=message_id, msg_type=msg_type, body=body, media_xml=media_xml)
|
|
111
|
+
with self._xmpp_session(token) as sock:
|
|
112
|
+
sock.send(msg.encode())
|
|
113
|
+
return message_id
|
|
114
|
+
|
|
115
|
+
def send_read_receipt(self, token: str, to_jid: str, msg_id: str, msg_type: str = "c") -> str:
|
|
116
|
+
"""Envía una confirmación de lectura (read receipt)."""
|
|
117
|
+
rid = util.generate_token(8)
|
|
118
|
+
msg = stanza.read_receipt(to_jid, msg_id, receipt_id=rid, msg_type=msg_type)
|
|
119
|
+
with self._xmpp_session(token) as sock:
|
|
120
|
+
sock.send(msg.encode())
|
|
121
|
+
return rid
|
|
122
|
+
|
|
123
|
+
# --- Recepción de mensajes ---
|
|
124
|
+
|
|
125
|
+
def listen_messages(self, token: str, callback: Callable[[dict], None]) -> None:
|
|
126
|
+
while True:
|
|
127
|
+
try:
|
|
128
|
+
with self._xmpp_session(token) as sock:
|
|
129
|
+
self._listen_loop(sock, callback)
|
|
130
|
+
except TokenExpiredError:
|
|
131
|
+
raise
|
|
132
|
+
except (ConnectionLostError, OSError, socket.error):
|
|
133
|
+
time.sleep(15)
|
|
134
|
+
|
|
135
|
+
def _listen_loop(self, sock, callback: Callable[[dict], None]) -> None:
|
|
136
|
+
stop_event = threading.Event()
|
|
137
|
+
ping_id = util.generate_token(5)
|
|
138
|
+
ka = threading.Thread(
|
|
139
|
+
target=self._keepalive_worker,
|
|
140
|
+
args=(sock, stop_event, ping_id),
|
|
141
|
+
daemon=True,
|
|
142
|
+
)
|
|
143
|
+
ka.start()
|
|
144
|
+
self._xml_parser.reset()
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
while True:
|
|
148
|
+
try:
|
|
149
|
+
response = self._recv_all(sock)
|
|
150
|
+
except OSError as e:
|
|
151
|
+
raise ConnectionLostError(e)
|
|
152
|
+
|
|
153
|
+
if response is None:
|
|
154
|
+
raise ConnectionLostError("Servidor cerro conexion")
|
|
155
|
+
|
|
156
|
+
if response == "":
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
stanzas = self._xml_parser.feed(response)
|
|
160
|
+
|
|
161
|
+
for msg in stanzas:
|
|
162
|
+
is_content = (
|
|
163
|
+
msg.get("body")
|
|
164
|
+
or msg.get("url")
|
|
165
|
+
or msg.get("contact_id")
|
|
166
|
+
or msg.get("sticker_id")
|
|
167
|
+
or msg.get("video_url")
|
|
168
|
+
or msg.get("buttons")
|
|
169
|
+
or msg.get("location_id")
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if is_content and not msg.get("deleted"):
|
|
173
|
+
msg_id = msg.get("id", "")
|
|
174
|
+
msg_from = msg.get("from", "")
|
|
175
|
+
if msg_id and msg_from:
|
|
176
|
+
try:
|
|
177
|
+
receipt = stanza.receipt(msg_from, msg_id)
|
|
178
|
+
sock.send(receipt.encode())
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
if (
|
|
183
|
+
is_content
|
|
184
|
+
or msg.get("chat_state")
|
|
185
|
+
or msg.get("receipt")
|
|
186
|
+
or msg.get("deleted")
|
|
187
|
+
):
|
|
188
|
+
callback(msg)
|
|
189
|
+
|
|
190
|
+
finally:
|
|
191
|
+
stop_event.set()
|
|
192
|
+
self._xml_parser.reset()
|
|
193
|
+
|
|
194
|
+
def _keepalive_worker(self, sock, stop: threading.Event, ping_id: str) -> None:
|
|
195
|
+
while not stop.is_set():
|
|
196
|
+
time.sleep(constants.KEEPALIVE_INTERVAL)
|
|
197
|
+
if stop.is_set():
|
|
198
|
+
break
|
|
199
|
+
try:
|
|
200
|
+
sock.send(stanza.ping(ping_id).encode())
|
|
201
|
+
except OSError:
|
|
202
|
+
break
|
todus/client/profile.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from ..types import FileType
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ToDusProfileMixin:
|
|
6
|
+
"""Mixin que contiene los métodos de manejo de perfil y avatar de ToDus."""
|
|
7
|
+
|
|
8
|
+
# --- Perfil ---
|
|
9
|
+
|
|
10
|
+
def update_profile(self, token: str, alias: str = "", bio: str = "", picture_url: str = "", thumbnail_url: str = "") -> bool:
|
|
11
|
+
headers = {
|
|
12
|
+
"Authorization": token,
|
|
13
|
+
"Content-Type": "application/json",
|
|
14
|
+
}
|
|
15
|
+
payload = {
|
|
16
|
+
"alias": alias,
|
|
17
|
+
"description": bio,
|
|
18
|
+
"picture_url": picture_url,
|
|
19
|
+
"picture_thumbnail_url": thumbnail_url,
|
|
20
|
+
}
|
|
21
|
+
try:
|
|
22
|
+
resp = self.session.post(
|
|
23
|
+
"https://auth.todus.cu/v2/todus/users.me.json",
|
|
24
|
+
json=payload,
|
|
25
|
+
headers=headers,
|
|
26
|
+
timeout=30,
|
|
27
|
+
)
|
|
28
|
+
return resp.status_code == 200
|
|
29
|
+
except Exception:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
def upload_avatar(self, token: str, image_data: bytes, thumbnail_data: bytes = None) -> tuple[str, str]:
|
|
33
|
+
if thumbnail_data is None:
|
|
34
|
+
thumbnail_data = image_data
|
|
35
|
+
|
|
36
|
+
up_url, down_url = self.reserve_upload_url(token, len(image_data), FileType.PROFILE)
|
|
37
|
+
resp = requests.put(
|
|
38
|
+
up_url,
|
|
39
|
+
data=image_data,
|
|
40
|
+
headers={"Content-Length": str(len(image_data)), "Content-Type": "application/octet-stream"},
|
|
41
|
+
timeout=60,
|
|
42
|
+
)
|
|
43
|
+
resp.raise_for_status()
|
|
44
|
+
profile_url = down_url
|
|
45
|
+
|
|
46
|
+
up_url, down_url = self.reserve_upload_url(token, len(thumbnail_data), FileType.PROFILE_THUMBNAIL)
|
|
47
|
+
resp = requests.put(
|
|
48
|
+
up_url,
|
|
49
|
+
data=thumbnail_data,
|
|
50
|
+
headers={"Content-Length": str(len(thumbnail_data)), "Content-Type": "application/octet-stream"},
|
|
51
|
+
timeout=60,
|
|
52
|
+
)
|
|
53
|
+
resp.raise_for_status()
|
|
54
|
+
thumbnail_url = down_url
|
|
55
|
+
|
|
56
|
+
return profile_url, thumbnail_url
|
todus/constants.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Constantes del protocolo ToDus."""
|
|
2
|
+
|
|
3
|
+
XMPP_HOST = "im.todus.cu"
|
|
4
|
+
XMPP_PORT = 5222
|
|
5
|
+
MUCLIGHT_HOST = "muclight.im.todus.cu"
|
|
6
|
+
AUTH_VERSION_NAME = "0.40.29"
|
|
7
|
+
AUTH_VERSION_CODE = "21833"
|
|
8
|
+
BUFFER_SIZE = 1024 * 1024
|
|
9
|
+
KEEPALIVE_INTERVAL = 25
|
|
10
|
+
DEFAULT_TIMEOUT = 15
|
todus/errors.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Excepciones de ToDus."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ToDusError(Exception):
|
|
5
|
+
"""Base para todos los errores de ToDus."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AuthenticationError(ToDusError):
|
|
9
|
+
"""Credenciales inválidas o sesión expirada."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TokenExpiredError(ToDusError):
|
|
13
|
+
"""El token JWT ya no es válido."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConnectionLostError(ToDusError):
|
|
17
|
+
"""Conexión XMPP perdida inesperadamente."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MessageError(ToDusError):
|
|
21
|
+
"""Error al enviar o recibir mensaje."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UploadError(ToDusError):
|
|
25
|
+
"""Error en subida/descarga de archivo."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ParseError(ToDusError):
|
|
29
|
+
"""Error parseando stanza XMPP."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RateLimitError(ToDusError):
|
|
33
|
+
"""Demasiadas peticiones en poco tiempo."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class StanzaError(ToDusError):
|
|
37
|
+
"""Stanza malformada o no soportada."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class GroupError(ToDusError):
|
|
41
|
+
"""Error relacionado con grupos MUC Light."""
|