rust
This commit is contained in:
@@ -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")))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 (())
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
(ns wiktlarp.main
|
||||
(:require [clojure.java.io :as io]
|
||||
[clojure.core.match :refer [match]]
|
||||
[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])
|
||||
(:import (java.time Instant Duration)
|
||||
(java.time.temporal ChronoUnit))
|
||||
(:gen-class))
|
||||
|
||||
(def chat-message-regexp #"(\*사망\* )?(.*?) : (.*)")
|
||||
|
||||
(def ^:dynamic *rcon*)
|
||||
|
||||
(def ^:dynamic *db*)
|
||||
|
||||
(def cooldown 3)
|
||||
|
||||
(def last-sent (atom (.minusSeconds (Instant/now) 3)))
|
||||
|
||||
(defmacro with-conn [[conn] & body]
|
||||
`(let [ds# (sql/get-datasource
|
||||
{:dbtype "sqlite"
|
||||
:dbname "db/words.db"})]
|
||||
(with-open [~conn (sql/get-connection ds#)]
|
||||
~@body)))
|
||||
|
||||
(defn query-word [word]
|
||||
(->> (sql/execute! *db* ["select info from words where word = ?"
|
||||
word])
|
||||
(map #(json/parse-string (:words/info %) keyword))))
|
||||
|
||||
(defn parse-fuckyou [x]
|
||||
(cond (= x "peggle")
|
||||
"(slang, Tiny Kitty Girl Pound) a video game, i think. idfk."
|
||||
(and (re-find #"chimpanzee" x)
|
||||
(re-find #"segway" x))
|
||||
"(slang, Tiny Kitty Girl Pound) chimpanzeee sirdnway"
|
||||
:else nil))
|
||||
|
||||
(defn gloss-word [word]
|
||||
(if-some [fuckyou (parse-fuckyou word)]
|
||||
fuckyou
|
||||
(->> word
|
||||
query-word
|
||||
(mapcat :senses)
|
||||
(mapcat (some-fn :raw_glosses :glosses))
|
||||
first)))
|
||||
|
||||
(defn etymology-word [word]
|
||||
(->> word
|
||||
query-word
|
||||
(map :etymology_text)
|
||||
first))
|
||||
|
||||
(defn parse-chat-message [x]
|
||||
(when-let [[_ _dead? author body]
|
||||
(re-matches chat-message-regexp x)]
|
||||
{:author author :body body}))
|
||||
|
||||
(defn say [s]
|
||||
(let [now (Instant/now)]
|
||||
(when (< (compare (Duration/ofSeconds 3)
|
||||
(Duration/between @last-sent now))
|
||||
0)
|
||||
(reset! last-sent now)
|
||||
(->> 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 find-etymology [s]
|
||||
(some-> (or (and (re-find #"ety(?:m(?:ology)?)?" s)
|
||||
(re-find #"\[\[([^]]+)]]" s))
|
||||
(re-find #"etymology of \s+(\w+)" s))
|
||||
second))
|
||||
|
||||
(defn parse-command [s]
|
||||
(if-let [x (find-etymology s)]
|
||||
[:etymology x]
|
||||
(if-let [x (find-word s)]
|
||||
[:define x]
|
||||
nil)))
|
||||
|
||||
(defn do-definition [w]
|
||||
(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 do-etymology [w]
|
||||
(l/infof "looking up word: %s" w)
|
||||
(let [r (etymology-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)]
|
||||
(match (parse-command s)
|
||||
[:define x] (do-definition x)
|
||||
[:etymology x] (do-etymology x)
|
||||
:else nil)))
|
||||
|
||||
(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")))))))
|
||||
Reference in New Issue
Block a user