lamprey_sdk/
http.rs

1use anyhow::{Context, Result};
2use common::v1::types::pagination::{PaginationQuery, PaginationResponse};
3use common::v1::types::{
4    media::MediaCreated, misc::UserIdReq, user_status::StatusPatch, ApplicationId, Channel,
5    ChannelCreate, ChannelId, ChannelPatch, ChannelReorder, Media, MediaCreate, MediaId, Message,
6    MessageCreate, MessageId, MessageModerate, MessagePatch, MessageVerId, PinsReorder,
7    PuppetCreate, Room, RoomBan, RoomBanBulkCreate, RoomCreate, RoomId, RoomPatch, SessionToken,
8    ThreadMember, ThreadMemberPut, User, UserId, UserPatch, UserWithRelationship,
9};
10use common::v1::types::{
11    MessageMigrate, RoomBanCreate, RoomMember, RoomMemberPatch, RoomMemberPut, SuspendRequest,
12    TransferOwnership, UserCreate,
13};
14use headers::HeaderMapExt;
15use reqwest::{header::HeaderMap, StatusCode, Url};
16use serde_json::json;
17use tracing::error;
18
19const DEFAULT_BASE: &str = "https://chat.celery.eu.org/";
20
21#[derive(Clone)]
22pub struct Http {
23    token: SessionToken,
24    base_url: Url,
25    client: reqwest::Client,
26}
27
28impl Http {
29    pub fn new(token: SessionToken) -> Self {
30        let base_url = Url::parse(DEFAULT_BASE).unwrap();
31        let mut h = HeaderMap::new();
32        h.typed_insert(headers::Authorization::bearer(&token.0).unwrap());
33        let client = reqwest::Client::builder()
34            .default_headers(h)
35            .build()
36            .unwrap();
37        Self {
38            token,
39            base_url,
40            client,
41        }
42    }
43
44    pub fn with_base_url(self, base_url: Url) -> Self {
45        let mut h = HeaderMap::new();
46        h.typed_insert(headers::Authorization::bearer(&self.token.0).unwrap());
47        let client = reqwest::Client::builder()
48            .default_headers(h)
49            .build()
50            .unwrap();
51        Self {
52            base_url,
53            client,
54            ..self
55        }
56    }
57
58    pub fn for_puppet(&self, id: UserId) -> Self {
59        let mut h = HeaderMap::new();
60        h.typed_insert(headers::Authorization::bearer(&self.token.0).unwrap());
61        h.insert("x-puppet-id", id.to_string().try_into().unwrap());
62        let client = reqwest::Client::builder()
63            .default_headers(h)
64            .build()
65            .unwrap();
66        Self {
67            client,
68            ..self.clone()
69        }
70    }
71
72    pub async fn media_upload(
73        &self,
74        target: &MediaCreated,
75        body: Vec<u8>,
76    ) -> Result<Option<Media>> {
77        let res = self
78            .client
79            .patch(target.upload_url.clone().unwrap())
80            .header("upload-offset", "0")
81            .header("content-type", "application/octet-stream")
82            .header("content-length", body.len())
83            .body(body)
84            .send()
85            .await?
86            .error_for_status()?;
87        match res.status() {
88            StatusCode::OK => {
89                let text = res.text().await?;
90                serde_json::from_str(&text)
91                    .with_context(|| {
92                        error!(response_body = %text, "failed to decode media upload response body");
93                        "failed to decode media upload response body"
94                    })
95                    .map(Some)
96            }
97            StatusCode::NO_CONTENT => Ok(None),
98            _ => unreachable!("technically reachable with a bad server"),
99        }
100    }
101
102    pub async fn thread_list(
103        &self,
104        channel_id: ChannelId,
105        query: &PaginationQuery<ChannelId>,
106    ) -> Result<PaginationResponse<Channel>> {
107        let url = self
108            .base_url
109            .join(&format!("/api/v1/channel/{channel_id}/thread"))?;
110        let res = self.client.get(url).query(query).send().await?;
111        let res = res.error_for_status()?;
112        let text = res.text().await?;
113        serde_json::from_str(&text).with_context(|| {
114            error!(response_body = %text, "failed to decode response body");
115            "failed to decode response body for thread_list"
116        })
117    }
118
119    pub async fn channel_list(
120        &self,
121        room_id: RoomId,
122        query: &PaginationQuery<ChannelId>,
123    ) -> Result<PaginationResponse<Channel>> {
124        let url = self
125            .base_url
126            .join(&format!("/api/v1/room/{room_id}/channel"))?;
127        let res = self.client.get(url).query(query).send().await?;
128        let res = res.error_for_status()?;
129        let text = res.text().await?;
130        serde_json::from_str(&text).with_context(|| {
131            error!(response_body = %text, "failed to decode response body");
132            "failed to decode response body for thread_list"
133        })
134    }
135
136    pub async fn message_list(
137        &self,
138        channel_id: ChannelId,
139        query: &PaginationQuery<MessageId>,
140    ) -> Result<PaginationResponse<Message>> {
141        let url = self
142            .base_url
143            .join(&format!("/api/v1/channel/{channel_id}/message"))?;
144        let res = self.client.get(url).query(query).send().await?;
145        let res = res.error_for_status()?;
146        let text = res.text().await?;
147        serde_json::from_str(&text).with_context(|| {
148            error!(response_body = %text, "failed to decode response body");
149            "failed to decode response body for message_list"
150        })
151    }
152}
153
154macro_rules! route {
155    ($method: ident $url:expr => $name:ident($($param:ident: $param_type:ty),*) -> $res:ty, $req:ty) => {
156        impl Http {
157            pub async fn $name(
158                &self,
159                $($param: $param_type,)*
160                body: &$req,
161            ) -> Result<$res> {
162                let url = self.base_url.join(&format!($url))?;
163                let res = self.client
164                    .$method(url)
165                    .header("content-type", "application/json")
166                    .json(body)
167                    .send()
168                    .await?;
169                let res = res.error_for_status()?;
170                let text = res.text().await?;
171                serde_json::from_str(&text).with_context(|| {
172                    error!(response_body = %text, "failed to decode response body");
173                    format!("failed to decode response body for {}", stringify!($name))
174                })
175            }
176        }
177    };
178
179    ($method: ident $url:expr => $name:ident($($param:ident: $param_type:ty),*) -> $res:ty) => {
180        impl Http {
181            pub async fn $name(
182                &self,
183                $($param: $param_type),*
184            ) -> Result<$res> {
185                let url = self.base_url.join(&format!($url))?;
186                let res = self.client
187                    .$method(url)
188                    .header("content-type", "application/json")
189                    .json(&json!({}))
190                    .send()
191                    .await?;
192                let res = res.error_for_status()?;
193                let text = res.text().await?;
194                serde_json::from_str(&text).with_context(|| {
195                    error!(response_body = %text, "failed to decode response body");
196                    format!("failed to decode response body for {}", stringify!($name))
197                })
198            }
199        }
200    };
201
202    ($method: ident $url:expr => $name:ident($($param:ident: $param_type:ty),*), $req:ty) => {
203        impl Http {
204            pub async fn $name(
205                &self,
206                $($param: $param_type),*,
207                body: &$req,
208            ) -> Result<()> {
209                let url = self.base_url.join(&format!($url))?;
210                let res = self.client
211                    .$method(url)
212                    .header("content-type", "application/json")
213                    .json(body)
214                    .send()
215                    .await?;
216                if let Err(e) = res.error_for_status_ref() {
217                    let text = res.text().await.unwrap_or_else(|_| "failed to read body".to_string());
218                    error!(name = stringify!($name), status = %e.status().unwrap(), response_body = %text, "request failed");
219                    return Err(anyhow::anyhow!(e).context(text));
220                }
221                Ok(())
222            }
223        }
224    };
225
226    ($method: ident $url:expr => $name:ident($($param:ident: $param_type:ty),*)) => {
227        impl Http {
228            pub async fn $name(
229                &self,
230                $($param: $param_type),*,
231            ) -> Result<()> {
232                let url = self.base_url.join(&format!($url))?;
233                let res = self.client
234                    .$method(url)
235                    .header("content-type", "application/json")
236                    .json(&json!({}))
237                    .send()
238                    .await?;
239                if let Err(e) = res.error_for_status_ref() {
240                    let text = res.text().await.unwrap_or_else(|_| "failed to read body".to_string());
241                    error!(name = stringify!($name), status = %e.status().unwrap(), response_body = %text, "request failed");
242                    return Err(anyhow::anyhow!(e).context(text));
243                }
244                Ok(())
245            }
246        }
247    };
248}
249
250route!(get    "/api/v1/media/{media_id}"                          => media_info_get(media_id: MediaId) -> Media);
251route!(post   "/api/v1/room/{room_id}/channel"                    => channel_create_room(room_id: RoomId) -> Channel, ChannelCreate);
252route!(patch  "/api/v1/channel/{channel_id}"                      => channel_update(channel_id: ChannelId) -> Channel, ChannelPatch);
253route!(get    "/api/v1/channel/{channel_id}"                      => channel_get(channel_id: ChannelId) -> Channel);
254route!(post   "/api/v1/media"                                     => media_create() -> MediaCreated, MediaCreate);
255route!(delete "/api/v1/channel/{channel_id}/message/{message_id}" => message_delete(channel_id: ChannelId, message_id: MessageId));
256route!(patch  "/api/v1/channel/{channel_id}/message/{message_id}" => message_edit(channel_id: ChannelId, message_id: MessageId) -> Message, MessagePatch);
257route!(get    "/api/v1/channel/{channel_id}/message/{message_id}" => message_get(channel_id: ChannelId, message_id: MessageId) -> Message);
258route!(post   "/api/v1/channel/{channel_id}/message"              => message_create(channel_id: ChannelId) -> Message, MessageCreate);
259route!(put    "/api/v1/channel/{channel_id}/message/{message_id}/reaction/{reaction}" => message_react(channel_id: ChannelId, message_id: MessageId, reaction: String));
260route!(delete "/api/v1/channel/{channel_id}/message/{message_id}/reaction/{reaction}" => message_unreact(channel_id: ChannelId, message_id: MessageId, reaction: String));
261route!(post   "/api/v1/channel/{channel_id}/typing"               => channel_typing(channel_id: ChannelId));
262route!(get    "/api/v1/user/{user_id}"                            => user_get(user_id: UserIdReq) -> UserWithRelationship);
263route!(put    "/api/v1/room/{room_id}/member/{user_id}"           => room_member_add(room_id: RoomId, user_id: UserIdReq) -> RoomMember, RoomMemberPut);
264route!(patch  "/api/v1/room/{room_id}/member/{user_id}"           => room_member_patch(room_id: RoomId, user_id: UserIdReq) -> RoomMember, RoomMemberPatch);
265// route!(post   "/api/v1/user"                                      => user_create() -> User, UserCreate);
266route!(patch  "/api/v1/user/{user_id}"                            => user_update(user_id: UserIdReq) -> User, UserPatch);
267route!(post   "/api/v1/user/{user_id}/status"                     => user_set_status(user_id: UserIdReq), StatusPatch);
268route!(put    "/api/v1/app/{app_id}/puppet/{puppet_id}"           => puppet_ensure(app_id: ApplicationId, puppet_id: String) -> User, PuppetCreate);
269route!(post   "/api/v1/channel"                                   => channel_create_dm() -> Channel, ChannelCreate);
270route!(patch  "/api/v1/room/{room_id}/channel"                    => channel_reorder(room_id: RoomId), ChannelReorder);
271route!(put    "/api/v1/channel/{channel_id}/remove"               => channel_remove(channel_id: ChannelId));
272route!(delete "/api/v1/channel/{channel_id}/remove"               => channel_restore(channel_id: ChannelId));
273route!(post   "/api/v1/channel/{channel_id}/upgrade"              => channel_upgrade(channel_id: ChannelId) -> Room);
274route!(post   "/api/v1/channel/{channel_id}/transfer-ownership"   => channel_transfer_ownership(channel_id: ChannelId), TransferOwnership);
275route!(post   "/api/v1/user/@self/dm/{target_id}"                 => dm_init(target_id: UserId) -> Channel);
276route!(get    "/api/v1/user/@self/dm/{target_id}"                 => dm_get(target_id: UserId) -> Channel);
277route!(get    "/api/v1/channel/{channel_id}/message/{message_id}/version/{version_id}" => message_version_get(channel_id: ChannelId, message_id: MessageId, version_id: MessageVerId) -> Message);
278route!(patch  "/api/v1/channel/{channel_id}/message"              => message_moderate(channel_id: ChannelId), MessageModerate);
279route!(post   "/api/v1/channel/{channel_id}/move-messages"        => message_move(channel_id: ChannelId), MessageMigrate);
280route!(put    "/api/v1/channel/{channel_id}/pin/{message_id}"     => message_pin_create(channel_id: ChannelId, message_id: MessageId));
281route!(delete "/api/v1/channel/{channel_id}/pin/{message_id}"     => message_pin_delete(channel_id: ChannelId, message_id: MessageId));
282route!(patch  "/api/v1/channel/{channel_id}/pin"                  => message_pin_reorder(channel_id: ChannelId), PinsReorder);
283route!(post   "/api/v1/room"                                      => room_create() -> Room, RoomCreate);
284route!(get    "/api/v1/room/{room_id}"                            => room_get(room_id: RoomId) -> Room);
285route!(patch  "/api/v1/room/{room_id}"                            => room_edit(room_id: RoomId) -> Room, RoomPatch);
286route!(delete "/api/v1/room/{room_id}"                            => room_delete(room_id: RoomId));
287route!(post   "/api/v1/room/{room_id}/undelete"                   => room_undelete(room_id: RoomId));
288route!(put    "/api/v1/room/{room_id}/ack"                        => room_ack(room_id: RoomId));
289route!(post   "/api/v1/room/{room_id}/quarantine"                 => room_quarantine(room_id: RoomId) -> Room);
290route!(delete "/api/v1/room/{room_id}/quarantine"                 => room_unquarantine(room_id: RoomId) -> Room);
291route!(post   "/api/v1/room/{room_id}/transfer-ownership"         => room_transfer_ownership(room_id: RoomId), TransferOwnership);
292route!(get    "/api/v1/room/{room_id}/member/{user_id}"           => room_member_get(room_id: RoomId, user_id: UserIdReq) -> RoomMember);
293route!(delete "/api/v1/room/{room_id}/member/{user_id}"           => room_member_delete(room_id: RoomId, user_id: UserIdReq));
294route!(put    "/api/v1/room/{room_id}/ban/{user_id}"              => room_ban_create(room_id: RoomId, user_id: UserIdReq), RoomBanCreate);
295route!(post   "/api/v1/room/{room_id}/ban"                        => room_ban_create_bulk(room_id: RoomId), RoomBanBulkCreate);
296route!(delete "/api/v1/room/{room_id}/ban/{user_id}"              => room_ban_remove(room_id: RoomId, user_id: UserIdReq));
297route!(get    "/api/v1/room/{room_id}/ban/{user_id}"              => room_ban_get(room_id: RoomId, user_id: UserIdReq) -> RoomBan);
298route!(get    "/api/v1/thread/{thread_id}/member/{user_id}"       => thread_member_get(thread_id: ChannelId, user_id: UserIdReq) -> ThreadMember);
299route!(put    "/api/v1/thread/{thread_id}/member/{user_id}"       => thread_member_add(thread_id: ChannelId, user_id: UserIdReq) -> ThreadMember, ThreadMemberPut);
300route!(delete "/api/v1/thread/{thread_id}/member/{user_id}"       => thread_member_delete(thread_id: ChannelId, user_id: UserIdReq));
301route!(delete "/api/v1/user/{user_id}"                            => user_delete(user_id: UserIdReq));
302route!(post   "/api/v1/user/{user_id}/undelete"                   => user_undelete(user_id: UserIdReq));
303route!(post   "/api/v1/guest"                                     => guest_create() -> User, UserCreate);
304route!(post   "/api/v1/user/{user_id}/suspend"                    => user_suspend(user_id: UserIdReq) -> User, SuspendRequest);
305route!(delete "/api/v1/user/{user_id}/suspend"                    => user_unsuspend(user_id: UserIdReq) -> User);