parent
702c701fd6
commit
37c1b7a9f8
@ -0,0 +1 @@ |
||||
target |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,14 @@ |
||||
[package] |
||||
name = "unionbot" |
||||
description = "A mastodon bot that reminds you periodically to join a union" |
||||
version = "0.1.0" |
||||
authors = ["Katharina Fey <kookie@spacekookie.de>"] |
||||
edition = "2018" |
||||
|
||||
[dependencies] |
||||
serde = { version = "1.0", features = ["derive"] } |
||||
elefren = "0.22" |
||||
toml = "0.5" |
||||
log = "0.4" |
||||
env_logger = "0.8" |
||||
colored = "2.0" |
@ -0,0 +1,19 @@ |
||||
# unionbot |
||||
|
||||
A mastodon bot that reminds you periodically to join a union, with |
||||
crowd-sourced suggestions. |
||||
|
||||
*TODO: insert screenshots here* |
||||
|
||||
## How to configure |
||||
|
||||
`unionbot` can be built with [`cargo`]. The main configuration is |
||||
provided via the `acab.toml` configuration file. Following is an |
||||
example. |
||||
|
||||
```toml |
||||
instance = "botsin.space" |
||||
username = "unionbot" |
||||
secret = "some-secrets-are-meant-to-be-committed" |
||||
unions = "unions/en.toml |
||||
``` |
@ -0,0 +1,6 @@ |
||||
instance = "https://botsin.space" |
||||
username = "unionbot" |
||||
id = "7mYGOUxnBGBcPnyPuKaIEmq-fvDbjIe3gSXiOVvKakg" |
||||
secret = "LDsz6PCx_qApDxGV7x1NdwSe-hxK9uN5qIP8pAku9Sg" |
||||
token = "IMUxGpqLF7ojDEy-vNUzGX0ZMzi5NP-1SDQUQD5Wqj4" |
||||
unions = "unions/en.toml" |
@ -0,0 +1,177 @@ |
||||
use crate::fatal; |
||||
use elefren::{ |
||||
data::Data, http_send::HttpSender, registration::Registered, scopes::Scopes, Mastodon, |
||||
}; |
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize}; |
||||
use std::{ |
||||
collections::BTreeMap, |
||||
fs::{File, OpenOptions}, |
||||
io::{self, Read, Write}, |
||||
path::{Path, PathBuf}, |
||||
}; |
||||
|
||||
pub type Location = String; |
||||
pub type Name = String; |
||||
|
||||
type InnerMap = BTreeMap<Location, BTreeMap<Name, BTreeMap<String, String>>>; |
||||
|
||||
#[derive(Debug, Deserialize, Serialize)] |
||||
pub struct UnionMap { |
||||
#[serde(flatten)] |
||||
inner: InnerMap, |
||||
#[serde(skip)] |
||||
path: PathBuf, |
||||
} |
||||
|
||||
#[derive(Debug, Deserialize, Serialize)] |
||||
pub struct Config { |
||||
/// The instance URL (example: `botsin.space`)
|
||||
pub instance: String, |
||||
/// The username (example: `bot@beep.boop`)
|
||||
pub username: String, |
||||
/// The required login secret
|
||||
#[serde(flatten)] |
||||
pub secret: Option<Secret>, |
||||
/// Union suggestion map
|
||||
pub unions: Unions, |
||||
} |
||||
|
||||
#[derive(Debug, Deserialize, Serialize)] |
||||
#[serde(untagged)] |
||||
pub enum Unions { |
||||
/// The configuration contains a path
|
||||
Path(PathBuf), |
||||
/// Which is then turned into the union map
|
||||
Map(UnionMap), |
||||
} |
||||
|
||||
impl Unions { |
||||
fn close(&mut self) { |
||||
let path = match self { |
||||
Unions::Path(ref path) => path, |
||||
Unions::Map(ref map) => &map.path, |
||||
}; |
||||
|
||||
*self = Unions::Path(path.clone()); |
||||
} |
||||
} |
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)] |
||||
pub struct Secret { |
||||
pub id: String, |
||||
pub secret: String, |
||||
pub token: String, |
||||
} |
||||
|
||||
impl<'d> From<&'d Data> for Secret { |
||||
fn from(d: &'d Data) -> Self { |
||||
Self { |
||||
id: d.client_id.to_string(), |
||||
secret: d.client_secret.to_string(), |
||||
token: d.token.to_string(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn load_path(p: &Path) -> String { |
||||
let mut f = File::open(p) |
||||
.unwrap_or_else(|e| fatal!("Failed to open path `{}`: {}", p.to_str().unwrap(), e)); |
||||
|
||||
let mut buf = String::new(); |
||||
f.read_to_string(&mut buf) |
||||
.unwrap_or_else(|e| fatal!("Failed to read file: {}", e)); |
||||
|
||||
info!("Loaded path {}", p.to_str().unwrap()); |
||||
buf |
||||
} |
||||
|
||||
fn parse_toml<T: DeserializeOwned>(s: String) -> T { |
||||
toml::from_str(s.as_str()).unwrap_or_else(|e| fatal!("Failed to parse toml: {}", e)) |
||||
} |
||||
|
||||
impl Config { |
||||
pub fn load(path: &Path) -> Self { |
||||
let cfg_buf = load_path(path); |
||||
let mut cfg: Config = parse_toml(cfg_buf); |
||||
|
||||
dbg!(&cfg); |
||||
|
||||
// Load the union map from the provided path
|
||||
cfg.load_unions(); |
||||
|
||||
// Return
|
||||
cfg |
||||
} |
||||
|
||||
/// Load the unions from the path
|
||||
pub fn load_unions(&mut self) { |
||||
let union_buf = load_path(self.unions_path()); |
||||
let inner: InnerMap = parse_toml(union_buf); |
||||
|
||||
// Swap the path for the map
|
||||
self.unions = Unions::Map(UnionMap { |
||||
inner, |
||||
path: self.unions_path().to_path_buf(), |
||||
}); |
||||
} |
||||
|
||||
pub fn registration(&self) -> Mastodon<HttpSender> { |
||||
let sec = self.assume_secret().clone(); |
||||
Data { |
||||
base: (&self.instance).clone().into(), |
||||
client_id: sec.id.into(), |
||||
client_secret: sec.secret.into(), |
||||
token: sec.token.into(), |
||||
redirect: "".into(), |
||||
} |
||||
.into() |
||||
} |
||||
|
||||
pub fn save(&mut self, path: &Path) -> io::Result<()> { |
||||
let mut f = OpenOptions::new() |
||||
.write(true) |
||||
.create(false) |
||||
.truncate(true) |
||||
.open(path)?; |
||||
|
||||
let string = self.as_toml(); |
||||
f.write_all(string.as_bytes())?; |
||||
|
||||
Ok(()) |
||||
} |
||||
|
||||
pub fn as_toml(&mut self) -> String { |
||||
self.unions.close(); |
||||
|
||||
let string = toml::to_string(self).unwrap(); |
||||
self.load_unions(); |
||||
string |
||||
} |
||||
|
||||
fn unions_path(&self) -> &Path { |
||||
match self.unions { |
||||
Unions::Path(ref pathbuf) => pathbuf.as_path(), |
||||
_ => unreachable!(), |
||||
} |
||||
} |
||||
|
||||
fn assume_secret(&self) -> &Secret { |
||||
self.secret.as_ref().unwrap() |
||||
} |
||||
|
||||
pub fn unions(&self, loc: Location) -> &BTreeMap<Name, BTreeMap<String, String>> { |
||||
match self.unions { |
||||
Unions::Map(ref map) => map.inner.get(&loc).unwrap(), |
||||
_ => unreachable!(), |
||||
} |
||||
} |
||||
|
||||
/// Update the secret in the configuration
|
||||
pub fn update_secret<S: Into<Secret>>(&mut self, secret: S) { |
||||
self.secret = Some(secret.into()); |
||||
} |
||||
|
||||
pub fn secret(&self) -> Option<Secret> { |
||||
self.secret.clone() |
||||
} |
||||
} |
@ -0,0 +1,36 @@ |
||||
#[macro_export] |
||||
macro_rules! fatal { |
||||
() => { |
||||
error!("Unknown failure!"); |
||||
std::process::exit(2) |
||||
}; |
||||
($($arg:tt)*) => ({ |
||||
error!($($arg)*); |
||||
std::process::exit(2) |
||||
}) |
||||
} |
||||
|
||||
use colored::*; |
||||
use env_logger::Builder; |
||||
use log::Level; |
||||
use std::io::Write; |
||||
|
||||
pub fn initialise() { |
||||
let mut b = Builder::from_default_env(); |
||||
b.format(|buf, record| { |
||||
let lvl = record.level().to_string(); |
||||
write!( |
||||
buf, |
||||
"[{}]: {}\n", |
||||
match record.level() { |
||||
Level::Error => lvl.red(), |
||||
Level::Warn => lvl.yellow(), |
||||
Level::Info => lvl.green(), |
||||
Level::Debug => lvl.purple(), |
||||
Level::Trace => lvl.cyan(), |
||||
}, |
||||
record.args() |
||||
) |
||||
}) |
||||
.init(); |
||||
} |
@ -0,0 +1,55 @@ |
||||
#[macro_use] |
||||
extern crate log; |
||||
#[macro_use] |
||||
mod log_util; |
||||
|
||||
mod config; |
||||
mod register; |
||||
mod toot; |
||||
|
||||
use config::Config; |
||||
|
||||
use elefren::{helpers::cli, MastodonClient}; |
||||
use std::path::PathBuf; |
||||
|
||||
fn main() { |
||||
std::env::set_var("RUST_LOG", "unionbot=debug,warn"); |
||||
|
||||
log_util::initialise(); |
||||
|
||||
let path = PathBuf::new().join("acab.toml"); |
||||
let mut cfg = Config::load(path.as_path()); |
||||
|
||||
// Register the app, or authenticate. Tries to re-write the
|
||||
// configuration file with the updated client secrets, but will
|
||||
// print to stdout if this fails.
|
||||
let masto = match cfg.secret() { |
||||
Some(_) => { |
||||
debug!("Attempting authentication with stored secret..."); |
||||
cfg.registration() |
||||
}, |
||||
None => { |
||||
warn!("No client secret available, attempting registration!"); |
||||
let reg = register::register(&cfg).unwrap(); |
||||
|
||||
let masto = cli::authenticate(reg).unwrap(); |
||||
|
||||
cfg.update_secret(&masto.data); |
||||
|
||||
info!("Persisting client configuration in {}", "acab.toml"); |
||||
if let Err(e) = cfg.save(path.as_path()) { |
||||
error!("Writing configuration file failed: {}", e); |
||||
error!("Dumping new configuration on stdout..."); |
||||
|
||||
println!("{}", cfg.as_toml()); |
||||
fatal!("Byeee"); |
||||
} |
||||
|
||||
masto |
||||
} |
||||
}; |
||||
|
||||
info!("Successfully authenticated! ^-^"); |
||||
|
||||
masto.new_status(toot::make(&cfg)).unwrap(); |
||||
} |
@ -0,0 +1,18 @@ |
||||
use crate::config::Config; |
||||
use elefren::{ |
||||
errors::Result, |
||||
http_send::HttpSender, |
||||
registration::{Registered, Registration}, |
||||
scopes::Scopes, |
||||
}; |
||||
|
||||
// use mammut::{apps::AppBuilder, Registration, Mastodon};
|
||||
|
||||
/// Attempt to register an account
|
||||
pub(crate) fn register(cfg: &Config) -> Result<Registered<HttpSender>> { |
||||
Registration::new(cfg.instance.as_str()) |
||||
.client_name("unionbot") |
||||
.redirect_uris("urn:ietf:wg:oauth:2.0:oob") |
||||
.scopes(Scopes::write_all()) |
||||
.build() |
||||
} |
@ -0,0 +1,10 @@ |
||||
use crate::config::Config; |
||||
use elefren::status_builder::{NewStatus, StatusBuilder, Visibility}; |
||||
|
||||
pub fn make(cfg: &Config) -> NewStatus { |
||||
StatusBuilder::new() |
||||
.status("Hello world, join a union!") |
||||
.visibility(Visibility::Public) |
||||
.build() |
||||
.unwrap() |
||||
} |
@ -0,0 +1,2 @@ |
||||
[berlin] |
||||
FAU = { name = "FAU Berlin", url = "https://berlin.fau.org" } |
Loading…
Reference in new issue