make this an IRC bot lol

Signed-off-by: Xe <me@christine.website>
This commit is contained in:
Cadey Ratio 2022-02-19 14:03:04 -05:00
parent e88f6a2b6b
commit 6898a868ee
6 changed files with 647 additions and 1672 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
.#*
/target /target
config.dhall config.yaml
var/* var/*

2042
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,25 +6,44 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
axum = "0.4"
axum-macros = "0.1"
axum-extra = "0.1"
base64 = "0.13"
color-eyre = "0.6" color-eyre = "0.6"
serde_dhall = "0.11" futures = "0.3"
irc = "0.15.0"
serde_json = "1" serde_json = "1"
serde_yaml = "0.8"
tracing = "0.1" tracing = "0.1"
tracing-futures = "0.2" tracing-futures = "0.2"
tracing-log = "0.1" tracing-log = "0.1"
tracing-subscriber = "0.2" tracing-subscriber = "0.2"
serde = { version = "1", features = [ "derive" ] } [dependencies.rusqlite]
reqwest = { version = "0.11", features = [ "json" ] } version = "0.26"
tokio = { version = "1", features = [ "full" ] } features = [ "bundled", "uuid", "serde_json", "chrono" ]
[dependencies.matrix-sdk] [dependencies.serde]
git = "https://github.com/matrix-org/matrix-rust-sdk" version = "1"
rev = "1563ecdf1acf52b74ebea3309e72e2f8f9e854f6" features = [ "derive" ]
features = [
"eyre", [dependencies.reqwest]
"qrcode", version = "0.11"
"encryption", features = [ "json" ]
"markdown",
"sled_cryptostore", [dependencies.tokio]
] version = "1"
features = [ "full" ]
[dependencies.tower]
version = "0.4"
features = [ "full" ]
[dependencies.tower-http]
version = "0.2"
features = [ "full" ]
[dependencies.uuid]
version = "0.8"
features = [ "serde", "v4" ]

View File

@ -17,6 +17,8 @@ pkgs.mkShell {
# dhall # dhall
dhall dhall
dhall-json dhall-json
jo
]; ];
RUST_LOG = "info"; RUST_LOG = "info";

View File

@ -1,31 +1,27 @@
use color_eyre::Result; #[macro_use]
use matrix_sdk::{ extern crate tracing;
config::{ClientConfig, SyncSettings},
room::Room, use axum::{
ruma::{ extract::Extension,
api::client::r0::uiaa, http::StatusCode,
assign, routing::{get, post},
events::{ AddExtensionLayer, Json, Router,
room::message::{MessageType, RoomMessageEventContent, TextMessageEventContent},
SyncMessageEvent,
},
UserId,
},
Client,
}; };
use color_eyre::Result;
use futures::prelude::*;
use irc::{client::prelude::*, proto::message::Tag};
use rusqlite::{params, Connection};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{env, path::PathBuf}; use std::{collections::HashMap, env, net::SocketAddr, path::PathBuf, sync::Arc};
use tokio::sync::Mutex;
use tower_http::trace::TraceLayer;
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Config { pub struct Config {
username: Box<UserId>, pub database_path: PathBuf,
password: String, pub addr: SocketAddr,
#[serde(rename = "clientID")] pub announce_to: String,
client_id: String, pub irc: irc::client::data::Config,
#[serde(rename = "keysPath")]
key_path: PathBuf,
#[serde(rename = "storePath")]
store_path: PathBuf,
} }
pub const APPLICATION_NAME: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); pub const APPLICATION_NAME: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
@ -34,68 +30,143 @@ pub const APPLICATION_NAME: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CA
async fn main() -> Result<()> { async fn main() -> Result<()> {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
color_eyre::install()?; color_eyre::install()?;
let cfg: Config =
serde_dhall::from_file(env::var("CONFIG_PATH").unwrap_or("./config.dhall".to_string()))
.parse()?;
let ccfg = ClientConfig::new() let cfg: Config = serde_yaml::from_str(&std::fs::read_to_string("config.yaml")?)?;
.store_path(cfg.store_path)
.user_agent(APPLICATION_NAME)?;
let client = Client::new_from_user_id_with_config(&cfg.username, ccfg).await?;
// First we need to log in. let db = Arc::new(Mutex::new({
client let db = Connection::open(cfg.database_path.clone())?;
.login(cfg.username.localpart(), &cfg.password, None, None) db.execute_batch(include_str!("./schema.sql"))?;
.await?; Ok::<Connection, color_eyre::eyre::ErrReport>(db)
}?));
// Ignore old messages let mut cli = Client::from_config(cfg.irc.clone()).await?;
client.sync_once(SyncSettings::default()).await?; cli.send_cap_req(&[
Capability::EchoMessage,
Capability::AccountTag,
Capability::ChgHost,
Capability::ServerTime,
Capability::Sasl,
])?;
client.register_event_handler(on_room_message).await; cli.identify()?;
let cfg = Arc::new(cfg);
// Syncing is important to synchronize the client state with the server. let middleware = tower::ServiceBuilder::new()
// This method will never return. .layer(TraceLayer::new_for_http())
client.sync(SyncSettings::default()).await; .layer(AddExtensionLayer::new(cfg.clone()))
.layer(AddExtensionLayer::new(cli.sender()))
.layer(AddExtensionLayer::new(db.clone()));
let app = Router::new()
.route("/.within/health", get(health))
.route("/.within/poke", post(poke_message))
.layer(middleware);
tokio::spawn(axum::Server::bind(&cfg.addr).serve(app.into_make_service()));
let mut stream = cli.stream()?;
while let Some(message) = stream.next().await.transpose()? {
if let Err(err) = handle_message(db.clone(), cfg.clone(), message).await {
error!("{}", err);
}
}
Ok(()) Ok(())
} }
async fn unfuck_crypto(client: Client, cfg: &Config) { #[derive(Debug, Clone, Serialize, Deserialize)]
if let Err(e) = client.bootstrap_cross_signing(None).await { struct DiscordWebhook {
if let Some(response) = e.uiaa_response() { content: String,
let auth_data = uiaa::AuthData::Password(assign!( channel: Option<String>,
uiaa::Password::new(uiaa::UserIdentifier::MatrixId(&*cfg.username.as_str()), &cfg.password), }
{ session: response.session.as_deref() }
));
client async fn poke_message(
.bootstrap_cross_signing(Some(auth_data)) Extension(cfg): Extension<Arc<Config>>,
.await Extension(sender): Extension<Sender>,
.expect("Couldn't bootstrap cross signing") Json(wh): Json<DiscordWebhook>,
} else { ) -> (StatusCode, String) {
panic!("Error durign cross signing bootstrap {:#?}", e); match sender.send_privmsg(wh.channel.unwrap_or(cfg.announce_to.clone()), wh.content) {
Ok(_) => (StatusCode::NO_CONTENT, "".to_string()),
Err(err) => {
error!("error sending message: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
"internal server error sending to IRC".to_string(),
)
} }
} }
} }
async fn on_room_message(event: SyncMessageEvent<RoomMessageEventContent>, room: Room) { async fn health(Extension(db): Extension<Arc<Mutex<Connection>>>) -> &'static str {
if let Room::Joined(room) = room { let db = db.lock().await;
let msg_body = match event.content.msgtype { match db.query_row("SELECT 1+1", [], |_| Ok(())) {
MessageType::Text(TextMessageEventContent { body, .. }) => body, Ok(_) => "OK",
_ => return, Err(why) => {
}; error!("{}", why);
"NOT OK"
if msg_body.contains("!party") {
let content = RoomMessageEventContent::text_plain("🎉🎊🥳 let's PARTY!! 🥳🎊🎉");
println!("sending");
// send our message to the room we found the "!party" command in
// the last parameter is an optional transaction id which we don't
// care about.
room.send(content, None).await.unwrap();
println!("message sent");
} }
} }
} }
async fn handle_message(db: Arc<Mutex<Connection>>, cfg: Arc<Config>, msg: Message) -> Result<()> {
let db = db.lock().await;
let msg2 = msg.clone();
let nick = msg2.source_nickname().unwrap_or("");
let tags = tags_map(msg2.tags.clone());
match msg.command {
Command::PRIVMSG(channel, message) => {
db.execute(
"INSERT INTO messages(channel, nick, event, message, tags) VALUES (?1, ?2, 'message', ?3, ?4)",
params![
channel,
nick,
message,
serde_json::to_value(tags)?
],
)?;
}
Command::KICK(channel, user, reason) => {
db.execute(
"INSERT INTO messages(channel, nick, event, message, tags) VALUES (?1, ?2, 'kick', ?3, ?4)",
params![
channel,
user,
format!("was kicked by {}: {}", nick, reason.unwrap_or("no reason given".to_string())),
serde_json::to_value(tags)?,
],
)?;
}
Command::PART(channel, reason) => {
db.execute(
"INSERT INTO messages(channel, nick, event, message, tags) VALUES (?1, ?2, 'part', ?3, ?4)",
params![channel, nick, reason, serde_json::to_value(tags)?],
)?;
}
Command::JOIN(channel, _, _) => {
db.execute(
"INSERT INTO messages(channel, nick, event, tags) VALUES (?1, ?2, 'join', ?3)",
params![channel, nick, serde_json::to_value(tags)?],
)?;
}
Command::QUIT(message) => {
db.execute(
"INSERT INTO messages(channel, nick, event, message tags) VALUES (?1, ?2, 'quit', ?3, ?4)",
params![cfg.announce_to, nick, message, serde_json::to_value(tags)?],
)?;
}
_ => {}
}
Ok(())
}
fn tags_map(tags: Option<Vec<Tag>>) -> HashMap<String, Option<String>> {
let mut result = HashMap::<String, Option<String>>::new();
if let None = tags {
return result;
}
for tag in tags.unwrap() {
result.insert(tag.0, tag.1);
}
result
}

12
src/schema.sql Normal file
View File

@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS messages
( channel TEXT
, nick TEXT NOT NULL
, event TEXT NOT NULL
, message TEXT
, tags TEXT DEFAULT '{}'
, ts GENERATED ALWAYS AS (json_extract(tags, '$.time'))
);
CREATE INDEX IF NOT EXISTS messages_channel_nick ON messages(channel, nick);
CREATE INDEX IF NOT EXISTS messages_channel ON messages(channel);
CREATE INDEX IF NOT EXISTS messages_ts ON messages(ts);