unionbot: initial code dump

wip/yesman
Katharina Fey 3 years ago
parent 702c701fd6
commit 37c1b7a9f8
  1. 1
      apps/unionbot/.gitignore
  2. 2085
      apps/unionbot/Cargo.lock
  3. 14
      apps/unionbot/Cargo.toml
  4. 19
      apps/unionbot/README.md
  5. 6
      apps/unionbot/acab.toml
  6. 177
      apps/unionbot/src/config.rs
  7. 36
      apps/unionbot/src/log_util.rs
  8. 55
      apps/unionbot/src/main.rs
  9. 18
      apps/unionbot/src/register.rs
  10. 10
      apps/unionbot/src/toot.rs
  11. 2
      apps/unionbot/unions/en.toml

@ -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…
Cancel
Save