Compare commits

6 Commits

Author SHA1 Message Date
msyds 793e2deae8 idk push for pais
build / build (push) Successful in 19s
2026-08-16 17:00:56 -06:00
msyds ab77858eda gitea action 2026-08-16 10:57:49 -06:00
msyds 1b329c6432 one assert per test 2026-08-16 10:57:49 -06:00
msyds 7379cffc03 nix 2026-08-16 01:22:26 -06:00
msyds 6a864e1e89 rust 2026-08-16 01:19:07 -06:00
msyds f4a413c362 idk 2026-08-16 01:17:12 -06:00
22 changed files with 2065 additions and 379 deletions
+11
View File
@@ -0,0 +1,11 @@
name: build
on: [push]
jobs:
build:
runs-on: nixos
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: build wiktlarp
run: nix build -L .#wiktlarp
+1
View File
@@ -19,3 +19,4 @@ raw-wiktexttract-data.jsonl
*.db *.db
raw-wiktextract-data.jsonlwords.db raw-wiktextract-data.jsonlwords.db
words.db-journal words.db-journal
/target
Generated
+1559
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
[package]
name = "wiktlarp"
version = "0.1.0"
edition = "2024"
[dependencies]
clap = { version = "4.6.6", features = ["derive", "string"] }
colog = "1.4.0"
diesel = { version = "2.3.12", features = ["r2d2", "serde_json", "sqlite"] }
indicatif = "0.18.6"
log = "0.4.33"
notify = "8.2.0"
rcon = { version = "0.6.0", features = ["rt-tokio"] }
regex = "1.13.1"
serde = "1.0.229"
serde_json = "1.0.151"
shellexpand = { version = "3.1.2", features = ["path"] }
tokio = { version = "1.53.1", features = ["full"] }
tokio-stream = "0.1.19"
watchfile = "0.1.1"
xdg = "3.0.0"
-17
View File
@@ -1,17 +0,0 @@
use diesel::prelude::*;
#[derive(Queryable, Selectable)]
#[diesel(table_name = crate::schema::words)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Word {
pub id : i32,
pub word : String,
pub info : String,
}
#[derive(Insertable)]
#[diesel(table_name = crate::schema::words)]
pub struct NewWord<'a> {
pub word : &'a str,
pub info : &'a str,
}
+11 -23
View File
@@ -1,27 +1,15 @@
{ mkCljBin { crane-lib
, fake-git
, lib , lib
, mkGraalBin , sqlite
}: }:
let crane-lib.buildPackage (lib.fix (finalAttrs: {
# mkCljBin sans fake-git. We don't need it, and I don't want it in pname = "wiktlarp";
# my dev shell.
mkCljBin' = args: (mkCljBin args).overrideAttrs (final: prev: {
nativeBuildInputs =
builtins.filter
# A possibly-sketchy predicate, lol.
(x: x != fake-git)
prev.nativeBuildInputs;
});
# bin-path = lib.makeBinPath [
# sqlite3
# ];
in mkCljBin' {
name = "wiktlarp";
version = "0.1.0"; version = "0.1.0";
projectSrc = lib.cleanSource ./.; src = ./.;
lockfile = ./deps-lock.json; # cargoLock.lockFile = ./Cargo.lock;
main-ns = "wiktlarp.main"; doCheck = true;
} meta.mainProgram = "wiktlarp";
nativeBuildInputs = [ sqlite ];
buildInputs = [ sqlite ];
}))
View File
Generated
+10 -125
View File
@@ -1,115 +1,21 @@
{ {
"nodes": { "nodes": {
"clj-nix": { "crane": {
"inputs": {
"devshell": "devshell",
"nix-fetcher-data": "nix-fetcher-data",
"nixpkgs": "nixpkgs"
},
"locked": { "locked": {
"lastModified": 1773151887, "lastModified": 1785782307,
"narHash": "sha256-YUiehwe2iTlwYSrnJ1pcw7KDNX+U42xlTx/2k/mo0P8=", "narHash": "sha256-MPaRdVkf6zZP5fCPxYCi8Dr4pZzgmXzg8T9nVEbp3Mw=",
"owner": "jlesquembre", "owner": "ipetkov",
"repo": "clj-nix", "repo": "crane",
"rev": "27dac4466c9d3939f6a4925bc09e0cb1d8f32d9c", "rev": "2c71e194474d13de031d729b729c968ddbe3507f",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "jlesquembre", "owner": "ipetkov",
"repo": "clj-nix", "repo": "crane",
"type": "github"
}
},
"devshell": {
"inputs": {
"nixpkgs": [
"clj-nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1768818222,
"narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=",
"owner": "numtide",
"repo": "devshell",
"rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1719745305,
"narHash": "sha256-xwgjVUpqSviudEkpQnioeez1Uo2wzrsMaJKJClh+Bls=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "c3c5ecc05edc7dafba779c6c1a61cd08ac6583e9",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"nix-fetcher-data": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": [
"clj-nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1755022803,
"narHash": "sha256-/QtBdVfZlrRJW5enUoWlBE2wrLXJBMJ45X0rZh0jiaU=",
"owner": "jlesquembre",
"repo": "nix-fetcher-data",
"rev": "9da3926b1459d6ff15268072d1c51351b82811b9",
"type": "github"
},
"original": {
"owner": "jlesquembre",
"repo": "nix-fetcher-data",
"type": "github" "type": "github"
} }
}, },
"nixpkgs": { "nixpkgs": {
"locked": {
"lastModified": 1772773019,
"narHash": "sha256-E1bxHxNKfDoQUuvriG71+f+s/NT0qWkImXsYZNFFfCs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "aca4d95fce4914b3892661bcb80b8087293536c6",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1717284937,
"narHash": "sha256-lIbdfCsf8LMFloheeE6N31+BMIeixqyQWbSr2vk79EQ=",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz"
}
},
"nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1775095191, "lastModified": 1775095191,
"narHash": "sha256-CsqRiYbgQyv01LS0NlC7shwzhDhjNDQSrhBX8VuD3nM=", "narHash": "sha256-CsqRiYbgQyv01LS0NlC7shwzhDhjNDQSrhBX8VuD3nM=",
@@ -127,29 +33,8 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"clj-nix": "clj-nix", "crane": "crane",
"nixpkgs": "nixpkgs_2", "nixpkgs": "nixpkgs"
"sydpkgs": "sydpkgs"
}
},
"sydpkgs": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1775943619,
"narHash": "sha256-UwowTI+MSA4SD+Oebxnj02vAou8GNjvZYrCQBwKe5q0=",
"ref": "refs/heads/main",
"rev": "e44c957573baa6a196e8de107cfc5bba31caf29a",
"revCount": 39,
"type": "git",
"url": "https://git.deertopia.net/msyds/sydpkgs"
},
"original": {
"type": "git",
"url": "https://git.deertopia.net/msyds/sydpkgs"
} }
} }
}, },
+13 -18
View File
@@ -1,14 +1,10 @@
{ {
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
clj-nix.url = "github:jlesquembre/clj-nix"; crane.url = "github:ipetkov/crane";
sydpkgs = {
url = "git+https://git.deertopia.net/msyds/sydpkgs";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
outputs = { self, nixpkgs, clj-nix, ... }@inputs: outputs = { self, nixpkgs, ... }@inputs:
let let
supportedSystems = [ supportedSystems = [
"aarch64-darwin" "aarch64-darwin"
@@ -20,9 +16,6 @@
each-system = f: nixpkgs.lib.genAttrs supportedSystems (system: f rec { each-system = f: nixpkgs.lib.genAttrs supportedSystems (system: f rec {
pkgs = import nixpkgs { pkgs = import nixpkgs {
inherit system; inherit system;
overlays = [
clj-nix.overlays.default
];
}; };
inherit (pkgs) lib; inherit (pkgs) lib;
inherit system; inherit system;
@@ -31,20 +24,22 @@
# Exposed as a REPL convenience. # Exposed as a REPL convenience.
_pkgs = each-system ({ pkgs, ... }: pkgs); _pkgs = each-system ({ pkgs, ... }: pkgs);
packages = each-system ({ pkgs, ... }: { packages = each-system ({ pkgs, lib, ... }: (lib.fix (p: {
default = pkgs.callPackage ./default.nix {}; default = p.wiktlarp;
wiktlarp = pkgs.callPackage ./default.nix {
crane-lib = inputs.crane.mkLib pkgs;
};
db = pkgs.callPackage ./db {}; db = pkgs.callPackage ./db {};
}); })));
devShells = each-system ({ pkgs, system, ... }: { devShells = each-system ({ pkgs, system, ... }: {
default = pkgs.mkShell { default = pkgs.mkShell {
inputsFrom = [ self.packages.${system}.db ]; inputsFrom = [
DATABASE_URL = "words.db"; self.packages.${system}.db
self.packages.${system}.default
];
DATABASE_URL = "db/words.db";
packages = with pkgs; [ packages = with pkgs; [
zprint
clojure
babashka
python3
(sqlite.override { interactive = true; }) (sqlite.override { interactive = true; })
python314Packages.wiktextract python314Packages.wiktextract
sqlite-web sqlite-web
+165
View File
@@ -0,0 +1,165 @@
use regex::{regex, Regex};
#[derive(Debug,Eq,PartialEq,Clone)]
pub struct EntryQuery<'a> {
pub term : &'a str,
pub language : Option <&'a str>,
pub pos : Option <&'a str>,
}
impl<'a> EntryQuery<'a> {
pub fn basic (term : &'a str) -> EntryQuery<'a> {
EntryQuery { term, language: Some ("English"), pos: None }
}
}
#[derive(Debug,Eq,PartialEq,Clone)]
pub enum Command<'a> {
Define (EntryQuery<'a>),
Etymology (EntryQuery<'a>),
Quit,
}
#[derive(Debug,Eq,PartialEq,Clone)]
pub struct ChatMessage<'a> {
author : &'a str,
content : &'a str,
}
impl<'a> ChatMessage<'a> {
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 (),
})
}
}
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_quit (s)
.or (Self::parse_etymology (s))
.or (Self::parse_define (s))
}
fn parse_quit (s : &'a str) -> Option<Command<'a>> {
if s == "123 I Browsed The Source Code And Found \
The Funny Quit Command LOL!!!!!" {
Some (Command::Quit)
} else {
None
}
}
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 {
// 왜 이렇게 필요하니?
#[allow(unused_imports)]
use super::*;
#[allow(unused_imports)]
use super::Command as Cmd;
#[test]
fn etym_wikilink_inline_mixed () {
assert_eq! (
Cmd::parse_str (
"blah blah etymology of [[lexicography]]"
),
Some (Cmd::Etymology (EntryQuery::basic ("lexicography")))
);
}
#[test]
fn etym_wikilink () {
assert_eq! (
Cmd::parse_str (
"etym [[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_inline () {
assert_eq! (
Cmd::parse_str ("bkah blah blah [[quirkchungus]] blbalb"),
Some (Cmd::Define (EntryQuery::basic ("quirkchungus")))
);
}
#[test]
fn define_wikilink_whole () {
assert_eq! (
Cmd::parse_str ("[[hoofjob]]"),
Some (Cmd::Define (EntryQuery::basic ("hoofjob")))
);
}
#[test]
fn define_wikilink_resembles_basic () {
assert_eq! (
Cmd::parse_str ("define I'm [[twenty]] years old"),
Some (Cmd::Define (EntryQuery::basic ("twenty")))
);
}
}
+56
View File
@@ -0,0 +1,56 @@
use notify::{Event, RecursiveMode, Result, Watcher};
use tokio::sync::mpsc as tokio_mpsc;
use std::{fs::File, io::{Read as _, Seek as _, SeekFrom}, path::{Path, PathBuf}, sync::mpsc};
use crate::error;
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 ()
}
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
}
}
pub async fn watch (
log_file : &Path,
txr : tokio_mpsc::Sender<String>,
) -> error::Result<()> {
let mut prev_size = file_size (log_file);
let (tx, rx) = mpsc::channel::<Result<Event>> ();
let mut watcher = notify::recommended_watcher (tx)
.map_err (error::Error::Notify)?;
watcher.watch (log_file, RecursiveMode::NonRecursive)
.map_err (error::Error::Notify)?;
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).expect ("utf-8 error?");
for l in buf.lines () {
txr.send (l.to_string ()).await.unwrap ();
}
} if new_size < prev_size {
log::warn! ("log file shrank!??");
}
prev_size = new_size;
}
Ok (())
}
+21 -29
View File
@@ -1,16 +1,16 @@
pub mod models;
pub mod schema;
use diesel::prelude::*; use diesel::prelude::*;
use diesel::connection::SimpleConnection; use diesel::connection::SimpleConnection;
use diesel::r2d2::{ConnectionManager, CustomizeConnection, Pool}; use diesel::r2d2::{ConnectionManager, CustomizeConnection, Pool};
use std::env; use std::env;
use crate::models::*; use models::*;
#[derive(Debug)] #[derive(Debug)]
pub struct Fuck (); pub struct Fuck ();
impl CustomizeConnection<SqliteConnection, diesel::r2d2::Error> for Fuck { fn init_conn (conn : &mut SqliteConnection) -> Result<(), diesel::r2d2::Error> {
fn on_acquire (&self, conn: &mut SqliteConnection) -> Result<
(), diesel::r2d2::Error
> {
conn.batch_execute (" conn.batch_execute ("
PRAGMA busy_timeout = 15000; PRAGMA busy_timeout = 15000;
PRAGMA journal_mode = WAL; PRAGMA journal_mode = WAL;
@@ -18,10 +18,17 @@ impl CustomizeConnection<SqliteConnection, diesel::r2d2::Error> for Fuck {
PRAGMA wal_autocheckpoint = 1000; PRAGMA wal_autocheckpoint = 1000;
PRAGMA wal_checkpoint(TRUNCATE); PRAGMA wal_checkpoint(TRUNCATE);
PRAGMA cache_size=-2000000 PRAGMA cache_size=-2000000
").unwrap (); ")?;
Ok (()) Ok (())
} }
impl CustomizeConnection<SqliteConnection, diesel::r2d2::Error> for Fuck {
fn on_acquire (&self, conn : &mut SqliteConnection) -> Result<
(), diesel::r2d2::Error
> {
init_conn (conn)
}
fn on_release (&self, _conn: SqliteConnection) {} fn on_release (&self, _conn: SqliteConnection) {}
} }
@@ -41,30 +48,18 @@ pub fn connect () -> SqliteConnection {
.expect ("DATABASE_URL must be set"); .expect ("DATABASE_URL must be set");
let mut conn = SqliteConnection::establish (&database_url) let mut conn = SqliteConnection::establish (&database_url)
.unwrap_or_else (|_| panic! ("Error connecting to {}", database_url)); .unwrap_or_else (|_| panic! ("Error connecting to {}", database_url));
conn.batch_execute (" init_conn (&mut conn).unwrap ();
PRAGMA journal_mode = WAL; -- better write-concurrency
PRAGMA synchronous = NORMAL; -- fsync only in critical moments
PRAGMA wal_autocheckpoint = 1000; -- write WAL changes back every 1000 pages, for an in average 1MB WAL file. May affect readers if number is increased
PRAGMA wal_checkpoint(TRUNCATE); -- free some space by truncating possibly massive WAL files from the last run.
PRAGMA busy_timeout = 5000; -- sleep if the database is busy
PRAGMA cache_size=-2000000
").unwrap ();
conn conn
} }
pub fn get_word (w : &str) -> Vec<Word> { pub fn get_word (w : &str) -> Vec<Word> {
let conn = &mut connect (); let conn = &mut connect ();
use crate::schema::words::dsl::*; use schema::words::dsl::*;
let results = words words
.filter (word.eq (w)) .filter (word.eq (w))
.limit (5)
.select (Word::as_select ()) .select (Word::as_select ())
.load (conn) .load (conn)
.expect ("error loading words"); .expect ("error loading words")
for wd in results {
println! ("{}", wd.word);
}
todo! ()
} }
pub fn entry_as_word (entry : &serde_json::Value) -> Option<&str> { pub fn entry_as_word (entry : &serde_json::Value) -> Option<&str> {
@@ -73,14 +68,11 @@ pub fn entry_as_word (entry : &serde_json::Value) -> Option<&str> {
pub fn insert_word ( pub fn insert_word (
conn : &mut SqliteConnection, conn : &mut SqliteConnection,
entry : serde_json::Value, info : &serde_json::Value,
) -> Option<()> { ) -> Option<()> {
use crate::schema::words; use schema::words;
let word = entry_as_word (&entry)?; let word = entry_as_word (info)?;
let new_word = NewWord { let new_word = NewWord { word, info };
word,
info: &entry.to_string (),
};
diesel::insert_into (words::table) diesel::insert_into (words::table)
.values (new_word) .values (new_word)
.execute (conn).map_or_else ( .execute (conn).map_or_else (
+34
View File
@@ -0,0 +1,34 @@
use diesel::prelude::*;
use serde_json as json;
#[derive(Queryable, Selectable, Debug)]
#[diesel(table_name = crate::db::schema::words)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Word {
pub id : i32,
pub word : String,
pub info : json::Value,
}
#[derive(Insertable,Debug)]
#[diesel(table_name = crate::db::schema::words)]
pub struct NewWord<'a> {
pub word : &'a str,
pub info : &'a json::Value,
}
impl Word {
pub fn glosses (&self) -> Vec<String> {
self.info["senses"].as_array ().unwrap ()
.iter ()
.flat_map (|x| {
x.get ("raw_glosses")
.map (|y| y.as_array ().unwrap ().clone ())
.or (x.get ("glosses")
.map (|y| y.as_array ().unwrap ().clone ()))
.unwrap_or (Vec::default ())
})
.map (|x| x.as_str ().unwrap ().to_string ())
.collect ()
}
}
+1 -1
View File
@@ -4,6 +4,6 @@ diesel::table! {
words (id) { words (id) {
id -> Integer, id -> Integer,
word -> Text, word -> Text,
info -> Text, info -> Json,
} }
} }
+7
View File
@@ -0,0 +1,7 @@
#[derive(Debug)]
pub enum Error {
Rcon (::rcon::Error),
Notify (::notify::Error),
}
pub type Result<T> = core::result::Result<T, Error>;
+101
View File
@@ -0,0 +1,101 @@
mod console;
mod command;
mod rcon;
mod error;
mod db;
use command::{ChatMessage, Command, EntryQuery};
use rcon::Rcon;
use std::{net::SocketAddr, path::PathBuf};
use tokio::sync::mpsc as mpsc;
use clap::Parser;
/// 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
///
/// This must match the value of TF2's `con_logfile` setting.
#[clap (
value_parser,
long,
short='f',
default_value=console::default_log_file ().into_os_string ()
)]
log_file: PathBuf,
/// RCON address to which chat commands are sent
#[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 () -> error::Result<()> {
colog::init();
let args = Args::parse ();
println! ("{:?}", args);
let (tx, mut rx) = mpsc::channel::<String> (32);
let mut rcon = Rcon::connect (
args.rcon_address, &args.rcon_password
).await?;
tokio::spawn (async move {
console::watch (
&args.log_file,
tx
).await.unwrap ();
});
while let Some (line) = rx.recv ().await {
println! ("line: {line}");
let Some (msg) = ChatMessage::parse (&line) else { continue };
let Some (cmd) = Command::parse (&msg) else { continue };
println! ("cmd: {:?}", cmd);
// wait a moment to avoid being ratelimitted in the case that
// we are responding to our own message.
tokio::time::sleep (
tokio::time::Duration::from_millis (750)
).await;
match cmd {
Command::Define (EntryQuery {term,language,..}) => {
let fuckyou9000 = db::get_word (term);
let w = fuckyou9000
.iter ()
.filter (|x| language.is_none ()
|| x.info["lang"].as_str () == language)
.flat_map (|x| x.glosses ())
.nth (0)
.unwrap_or (
"no entry found (u_u) contribute it! };3".to_string ()
);
let mut s = format! ("{term}: {:?}", w);
s.truncate (127); // fuck you
rcon.say (&s).await.unwrap ();
},
Command::Etymology (entry_query) => {
todo! ()
}
Command::Quit => {
rcon.cmd ("disconnect").await.unwrap ();
}
}
}
Ok (())
}
+49
View File
@@ -0,0 +1,49 @@
use crate::error;
use ::rcon::Connection;
use std::net::SocketAddr;
use tokio::net::TcpStream;
/// An RCON connection config. Methods will lazily (re)connect
pub struct Rcon {
conn : Connection<TcpStream>
}
impl AsRef<Connection<TcpStream>> for Rcon {
fn as_ref (&self) -> &Connection<TcpStream> {
&self.conn
}
}
impl Rcon {
pub async fn connect (
address : SocketAddr,
password : &str
) -> error::Result<Rcon> {
let r = <Connection<TcpStream>>::builder ()
.connect (address, password)
.await;
match r {
Ok (conn) => {
log::info! ("connected to rcon");
tokio::time::sleep (
tokio::time::Duration::from_millis (500)
).await;
Ok (Rcon { conn })
},
Err (e) => Err (error::Error::Rcon (e)),
}
}
pub async fn cmd (&mut self, s : &str) -> error::Result<String> {
self.conn.cmd (s).await.map_err (error::Error::Rcon)
}
pub async fn say (&mut self, s : &str) -> error::Result<String> {
if s.contains ("\"") && s.contains ("quit") {
self.cmd ("say \"try harder pal!!!!!\"").await
} else {
let probably_safe_s = s.replace("\"", "'");
self.cmd (&format! ("say \"{probably_safe_s}\"")).await
}
}
}
-161
View File
@@ -1,161 +0,0 @@
(ns wiktlarp.main
(:require [clojure.java.io :as io]
[cheshire.core :as json]
[babashka.fs :as fs]
[babashka.process :as p]
[progrock.core :as prog]
[clj-rcon.core :as rcon]
[clojure.tools.logging :as l]
[clojure.string :as str]
[hawk.core :as hawk]
[next.jdbc :as sql]
[integrant.core :as ig])
(:gen-class))
(def chat-message-regexp #"(\*사망\* )?(.*?) : (.*)")
(def ^:dynamic *rcon*)
(def ^:dynamic *db*)
(defmacro with-conn [[conn] & body]
`(let [ds# (sql/get-datasource
{:dbtype "sqlite"
:dbname "db/words.db"})]
(with-open [~conn (sql/get-connection ds#)]
(sql/execute! ~conn ["select load_extension (?)"
(System/getenv "SPELLFIX")])
~@body)))
(defn query-word [word]
(->> (sql/execute! *db* ["select info from words where word = ?"
word])
(map #(json/parse-string (:words/info %) keyword))))
(defn gloss-word [word]
(->> word
query-word
(mapcat :senses)
(mapcat (some-fn :raw_glosses :glosses))
first))
(defn parse-chat-message [x]
(when-let [[_ _dead? author body]
(re-matches chat-message-regexp x)]
{:author author :body body}))
(defn say [s]
(->> s
(take 128) ; truncate to 128 chars
(apply str)
(format "say \"%s\"")
(rcon/exec *rcon*)))
(defn find-word [s]
(some-> (or (re-find #"\[\[([^]]+)]]" s)
(re-find #"define\s+(\w+)" s))
second))
(defn do-definition [s]
(when-some [w (find-word s)]
(l/infof "looking up word: %s" w)
(let [r (gloss-word w)]
(if r
(l/infof "found word! %s" r)
(l/infof "no entry... %s" w))
(Thread/sleep 750) ; avoid ratelimit lol
(say (format "%s: %s"
w (or r "no entry found (u_u)"))))))
(defn rcon-connect [host port password]
(l/info "attempting rcon connection...")
(or (try (let [c @(rcon/connect host port password)]
@(rcon/exec c "echo \"wikilarper connected!\"")
(l/info "connected to rcon!")
c)
(catch java.net.ConnectException e
(l/info (ex-message e))
nil))
(do (Thread/sleep 5000)
(recur host port password))))
(defn handle-console-event [s]
(l/infof "! %s" s)
(when-let [{:keys [author body]} (parse-chat-message s)]
(do-definition body)))
(def ^:dynamic *prev-size*)
(defn console-handler [log-file]
(let [prev @*prev-size*
content (slurp log-file)
size (count content)]
(try (cond (< prev size) (-> content
(subs prev)
str/trim-newline
handle-console-event)
(< size prev) (l/warn "log file shrunk?")
:else nil)
(finally (reset! *prev-size* size)))))
(def config
{:rcon/connection {:ip "127.0.0.1"
:port 27015
:password "monitor"}
:database/connection {:dbtype "sqlite"
:dbname "db/words.db"}
:console/watcher
{:log-file
(-> (str "~/.local/share/Steam/steamapps/common/Team Fortress 2/"
"tf/console.log")
fs/expand-home
fs/file)
:handler (ig/ref :wikilinker/handler)}
:wikilinker/handler {:rcon (ig/ref :rcon/connection)
:db (ig/ref :database/connection)}})
(defmethod ig/init-key :rcon/connection [_ {:keys [ip port password]}]
(rcon-connect ip port password))
(defmethod ig/halt-key! :rcon/connection [_ conn]
(.close conn))
(defmethod ig/init-key :database/connection [_ dsinfo]
(sql/get-datasource dsinfo))
(defmethod ig/halt-key! :database/connection [_ conn]
(.close conn))
(defmethod ig/init-key :console/watcher [_ {:keys [log-file handler]}]
(l/infof "watching file %s" log-file)
(hawk/watch!
[{:paths [(str log-file)]
:handler
(binding [*prev-size* (atom (if (fs/exists? log-file)
(-> log-file slurp count)
0))]
(bound-fn [_ctx ev]
(try (handler log-file)
(catch Exception e
(l/errorf e "exception in console handler")
(throw e)))))}]))
(defmethod ig/halt-key! :console/watcher [_ watcher]
(hawk/stop! watcher))
(defmethod ig/init-key :wikilinker/handler
[_ {:keys [rcon db]}]
(binding [*rcon* rcon
*db* db]
(bound-fn [log-file] (console-handler log-file))))
(defn -main []
(let [system (ig/init config)]
(.addShutdownHook (Runtime/getRuntime)
(Thread. #(do (ig/halt! system)
(binding [*out* *err*]
(println "shutting down")))))))