Browse Source

Initial commit

master
Aaron Power 5 years ago
commit
36ae371beb
  1. 2
      .gitignore
  2. 20
      Cargo.toml
  3. 13
      LICENCE-APACHE
  4. 21
      LICENCE-MIT
  5. 5
      README.md
  6. 32
      src/apps.rs
  7. 17
      src/entities/account.rs
  8. 20
      src/entities/attachment.rs
  9. 7
      src/entities/card.rs
  10. 7
      src/entities/context.rs
  11. 7
      src/entities/instance.rs
  12. 28
      src/entities/mod.rs
  13. 20
      src/entities/notification.rs
  14. 8
      src/entities/relationship.rs
  15. 5
      src/entities/report.rs
  16. 8
      src/entities/search_result.rs
  17. 49
      src/entities/status.rs
  18. 374
      src/lib.rs
  19. 42
      src/status_builder.rs

2
.gitignore

@ -0,0 +1,2 @@
target
Cargo.lock

20
Cargo.toml

@ -0,0 +1,20 @@
[package]
name = "mammut"
version = "0.1.0"
description = "A wrapper around the Mastodon API."
authors = ["Aaron Power <theaaronepower@gmail.com>"]
license = "MIT/Apache-2.0"
readme = "README.md"
repository = "https://github.com/Aaronepower/mastodon.rs.git"
keywords = ["api", "web", "social", "mastodon", "wrapper"]
categories = ["web-programming", "http-client"]
[dependencies]
reqwest = "0.5"
serde = "0.9"
serde_json = "0.9"
serde_derive = "0.9"
[dependencies.chrono]
version = "0.3"
features = ["serde"]

13
LICENCE-APACHE

@ -0,0 +1,13 @@
Copyright 2016 Aaron Power
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

21
LICENCE-MIT

@ -0,0 +1,21 @@
MIT License (MIT)
Copyright (c) 2016 Aaron Power
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

5
README.md

@ -0,0 +1,5 @@
# Mammut. A API Wrapper for the Mastodon API.
### [Documentation](https://docs.rs/mastodon)
A wrapper around the [API](https://github.com/tootsuite/mastodon/blob/master/docs/Using-the-API/API.md#tag) for [Mastodon](https://mastodon.social/)

32
src/apps.rs

@ -0,0 +1,32 @@
#[derive(Debug, Default, Serialize)]
pub struct AppBuilder<'a> {
pub client_name: &'a str,
pub redirect_uris: &'a str,
pub scopes: Scope,
#[serde(skip_serializing_if="Option::is_none")]
pub website: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, Serialize)]
pub enum Scope {
#[serde(rename = "read write follow")]
All,
#[serde(rename = "follow")]
Follow,
#[serde(rename = "read")]
Read,
#[serde(rename = "read follow")]
ReadFollow,
#[serde(rename = "read write")]
ReadWrite,
#[serde(rename = "write")]
Write,
#[serde(rename = "write follow")]
WriteFollow,
}
impl Default for Scope {
fn default() -> Self {
Scope::Read
}
}

17
src/entities/account.rs

@ -0,0 +1,17 @@
use chrono::prelude::*;
#[derive(Deserialize)]
pub struct Account {
pub id: u64,
pub username: String,
pub acct: String,
pub display_name: String,
pub note: String,
pub url: String,
pub avatar: String,
pub header: String,
pub locked: bool,
pub created_at: DateTime<UTC>,
pub followers_count: u64,
pub following_count: u64,
pub statuses_count: u64,
}

20
src/entities/attachment.rs

@ -0,0 +1,20 @@
#[derive(Debug, Clone, Deserialize)]
pub struct Attachment {
pub id: u64,
#[serde(rename="type")]
pub media_type: MediaType,
pub url: String,
pub remote_url: String,
pub preview_url: String,
pub text_url: String,
}
#[derive(Debug, Deserialize, Clone, Copy)]
pub enum MediaType {
#[serde(rename = "image")]
Image,
#[serde(rename = "video")]
Video,
#[serde(rename = "gifv")]
Gifv,
}

7
src/entities/card.rs

@ -0,0 +1,7 @@
#[derive(Deserialize)]
pub struct Card {
pub url: String,
pub title: String,
pub description: String,
pub image: String,
}

7
src/entities/context.rs

@ -0,0 +1,7 @@
use super::status::Status;
#[derive(Deserialize)]
pub struct Context {
pub ancestors: Vec<Status>,
pub descendants: Vec<Status>,
}

7
src/entities/instance.rs

@ -0,0 +1,7 @@
#[derive(Deserialize)]
pub struct Instance {
pub uri: String,
pub title: String,
pub description: String,
pub email: String,
}

28
src/entities/mod.rs

@ -0,0 +1,28 @@
pub mod account;
pub mod attachment;
pub mod card;
pub mod context;
pub mod instance;
pub mod notification;
pub mod relationship;
pub mod report;
pub mod search_result;
pub mod status;
/// An empty JSON object.
#[derive(Deserialize)]
pub struct Empty {}
pub mod prelude {
pub use super::Empty;
pub use super::account::Account;
pub use super::attachment::{Attachment, MediaType};
pub use super::card::Card;
pub use super::context::Context;
pub use super::instance::Instance;
pub use super::notification::Notification;
pub use super::relationship::Relationship;
pub use super::report::Report;
pub use super::search_result::SearchResult;
pub use super::status::{Status, Application};
}

20
src/entities/notification.rs

@ -0,0 +1,20 @@
use chrono::prelude::*;
use super::account::Account;
use super::status::Status;
#[derive(Deserialize)]
pub struct Notification {
pub id: u64,
pub notification_type: NotificationType,
pub created_at: DateTime<UTC>,
pub account: Account,
pub status: Option<Status>,
}
#[derive(Deserialize)]
pub enum NotificationType {
Mention,
Reblog,
Favourite,
Follow,
}

8
src/entities/relationship.rs

@ -0,0 +1,8 @@
#[derive(Deserialize)]
pub struct Relationship {
pub following: bool,
pub followed_by: bool,
pub blocking: bool,
pub muting: bool,
pub requested: bool,
}

5
src/entities/report.rs

@ -0,0 +1,5 @@
#[derive(Deserialize)]
pub struct Report {
pub id: u64,
pub action_taken: String,
}

8
src/entities/search_result.rs

@ -0,0 +1,8 @@
use super::prelude::{Account, Status};
#[derive(Deserialize)]
pub struct SearchResult {
pub accounts: Vec<Account>,
pub statuses: Vec<Status>,
pub hashtags: Vec<String>,
}

49
src/entities/status.rs

@ -0,0 +1,49 @@
use chrono::prelude::*;
use super::prelude::*;
use status_builder::Visibility;
#[derive(Deserialize)]
pub struct Status {
pub id: i64,
pub uri: String,
pub url: String,
pub account: Account,
pub in_reply_to_id: Option<u64>,
pub in_reply_to_account_id: Option<u64>,
pub reblog: Option<Box<Status>>,
pub content: String,
pub created_at: DateTime<UTC>,
pub reblogs_count: u64,
pub favourites_count: u64,
pub reblogged: bool,
pub favourited: bool,
pub sensitive: bool,
pub spoiler_text: String,
pub visibility: Visibility,
pub media_attachments: Vec<Attachment>,
pub mentions: Vec<Mention>,
pub tags: Vec<Tag>,
pub application: Application,
}
#[derive(Deserialize)]
pub struct Mention {
pub url: String,
pub username: String,
pub acct: String,
pub id: u64,
}
#[derive(Deserialize)]
pub struct Tag {
pub name: String,
pub url: String,
}
#[derive(Deserialize)]
pub struct Application {
pub name: String,
pub website: String,
}

374
src/lib.rs

@ -0,0 +1,374 @@
//! # Mammut: API Wrapper around the Mastodon API.
//!
//! Most of the api is documented on [Mastodon's
//! github](https://github.com/tootsuite/mastodon/blob/master/docs/Using-the-API/API.md#tag)
#![deny(unused_must_use)]
#[cfg_attr(test, deny(warnings))]
#[macro_use] extern crate serde_derive;
#[macro_use] extern crate serde_json as json;
extern crate chrono;
extern crate reqwest;
extern crate serde;
pub mod apps;
pub mod status_builder;
pub mod entities;
use json::Error as SerdeError;
use reqwest::Error as HttpError;
use reqwest::Client;
use reqwest::header::{Authorization, Bearer, Headers};
use entities::prelude::*;
use status_builder::StatusBuilder;
pub type Result<T> = std::result::Result<T, Error>;
macro_rules! methods {
($($method:ident,)+) => {
$(
fn $method<T: serde::Deserialize>(&self, url: String)
-> Result<T>
{
Ok(self.client.$method(&url)
.headers(self.access_token.clone().unwrap())
.send()?
.json()?)
}
)+
};
}
macro_rules! route {
((post ($($param:ident: $typ:ty,)*)) $name:ident: $url:expr => $ret:ty, $($rest:tt)*) => {
/// Requires `access_token` or will return error.
pub fn $name(&self, $($param: $typ,)*) -> Result<$ret> {
self.has_access_token()?;
let form_data = json!({
$(
stringify!($param): $param,
)*
});
Ok(self.client.post(&self.route(concat!("/api/v1/", $url)))
.headers(self.access_token.clone().unwrap())
.form(&form_data)
.send()?
.json()?)
}
route!{$($rest)*}
};
(($method:ident) $name:ident: $url:expr => $ret:ty, $($rest:tt)*) => {
/// Requires `access_token` or will return error.
pub fn $name(&self) -> Result<$ret> {
self.has_access_token()?;
self.$method(self.route(concat!("/api/v1/", $url)))
}
route!{$($rest)*}
};
() => {}
}
macro_rules! route_id {
($(($method:ident) $name:ident: $url:expr => $ret:ty,)*) => {
$(
/// Requires `access_token` or will return error.
pub fn $name(&self, id: u64) -> Result<$ret> {
self.has_access_token()?;
self.$method(self.route(&format!(concat!("/api/v1/", $url), id)))
}
)*
}
}
#[derive(Clone, Debug)]
pub struct Mastodon {
base_url: String,
client: Client,
client_id: Option<String>,
client_secret: Option<String>,
redirect_uri: Option<String>,
access_token: Option<Headers>,
id: Option<u64>,
}
#[derive(Deserialize)]
struct OAuth {
client_id: String,
client_secret: String,
id: u64,
redirect_uri: String,
}
#[derive(Debug)]
pub enum Error {
Serde(SerdeError),
Http(HttpError),
ClientIdRequired,
ClientSecretRequired,
AccessTokenRequired,
}
impl Mastodon {
/// Inits new Mastodon object. `base_url` is expected in the following
/// format `https://mastodon.social` with no leading forward slash.
///
/// ```
/// use mammut::Mastodon;
///
/// let mastodon = Mastodon::new("https://mastodon.social").unwrap();
/// ```
pub fn new<I: Into<String>>(base_url: I) -> Result<Self> {
Ok(Mastodon {
base_url: base_url.into(),
client: Client::new()?,
client_id: None,
client_secret: None,
redirect_uri: None,
access_token: None,
id: None,
})
}
/// Register the application with the server from `base_url`.
///
/// ```
/// # extern crate mammut;
/// # fn main() {
/// # try().unwrap();
/// # }
///
/// # fn try() -> mammut::Result<()> {
/// use mammut::Mastodon;
/// use mammut::apps::{AppBuilder, Scope};
///
/// let app = AppBuilder {
/// client_name: "mammut_test",
/// redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
/// scopes: Scope::Read,
/// website: None,
/// };
///
/// let mut mastodon = Mastodon::new("https://mastodon.social")?;
/// mastodon.register(app)?;
/// # Ok(())
/// # }
/// ```
pub fn register(&mut self, app_builder: apps::AppBuilder) -> Result<()> {
let url = self.route("/api/v1/apps");
let app: OAuth = self.client.post(&url).form(&app_builder).send()?.json()?;
self.id = Some(app.id);
self.client_id = Some(app.client_id);
self.client_secret = Some(app.client_secret);
self.redirect_uri = Some(app.redirect_uri);
Ok(())
}
/// Returns the full url needed for authorisation. This needs to be opened
/// in a browser.
pub fn authorise(&mut self) -> Result<String> {
self.is_registered()?;
let url = format!(
"{}/oauth/authorize?client_id={}&redirect_uri={}&response_type=code",
self.base_url,
self.client_id.clone().unwrap(),
self.redirect_uri.clone().unwrap(),
);
Ok(url)
}
/// Set `access_token` required to use any method about the user.
pub fn set_access_token(&mut self, access_token: String) {
let mut headers = Headers::new();
headers.set(Authorization(Bearer { token: access_token }));
self.access_token = Some(headers);
}
route! {
(get) verify: "accounts/verify_credentials" => Account,
(get) blocks: "blocks" => Vec<Account>,
(get) follow_requests: "follow_requests" => Vec<Account>,
(get) mutes: "mutes" => Vec<Account>,
(get) notifications: "notifications" => Vec<Notification>,
(get) reports: "reports" => Vec<Report>,
(get) get_home_timeline: "timelines/home" => Vec<Status>,
(post (id: u64,)) allow_follow_request: "accounts/follow_requests/authorize" => Empty,
(post (id: u64,)) reject_follow_request: "accounts/follow_requests/reject" => Empty,
(post (uri: String,)) follows: "follows" => Account,
(post) clear_notifications: "notifications/clear" => Empty,
(post (file: Vec<u8>,)) media: "media" => Attachment,
(post (account_id: u64, status_ids: Vec<u64>, comment: String,)) report:
"reports" => Report,
(post (q: String, resolve: bool,)) search: "search" => SearchResult,
(post (status: StatusBuilder,)) new_status: "statuses" => Status,
}
route_id! {
(get) get_account: "accounts/{}" => Account,
(get) followers: "accounts/{}/followers" => Vec<Account>,
(get) following: "accounts/{}/following" => Vec<Account>,
(get) follow: "accounts/{}/follow" => Account,
(get) unfollow: "accounts/{}/unfollow" => Account,
(get) block: "accounts/{}/block" => Account,
(get) unblock: "accounts/{}/unblock" => Account,
(get) mute: "accounts/{}/mute" => Account,
(get) unmute: "accounts/{}/unmute" => Account,
(get) get_notification: "notifications/{}" => Notification,
(get) get_status: "statuses/{}" => Status,
(get) get_context: "statuses/{}/context" => Context,
(get) get_card: "statuses/{}/card" => Card,
(get) reblogged_by: "statuses/{}/reblogged_by" => Vec<Account>,
(get) favourited_by: "statuses/{}/favourited_by" => Vec<Account>,
(post) reblog: "statuses/{}/reblog" => Status,
(post) unreblog: "statuses/{}/unreblog" => Status,
(post) favourite: "statuses/{}/favourite" => Status,
(post) unfavourite: "statuses/{}/unfavourite" => Status,
(delete) delete_status: "statuses/{}" => Empty,
}
pub fn get_public_timeline(&self, local: bool) -> Result<Vec<Status>> {
self.has_access_token()?;
let mut url = self.route("/api/v1/timelines/public");
if local {
url += "?local=1";
}
self.get(url)
}
pub fn get_tagged_timeline(&self, hashtag: String, local: bool) -> Result<Vec<Status>> {
self.has_access_token()?;
let mut url = self.route("/api/v1/timelines/tag/");
url += &hashtag;
if local {
url += "?local=1";
}
self.get(url)
}
pub fn statuses(&self, id: u64, only_media: bool, exclude_replies: bool)
-> Result<Vec<Status>>
{
self.has_access_token()?;
let mut url = format!("{}/api/v1/accounts/{}/statuses", self.base_url, id);
if only_media {
url += "?only_media=1";
}
if exclude_replies {
url += if only_media {
"&"
} else {
"?"
};
url += "exclude_replies=1";
}
self.get(url)
}
pub fn relationships(&self, ids: &[u64]) -> Result<Vec<Relationship>> {
self.has_access_token()?;
let mut url = self.route("/api/v1/accounts/relationships?");
if ids.len() == 1 {
url += "id=";
url += &ids[0].to_string();
} else {
for id in ids {
url += "id[]=";
url += &id.to_string();
url += "&";
}
url.pop();
}
self.get(url)
}
// TODO: Add a limit fn
pub fn search_accounts(&self, query: &str) -> Result<Vec<Account>> {
self.has_access_token()?;
self.get(format!("{}/api/v1/accounts/search?q={}", self.base_url, query))
}
pub fn instance(&self) -> Result<Instance> {
self.is_registered()?;
self.get(self.route("/api/v1/instance"))
}
fn has_access_token(&self) -> Result<()> {
if self.access_token.is_none() {
Err(Error::AccessTokenRequired)
} else {
Ok(())
}
}
fn is_registered(&self) -> Result<()> {
if self.client_id.is_none() {
Err(Error::ClientIdRequired)
} else if self.client_secret.is_none() {
Err(Error::ClientSecretRequired)
} else {
Ok(())
}
}
methods![get, post, delete,];
fn route(&self, url: &str) -> String {
let mut s = self.base_url.clone();
s += url;
s
}
}
macro_rules! from {
($($typ:ident, $variant:ident,)*) => {
$(
impl From<$typ> for Error {
fn from(from: $typ) -> Self {
use Error::*;
$variant(from)
}
}
)*
}
}
from! {
SerdeError, Serde,
HttpError, Http,
}

42
src/status_builder.rs

@ -0,0 +1,42 @@
#[derive(Debug, Default, Clone, Serialize)]
pub struct StatusBuilder {
status: String,
#[serde(skip_serializing_if="Option::is_none")]
in_reply_to_id: Option<u64>,
#[serde(skip_serializing_if="Option::is_none")]
media_ids: Option<Vec<u64>>,
#[serde(skip_serializing_if="Option::is_none")]
sensitive: Option<bool>,
#[serde(skip_serializing_if="Option::is_none")]
spoiler_text: Option<String>,
#[serde(skip_serializing_if="Option::is_none")]
visibility: Option<Visibility>,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
pub enum Visibility {
#[serde(rename = "direct")]
Direct,
#[serde(rename = "private")]
Private,
#[serde(rename = "unlisted")]
Unlisted,
#[serde(rename = "public")]
Public,
}
impl StatusBuilder {
pub fn new(status: String) -> Self {
StatusBuilder {
status: status,
..Self::default()
}
}
}
impl Default for Visibility {
fn default() -> Self {
Visibility::Public
}
}
Loading…
Cancel
Save