Compare commits

..

No commits in common. "2040eb00787d1a1a2c59aeec0ec5b7143d7334a0" and "e7300e1a37dfab2f59587fb536d01f7d4088aa11" have entirely different histories.

9 changed files with 52 additions and 128 deletions

1
Cargo.lock generated
View File

@ -1769,7 +1769,6 @@ dependencies = [
"mlua", "mlua",
"ncr", "ncr",
"parking_lot", "parking_lot",
"serde",
"serde_json", "serde_json",
"tokio", "tokio",
"zip", "zip",

View File

@ -40,7 +40,6 @@ mimalloc = { version = "0", optional = true }
mlua = { version = "0", features = ["async", "luajit", "send"] } mlua = { version = "0", features = ["async", "luajit", "send"] }
ncr = { version = "0", features = ["cfb8", "ecb", "gcm"] } ncr = { version = "0", features = ["cfb8", "ecb", "gcm"] }
parking_lot = "0" parking_lot = "0"
serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
tokio = { version = "1", features = ["macros"] } tokio = { version = "1", features = ["macros"] }
zip = { version = "2", default-features = false, features = ["flate2"] } zip = { version = "2", default-features = false, features = ["flate2"] }

View File

@ -5,6 +5,7 @@ A Minecraft bot with Lua scripting support, written in Rust with [azalea](https:
## Features ## Features
- Running Lua from - Running Lua from
- `errornowatcher.lua`
- in-game chat messages - in-game chat messages
- Matrix chat messages - Matrix chat messages
- POST requests to HTTP server - POST requests to HTTP server
@ -24,4 +25,4 @@ $ cargo build --release
$ # ./target/release/errornowatcher $ # ./target/release/errornowatcher
``` ```
Make sure the `Server` and `Username` globals are defined in `main.lua` before starting the bot. Make sure the `Server` and `Username` globals are defined in `errornowatcher.lua` before starting the bot.

View File

@ -6,11 +6,11 @@ use std::path::PathBuf;
#[derive(Parser)] #[derive(Parser)]
#[command(version = build_info::version_formatted())] #[command(version = build_info::version_formatted())]
pub struct Arguments { pub struct Arguments {
/// Path to main Lua file /// Path to Lua entry point
#[arg(short, long)] #[arg(short, long, default_value = "errornowatcher.lua")]
pub script: Option<PathBuf>, pub script: PathBuf,
/// Code to execute (after script) /// Code to execute after loading script
#[arg(short, long)] #[arg(short, long)]
pub exec: Option<String>, pub exec: Option<String>,
} }

View File

@ -210,12 +210,12 @@ pub async fn handle_event(client: Client, event: Event, state: State) -> Result<
exit(0); exit(0);
})?; })?;
#[cfg(feature = "matrix")]
matrix_init(&client, state.clone());
let globals = state.lua.globals(); let globals = state.lua.globals();
lua_init(client, &state, &globals).await?; lua_init(client, &state, &globals).await?;
#[cfg(feature = "matrix")]
matrix_init(state.clone(), &globals);
let Some(address): Option<SocketAddr> = globals let Some(address): Option<SocketAddr> = globals
.get::<String>("HttpAddress") .get::<String>("HttpAddress")
.ok() .ok()
@ -276,17 +276,13 @@ async fn lua_init(client: Client, state: &State, globals: &Table) -> Result<()>
} }
#[cfg(feature = "matrix")] #[cfg(feature = "matrix")]
fn matrix_init(client: &Client, state: State) { fn matrix_init(state: State, globals: &Table) {
let globals = state.lua.globals();
if let Ok(homeserver_url) = globals.get::<String>("MatrixHomeserverUrl") if let Ok(homeserver_url) = globals.get::<String>("MatrixHomeserverUrl")
&& let Ok(username) = globals.get::<String>("MatrixUsername") && let Ok(username) = globals.get::<String>("MatrixUsername")
&& let Ok(password) = globals.get::<String>("MatrixPassword") && let Ok(password) = globals.get::<String>("MatrixPassword")
{ {
let name = client.username();
tokio::spawn(async move { tokio::spawn(async move {
if let Err(error) = if let Err(error) = matrix::login(state, homeserver_url, username, &password).await {
matrix::login(homeserver_url, username, &password, state, globals, name).await
{
error!("failed to log into matrix account: {error:?}"); error!("failed to log into matrix account: {error:?}");
} }
}); });

View File

@ -27,7 +27,6 @@ use clap::Parser;
use commands::{CommandSource, register}; use commands::{CommandSource, register};
use futures::lock::Mutex; use futures::lock::Mutex;
use futures_locks::RwLock; use futures_locks::RwLock;
use log::debug;
use mlua::{Function, Lua, Table}; use mlua::{Function, Lua, Table};
use replay::{plugin::RecordPlugin, recorder::Recorder}; use replay::{plugin::RecordPlugin, recorder::Recorder};
use std::{ use std::{
@ -59,28 +58,24 @@ async fn main() -> anyhow::Result<()> {
let event_listeners = Arc::new(RwLock::new(HashMap::new())); let event_listeners = Arc::new(RwLock::new(HashMap::new()));
let lua = unsafe { Lua::unsafe_new() }; let lua = unsafe { Lua::unsafe_new() };
let globals = lua.globals(); let globals = lua.globals();
lua::register_globals(&lua, &globals, event_listeners.clone())?;
if let Some(path) = args.script { lua::register_globals(&lua, &globals, event_listeners.clone())?;
globals.set("SCRIPT_PATH", &*path)?; globals.set("SCRIPT_PATH", &*args.script)?;
lua.load(read_to_string(path)?).exec()?; lua.load(
} else if let Some(code) = ["main.lua", "errornowatcher.lua"].iter().find_map(|path| { read_to_string(&args.script)
debug!("trying to load code from {path}"); .with_context(|| format!("failed to read {}", args.script.display()))?,
globals.set("SCRIPT_PATH", *path).ok()?; )
read_to_string(path).ok() .exec()?;
}) {
lua.load(code).exec()?;
}
if let Some(code) = args.exec { if let Some(code) = args.exec {
lua.load(code).exec()?; lua.load(code).exec()?;
} }
let server = globals let server = globals
.get::<String>("Server") .get::<String>("Server")
.context("lua globals missing Server variable")?; .expect("Server should be in lua globals");
let username = globals let username = globals
.get::<String>("Username") .get::<String>("Username")
.context("lua globals missing Username variable")?; .expect("Username should be in lua globals");
let mut commands = CommandDispatcher::new(); let mut commands = CommandDispatcher::new();
register(&mut commands); register(&mut commands);

View File

@ -1,4 +1,4 @@
use super::MatrixContext; use super::{COMMAND_PREFIX, Context};
use crate::{ use crate::{
events::call_listeners, events::call_listeners,
lua::{self, matrix::room::Room as LuaRoom}, lua::{self, matrix::room::Room as LuaRoom},
@ -19,7 +19,7 @@ use tokio::time::sleep;
pub async fn on_regular_room_message( pub async fn on_regular_room_message(
event: OriginalSyncRoomMessageEvent, event: OriginalSyncRoomMessageEvent,
room: Room, room: Room,
ctx: Ctx<MatrixContext>, ctx: Ctx<Context>,
) -> Result<()> { ) -> Result<()> {
if room.state() != RoomState::Joined { if room.state() != RoomState::Joined {
return Ok(()); return Ok(());
@ -35,9 +35,9 @@ pub async fn on_regular_room_message(
.get::<Vec<String>>("MatrixOwners") .get::<Vec<String>>("MatrixOwners")
.unwrap_or_default() .unwrap_or_default()
.contains(&event.sender.to_string()) .contains(&event.sender.to_string())
&& text_content.body.starts_with(&ctx.name) && text_content.body.starts_with(COMMAND_PREFIX)
{ {
let body = text_content.body[ctx.name.len()..] let body = text_content.body[COMMAND_PREFIX.len()..]
.trim_start_matches(':') .trim_start_matches(':')
.trim(); .trim();
let split = body.split_once(char::is_whitespace).unzip(); let split = body.split_once(char::is_whitespace).unzip();
@ -90,7 +90,7 @@ pub async fn on_stripped_state_member(
member: StrippedRoomMemberEvent, member: StrippedRoomMemberEvent,
client: Client, client: Client,
room: Room, room: Room,
ctx: Ctx<MatrixContext>, ctx: Ctx<Context>,
) -> Result<()> { ) -> Result<()> {
if let Some(user_id) = client.user_id() if let Some(user_id) = client.user_id()
&& member.state_key == user_id && member.state_key == user_id

View File

@ -2,122 +2,56 @@ mod bot;
mod verification; mod verification;
use crate::{State, lua::matrix::client::Client as LuaClient}; use crate::{State, lua::matrix::client::Client as LuaClient};
use anyhow::{Context, Result}; use anyhow::Result;
use bot::{on_regular_room_message, on_stripped_state_member}; use bot::{on_regular_room_message, on_stripped_state_member};
use log::{error, warn}; use matrix_sdk::{Client, config::SyncSettings};
use matrix_sdk::{ use std::{fs, sync::Arc};
Client, Error, LoopCtrl, authentication::matrix::MatrixSession, config::SyncSettings,
};
use mlua::Table;
use serde::{Deserialize, Serialize};
use std::{path::Path, sync::Arc};
use tokio::fs;
use verification::{on_device_key_verification_request, on_room_message_verification_request}; use verification::{on_device_key_verification_request, on_room_message_verification_request};
const COMMAND_PREFIX: &str = "ErrorNoWatcher";
#[derive(Clone)] #[derive(Clone)]
pub struct MatrixContext { pub struct Context {
state: State, state: State,
name: String,
}
#[derive(Clone, Serialize, Deserialize)]
struct Session {
#[serde(skip_serializing_if = "Option::is_none")]
sync_token: Option<String>,
user_session: MatrixSession,
}
async fn persist_sync_token(
session_file: &Path,
session: &mut Session,
sync_token: String,
) -> Result<()> {
session.sync_token = Some(sync_token);
fs::write(session_file, serde_json::to_string(&session)?).await?;
Ok(())
} }
pub async fn login( pub async fn login(
state: State,
homeserver_url: String, homeserver_url: String,
username: String, username: String,
password: &str, password: &str,
state: State,
globals: Table,
name: String,
) -> Result<()> { ) -> Result<()> {
let root_dir = dirs::data_dir() let mut client = Client::builder().homeserver_url(homeserver_url);
.context("no data directory")? if let Some(db_path) = dirs::data_dir().map(|path| path.join("errornowatcher").join("matrix"))
.join("errornowatcher") && fs::create_dir_all(&db_path).is_ok()
.join(&name)
.join("matrix");
let mut builder = Client::builder().homeserver_url(homeserver_url);
if !fs::try_exists(&root_dir).await.unwrap_or_default()
&& let Err(error) = fs::create_dir_all(&root_dir).await
{ {
warn!("failed to create directory for matrix sqlite3 store: {error:?}"); client = client.sqlite_store(db_path, None);
} else {
builder = builder.sqlite_store(&root_dir, None);
} }
let client = builder.build().await?;
let mut new_session; let client = Arc::new(client.build().await?);
let session_file = root_dir.join("session.json"); client
let mut sync_settings = SyncSettings::default(); .matrix_auth()
if let Some(session) = fs::read_to_string(&session_file)
.await
.ok()
.and_then(|data| serde_json::from_str::<Session>(&data).ok())
{
new_session = session.clone();
if let Some(sync_token) = session.sync_token {
sync_settings = sync_settings.token(sync_token);
}
client.restore_session(session.user_session).await?;
} else {
let matrix_auth = client.matrix_auth();
matrix_auth
.login_username(username, password) .login_username(username, password)
.initial_device_display_name(&name) .device_id("ERRORNOWATCHER")
.initial_device_display_name("ErrorNoWatcher")
.await?; .await?;
new_session = Session {
user_session: matrix_auth.session().context("should have session")?,
sync_token: None,
};
fs::write(&session_file, serde_json::to_string(&new_session)?).await?;
}
client.add_event_handler_context(MatrixContext { state, name });
client.add_event_handler(on_stripped_state_member); client.add_event_handler(on_stripped_state_member);
loop { let response = client.sync_once(SyncSettings::default()).await?;
match client.sync_once(sync_settings.clone()).await {
Ok(response) => {
sync_settings = sync_settings.token(response.next_batch.clone());
persist_sync_token(&session_file, &mut new_session, response.next_batch).await?;
break;
}
Err(error) => {
error!("failed to do initial sync: {error:?}");
}
}
}
client.add_event_handler(on_device_key_verification_request); client.add_event_handler(on_device_key_verification_request);
client.add_event_handler(on_room_message_verification_request); client.add_event_handler(on_room_message_verification_request);
client.add_event_handler(on_regular_room_message); client.add_event_handler(on_regular_room_message);
let client = Arc::new(client); state
globals.set("matrix", LuaClient(client.clone()))?; .lua
.globals()
.set("matrix", LuaClient(client.clone()))?;
client.add_event_handler_context(Context { state });
client client
.sync_with_result_callback(sync_settings, |sync_result| async { .sync(SyncSettings::default().token(response.next_batch))
let mut new_session = new_session.clone();
persist_sync_token(&session_file, &mut new_session, sync_result?.next_batch)
.await
.map_err(|err| Error::UnknownError(err.into()))?;
Ok(LoopCtrl::Continue)
})
.await?; .await?;
Ok(()) Ok(())
} }