make this an IRC bot lol
Signed-off-by: Xe <me@christine.website>
This commit is contained in:
parent
e88f6a2b6b
commit
6898a868ee
|
@ -1,3 +1,4 @@
|
||||||
|
.#*
|
||||||
/target
|
/target
|
||||||
config.dhall
|
config.yaml
|
||||||
var/*
|
var/*
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
47
Cargo.toml
47
Cargo.toml
|
@ -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" ]
|
||||||
|
|
|
@ -17,6 +17,8 @@ pkgs.mkShell {
|
||||||
# dhall
|
# dhall
|
||||||
dhall
|
dhall
|
||||||
dhall-json
|
dhall-json
|
||||||
|
|
||||||
|
jo
|
||||||
];
|
];
|
||||||
|
|
||||||
RUST_LOG = "info";
|
RUST_LOG = "info";
|
||||||
|
|
213
src/main.rs
213
src/main.rs
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
Loading…
Reference in New Issue