From 793e2deae8fe6c43e84b4248e9f4cc80f875d6ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Madeleine=20Sydney=20=C5=9Alaga?= Date: Sun, 16 Aug 2026 11:01:28 -0600 Subject: [PATCH] idk push for pais --- .gitignore | 4 - Cargo.lock | 242 +++++++++++++++++- Cargo.toml | 7 +- clj/wiktlarp/main.clj | 212 --------------- db/src/models.rs | 17 -- default.nix | 3 + db/diesel.toml => diesel.toml | 0 flake.nix | 2 +- {db/migrations => migrations}/.diesel_lock | 0 {db/migrations => migrations}/.keep | 0 .../down.sql | 0 .../up.sql | 0 src/command/mod.rs | 46 ++-- src/console.rs | 64 +++-- db/src/db.rs => src/db/mod.rs | 60 ++--- src/db/models.rs | 34 +++ {db/src => src/db}/schema.rs | 2 +- src/error.rs | 7 + src/main.rs | 91 ++++--- src/rcon.rs | 49 ++++ 20 files changed, 480 insertions(+), 360 deletions(-) delete mode 100644 clj/wiktlarp/main.clj rename db/diesel.toml => diesel.toml (100%) rename {db/migrations => migrations}/.diesel_lock (100%) rename {db/migrations => migrations}/.keep (100%) rename {db/migrations => migrations}/2026-08-04-180241-0000_create-words/down.sql (100%) rename {db/migrations => migrations}/2026-08-04-180241-0000_create-words/up.sql (100%) rename db/src/db.rs => src/db/mod.rs (56%) create mode 100644 src/db/models.rs rename {db/src => src/db}/schema.rs (85%) create mode 100644 src/error.rs create mode 100644 src/rcon.rs diff --git a/.gitignore b/.gitignore index 4d5a486..13f1b73 100644 --- a/.gitignore +++ b/.gitignore @@ -19,8 +19,4 @@ raw-wiktexttract-data.jsonl *.db raw-wiktextract-data.jsonlwords.db words.db-journal - - -# Added by cargo - /target diff --git a/Cargo.lock b/Cargo.lock index 638fc9e..def8a25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,6 +61,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.13.1" @@ -146,12 +152,44 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "colog" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df62599ba6adc9c6c04a54278c8209125343dc4775f57b9d76c9a4287e58f2bd" +dependencies = [ + "colored", + "env_logger", + "log", +] + [[package]] name = "colorchoice" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "console" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width", + "windows-sys 0.61.2", +] + [[package]] name = "darling" version = "0.21.3" @@ -187,6 +225,37 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + [[package]] name = "deranged" version = "0.5.8" @@ -203,6 +272,7 @@ dependencies = [ "downcast-rs", "libsqlite3-sys", "r2d2", + "serde_json", "sqlite-wasm-rs", "time", ] @@ -276,6 +346,35 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -333,6 +432,30 @@ dependencies = [ "libc", ] +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -381,13 +504,26 @@ dependencies = [ "hashbrown 0.17.1", ] +[[package]] +name = "indicatif" +version = "0.18.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" +dependencies = [ + "console", + "portable-atomic", + "unicode-width", + "unit-prefix", + "web-time", +] + [[package]] name = "inotify" version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592" dependencies = [ - "bitflags", + "bitflags 2.13.1", "inotify-sys", "libc", ] @@ -413,6 +549,42 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "js-sys" version = "0.3.104" @@ -420,6 +592,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -439,7 +612,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" dependencies = [ - "bitflags", + "bitflags 2.13.1", "libc", ] @@ -507,7 +680,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags", + "bitflags 2.13.1", "fsevent-sys", "inotify", "kqueue", @@ -525,7 +698,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" dependencies = [ - "bitflags", + "bitflags 2.13.1", ] [[package]] @@ -596,6 +769,21 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -671,7 +859,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.13.1", ] [[package]] @@ -851,6 +1039,12 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.15.2" @@ -1008,6 +1202,17 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.8.23" @@ -1086,12 +1291,24 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -1191,18 +1408,33 @@ dependencies = [ "tracing", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "wiktlarp" version = "0.1.0" dependencies = [ "clap", + "colog", "diesel", + "indicatif", "log", "notify", "rcon", "regex", + "serde", + "serde_json", "shellexpand", "tokio", + "tokio-stream", "watchfile", "xdg", ] diff --git a/Cargo.toml b/Cargo.toml index f970cbe..4640417 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,12 +5,17 @@ edition = "2024" [dependencies] clap = { version = "4.6.6", features = ["derive", "string"] } -diesel = { version = "2.3.12", features = ["r2d2", "sqlite"] } +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" diff --git a/clj/wiktlarp/main.clj b/clj/wiktlarp/main.clj deleted file mode 100644 index 2dd0f07..0000000 --- a/clj/wiktlarp/main.clj +++ /dev/null @@ -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"))))))) diff --git a/db/src/models.rs b/db/src/models.rs index 2d96726..e69de29 100644 --- a/db/src/models.rs +++ b/db/src/models.rs @@ -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, -} diff --git a/default.nix b/default.nix index 56d7e8c..6a6a38e 100644 --- a/default.nix +++ b/default.nix @@ -1,5 +1,6 @@ { crane-lib , lib +, sqlite }: crane-lib.buildPackage (lib.fix (finalAttrs: { @@ -9,4 +10,6 @@ crane-lib.buildPackage (lib.fix (finalAttrs: { # cargoLock.lockFile = ./Cargo.lock; doCheck = true; meta.mainProgram = "wiktlarp"; + nativeBuildInputs = [ sqlite ]; + buildInputs = [ sqlite ]; })) diff --git a/db/diesel.toml b/diesel.toml similarity index 100% rename from db/diesel.toml rename to diesel.toml diff --git a/flake.nix b/flake.nix index 0ad220d..6d0cd28 100644 --- a/flake.nix +++ b/flake.nix @@ -38,7 +38,7 @@ self.packages.${system}.db self.packages.${system}.default ]; - DATABASE_URL = "words.db"; + DATABASE_URL = "db/words.db"; packages = with pkgs; [ (sqlite.override { interactive = true; }) python314Packages.wiktextract diff --git a/db/migrations/.diesel_lock b/migrations/.diesel_lock similarity index 100% rename from db/migrations/.diesel_lock rename to migrations/.diesel_lock diff --git a/db/migrations/.keep b/migrations/.keep similarity index 100% rename from db/migrations/.keep rename to migrations/.keep diff --git a/db/migrations/2026-08-04-180241-0000_create-words/down.sql b/migrations/2026-08-04-180241-0000_create-words/down.sql similarity index 100% rename from db/migrations/2026-08-04-180241-0000_create-words/down.sql rename to migrations/2026-08-04-180241-0000_create-words/down.sql diff --git a/db/migrations/2026-08-04-180241-0000_create-words/up.sql b/migrations/2026-08-04-180241-0000_create-words/up.sql similarity index 100% rename from db/migrations/2026-08-04-180241-0000_create-words/up.sql rename to migrations/2026-08-04-180241-0000_create-words/up.sql diff --git a/src/command/mod.rs b/src/command/mod.rs index ac649c8..0ecbc16 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -2,14 +2,14 @@ use regex::{regex, Regex}; #[derive(Debug,Eq,PartialEq,Clone)] pub struct EntryQuery<'a> { - term : &'a str, - language : Option <&'a str>, - pos : Option <&'a str>, + 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: None, pos: None } + EntryQuery { term, language: Some ("English"), pos: None } } } @@ -17,6 +17,7 @@ impl<'a> EntryQuery<'a> { pub enum Command<'a> { Define (EntryQuery<'a>), Etymology (EntryQuery<'a>), + Quit, } #[derive(Debug,Eq,PartialEq,Clone)] @@ -26,10 +27,6 @@ pub struct ChatMessage<'a> { } impl<'a> ChatMessage<'a> { - pub fn unauthored (content : &'a str) -> ChatMessage<'a> { - ChatMessage { author: "", content } - } - pub fn parse (line : &'a str) -> Option> { let re : &Regex = regex! (r"(?:\*사망\* )?(.*?) : (.*)"); let r = re.captures (line)?; @@ -40,26 +37,24 @@ impl<'a> ChatMessage<'a> { } } -fn find_query<'a> (s : &'a str) -> Option> { - 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> { Self::parse_str (msg.content) } pub fn parse_str (s : &'a str) -> Option> { - Self::parse_etymology (s).or (Self::parse_define (s)) + Self::parse_quit (s) + .or (Self::parse_etymology (s)) + .or (Self::parse_define (s)) + } + + fn parse_quit (s : &'a str) -> Option> { + 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> { @@ -76,7 +71,9 @@ impl<'a> Command<'a> { } fn parse_etymology (s : &'a str) -> Option> { - let basic_re = regex! (r"^\s*etymology of (.*)|ety(?:m(?:ology(?: of)?)?)? (\w+)"); + 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) @@ -90,7 +87,10 @@ impl<'a> Command<'a> { } mod tests { + // 왜 이렇게 필요하니? + #[allow(unused_imports)] use super::*; + #[allow(unused_imports)] use super::Command as Cmd; #[test] diff --git a/src/console.rs b/src/console.rs index 41f3ba1..0cfdb57 100644 --- a/src/console.rs +++ b/src/console.rs @@ -1,5 +1,7 @@ use notify::{Event, RecursiveMode, Result, Watcher}; -use std::{fs::File, io::{Read as _, Seek as _, SeekFrom, pipe}, path::{Path, PathBuf}, sync::mpsc}; +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 () @@ -9,10 +11,6 @@ pub fn default_log_file () -> PathBuf { .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)); @@ -22,37 +20,37 @@ fn file_size (file : &Path) -> u64 { } } -impl Console { - pub fn watch (log_file : &Path, cb : F) -> Result< - // mpsc::Receiver> - () - > - where F : Fn (&str) -> () - { - let mut prev_size = file_size (log_file); - let (tx, rx) = mpsc::channel::> (); - 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 - }; +pub async fn watch ( + log_file : &Path, + txr : tokio_mpsc::Sender, +) -> error::Result<()> { + let mut prev_size = file_size (log_file); + let (tx, rx) = mpsc::channel::> (); + 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); + 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!??"); + 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 (); } - prev_size = new_size; + } if new_size < prev_size { + log::warn! ("log file shrank!??"); } - Ok (()) + prev_size = new_size; } + Ok (()) } diff --git a/db/src/db.rs b/src/db/mod.rs similarity index 56% rename from db/src/db.rs rename to src/db/mod.rs index 6ecf2b6..a26de2f 100644 --- a/db/src/db.rs +++ b/src/db/mod.rs @@ -1,25 +1,32 @@ +pub mod models; +pub mod schema; + use diesel::prelude::*; use diesel::connection::SimpleConnection; use diesel::r2d2::{ConnectionManager, CustomizeConnection, Pool}; use std::env; -use crate::models::*; +use models::*; #[derive(Debug)] pub struct Fuck (); +fn init_conn (conn : &mut SqliteConnection) -> Result<(), diesel::r2d2::Error> { + conn.batch_execute (" + PRAGMA busy_timeout = 15000; + PRAGMA journal_mode = WAL; + PRAGMA synchronous = NORMAL; + PRAGMA wal_autocheckpoint = 1000; + PRAGMA wal_checkpoint(TRUNCATE); + PRAGMA cache_size=-2000000 + ")?; + Ok (()) +} + impl CustomizeConnection for Fuck { - fn on_acquire (&self, conn: &mut SqliteConnection) -> Result< + fn on_acquire (&self, conn : &mut SqliteConnection) -> Result< (), diesel::r2d2::Error > { - conn.batch_execute (" - PRAGMA busy_timeout = 15000; - PRAGMA journal_mode = WAL; - PRAGMA synchronous = NORMAL; - PRAGMA wal_autocheckpoint = 1000; - PRAGMA wal_checkpoint(TRUNCATE); - PRAGMA cache_size=-2000000 - ").unwrap (); - Ok (()) + init_conn (conn) } fn on_release (&self, _conn: SqliteConnection) {} @@ -41,30 +48,18 @@ pub fn connect () -> SqliteConnection { .expect ("DATABASE_URL must be set"); let mut conn = SqliteConnection::establish (&database_url) .unwrap_or_else (|_| panic! ("Error connecting to {}", database_url)); - conn.batch_execute (" - 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 (); + init_conn (&mut conn).unwrap (); conn } pub fn get_word (w : &str) -> Vec { let conn = &mut connect (); - use crate::schema::words::dsl::*; - let results = words + use schema::words::dsl::*; + words .filter (word.eq (w)) - .limit (5) .select (Word::as_select ()) .load (conn) - .expect ("error loading words"); - for wd in results { - println! ("{}", wd.word); - } - todo! () + .expect ("error loading words") } 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 ( conn : &mut SqliteConnection, - entry : serde_json::Value, + info : &serde_json::Value, ) -> Option<()> { - use crate::schema::words; - let word = entry_as_word (&entry)?; - let new_word = NewWord { - word, - info: &entry.to_string (), - }; + use schema::words; + let word = entry_as_word (info)?; + let new_word = NewWord { word, info }; diesel::insert_into (words::table) .values (new_word) .execute (conn).map_or_else ( diff --git a/src/db/models.rs b/src/db/models.rs new file mode 100644 index 0000000..89ebb52 --- /dev/null +++ b/src/db/models.rs @@ -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 { + 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 () + } +} diff --git a/db/src/schema.rs b/src/db/schema.rs similarity index 85% rename from db/src/schema.rs rename to src/db/schema.rs index ec448ae..3a964fd 100644 --- a/db/src/schema.rs +++ b/src/db/schema.rs @@ -4,6 +4,6 @@ diesel::table! { words (id) { id -> Integer, word -> Text, - info -> Text, + info -> Json, } } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..4122ece --- /dev/null +++ b/src/error.rs @@ -0,0 +1,7 @@ +#[derive(Debug)] +pub enum Error { + Rcon (::rcon::Error), + Notify (::notify::Error), +} + +pub type Result = core::result::Result; diff --git a/src/main.rs b/src/main.rs index 0d6f096..de42cd0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,18 @@ mod console; mod command; +mod rcon; +mod error; +mod db; -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 command::{ChatMessage, Command, EntryQuery}; +use rcon::Rcon; +use std::{net::SocketAddr, path::PathBuf}; +use tokio::sync::mpsc as mpsc; 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( +/// LARP as Wiktionary in TF2's in-game chat. +#[derive (Parser, Debug)] +#[command ( name = "wiktlarp", version, about, @@ -23,7 +20,9 @@ use console::Console; )] struct Args { /// Path to TF2's console log - #[clap( + /// + /// This must match the value of TF2's `con_logfile` setting. + #[clap ( value_parser, long, short='f', @@ -31,8 +30,8 @@ struct Args { )] log_file: PathBuf, - /// RCON address to send chat commands to - #[clap( + /// RCON address to which chat commands are sent + #[clap ( value_parser, long, short='a', @@ -41,28 +40,62 @@ struct Args { rcon_address: SocketAddr, /// RCON password - #[clap(value_parser,long,short='p',default_value="monitor")] + #[clap (value_parser,long,short='p',default_value="monitor")] rcon_password: String, } #[tokio::main] -async fn main () -> Result<()> { +async fn main () -> error::Result<()> { + colog::init(); let args = Args::parse (); println! ("{:?}", args); - let mut client = >::builder () - .connect (args.rcon_address.to_string (), &args.rcon_password) - .await.unwrap (); + let (tx, mut rx) = mpsc::channel:: (32); + let mut rcon = Rcon::connect ( + args.rcon_address, &args.rcon_password + ).await?; - 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); + 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 (()) } diff --git a/src/rcon.rs b/src/rcon.rs new file mode 100644 index 0000000..850c948 --- /dev/null +++ b/src/rcon.rs @@ -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 +} + +impl AsRef> for Rcon { + fn as_ref (&self) -> &Connection { + &self.conn + } +} + +impl Rcon { + pub async fn connect ( + address : SocketAddr, + password : &str + ) -> error::Result { + let r = >::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 { + self.conn.cmd (s).await.map_err (error::Error::Rcon) + } + + pub async fn say (&mut self, s : &str) -> error::Result { + 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 + } + } +}