1#![deny(
61 missing_docs,
62 warnings,
63 missing_debug_implementations,
64 missing_copy_implementations,
65 trivial_casts,
66 trivial_numeric_casts,
67 unsafe_code,
68 unstable_features,
69 unused_import_braces,
70 unused_qualifications
71)]
72#![allow(intra_doc_link_resolution_failure)]
73
74#[macro_use]
75extern crate log;
76#[macro_use]
77extern crate serde_derive;
78#[macro_use]
79extern crate doc_comment;
80extern crate hyper_old_types;
81extern crate isolang;
82#[macro_use]
83extern crate serde_json;
84extern crate chrono;
85extern crate reqwest;
86extern crate serde;
87extern crate serde_qs;
88extern crate serde_urlencoded;
89extern crate tap_reader;
90extern crate try_from;
91extern crate url;
92extern crate tungstenite;
93
94#[cfg(feature = "env")]
95extern crate envy;
96
97#[cfg(feature = "toml")]
98extern crate toml as tomlcrate;
99
100#[cfg(test)]
101extern crate tempfile;
102
103#[cfg(test)]
104#[cfg_attr(all(test, any(feature = "toml", feature = "json")), macro_use)]
105extern crate indoc;
106
107use std::{
108 borrow::Cow,
109 io::BufRead,
110 ops,
111};
112
113use reqwest::{Client, RequestBuilder, Response};
114use tap_reader::Tap;
115use tungstenite::client::AutoStream;
116
117use entities::prelude::*;
118use http_send::{HttpSend, HttpSender};
119use page::Page;
120
121pub use data::Data;
122pub use errors::{ApiError, Error, Result};
123pub use isolang::Language;
124pub use mastodon_client::{MastodonClient, MastodonUnauthenticated};
125pub use registration::Registration;
126pub use requests::{
127 AddFilterRequest,
128 AddPushRequest,
129 StatusesRequest,
130 UpdateCredsRequest,
131 UpdatePushRequest,
132};
133pub use status_builder::{NewStatus, StatusBuilder};
134
135pub mod apps;
137pub mod data;
139pub mod entities;
141pub mod errors;
143pub mod helpers;
145pub mod http_send;
147mod mastodon_client;
148pub mod page;
150pub mod registration;
152pub mod requests;
154pub mod scopes;
156pub mod status_builder;
158#[macro_use]
159mod macros;
160pub mod prelude {
162 pub use scopes::Scopes;
163 pub use Data;
164 pub use Mastodon;
165 pub use MastodonClient;
166 pub use NewStatus;
167 pub use Registration;
168 pub use StatusBuilder;
169 pub use StatusesRequest;
170}
171
172#[derive(Clone, Debug)]
174pub struct Mastodon<H: HttpSend = HttpSender> {
175 client: Client,
176 http_sender: H,
177 pub data: Data,
179}
180
181impl<H: HttpSend> Mastodon<H> {
182 methods![get, post, delete,];
183
184 fn route(&self, url: &str) -> String {
185 format!("{}{}", self.base, url)
186 }
187
188 pub(crate) fn send(&self, req: RequestBuilder) -> Result<Response> {
189 Ok(self
190 .http_sender
191 .send(&self.client, req.bearer_auth(&self.token))?)
192 }
193}
194
195impl From<Data> for Mastodon<HttpSender> {
196 fn from(data: Data) -> Mastodon<HttpSender> {
198 let mut builder = MastodonBuilder::new(HttpSender);
199 builder.data(data);
200 builder
201 .build()
202 .expect("We know `data` is present, so this should be fine")
203 }
204}
205
206impl<H: HttpSend> MastodonClient<H> for Mastodon<H> {
207 type Stream = EventReader<WebSocket>;
208
209 paged_routes! {
210 (get) favourites: "favourites" => Status,
211 (get) blocks: "blocks" => Account,
212 (get) domain_blocks: "domain_blocks" => String,
213 (get) follow_requests: "follow_requests" => Account,
214 (get) get_home_timeline: "timelines/home" => Status,
215 (get) get_emojis: "custom_emojis" => Emoji,
216 (get) mutes: "mutes" => Account,
217 (get) notifications: "notifications" => Notification,
218 (get) reports: "reports" => Report,
219 (get (q: &'a str, #[serde(skip_serializing_if = "Option::is_none")] limit: Option<u64>, following: bool,)) search_accounts: "accounts/search" => Account,
220 (get) get_endorsements: "endorsements" => Account,
221 }
222
223 paged_routes_with_id! {
224 (get) followers: "accounts/{}/followers" => Account,
225 (get) following: "accounts/{}/following" => Account,
226 (get) reblogged_by: "statuses/{}/reblogged_by" => Account,
227 (get) favourited_by: "statuses/{}/favourited_by" => Account,
228 }
229
230 route! {
231 (delete (domain: String,)) unblock_domain: "domain_blocks" => Empty,
232 (get) instance: "instance" => Instance,
233 (get) verify_credentials: "accounts/verify_credentials" => Account,
234 (post (account_id: &str, status_ids: Vec<&str>, comment: String,)) report: "reports" => Report,
235 (post (domain: String,)) block_domain: "domain_blocks" => Empty,
236 (post (id: &str,)) authorize_follow_request: "accounts/follow_requests/authorize" => Empty,
237 (post (id: &str,)) reject_follow_request: "accounts/follow_requests/reject" => Empty,
238 (get (q: &'a str, resolve: bool,)) search: "search" => SearchResult,
239 (get (local: bool,)) get_public_timeline: "timelines/public" => Vec<Status>,
240 (post (uri: Cow<'static, str>,)) follows: "follows" => Account,
241 (post multipart (file: Cow<'static, str>,)) media: "media" => Attachment,
242 (post) clear_notifications: "notifications/clear" => Empty,
243 (post (id: &str,)) dismiss_notification: "notifications/dismiss" => Empty,
244 (get) get_push_subscription: "push/subscription" => Subscription,
245 (delete) delete_push_subscription: "push/subscription" => Empty,
246 (get) get_filters: "filters" => Vec<Filter>,
247 (get) get_follow_suggestions: "suggestions" => Vec<Account>,
248 }
249
250 route_v2! {
251 (get (q: &'a str, resolve: bool,)) search_v2: "search" => SearchResultV2,
252 }
253
254 route_id! {
255 (get) get_account: "accounts/{}" => Account,
256 (post) follow: "accounts/{}/follow" => Relationship,
257 (post) unfollow: "accounts/{}/unfollow" => Relationship,
258 (post) block: "accounts/{}/block" => Relationship,
259 (post) unblock: "accounts/{}/unblock" => Relationship,
260 (get) mute: "accounts/{}/mute" => Relationship,
261 (get) unmute: "accounts/{}/unmute" => Relationship,
262 (get) get_notification: "notifications/{}" => Notification,
263 (get) get_status: "statuses/{}" => Status,
264 (get) get_context: "statuses/{}/context" => Context,
265 (get) get_card: "statuses/{}/card" => Card,
266 (post) reblog: "statuses/{}/reblog" => Status,
267 (post) unreblog: "statuses/{}/unreblog" => Status,
268 (post) favourite: "statuses/{}/favourite" => Status,
269 (post) unfavourite: "statuses/{}/unfavourite" => Status,
270 (delete) delete_status: "statuses/{}" => Empty,
271 (get) get_filter: "filters/{}" => Filter,
272 (delete) delete_filter: "filters/{}" => Empty,
273 (delete) delete_from_suggestions: "suggestions/{}" => Empty,
274 (post) endorse_user: "accounts/{}/pin" => Relationship,
275 (post) unendorse_user: "accounts/{}/unpin" => Relationship,
276 }
277
278 fn add_filter(&self, request: &mut AddFilterRequest) -> Result<Filter> {
279 let url = self.route("/api/v1/filters");
280 let response = self.send(self.client.post(&url).json(&request))?;
281
282 let status = response.status();
283
284 if status.is_client_error() {
285 return Err(Error::Client(status.clone()));
286 } else if status.is_server_error() {
287 return Err(Error::Server(status.clone()));
288 }
289
290 deserialise(response)
291 }
292
293 fn update_filter(&self, id: &str, request: &mut AddFilterRequest) -> Result<Filter> {
295 let url = self.route(&format!("/api/v1/filters/{}", id));
296 let response = self.send(self.client.put(&url).json(&request))?;
297
298 let status = response.status();
299
300 if status.is_client_error() {
301 return Err(Error::Client(status.clone()));
302 } else if status.is_server_error() {
303 return Err(Error::Server(status.clone()));
304 }
305
306 deserialise(response)
307 }
308
309 fn update_credentials(&self, builder: &mut UpdateCredsRequest) -> Result<Account> {
310 let changes = builder.build()?;
311 let url = self.route("/api/v1/accounts/update_credentials");
312 let response = self.send(self.client.patch(&url).json(&changes))?;
313
314 let status = response.status();
315
316 if status.is_client_error() {
317 return Err(Error::Client(status.clone()));
318 } else if status.is_server_error() {
319 return Err(Error::Server(status.clone()));
320 }
321
322 deserialise(response)
323 }
324
325 fn new_status(&self, status: NewStatus) -> Result<Status> {
327 let response = self.send(
328 self.client
329 .post(&self.route("/api/v1/statuses"))
330 .json(&status),
331 )?;
332
333 deserialise(response)
334 }
335
336 fn get_tagged_timeline(&self, hashtag: String, local: bool) -> Result<Vec<Status>> {
339 let base = "/api/v1/timelines/tag/";
340 let url = if local {
341 self.route(&format!("{}{}?local=1", base, hashtag))
342 } else {
343 self.route(&format!("{}{}", base, hashtag))
344 };
345
346 self.get(url)
347 }
348
349 fn statuses<'a, 'b: 'a, S>(&'b self, id: &'b str, request: S) -> Result<Page<Status, H>>
392 where
393 S: Into<Option<StatusesRequest<'a>>>,
394 {
395 let mut url = format!("{}/api/v1/accounts/{}/statuses", self.base, id);
396
397 if let Some(request) = request.into() {
398 url = format!("{}{}", url, request.to_querystring()?);
399 }
400
401 let response = self.send(self.client.get(&url))?;
402
403 Page::new(self, response)
404 }
405
406 fn relationships(&self, ids: &[&str]) -> Result<Page<Relationship, H>> {
409 let mut url = self.route("/api/v1/accounts/relationships?");
410
411 if ids.len() == 1 {
412 url += "id=";
413 url += &ids[0];
414 } else {
415 for id in ids {
416 url += "id[]=";
417 url += &id;
418 url += "&";
419 }
420 url.pop();
421 }
422
423 let response = self.send(self.client.get(&url))?;
424
425 Page::new(self, response)
426 }
427
428 fn add_push_subscription(&self, request: &AddPushRequest) -> Result<Subscription> {
430 let request = request.build()?;
431 let response = self.send(
432 self.client
433 .post(&self.route("/api/v1/push/subscription"))
434 .json(&request),
435 )?;
436
437 deserialise(response)
438 }
439
440 fn update_push_data(&self, request: &UpdatePushRequest) -> Result<Subscription> {
443 let request = request.build();
444 let response = self.send(
445 self.client
446 .put(&self.route("/api/v1/push/subscription"))
447 .json(&request),
448 )?;
449
450 deserialise(response)
451 }
452
453 fn follows_me(&self) -> Result<Page<Account, H>> {
455 let me = self.verify_credentials()?;
456 Ok(self.followers(&me.id)?)
457 }
458
459 fn followed_by_me(&self) -> Result<Page<Account, H>> {
461 let me = self.verify_credentials()?;
462 Ok(self.following(&me.id)?)
463 }
464
465 fn streaming_user(&self) -> Result<Self::Stream> {
496 let mut url: url::Url = self.route("/api/v1/streaming").parse()?;
497 url.query_pairs_mut()
498 .append_pair("access_token", &self.token)
499 .append_pair("stream", "user");
500 let mut url: url::Url = reqwest::get(url.as_str())?.url().as_str().parse()?;
501 let new_scheme = match url.scheme() {
502 "http" => "ws",
503 "https" => "wss",
504 x => return Err(Error::Other(format!("Bad URL scheme: {}", x))),
505 };
506 url.set_scheme(new_scheme).map_err(|_| Error::Other("Bad URL scheme!".to_string()))?;
507
508 let client = tungstenite::connect(url.as_str())?.0;
509
510 Ok(EventReader(WebSocket(client)))
511 }
512
513 fn streaming_public(&self) -> Result<Self::Stream> {
515 let mut url: url::Url = self.route("/api/v1/streaming").parse()?;
516 url.query_pairs_mut()
517 .append_pair("access_token", &self.token)
518 .append_pair("stream", "public");
519 let mut url: url::Url = reqwest::get(url.as_str())?.url().as_str().parse()?;
520 let new_scheme = match url.scheme() {
521 "http" => "ws",
522 "https" => "wss",
523 x => return Err(Error::Other(format!("Bad URL scheme: {}", x))),
524 };
525 url.set_scheme(new_scheme).map_err(|_| Error::Other("Bad URL scheme!".to_string()))?;
526
527 let client = tungstenite::connect(url.as_str())?.0;
528
529 Ok(EventReader(WebSocket(client)))
530 }
531
532 fn streaming_local(&self) -> Result<Self::Stream> {
534 let mut url: url::Url = self.route("/api/v1/streaming").parse()?;
535 url.query_pairs_mut()
536 .append_pair("access_token", &self.token)
537 .append_pair("stream", "public:local");
538 let mut url: url::Url = reqwest::get(url.as_str())?.url().as_str().parse()?;
539 let new_scheme = match url.scheme() {
540 "http" => "ws",
541 "https" => "wss",
542 x => return Err(Error::Other(format!("Bad URL scheme: {}", x))),
543 };
544 url.set_scheme(new_scheme).map_err(|_| Error::Other("Bad URL scheme!".to_string()))?;
545
546 let client = tungstenite::connect(url.as_str())?.0;
547
548 Ok(EventReader(WebSocket(client)))
549 }
550
551 fn streaming_public_hashtag(&self, hashtag: &str) -> Result<Self::Stream> {
553 let mut url: url::Url = self.route("/api/v1/streaming").parse()?;
554 url.query_pairs_mut()
555 .append_pair("access_token", &self.token)
556 .append_pair("stream", "hashtag")
557 .append_pair("tag", hashtag);
558 let mut url: url::Url = reqwest::get(url.as_str())?.url().as_str().parse()?;
559 let new_scheme = match url.scheme() {
560 "http" => "ws",
561 "https" => "wss",
562 x => return Err(Error::Other(format!("Bad URL scheme: {}", x))),
563 };
564 url.set_scheme(new_scheme).map_err(|_| Error::Other("Bad URL scheme!".to_string()))?;
565
566 let client = tungstenite::connect(url.as_str())?.0;
567
568 Ok(EventReader(WebSocket(client)))
569 }
570
571 fn streaming_local_hashtag(&self, hashtag: &str) -> Result<Self::Stream> {
573 let mut url: url::Url = self.route("/api/v1/streaming").parse()?;
574 url.query_pairs_mut()
575 .append_pair("access_token", &self.token)
576 .append_pair("stream", "hashtag:local")
577 .append_pair("tag", hashtag);
578 let mut url: url::Url = reqwest::get(url.as_str())?.url().as_str().parse()?;
579 let new_scheme = match url.scheme() {
580 "http" => "ws",
581 "https" => "wss",
582 x => return Err(Error::Other(format!("Bad URL scheme: {}", x))),
583 };
584 url.set_scheme(new_scheme).map_err(|_| Error::Other("Bad URL scheme!".to_string()))?;
585
586 let client = tungstenite::connect(url.as_str())?.0;
587
588 Ok(EventReader(WebSocket(client)))
589 }
590
591 fn streaming_list(&self, list_id: &str) -> Result<Self::Stream> {
593 let mut url: url::Url = self.route("/api/v1/streaming").parse()?;
594 url.query_pairs_mut()
595 .append_pair("access_token", &self.token)
596 .append_pair("stream", "list")
597 .append_pair("list", list_id);
598 let mut url: url::Url = reqwest::get(url.as_str())?.url().as_str().parse()?;
599 let new_scheme = match url.scheme() {
600 "http" => "ws",
601 "https" => "wss",
602 x => return Err(Error::Other(format!("Bad URL scheme: {}", x))),
603 };
604 url.set_scheme(new_scheme).map_err(|_| Error::Other("Bad URL scheme!".to_string()))?;
605
606 let client = tungstenite::connect(url.as_str())?.0;
607
608 Ok(EventReader(WebSocket(client)))
609 }
610
611 fn streaming_direct(&self) -> Result<Self::Stream> {
613 let mut url: url::Url = self.route("/api/v1/streaming").parse()?;
614 url.query_pairs_mut()
615 .append_pair("access_token", &self.token)
616 .append_pair("stream", "direct");
617 let mut url: url::Url = reqwest::get(url.as_str())?.url().as_str().parse()?;
618 let new_scheme = match url.scheme() {
619 "http" => "ws",
620 "https" => "wss",
621 x => return Err(Error::Other(format!("Bad URL scheme: {}", x))),
622 };
623 url.set_scheme(new_scheme).map_err(|_| Error::Other("Bad URL scheme!".to_string()))?;
624
625 let client = tungstenite::connect(url.as_str())?.0;
626
627 Ok(EventReader(WebSocket(client)))
628 }
629}
630
631#[derive(Debug)]
632pub struct WebSocket(tungstenite::protocol::WebSocket<AutoStream>);
634
635pub trait EventStream {
637 fn read_message(&mut self) -> Result<String>;
639}
640
641impl<R: BufRead> EventStream for R {
642 fn read_message(&mut self) -> Result<String> {
643 let mut buf = String::new();
644 self.read_line(&mut buf)?;
645 Ok(buf)
646 }
647}
648
649impl EventStream for WebSocket {
650 fn read_message(&mut self) -> Result<String> {
651 Ok(self.0.read_message()?.into_text()?)
652 }
653}
654
655#[derive(Debug)]
656pub struct EventReader<R: EventStream>(R);
658impl<R: EventStream> Iterator for EventReader<R> {
659 type Item = Event;
660
661 fn next(&mut self) -> Option<Self::Item> {
662 let mut lines = Vec::new();
663 loop {
664 if let Ok(line) = self.0.read_message() {
665 let line = line.trim().to_string();
666 if line.starts_with(":") || line.is_empty() {
667 continue;
668 }
669 lines.push(line);
670 if let Ok(event) = self.make_event(&lines) {
671 lines.clear();
672 return Some(event);
673 } else {
674 continue;
675 }
676 }
677 }
678 }
679}
680
681impl<R: EventStream> EventReader<R> {
682 fn make_event(&self, lines: &[String]) -> Result<Event> {
683 let event;
684 let data;
685 if let Some(event_line) = lines
686 .iter()
687 .find(|line| line.starts_with("event:"))
688 {
689 event = event_line[6..].trim().to_string();
690 data = lines.iter().find(|line| line.starts_with("data:")).map(|x| x[5..].trim().to_string());
691 } else {
692 #[derive(Deserialize)]
693 struct Message {
694 pub event: String,
695 pub payload: Option<String>,
696 }
697 let message = serde_json::from_str::<Message>(&lines[0])?;
698 event = message.event;
699 data = message.payload;
700 }
701 let event: &str = &event;
702 Ok(match event {
703 "notification" => {
704 let data = data.ok_or_else(|| {
705 Error::Other("Missing `data` line for notification".to_string())
706 })?;
707 let notification = serde_json::from_str::<Notification>(&data)?;
708 Event::Notification(notification)
709 },
710 "update" => {
711 let data =
712 data.ok_or_else(|| Error::Other("Missing `data` line for update".to_string()))?;
713 let status = serde_json::from_str::<Status>(&data)?;
714 Event::Update(status)
715 },
716 "delete" => {
717 let data =
718 data.ok_or_else(|| Error::Other("Missing `data` line for delete".to_string()))?;
719 Event::Delete(data)
720 },
721 "filters_changed" => Event::FiltersChanged,
722 _ => return Err(Error::Other(format!("Unknown event `{}`", event))),
723 })
724 }
725}
726
727impl<H: HttpSend> ops::Deref for Mastodon<H> {
728 type Target = Data;
729
730 fn deref(&self) -> &Self::Target {
731 &self.data
732 }
733}
734
735struct MastodonBuilder<H: HttpSend> {
736 client: Option<Client>,
737 http_sender: H,
738 data: Option<Data>,
739}
740
741impl<H: HttpSend> MastodonBuilder<H> {
742 pub fn new(sender: H) -> Self {
743 MastodonBuilder {
744 http_sender: sender,
745 client: None,
746 data: None,
747 }
748 }
749
750 pub fn client(&mut self, client: Client) -> &mut Self {
751 self.client = Some(client);
752 self
753 }
754
755 pub fn data(&mut self, data: Data) -> &mut Self {
756 self.data = Some(data);
757 self
758 }
759
760 pub fn build(self) -> Result<Mastodon<H>> {
761 Ok(if let Some(data) = self.data {
762 Mastodon {
763 client: self.client.unwrap_or_else(|| Client::new()),
764 http_sender: self.http_sender,
765 data,
766 }
767 } else {
768 return Err(Error::MissingField("missing field 'data'"));
769 })
770 }
771}
772
773#[derive(Clone, Debug)]
775pub struct MastodonUnauth<H: HttpSend = HttpSender> {
776 client: Client,
777 http_sender: H,
778 base: url::Url,
779}
780
781impl MastodonUnauth<HttpSender> {
782 pub fn new(base: &str) -> Result<MastodonUnauth<HttpSender>> {
784 let base = if base.starts_with("https://") {
785 base.to_string()
786 } else {
787 format!("https://{}", base)
788 };
789 Ok(MastodonUnauth {
790 client: Client::new(),
791 http_sender: HttpSender,
792 base: url::Url::parse(&base)?,
793 })
794 }
795}
796
797impl<H: HttpSend> MastodonUnauth<H> {
798 fn route(&self, url: &str) -> Result<url::Url> {
799 Ok(self.base.join(url)?)
800 }
801
802 fn send(&self, req: RequestBuilder) -> Result<Response> {
803 Ok(self.http_sender.send(&self.client, req)?)
804 }
805}
806
807impl<H: HttpSend> MastodonUnauthenticated<H> for MastodonUnauth<H> {
808 fn get_status(&self, id: &str) -> Result<Status> {
810 let route = self.route("/api/v1/statuses")?;
811 let route = route.join(id)?;
812 let response = self.send(self.client.get(route))?;
813 deserialise(response)
814 }
815
816 fn get_context(&self, id: &str) -> Result<Context> {
818 let route = self.route("/api/v1/statuses")?;
819 let route = route.join(id)?;
820 let route = route.join("context")?;
821 let response = self.send(self.client.get(route))?;
822 deserialise(response)
823 }
824
825 fn get_card(&self, id: &str) -> Result<Card> {
827 let route = self.route("/api/v1/statuses")?;
828 let route = route.join(id)?;
829 let route = route.join("card")?;
830 let response = self.send(self.client.get(route))?;
831 deserialise(response)
832 }
833}
834
835fn deserialise<T: for<'de> serde::Deserialize<'de>>(response: Response) -> Result<T> {
838 let mut reader = Tap::new(response);
839
840 match serde_json::from_reader(&mut reader) {
841 Ok(t) => {
842 debug!("{}", String::from_utf8_lossy(&reader.bytes));
843 Ok(t)
844 },
845 Err(e) => {
848 error!("{}", String::from_utf8_lossy(&reader.bytes));
849 if let Ok(error) = serde_json::from_slice(&reader.bytes) {
850 return Err(Error::Api(error));
851 }
852 Err(e.into())
853 },
854 }
855}