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);
265route!(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);