more fixes and matrix work

This commit is contained in:
lily 2026-07-31 21:28:29 -04:00
parent 36c21c3c2b
commit 89486478fc
6 changed files with 1025 additions and 18 deletions

1
.gitignore vendored
View file

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

872
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -9,4 +9,8 @@ tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tracing = "0.1"
tracing-subscriber = "0.3"
tracing-subscriber = "0.3"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
urlencoding = "2"
uuid = { version = "1", features = ["v4"] }
anyhow = "1"

View file

@ -5,4 +5,5 @@ services:
ports:
- "9000:9000"
extra_hosts:
- "host.docker.internal:host-gateway"
- "host.docker.internal:host-gateway"
env_file: .env

View file

@ -1,11 +1,12 @@
use axum::{
Json, Router,
extract::{Path, Query},
extract::{Path, Query, State},
http::StatusCode,
routing::{get, put},
};
use serde::{Deserialize, Serialize};
use serde::Deserialize;
use serde_json::{Value, json};
use std::sync::Arc;
#[derive(Deserialize)]
struct AuthQuery {
@ -17,55 +18,100 @@ struct Transaction {
events: Vec<Value>,
}
const HS_TOKEN: &str = "6fab517dbd08182ccd5d935f0b6e2719dea572fa8dc15206a6f6fb9f2948accb";
mod matrix;
use matrix::MatrixClient;
use std::env;
fn env_var(name: &str) -> String {
env::var(name).unwrap_or_else(|_| panic!("missing env {}", name))
}
struct AppState {
hs_token: String,
bot_mxid: String,
matrix: MatrixClient,
}
/// TODO: change post handler and then path to correct location
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let hs_url = env_var("HS_URL");
let hs_token = env_var("HS_TOKEN");
let as_token = env_var("AS_TOKEN");
let bot_mxid = env_var("BOT_MXID");
let state = Arc::new(AppState {
hs_token,
bot_mxid,
matrix: MatrixClient::new(hs_url, as_token),
});
let app = Router::new()
.route("/_matrix/app/v1/transactions/{txn_id}", put(transactions))
.route("/_matrix/app/v1/users/{user_id}", get(query_user))
.route("/_matrix/app/v1/rooms/{room_alias}", get(query_alias))
.route("/health", get(|| async { "ok" }));
.route("/health", get(|| async { "ok" }))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:9000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
fn check_auth(q: &AuthQuery) -> Result<(), StatusCode> {
fn check_auth(q: &AuthQuery, hs_token: &str) -> Result<(), StatusCode> {
match q.access_token.as_deref() {
Some(t) if t == HS_TOKEN => Ok(()),
Some(t) if t == hs_token => Ok(()),
_ => Err(StatusCode::FORBIDDEN),
}
}
async fn transactions(
State(state): State<Arc<AppState>>,
Path(txn_id): Path<String>,
Query(auth): Query<AuthQuery>,
Json(body): Json<Transaction>,
) -> Result<Json<Value>, StatusCode> {
check_auth(&auth)?;
check_auth(&auth, &state.hs_token)?;
tracing::info!("txn {} with {} events", txn_id, body.events.len());
for ev in &body.events {
println!("{}", serde_json::to_string_pretty(ev).unwrap());
if let Err(e) = handle_event(&state.matrix, &state.bot_mxid, ev).await {
tracing::error!("error handling event: {}", e);
}
}
Ok(Json(json!({})))
}
async fn query_user(
State(state): State<Arc<AppState>>,
Path(user_id): Path<String>,
Query(auth): Query<AuthQuery>,
) -> Result<Json<Value>, StatusCode> {
check_auth(&auth)?;
check_auth(&auth, &state.hs_token)?;
tracing::info!("user query: {}", user_id);
Err(StatusCode::NOT_FOUND)
}
async fn query_alias(
State(state): State<Arc<AppState>>,
Path(room_alias): Path<String>,
Query(auth): Query<AuthQuery>,
) -> Result<Json<Value>, StatusCode> {
check_auth(&auth)?;
check_auth(&auth, &state.hs_token)?;
tracing::info!("alias query: {}", room_alias);
Err(StatusCode::NOT_FOUND)
}
async fn handle_event(client: &MatrixClient, bot_mxid: &str, ev: &Value) -> anyhow::Result<()> {
let ev_type = ev["type"].as_str().unwrap_or("");
let room_id = ev["room_id"].as_str().unwrap_or("");
if ev_type == "m.room.member"
&& ev["state_key"].as_str() == Some(bot_mxid)
&& ev["content"]["membership"].as_str() == Some("invite")
{
tracing::info!("joining {} as {}", room_id, bot_mxid);
client.join_room(room_id, bot_mxid).await?;
client.send_text(room_id, bot_mxid, "hi, i'm alive").await?;
}
Ok(())
}

93
src/matrix.rs Normal file
View file

@ -0,0 +1,93 @@
use anyhow::{Result, anyhow};
use reqwest::Client;
use serde_json::{Value, json};
pub struct MatrixClient {
http: Client,
homeserver: String,
as_token: String,
}
impl MatrixClient {
pub fn new(homeserver: impl Into<String>, as_token: impl Into<String>) -> Self {
Self {
http: Client::new(),
homeserver: homeserver.into(),
as_token: as_token.into(),
}
}
async fn req(
&self,
method: reqwest::Method,
path: &str,
user_id: Option<&str>,
body: Option<Value>,
) -> Result<Value> {
let mut url = format!("{}{}", self.homeserver, path);
if let Some(u) = user_id {
let sep = if path.contains('?') { '&' } else { '?' };
url.push(sep);
url.push_str(&format!("user_id={}", urlencoding::encode(u)));
}
let mut req = self.http.request(method, &url).bearer_auth(&self.as_token);
if let Some(b) = body {
req = req.json(&b);
}
let res = req.send().await?;
let status = res.status();
let text = res.text().await?;
if !status.is_success() {
return Err(anyhow!("matrix {} {}: {}", status, url, text));
}
Ok(serde_json::from_str(&text).unwrap_or(Value::Null))
}
pub async fn join_room(&self, room_id: &str, user_id: &str) -> Result<()> {
let path = format!(
"/_matrix/client/v3/rooms/{}/join",
urlencoding::encode(room_id)
);
self.req(reqwest::Method::POST, &path, Some(user_id), Some(json!({})))
.await?;
Ok(())
}
pub async fn send_text(&self, room_id: &str, user_id: &str, body: &str) -> Result<String> {
let txn = uuid::Uuid::new_v4().to_string();
let path = format!(
"/_matrix/client/v3/rooms/{}/send/m.room.message/{}",
urlencoding::encode(room_id),
txn
);
let res = self
.req(
reqwest::Method::PUT,
&path,
Some(user_id),
Some(json!({ "msgtype": "m.text", "body": body })),
)
.await?;
Ok(res["event_id"].as_str().unwrap_or("").to_string())
}
pub async fn register_ghost(&self, localpart: &str) -> Result<()> {
let body = json!({
"type": "m.login.application_service",
"username": localpart,
});
match self
.req(
reqwest::Method::POST,
"/_matrix/client/v3/register",
None,
Some(body),
)
.await
{
Ok(_) => Ok(()),
Err(e) if e.to_string().contains("M_USER_IN_USE") => Ok(()),
Err(e) => Err(e),
}
}
}