summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAlex Crichton <alex@alexcrichton.com>2018-11-01 20:08:59 -0700
committerAlex Crichton <alex@alexcrichton.com>2018-11-01 20:08:59 -0700
commit79a49bb29ab606f908c19ebeeb8611fbf69e611f (patch)
tree90250243545521856ec412bba306b36d36285a07 /src
Initial commit
Diffstat (limited to 'src')
-rw-r--r--src/main.rs269
1 files changed, 269 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..49846da
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,269 @@
+use std::cell::RefCell;
+use std::collections::{HashMap, HashSet};
+use std::env;
+use std::fs;
+use std::str;
+
+use curl::easy::{Easy, Form};
+use failure::{bail, format_err, Error, ResultExt};
+
+#[derive(serde_derive::Deserialize)]
+struct Mailmap {
+ lists: Vec<List>,
+}
+
+#[derive(serde_derive::Deserialize)]
+struct List {
+ address: String,
+ access_level: String,
+ members: Vec<String>,
+}
+
+mod api {
+ #[derive(serde_derive::Deserialize)]
+ pub struct ListResponse {
+ pub items: Vec<List>,
+ pub paging: Paging,
+ }
+
+ #[derive(serde_derive::Deserialize)]
+ pub struct List {
+ pub access_level: String,
+ pub address: String,
+ pub members_count: u64,
+ }
+
+ #[derive(serde_derive::Deserialize)]
+ pub struct Paging {
+ pub first: String,
+ pub last: String,
+ pub next: String,
+ pub previous: String,
+ }
+
+ #[derive(serde_derive::Deserialize)]
+ pub struct MembersResponse {
+ pub items: Vec<Member>,
+ pub paging: Paging,
+ }
+
+ #[derive(serde_derive::Deserialize)]
+ pub struct Member {
+ pub address: String,
+ }
+}
+
+#[derive(serde_derive::Deserialize)]
+struct Empty {}
+
+fn main() {
+ env_logger::init();
+ if let Err(e) = run() {
+ eprintln!("error: {}", e);
+ for e in e.iter_causes() {
+ eprintln!(" cause: {}", e);
+ }
+ std::process::exit(1);
+ }
+}
+
+fn run() -> Result<(), Error> {
+ let mailmap = fs::read_to_string("mailmap.toml")
+ .with_context(|_| "failed to read `mailmap.toml`")?;
+
+ let mailmap: Mailmap = toml::from_str(&mailmap)
+ .with_context(|_| "failed to deserialize toml mailmap")?;
+
+ let mut lists = Vec::new();
+ let mut response = get::<api::ListResponse>("/lists/pages")?;
+ while response.items.len() > 0 {
+ lists.extend(response.items);
+ response = get::<api::ListResponse>(&response.paging.next)?;
+ }
+
+ let mut addr2list = HashMap::new();
+ for list in mailmap.lists.iter() {
+ if addr2list.insert(&list.address, list).is_some() {
+ bail!("duplicate address: {}", list.address);
+ }
+ }
+
+ for prev_list in lists {
+ let address = &prev_list.address;
+ match addr2list.remove(address) {
+ Some(new_list) => {
+ sync(&prev_list, &new_list)
+ .with_context(|_| format!("failed to sync {}", address))?
+ }
+ None => {
+ del(&prev_list)
+ .with_context(|_| format!("failed to delete {}", address))?
+ }
+ }
+ }
+
+ for (_, list) in addr2list.iter() {
+ create(list)
+ .with_context(|_| format!("failed to create {}", list.address))?;
+ }
+
+ Ok(())
+}
+
+fn create(new: &List) -> Result<(), Error> {
+ let mut form = Form::new();
+ form.part("address").contents(new.address.as_bytes()).add()?;
+ form.part("access_level").contents(new.access_level.as_bytes()).add()?;
+ post::<Empty>("/lists", form)?;
+
+ add_members(&new.address, &new.members)?;
+ Ok(())
+}
+
+fn sync(prev: &api::List, new: &List) -> Result<(), Error> {
+ assert_eq!(prev.address, new.address);
+ let url = format!("/lists/{}", prev.address);
+ if prev.access_level != new.access_level {
+ let mut form = Form::new();
+ form.part("access_level").contents(new.access_level.as_bytes()).add()?;
+ put::<Empty>(&url, form)?;
+ }
+
+ let url = format!("{}/members/pages", url);
+ let mut prev_members = HashSet::new();
+ let mut response = get::<api::MembersResponse>(&url)?;
+ while response.items.len() > 0 {
+ prev_members.extend(response.items.into_iter().map(|member| member.address));
+ response = get::<api::MembersResponse>(&response.paging.next)?;
+ }
+
+ let mut to_add = Vec::new();
+ for member in new.members.iter() {
+ if !prev_members.remove(member) {
+ to_add.push(member.clone());
+ }
+ }
+
+ if to_add.len() > 0 {
+ add_members(&new.address, &to_add)?;
+ }
+ for member in prev_members {
+ delete::<Empty>(&format!("/lists/{}/members/{}", new.address, member))?;
+ }
+
+ Ok(())
+}
+
+fn add_members(address: &str, members: &[String]) -> Result<(), Error> {
+ let url = format!("/lists/{}/members.json", address);
+ let data = serde_json::to_string(members)?;
+ let mut form = Form::new();
+ form.part("members").contents(data.as_bytes()).add()?;
+ post::<Empty>(&url, form)?;
+ Ok(())
+}
+
+fn del(prev: &api::List) -> Result<(), Error> {
+ delete::<Empty>(&format!("/lists/{}", prev.address))?;
+ Ok(())
+}
+
+fn get<T: for<'de> serde::Deserialize<'de>>(url: &str) -> Result<T, Error> {
+ execute(url, Method::Get)
+}
+
+fn post<T: for<'de> serde::Deserialize<'de>>(
+ url: &str,
+ form: Form,
+) -> Result<T, Error> {
+ execute(url, Method::Post(form))
+}
+
+fn put<T: for<'de> serde::Deserialize<'de>>(
+ url: &str,
+ form: Form,
+) -> Result<T, Error> {
+ execute(url, Method::Put(form))
+}
+
+fn delete<T: for<'de> serde::Deserialize<'de>>(url: &str) -> Result<T, Error> {
+ execute(url, Method::Delete)
+}
+
+enum Method {
+ Get,
+ Delete,
+ Post(Form),
+ Put(Form),
+}
+
+fn execute<T: for<'de> serde::Deserialize<'de>>(
+ url: &str,
+ method: Method,
+) -> Result<T, Error> {
+ thread_local!(static HANDLE: RefCell<Easy> = RefCell::new(Easy::new()));
+ let password = env::var("MAILGUN_API_TOKEN")
+ .map_err(|_| format_err!("must set $MAILGUN_API_TOKEN"))?;
+ let result = HANDLE.with(|handle| {
+ let mut handle = handle.borrow_mut();
+ handle.reset();
+ let url = if url.starts_with("https://") {
+ url.to_string()
+ } else {
+ format!("https://api.mailgun.net/v3{}", url)
+ };
+ handle.url(&url)?;
+ match method {
+ Method::Get => {
+ log::debug!("GET {}", url);
+ handle.get(true)?;
+ }
+ Method::Delete => {
+ log::debug!("DELETE {}", url);
+ handle.custom_request("DELETE")?;
+ }
+ Method::Post(form) => {
+ log::debug!("POST {}", url);
+ handle.httppost(form)?;
+ }
+ Method::Put(form) => {
+ log::debug!("PUT {}", url);
+ handle.httppost(form)?;
+ handle.custom_request("PUT")?;
+ }
+ }
+ handle.username("api")?;
+ handle.password(&password)?;
+ handle.useragent("rust-lang/rust membership update")?;
+ // handle.verbose(true)?;
+ let mut result = Vec::new();
+ let mut headers = Vec::new();
+ {
+ let mut transfer = handle.transfer();
+ transfer.write_function(|data| {
+ result.extend_from_slice(data);
+ Ok(data.len())
+ })?;
+ transfer.header_function(|header| {
+ if let Ok(s) = str::from_utf8(header) {
+ headers.push(s.to_string());
+ }
+ true
+ })?;
+ transfer.perform()?;
+ }
+
+ let result = String::from_utf8(result)
+ .map_err(|_| format_err!("response was invalid utf-8"))?;
+
+ log::trace!("headers: {:#?}", headers);
+ log::trace!("json: {}", result);
+ let code = handle.response_code()?;
+ if code != 200 {
+ bail!("failed to get a 200 code, got {}\n\n{}", code, result)
+ }
+ Ok(serde_json::from_str(&result)
+ .with_context(|_| "failed to parse json response")?)
+ });
+ Ok(result.with_context(|_| format!("failed to send request to {}", url))?)
+}