Skip to content

Commit b22e72e

Browse files
committed
feat: add upload functionality and MediaType enum for media handling
1 parent a020337 commit b22e72e

9 files changed

Lines changed: 209 additions & 24 deletions

File tree

python/tryx/client.pyi

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ from typing import Any, Awaitable, Callable, Type
44

55
# from tryx.events import Message
66
from .waproto.whatsapp_pb2 import Message as MessageProto
7-
from .types import JID
7+
from .types import JID, UploadResponse
88
from .backend import SqliteBackend
9+
from .wacore import MediaType
910

1011
class Tryx:
1112
def __init__(self, backend: SqliteBackend) -> None: ...
@@ -17,6 +18,9 @@ class Tryx:
1718

1819
class TryxClient:
1920
async def send_message(self, chat: JID, message: MessageProto) -> str: ...
21+
async def upload(self, data: bytes, media_type: MediaType) -> UploadResponse: ...
22+
async def upload_file(self, path: str, media_type: MediaType) -> UploadResponse: ...
23+
2024

2125
class Nu[T]:
2226
X: T

python/tryx/types.pyi

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,16 @@ class MessageInfo:
6060
@property
6161
def device_sent_meta(self) -> DeviceSentMeta | None: ...
6262

63+
class UploadResponse:
64+
@property
65+
def url(self) -> str: ...
66+
@property
67+
def direct_path(self) -> str: ...
68+
@property
69+
def media_key(self) -> bytes: ...
70+
@property
71+
def file_enc_sha256(self) -> str: ...
72+
@property
73+
def file_sha256(self) -> str: ...
74+
@property
75+
def file_length(self) -> int: ...

python/tryx/wacore.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from ._tryx import wacore # type: ignore
2+
3+
for name in dir(wacore): # type: ignore
4+
obj = getattr(wacore, name) # type: ignore
5+
if isinstance(obj, type):
6+
globals()[name] = obj
7+
8+
__all__ = [name for name in dir(wacore) if isinstance(getattr(wacore, name), type)] # type: ignore

python/tryx/wacore.pyi

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from enum import Enum
2+
3+
4+
class MediaType(Enum):
5+
Image = 1
6+
Video = 2
7+
Audio = 3
8+
Document = 4
9+
History = 5
10+
AppState = 6
11+
Sticker = 7
12+
StickerPack = 8
13+
LinkThumbnail = 9

src/client.rs

Lines changed: 94 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ use tracing::{debug, error, info, warn};
2121
use tracing_subscriber::EnvFilter;
2222

2323
use crate::backend::{SqliteBackend, BackendBase};
24-
use crate::events::{Connected, Message as WAMessage, PairingQrCode};
24+
use crate::events::{Connected, LoggedOut, Message as WAMessage, PairingQrCode};
2525
use crate::exceptions::UnsupportedBackend;
26-
use crate::types::JID;
26+
use crate::types::{JID, UploadResponse};
2727
use crate::dispatcher::Dispatcher;
28+
use crate::wacore::MediaType;
2829

2930
static LOG_INIT: Once = Once::new();
3031

@@ -54,6 +55,54 @@ pub struct TryxClient {
5455

5556
#[pymethods]
5657
impl TryxClient {
58+
fn upload_file<'py>(&self, py: Python<'py>, path: String, media_type: Py<MediaType>) -> PyResult<Bound<'py, PyAny>> {
59+
let client = self.client_rx.borrow().clone().ok_or_else(|| {
60+
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>("Bot is not running")
61+
})?;
62+
let media_type_enum = media_type.bind(py).borrow_mut().to_wacore_enum();
63+
let locals = get_current_locals(py)?;
64+
let data = std::fs::read(path.clone()).map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
65+
future_into_py_with_locals(py, locals, async move {
66+
let url = client
67+
.upload(data, media_type_enum)
68+
.await
69+
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
70+
let result= UploadResponse {
71+
url: url.url,
72+
direct_path: url.direct_path,
73+
media_key: url.media_key,
74+
file_enc_sha256: url.file_enc_sha256,
75+
file_sha256: url.file_sha256,
76+
file_length: url.file_length,
77+
};
78+
Ok(result)
79+
})
80+
}
81+
fn upload<'py>(&self, py: Python<'py>, data: &[u8], media_type: Py<MediaType>) -> PyResult<Bound<'py, PyAny>> {
82+
let client = self.client_rx.borrow().clone().ok_or_else(|| {
83+
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>("Bot is not running")
84+
})?;
85+
let data_vec = data.to_vec();
86+
let mtype = media_type.bind(py).borrow_mut().to_wacore_enum();
87+
let locals = get_current_locals(py)?;
88+
future_into_py_with_locals::<_, UploadResponse>(py, locals, async move {
89+
let url = client
90+
.upload(data_vec, mtype)
91+
.await
92+
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
93+
let result= UploadResponse {
94+
url: url.url,
95+
direct_path: url.direct_path,
96+
media_key: url.media_key,
97+
file_enc_sha256: url.file_enc_sha256,
98+
file_sha256: url.file_sha256,
99+
file_length: url.file_length,
100+
};
101+
Ok(result)
102+
})
103+
// Ok(url)
104+
// })
105+
}
57106
fn send_message<'py>(&self, py: Python<'py>, to: Py<JID>, message: Py<PyAny>) -> PyResult<Bound<'py, PyAny>> {
58107
let client = self.client_rx.borrow().clone().ok_or_else(|| {
59108
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>("Bot is not running")
@@ -91,28 +140,47 @@ impl Tryx {
91140
tryx_client: Py<TryxClient>,
92141
client_tx: watch::Sender<Option<Arc<Client>>>,
93142
) -> PyResult<()> {
143+
let (pairing_qr_callbacks, message_callbacks, connected_callbacks, logout_callbacks) =
144+
Python::attach(|py| {
145+
let dispatcher = handlers.bind(py).borrow();
146+
(
147+
dispatcher.pairing_qr_handlers(py),
148+
dispatcher.message_handlers(py),
149+
dispatcher.conneccted_handlers(py),
150+
dispatcher.logout_handlers(py),
151+
)
152+
});
153+
let pairing_qr_callbacks = Arc::new(pairing_qr_callbacks);
154+
let message_callbacks = Arc::new(message_callbacks);
155+
let connected_callbacks = Arc::new(connected_callbacks);
156+
let logout_callbacks = Arc::new(logout_callbacks);
157+
info!(
158+
pairing_qr_handlers = pairing_qr_callbacks.len(),
159+
message_handlers = message_callbacks.len(),
160+
connected_handlers = connected_callbacks.len(),
161+
logout_handlers = logout_callbacks.len(),
162+
"cached dispatcher handlers for runtime"
163+
);
164+
94165
info!("building WhatsApp bot");
95166
let mut bot = Bot::builder()
96167
.with_backend(backend)
97168
.with_transport_factory(TokioWebSocketTransportFactory::new())
98169
.with_http_client(UreqHttpClient::new())
99170
.on_event(move |event, _client| {
100-
let handlers = Python::attach(|py| handlers.clone_ref(py));
101171
let locals = locals.clone();
172+
let pairing_qr_callbacks = Arc::clone(&pairing_qr_callbacks);
173+
let message_callbacks = Arc::clone(&message_callbacks);
174+
let connected_callbacks = Arc::clone(&connected_callbacks);
175+
let logout_callbacks = Arc::clone(&logout_callbacks);
102176
let tryx_client = Python::attach(|py| tryx_client.clone_ref(py));
103177
async move {
104178
match event {
105179
Event::PairingQrCode { code, timeout } => {
106180
info!(timeout_secs = timeout.as_secs(), "received pairing QR event");
107-
let callbacks = Python::attach(|py| {
108-
handlers
109-
.bind(py)
110-
.borrow()
111-
.pairing_qr_handlers(py)
112-
});
113-
info!(handlers = callbacks.len(), "dispatching pairing QR handlers");
181+
info!(handlers = pairing_qr_callbacks.len(), "dispatching pairing QR handlers");
114182

115-
for (idx, callback) in callbacks.into_iter().enumerate() {
183+
for (idx, callback) in pairing_qr_callbacks.iter().enumerate() {
116184
debug!(handler_index = idx, "calling pairing QR Python callback");
117185
let locals = locals.clone();
118186
let py_future = Python::attach(|py| -> PyResult<_> {
@@ -149,15 +217,9 @@ impl Tryx {
149217
}
150218
Event::Message(msg, info) => {
151219
debug!(message_id = %info.id, "received message event");
152-
let callbacks = Python::attach(|py| {
153-
handlers
154-
.bind(py)
155-
.borrow()
156-
.message_handlers(py)
157-
});
158-
info!(handlers = callbacks.len(), message_id = %info.id, "dispatching message handlers");
220+
info!(handlers = message_callbacks.len(), message_id = %info.id, "dispatching message handlers");
159221

160-
for (idx, callback) in callbacks.into_iter().enumerate() {
222+
for (idx, callback) in message_callbacks.iter().enumerate() {
161223
debug!(handler_index = idx, message_id = %info.id, "calling message Python callback");
162224
let locals = locals.clone();
163225
let py_future = Python::attach(|py| -> PyResult<_> {
@@ -194,8 +256,7 @@ impl Tryx {
194256
}
195257
}
196258
Event::Connected(_) => {
197-
let callbacks = Python::attach(|py| handlers.clone_ref(py).bind(py).borrow().conneccted_handlers(py));
198-
for (idx, callback) in callbacks.into_iter().enumerate() {
259+
for (idx, callback) in connected_callbacks.iter().enumerate() {
199260
debug!(handler_index = idx, "calling connected event handler");
200261
let _ = Python::attach(|py| -> PyResult<_> {
201262
let awaitable = callback.bind(py).call1((Connected{},))?;
@@ -204,6 +265,17 @@ impl Tryx {
204265
});
205266
}
206267
}
268+
Event::LoggedOut(logout) => {
269+
for (idx, callback) in logout_callbacks.iter().enumerate() {
270+
debug!(handler_index = idx, "calling logged out event handler");
271+
let _ = Python::attach(|py| -> PyResult<_> {
272+
let awaitable = callback.bind(py).call1((LoggedOut::new(logout.clone()),))?;
273+
let fut = into_future(awaitable)?;
274+
Ok(fut)
275+
});
276+
}
277+
278+
}
207279
_ => {
208280
debug!("received event without registered dispatcher path");
209281
}
@@ -377,4 +449,5 @@ impl Tryx {
377449
})
378450
})
379451
}
452+
380453
}

src/dispatcher.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,14 @@ impl Dispatcher {
117117
debug!(handlers = handlers.len(), "collected message handlers");
118118
handlers
119119
}
120+
pub fn logout_handlers(&self, py: Python<'_>) -> Vec<Py<PyAny>> {
121+
let handlers = self.logged_out
122+
.iter()
123+
.map(|handler| handler.clone_ref(py))
124+
.collect::<Vec<_>>();
125+
debug!(handlers = handlers.len(), "collected logged out handlers");
126+
handlers
127+
}
120128
pub fn conneccted_handlers(&self, py: Python<'_>) -> Vec<Py<PyAny>> {
121129
let handlers = self.connected
122130
.iter()
@@ -182,7 +190,6 @@ fn dispatch_event_from_type(py: Python, event_type: &Bound<PyAny>) -> PyResult<D
182190
Ok(DispatchEvent::Message)
183191
} else {
184192
Err(PyErr::new::<UnsupportedEventType, _>("Unsupported event type"))
185-
186193
}
187194
}
188195

src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ fn _tryx(_py: &Bound<PyModule>) -> PyResult<()> {
2323
let types_module = PyModule::new(_py.py(), "types")?;
2424
types_module.add_class::<types::JID>()?;
2525
types_module.add_class::<types::MessageInfo>()?;
26+
types_module.add_class::<types::UploadResponse>()?;
2627
_py.add_submodule(&types_module)?;
28+
let wacore_module = PyModule::new(_py.py(), "wacore")?;
29+
wacore_module.add_class::<wacore::MediaType>()?;
30+
_py.add_submodule(&wacore_module)?;
2731
Ok(())
2832
}
2933

@@ -32,4 +36,5 @@ mod client;
3236
mod events;
3337
mod types;
3438
mod dispatcher;
35-
mod exceptions;
39+
mod exceptions;
40+
mod wacore;

src/types.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,3 +266,19 @@ impl MessageInfo {
266266
format!("MessageInfo(id='{}', type='{}', push_name='{}')", self.id, self.r#type, self.push_name)
267267
}
268268
}
269+
270+
#[pyclass]
271+
pub struct UploadResponse {
272+
#[pyo3(get)]
273+
pub url: String,
274+
#[pyo3(get)]
275+
pub direct_path: String,
276+
#[pyo3(get)]
277+
pub media_key: Vec<u8>,
278+
#[pyo3(get)]
279+
pub file_enc_sha256: Vec<u8>,
280+
#[pyo3(get)]
281+
pub file_sha256: Vec<u8>,
282+
#[pyo3(get)]
283+
pub file_length: u64,
284+
}

src/wacore.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
use pyo3::pyclass;
2+
use wacore::download::{MediaType as WACoreMediaType};
3+
4+
#[pyclass]
5+
pub enum MediaType {
6+
Image,
7+
Video,
8+
Audio,
9+
Document,
10+
History,
11+
AppState,
12+
Sticker,
13+
StickerPack,
14+
LinkThumbnail,
15+
}
16+
impl From<MediaType> for WACoreMediaType {
17+
fn from(value: MediaType) -> Self {
18+
match value {
19+
MediaType::Image => WACoreMediaType::Image,
20+
MediaType::Video => WACoreMediaType::Video,
21+
MediaType::Audio => WACoreMediaType::Audio,
22+
MediaType::Document => WACoreMediaType::Document,
23+
MediaType::History => WACoreMediaType::History,
24+
MediaType::AppState => WACoreMediaType::AppState,
25+
MediaType::Sticker => WACoreMediaType::Sticker,
26+
MediaType::StickerPack => WACoreMediaType::StickerPack,
27+
MediaType::LinkThumbnail => WACoreMediaType::LinkThumbnail,
28+
}
29+
}
30+
}
31+
32+
impl MediaType {
33+
pub fn to_wacore_enum(&self) -> WACoreMediaType {
34+
match &self {
35+
MediaType::Image => WACoreMediaType::Image,
36+
MediaType::Video => WACoreMediaType::Video,
37+
MediaType::Audio => WACoreMediaType::Audio,
38+
MediaType::Document => WACoreMediaType::Document,
39+
MediaType::History => WACoreMediaType::History,
40+
MediaType::AppState => WACoreMediaType::AppState,
41+
MediaType::Sticker => WACoreMediaType::Sticker,
42+
MediaType::StickerPack => WACoreMediaType::StickerPack,
43+
MediaType::LinkThumbnail => WACoreMediaType::LinkThumbnail,
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)