This commit is contained in:
2026-08-15 17:16:25 -06:00
parent f4a413c362
commit 6a864e1e89
7 changed files with 1633 additions and 0 deletions
+5
View File
@@ -19,3 +19,8 @@ raw-wiktexttract-data.jsonl
*.db
raw-wiktextract-data.jsonlwords.db
words.db-journal
# Added by cargo
/target
Generated
+1327
View File
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "wiktlarp"
version = "0.1.0"
edition = "2024"
[dependencies]
clap = { version = "4.6.6", features = ["derive", "string"] }
diesel = { version = "2.3.12", features = ["r2d2", "sqlite"] }
log = "0.4.33"
notify = "8.2.0"
rcon = { version = "0.6.0", features = ["rt-tokio"] }
regex = "1.13.1"
shellexpand = { version = "3.1.2", features = ["path"] }
tokio = { version = "1.53.1", features = ["full"] }
watchfile = "0.1.1"
xdg = "3.0.0"
+159
View File
@@ -0,0 +1,159 @@
use regex::{regex, Regex};
#[derive(Debug,Eq,PartialEq,Clone)]
pub struct EntryQuery<'a> {
term : &'a str,
language : Option <&'a str>,
pos : Option <&'a str>,
}
impl<'a> EntryQuery<'a> {
pub fn basic (term : &'a str) -> EntryQuery<'a> {
EntryQuery { term, language: None, pos: None }
}
}
#[derive(Debug,Eq,PartialEq,Clone)]
pub enum Command<'a> {
Define (EntryQuery<'a>),
Etymology (EntryQuery<'a>),
}
#[derive(Debug,Eq,PartialEq,Clone)]
pub struct ChatMessage<'a> {
author : &'a str,
content : &'a str,
}
impl<'a> ChatMessage<'a> {
pub fn unauthored (content : &'a str) -> ChatMessage<'a> {
ChatMessage { author: "", content }
}
pub fn parse (line : &'a str) -> Option<ChatMessage<'a>> {
let re : &Regex = regex! (r"(?:\*사망\* )?(.*?) : (.*)");
let r = re.captures (line)?;
Some (ChatMessage {
author: r.get (1)?.as_str (),
content: r.get (2)?.as_str (),
})
}
}
fn find_query<'a> (s : &'a str) -> Option<EntryQuery<'a>> {
let basic_re = regex! (r"^\s*define (.*)|define (\w+)");
let wikilink_re = regex! (r"(?m)\[\[([^]]*)]]");
let term = if let Some (r) = wikilink_re.captures (s) {
r.get (1)
} else if let Some (r) = basic_re.captures (s) {
r.get (1).or (r.get (2))
} else {
None
};
Some (EntryQuery::basic (term?.as_str ()))
}
impl<'a> Command<'a> {
pub fn parse (msg : &'a ChatMessage<'a>) -> Option<Command<'a>> {
Self::parse_str (msg.content)
}
pub fn parse_str (s : &'a str) -> Option<Command<'a>> {
Self::parse_etymology (s).or (Self::parse_define (s))
}
fn parse_define (s : &'a str) -> Option<Command<'a>> {
let basic_re = regex! (r"^\s*define (.*)|define (\w+)");
let wikilink_re = regex! (r"\[\[([^]]*)]]");
let term = if let Some (r) = wikilink_re.captures (s) {
r.get (1)
} else if let Some (r) = basic_re.captures (s) {
r.get (1).or (r.get (2))
} else {
None
};
Some (Command::Define (EntryQuery::basic (term?.as_str ())))
}
fn parse_etymology (s : &'a str) -> Option<Command<'a>> {
let basic_re = regex! (r"^\s*etymology of (.*)|ety(?:m(?:ology(?: of)?)?)? (\w+)");
let wikilink_re = regex! (r"ety(?:m(?:ology(?: of)?)?)? \[\[([^]]*)]]");
let term = if let Some (r) = wikilink_re.captures (s) {
r.get (1)
} else if let Some (r) = basic_re.captures (s) {
r.get (1).or (r.get (2))
} else {
None
};
Some (Command::Etymology (EntryQuery::basic (term?.as_str ())))
}
}
mod tests {
use super::*;
use super::Command as Cmd;
#[test]
fn etym_wikilink () {
assert_eq! (
Cmd::parse_str (
"blah blah etymology of [[lexicography]]"
),
Some (Cmd::Etymology (EntryQuery::basic ("lexicography")))
);
assert_eq! (
Cmd::parse_str (
"etym [[lexicography]]"
),
Some (Cmd::Etymology (EntryQuery::basic ("lexicography")))
);
assert_eq! (
Cmd::parse_str (
"ety [[lexicography]]"
),
Some (Cmd::Etymology (EntryQuery::basic ("lexicography")))
);
}
#[test]
fn etym_basic () {
assert_eq! (
Cmd::parse_str (
"wiktionary, can you provide the etymology of lexicography??"
),
Some (Cmd::Etymology (EntryQuery::basic ("lexicography")))
)
}
#[test]
fn define_basic () {
assert_eq! (
Cmd::parse_str ("wiktionary, can you define lexicography??"),
Some (Cmd::Define (EntryQuery::basic ("lexicography")))
);
}
#[test]
fn define_line () {
assert_eq! (
Cmd::parse_str ("define I'm twenty years old"),
Some (Cmd::Define (EntryQuery::basic ("I'm twenty years old")))
);
}
#[test]
fn define_wikilink () {
assert_eq! (
Cmd::parse_str ("[[hoofjob]]"),
Some (Cmd::Define (EntryQuery::basic ("hoofjob")))
);
assert_eq! (
Cmd::parse_str ("bkah blah blah [[quirkchungus]] blbalb"),
Some (Cmd::Define (EntryQuery::basic ("quirkchungus")))
);
assert_eq! (
Cmd::parse_str ("define I'm [[twenty]] years old"),
Some (Cmd::Define (EntryQuery::basic ("twenty")))
);
}
}
+58
View File
@@ -0,0 +1,58 @@
use notify::{Event, RecursiveMode, Result, Watcher};
use std::{fs::File, io::{Read as _, Seek as _, SeekFrom, pipe}, path::{Path, PathBuf}, sync::mpsc};
pub fn default_log_file () -> PathBuf {
let base = xdg::BaseDirectories::new ()
.data_home
.expect ("could not find default log file.");
base.join ("Steam/steamapps/common/Team Fortress 2/tf/console.log")
.to_path_buf ()
}
pub struct Console {
prev_size : usize,
}
fn file_size (file : &Path) -> u64 {
if let Ok (mut f) = File::open (file) {
let x = f.seek (SeekFrom::End (0));
x.unwrap_or (0)
} else {
0
}
}
impl Console {
pub fn watch<F> (log_file : &Path, cb : F) -> Result<
// mpsc::Receiver<std::result::Result<Event, notify::Error>>
()
>
where F : Fn (&str) -> ()
{
let mut prev_size = file_size (log_file);
let (tx, rx) = mpsc::channel::<Result<Event>> ();
let mut watcher = notify::recommended_watcher (tx)?;
watcher.watch (log_file, RecursiveMode::NonRecursive)?;
for res in rx {
if let Err (err) = res {
log::error! ("{:?}", err);
continue
};
let new_size = file_size (log_file);
if prev_size < new_size {
let mut f = File::open (log_file)
.expect ("failed to read log file");
f.seek (SeekFrom::Start (prev_size)).unwrap ();
let mut buf = String::new ();
f.read_to_string (&mut buf).unwrap ();
cb (&buf)
} if new_size < prev_size {
log::warn! ("log file shrank!??");
}
prev_size = new_size;
}
Ok (())
}
}
+68
View File
@@ -0,0 +1,68 @@
mod console;
mod command;
use command::{ChatMessage, Command};
use rcon::{Connection, Error};
use std::{net::{SocketAddr}, path::{Path, PathBuf}, sync::mpsc, time::Duration};
use tokio::net::TcpStream;
use notify::{Event, RecursiveMode, Result, Watcher};
use clap::Parser;
use console::Console;
// const log_file : Path = Path::new (
// "~/.local/share/Steam/steamapps/common/Team Fortress 2/tf/console.log"
// );
/// Larp as Wiktionary in TF2's in-game chat.
#[derive(Parser, Debug)]
#[command(
name = "wiktlarp",
version,
about,
long_about,
)]
struct Args {
/// Path to TF2's console log
#[clap(
value_parser,
long,
short='f',
default_value=console::default_log_file ().into_os_string ()
)]
log_file: PathBuf,
/// RCON address to send chat commands to
#[clap(
value_parser,
long,
short='a',
default_value="127.0.0.1:27015"
)]
rcon_address: SocketAddr,
/// RCON password
#[clap(value_parser,long,short='p',default_value="monitor")]
rcon_password: String,
}
#[tokio::main]
async fn main () -> Result<()> {
let args = Args::parse ();
println! ("{:?}", args);
let mut client = <Connection<TcpStream>>::builder ()
.connect (args.rcon_address.to_string (), &args.rcon_password)
.await.unwrap ();
Console::watch (
&args.log_file,
|line| {
println! ("line: {line}");
let Some (msg) = ChatMessage::parse (line) else { return };
let Some (cmd) = Command::parse (&msg) else { return };
println! ("cmd: {:?}", cmd);
}
)?;
Ok (())
}