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