├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples └── ping.rs └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mars2" 3 | version = "0.1.0" 4 | authors = ["Ticki "] 5 | description = "A Mattermost and Slack API wrapper." 6 | repository = "https://github.com/ticki/mars" 7 | documentation = "https://docs.rs/crate/mars" 8 | license = "MIT" 9 | keywords = ["mattermost", "slack", "api", "chat", "bot"] 10 | exclude = ["target", "Cargo.lock"] 11 | 12 | [dependencies] 13 | hyper = "0" 14 | serde = "0" 15 | serde_json = "0" 16 | serde_macros = "0" 17 | extra = "0" 18 | 19 | [dependencies.url] 20 | version = "1" 21 | features = ["query_encoding"] 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ticki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Mars 2 | ==== 3 | 4 | **Ma**ttermost API wrapper in **R**u**s**t. 5 | 6 | Mars is used mainly for writing Mattermost and Slack chatbots. 7 | 8 | Usage 9 | ----- 10 | 11 | See the documentation and `examples/`. 12 | 13 | Examples 14 | -------- 15 | 16 | Check out [Playbot](https://github.com/redox-os/playbot), the friendly Rust running bot. 17 | -------------------------------------------------------------------------------- /examples/ping.rs: -------------------------------------------------------------------------------- 1 | extern crate mars; 2 | 3 | use mars::{Bot, Response}; 4 | 5 | fn main() { 6 | Bot::new("TOKENHERE", |_| Response { 7 | username: Some("pong-bot".into()), 8 | text: "pong".into(), 9 | icon_url: Some("https://i.ytimg.com/vi/dQw4w9WgXcQ/mqdefault.jpg".into()), 10 | }).init("127.0.0.1:80").unwrap(); 11 | } 12 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(custom_derive, plugin)] 2 | #![plugin(serde_macros)] 3 | 4 | extern crate extra; 5 | extern crate hyper; 6 | extern crate url; 7 | extern crate serde; 8 | extern crate serde_json as json; 9 | 10 | use url::form_urlencoded as post; 11 | 12 | use extra::option::OptionalExt; 13 | 14 | use std::borrow::Cow; 15 | use std::io::{self, Read, Write}; 16 | 17 | /// A Mattermost request. 18 | /// 19 | /// These are often sent by the Mattermost server, due to being triggered by a slash command or 20 | /// a keyword. To see how to configure it, see the [Mattermost 21 | /// docs](http://docs.mattermost.com/developer/webhooks-outgoing.html). 22 | pub struct Request<'a> { 23 | /// The alphanumeric channel identifier. 24 | pub channel_id: Cow<'a, str>, 25 | /// The name of the channel. 26 | pub channel_name: Cow<'a, str>, 27 | /// The domain name of the team. 28 | pub team_domain: Cow<'a, str>, 29 | /// The alphanumeric team identifier. 30 | pub team_id: Cow<'a, str>, 31 | /// The text message payload. 32 | pub text: Cow<'a, str>, 33 | /// The timestamp. 34 | pub timestamp: Cow<'a, str>, 35 | /// The API token. 36 | pub token: Cow<'a, str>, 37 | /// The trigger of this request. 38 | pub trigger: Cow<'a, str>, 39 | /// The trigger user's alphanumeric identifier. 40 | pub user_id: Cow<'a, str>, 41 | /// The trigger user's username. 42 | pub username: Cow<'a, str>, 43 | } 44 | 45 | impl<'a> Default for Request<'a> { 46 | fn default() -> Request<'a> { 47 | Request { 48 | channel_id: Cow::Borrowed(""), 49 | channel_name: Cow::Borrowed(""), 50 | team_domain: Cow::Borrowed(""), 51 | team_id: Cow::Borrowed(""), 52 | text: Cow::Borrowed(""), 53 | timestamp: Cow::Borrowed(""), 54 | token: Cow::Borrowed(""), 55 | trigger: Cow::Borrowed(""), 56 | user_id: Cow::Borrowed(""), 57 | username: Cow::Borrowed(""), 58 | } 59 | } 60 | } 61 | 62 | impl<'a> Request<'a> { 63 | fn from_bytes(b: &[u8]) -> Request { 64 | let mut req = Request::default(); 65 | let query = post::parse(b); 66 | 67 | for (a, b) in query { 68 | match &*a { 69 | "channel_id" => req.channel_id = b, 70 | "channel_name" => req.channel_name = b, 71 | "team_domain" => req.team_domain = b, 72 | "team_id" => req.team_id = b, 73 | "text" => req.text = b, 74 | "timestamp" => req.timestamp = b, 75 | "token" => req.token = b, 76 | "trigger_word" => req.trigger = b, 77 | "user_id" => req.user_id = b, 78 | "user_name" => req.username = b, 79 | _ => (), 80 | } 81 | } 82 | 83 | req 84 | } 85 | } 86 | 87 | /// The response to the request. 88 | #[derive(Serialize)] 89 | pub struct Response<'a> { 90 | /// The bot's username. 91 | #[serde(skip_serializing_if="Option::is_none")] 92 | pub username: Option,>, 93 | /// The payload text. 94 | pub text: Cow<'a, str>, 95 | /// The URL to the bot's avatar. 96 | #[serde(skip_serializing_if="Option::is_none")] 97 | pub icon_url: Option>, 98 | } 99 | 100 | impl<'a> Response<'a> { 101 | fn send(self, mut to: W) -> io::Result<()> { 102 | match json::to_writer(&mut to, &self) { 103 | Ok(()) => Ok(()), 104 | Err(json::Error::Io(x)) => Err(x), 105 | Err(x) => Err(io::Error::new(io::ErrorKind::InvalidData, x)), 106 | } 107 | } 108 | } 109 | 110 | /// A Mattermost bot. 111 | pub struct Bot { 112 | handler: F, 113 | token: &'static str, 114 | } 115 | 116 | impl<'a, F> Bot where F: 'static + Sync + Send + Fn(Request) -> Response<'a> { 117 | /// Create a new bot with a given handler. 118 | pub fn new(token: &'static str, handler: F) -> Bot { 119 | Bot { 120 | handler: handler, 121 | token: token, 122 | } 123 | } 124 | 125 | /// Initialize the bot. 126 | pub fn init(self, ip: &str) -> Result<(), hyper::Error> { 127 | try!(try!(hyper::Server::http(ip)).handle(move |mut req: hyper::server::Request, mut res: hyper::server::Response| { 128 | let mut stderr = io::stderr(); 129 | let mut vec = Vec::new(); 130 | 131 | if let None = req.read_to_end(&mut vec).warn(&mut stderr) { 132 | *res.status_mut() = hyper::BadRequest; 133 | return; 134 | } 135 | 136 | let trig = Request::from_bytes(&vec); 137 | if &trig.token != self.token { 138 | let _ = write!(stderr, "warning: token mismatch."); 139 | 140 | // Token mismatch. 141 | *res.status_mut() = hyper::status::StatusCode::Unauthorized; 142 | return; 143 | } 144 | 145 | 146 | if let Some(res) = res.start().warn(&mut stderr) { 147 | (self.handler)(trig).send(res).warn(&mut stderr); 148 | } 149 | })); 150 | 151 | Ok(()) 152 | } 153 | } 154 | --------------------------------------------------------------------------------