Initial commit

This commit is contained in:
Agatha Lovelace 2023-06-29 17:12:23 +02:00
commit 9be5f1c1b6
Signed by: sorceress
GPG Key ID: 01D0B3AB10CED4F8
6 changed files with 3393 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
.env

3085
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

16
Cargo.toml Normal file
View File

@ -0,0 +1,16 @@
[package]
name = "matrix-ril100"
version = "0.1.0"
edition = "2021"
authors = ["Agatha V. Lovelace <agatha@technogothic.net>"]
description = "A matrix bot that looks up RIL100 codes and station names"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dotenv = "0.15.0"
matrix-sdk = "0.6.2"
miette = { version = "5.9.0", features = ["fancy"] }
reqwest = { version = "0.11.18", features = ["json"] }
serde_json = "1.0.99"
tokio = { version = "1.29.0", features = ["full"] }

95
flake.lock Normal file
View File

@ -0,0 +1,95 @@
{
"nodes": {
"naersk": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1687852486,
"narHash": "sha256-2rXkhKUVQxbVaC+TITPpILiy/dSbordOLs87eoWHYxA=",
"owner": "nix-community",
"repo": "naersk",
"rev": "df10963b956962913b693a638746a95d6c506404",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "master",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1687946342,
"narHash": "sha256-vRxti8pOuXS0rJmqjbD8ueEEFXWSK22ISHoCWkhgzzg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1c851e8c92b76a00ce84167984a7ec7ba2b1f29c",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1687946342,
"narHash": "sha256-vRxti8pOuXS0rJmqjbD8ueEEFXWSK22ISHoCWkhgzzg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1c851e8c92b76a00ce84167984a7ec7ba2b1f29c",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"naersk": "naersk",
"nixpkgs": "nixpkgs_2",
"utils": "utils"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1687709756,
"narHash": "sha256-Y5wKlQSkgEK2weWdOu4J3riRd+kV/VCgHsqLNTTWQ/0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "dbabf0ca0c0c4bce6ea5eaf65af5cb694d2082c7",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

33
flake.nix Normal file
View File

@ -0,0 +1,33 @@
{
inputs = {
naersk.url = "github:nix-community/naersk/master";
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, utils, naersk }:
utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
naersk-lib = pkgs.callPackage naersk { };
in {
packages.default = naersk-lib.buildPackage {
src = ./.;
nativeBuildInputs = with pkgs; [ pkg-config ];
buildInputs = with pkgs; [ openssl ];
};
devShells.default = with pkgs;
mkShell {
buildInputs = [
cargo
rustc
rustfmt
pre-commit
rustPackages.clippy
rust-analyzer
];
RUST_SRC_PATH = rustPlatform.rustLibSrc;
};
});
}

162
src/main.rs Normal file
View File

@ -0,0 +1,162 @@
use matrix_sdk::{
config::SyncSettings,
room::Room,
ruma::{
events::room::message::{
MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent,
},
OwnedUserId,
},
Client,
};
use miette::{miette, Context, IntoDiagnostic, Result};
use dotenv::dotenv;
use reqwest::Url;
use serde_json::Value;
use std::env;
#[tokio::main]
async fn main() -> Result<()> {
dotenv().ok();
let hs = env::var("MATRIX_HOMESERVER")
.into_diagnostic()
.wrap_err("Variable: MATRIX_HOMESERVER")?;
let username = env::var("MATRIX_USERNAME")
.into_diagnostic()
.wrap_err("Variable: MATRIX_USERNAME")?;
let password = env::var("MATRIX_PASSWORD")
.into_diagnostic()
.wrap_err("Variable: MATRIX_PASSWORD")?;
let hs_url = Url::parse(&hs).into_diagnostic()?;
let client = Client::new(hs_url.clone()).await.into_diagnostic()?;
client
.login_username(&username, &password)
.initial_device_display_name("RIL100 Bot")
.send()
.await
.into_diagnostic()?;
let response = client
.sync_once(SyncSettings::default())
.await
.into_diagnostic()?;
println!("Logged in as RIL100 bot");
let homeserver = hs_url
.host_str()
.ok_or_else(|| return miette!("Homeserver URL does not contain a host 🤨"))?
.to_string();
let user_id = client
.user_id()
.ok_or_else(|| return miette!("Client does not have a User ID"))?
.to_owned();
client.add_event_handler(move |ev, room| {
on_room_message(
ev,
room,
username.clone(),
homeserver.clone(),
user_id.clone(),
)
});
let settings = SyncSettings::default().token(response.next_batch);
client.sync(settings).await.into_diagnostic()?;
Ok(())
}
async fn on_room_message(
event: OriginalSyncRoomMessageEvent,
room: Room,
username: String,
homeserver: String,
current_user: OwnedUserId,
) -> Result<()> {
// Make sure room is joined
let Room::Joined(room) = room else { return Ok(()) };
// Check if message has text content
let MessageType::Text(text) = event.clone().content.msgtype else { return Ok(()) };
// Do not reply to own messages
if event.sender == current_user {
return Ok(());
};
// Only reply to mentions
if text.formatted.map_or(false, |v| {
v.body.contains(&format!(
"<a href=\"https://matrix.to/#/@{username}:{homeserver}\">"
))
}) {
// Drop the mention
let query = text
.body
.split_once(": ")
.ok_or_else(|| {
return miette!("Message both contains and doesn't contain a mention 🤨");
})?
.1
.trim()
.to_string();
let response = if query.chars().all(char::is_uppercase) {
let body = reqwest::get(format!("https://v6.db.transport.rest/stations/{query}"))
.await
.into_diagnostic()?
.json::<Value>()
.await
.into_diagnostic()?;
let name = body
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
format!("<b>{query}</b> is <b>{name}</b>")
} else {
let body = reqwest::get(format!(
"https://v6.db.transport.rest/stations?query={query}&results=1"
))
.await
.into_diagnostic()?
.json::<Value>()
.await
.into_diagnostic()?;
// Get first object field
let body = body.as_object().and_then(|v| v.values().next());
let code = body
.and_then(|v| v.get("ril100"))
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let station = body
.and_then(|v| v.get("name"))
.and_then(|v| v.as_str())
.unwrap_or(&query);
format!("<b>{station}</b> is <b>{code}</b>")
};
room.send(
RoomMessageEventContent::text_html(
response.replace("<b>", "").replace("</b>", ""),
response,
)
.make_reply_to(&event.into_full_event(room.room_id().into())),
None,
)
.await
.into_diagnostic()?;
};
Ok(())
}