Compare commits
16
Commits
1ae6fd5f4b
...
rust
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
793e2deae8 | ||
|
|
ab77858eda | ||
|
|
1b329c6432 | ||
|
|
7379cffc03 | ||
|
|
6a864e1e89 | ||
|
|
f4a413c362 | ||
|
|
f0dc882945 | ||
|
|
72a8af7601 | ||
|
|
6eb7e98864 | ||
|
|
c4d4bc8735 | ||
|
|
05d48c0c3b | ||
|
|
a2404d294e | ||
|
|
5a6cb93bab | ||
|
|
acc1ad834e | ||
|
|
9d7946cbad | ||
|
|
2d603199a2 |
@@ -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
|
||||
+3
-1
@@ -17,4 +17,6 @@ build/
|
||||
enwiktionary-latest-pages-articles.xml.bz2
|
||||
raw-wiktexttract-data.jsonl
|
||||
*.db
|
||||
raw-wiktextract-data.jsonl
|
||||
raw-wiktextract-data.jsonlwords.db
|
||||
words.db-journal
|
||||
/target
|
||||
|
||||
Generated
+1559
File diff suppressed because it is too large
Load Diff
+21
@@ -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"
|
||||
@@ -1,3 +1,26 @@
|
||||
# wiktionary-tf2
|
||||
# wiktlarp-tf2
|
||||
|
||||
this is a really stupid script to gloss `[[wikilinked]]` words sent in tf2 chat.
|
||||
larp as en.wiktionary.org.
|
||||
|
||||

|
||||
|
||||
this is a really stupid script to gloss `[[wikilinked]]` words sent in
|
||||
tf2 chat. it works by reading TF2's console log file for events, and
|
||||
responding over RCON.
|
||||
|
||||
```
|
||||
con_logfile console.log
|
||||
ip 0.0.0.0
|
||||
rcon_password monitor
|
||||
net_start
|
||||
```
|
||||
|
||||
wiktionary entries are discovered via a wiktextract jsonl dump
|
||||
converted to sqlite via the rust program in `db`. it is also desirable
|
||||
to create an index for the words:
|
||||
|
||||
```clj
|
||||
(let [ds (sql/get-datasource {:dbtype "sqlite" :dbname "db/words.db"})]
|
||||
(with-open [conn (sql/get-connection ds)]
|
||||
(sql/execute! conn ["create index idx on words(word)"])))
|
||||
```
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
raw-wiktextract-data.jsonl.gz
|
||||
target/
|
||||
words.db
|
||||
words.db-journal
|
||||
words.db-wal
|
||||
words.db-shm
|
||||
words.preview.db
|
||||
flamegraph.svg
|
||||
Generated
+1075
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "db"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.6.5", features = ["derive"] }
|
||||
clio = { version = "0.3.5", features = ["clap-parse"] }
|
||||
diesel = { version = "2.3.11", features = ["r2d2", "sqlite"] }
|
||||
flate2 = { version = "1.1.9", features = ["zlib-rs"] }
|
||||
indicatif = { version = "0.18.6", features = ["rayon"] }
|
||||
rayon = "1.12.0"
|
||||
serde = "1.0.229"
|
||||
serde_json = "1.0.151"
|
||||
wiktionary-schema = "0.3.0"
|
||||
@@ -0,0 +1,9 @@
|
||||
{ rustPlatform
|
||||
}:
|
||||
|
||||
rustPlatform.buildRustPackage (finalAttrs: {
|
||||
pname = "make-db";
|
||||
version = "0.1.0";
|
||||
src = ./.;
|
||||
cargoLock.lockFile = ./Cargo.lock;
|
||||
})
|
||||
@@ -0,0 +1,80 @@
|
||||
pub mod models;
|
||||
pub mod schema;
|
||||
pub mod db;
|
||||
|
||||
use diesel::SqliteConnection;
|
||||
use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};
|
||||
use flate2::read::GzDecoder;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use indicatif::ParallelProgressIterator;
|
||||
use serde_json::Value;
|
||||
use wiktionary_schema::en::WordData;
|
||||
use std::{fs::File, io::{self, BufRead, BufReader}, path::PathBuf, sync::RwLock};
|
||||
use clap::Parser;
|
||||
use rayon::prelude::*;
|
||||
|
||||
/// Build an SQLite database from a Wiktextract JSONLines dump.
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "make-db", version, about, long_about = None)]
|
||||
struct Args {
|
||||
/// Input file
|
||||
#[clap(long, value_parser)]
|
||||
wiktextract_dump : PathBuf,
|
||||
|
||||
/// Output file
|
||||
#[clap(long, value_parser)]
|
||||
database : Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn open_wiktextract_dump (path : PathBuf) -> Result<
|
||||
BufReader<GzDecoder<File>>, io::Error
|
||||
> {
|
||||
Ok (BufReader::new (GzDecoder::new (File::open (path)?)))
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style)]
|
||||
const wiktextract_dump_lines : u64 = 10736399;
|
||||
|
||||
fn do_line (
|
||||
conn: &mut PooledConnection<ConnectionManager<SqliteConnection>>,
|
||||
retries : &RwLock<Vec<String>>,
|
||||
line : String
|
||||
) {
|
||||
if let Ok (word) = serde_json::from_str::<Value> (&line) {
|
||||
if let None = db::insert_word (conn, word) {
|
||||
retries.write ().unwrap ().push (line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), String> {
|
||||
let args = Args::parse ();
|
||||
let d = open_wiktextract_dump (args.wiktextract_dump).unwrap ();
|
||||
let bar = ProgressBar::new (wiktextract_dump_lines);
|
||||
bar.set_style (
|
||||
ProgressStyle::with_template(
|
||||
"{elapsed} / ETA {eta} / rate {per_sec} {bar:40.cyan/blue} {pos:>7}/{len:7} {msg}"
|
||||
) .unwrap()
|
||||
);
|
||||
let lines = d.lines ().map (|l| l.unwrap ().to_owned ())
|
||||
.par_bridge ()
|
||||
.progress_with (bar);
|
||||
let pool = db::get_pool ();
|
||||
let retries = RwLock::new (vec! []);
|
||||
|
||||
lines.for_each (|line| {
|
||||
let conn = &mut pool.clone ().get ().unwrap ();
|
||||
do_line (conn, &retries, line)
|
||||
});
|
||||
|
||||
println! ("do retries now");
|
||||
let conn = &mut pool.clone ().get ().unwrap ();
|
||||
for line in retries.read ().unwrap ().iter () {
|
||||
println! ("retrying");
|
||||
if let Ok (word) = serde_json::from_str::<Value> (&line) {
|
||||
db::insert_word (conn, word);
|
||||
}
|
||||
}
|
||||
|
||||
Ok (())
|
||||
}
|
||||
+11
-23
@@ -1,27 +1,15 @@
|
||||
{ mkCljBin
|
||||
, fake-git
|
||||
{ crane-lib
|
||||
, lib
|
||||
, mkGraalBin
|
||||
, sqlite
|
||||
}:
|
||||
|
||||
let
|
||||
# mkCljBin sans fake-git. We don't need it, and I don't want it in
|
||||
# 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 = "wiktionary-tf2";
|
||||
crane-lib.buildPackage (lib.fix (finalAttrs: {
|
||||
pname = "wiktlarp";
|
||||
version = "0.1.0";
|
||||
projectSrc = lib.cleanSource ./.;
|
||||
lockfile = ./deps-lock.json;
|
||||
main-ns = "wiktionary-tf2.main";
|
||||
}
|
||||
src = ./.;
|
||||
# cargoLock.lockFile = ./Cargo.lock;
|
||||
doCheck = true;
|
||||
meta.mainProgram = "wiktlarp";
|
||||
nativeBuildInputs = [ sqlite ];
|
||||
buildInputs = [ sqlite ];
|
||||
}))
|
||||
|
||||
+25
-13
@@ -1,14 +1,6 @@
|
||||
{
|
||||
"lock-version": 4,
|
||||
"git-deps": [
|
||||
{
|
||||
"lib": "io.github.anteoas/hawkeye",
|
||||
"url": "https://github.com/anteoas/hawkeye.git",
|
||||
"rev": "5b3f5a99e9edfe482174258a864b577f7a4eeb47",
|
||||
"git-dir": "https/github.com/anteoas/hawkeye",
|
||||
"hash": "sha256-ghKXjf4e3U8rlWCpffPRHqvf367RBs+VnSpRF6tI0xk="
|
||||
}
|
||||
],
|
||||
"git-deps": [],
|
||||
"mvn-deps": [
|
||||
{
|
||||
"mvn-path": "aleph/aleph/0.4.1/aleph-0.4.1.jar",
|
||||
@@ -190,6 +182,16 @@
|
||||
"mvn-repo": "https://repo.clojars.org/",
|
||||
"hash": "sha256-q4PzoWHUY53W2TZWihPpw+qXB4QWWVnS1iW3WlvIxFg="
|
||||
},
|
||||
{
|
||||
"mvn-path": "integrant/integrant/1.0.1/integrant-1.0.1.jar",
|
||||
"mvn-repo": "https://repo.clojars.org/",
|
||||
"hash": "sha256-dHxoCJNqPEcXakMdx6T89cQKH7ZQf9LmZtMO4fzr7f0="
|
||||
},
|
||||
{
|
||||
"mvn-path": "integrant/integrant/1.0.1/integrant-1.0.1.pom",
|
||||
"mvn-repo": "https://repo.clojars.org/",
|
||||
"hash": "sha256-zaEWby3a9q8gwevhNdchEwERgzbUG9NgqSBQ83C3Wdo="
|
||||
},
|
||||
{
|
||||
"mvn-path": "io/aleph/dirigiste/0.1.3/dirigiste-0.1.3.jar",
|
||||
"mvn-repo": "https://repo1.maven.org/maven2/",
|
||||
@@ -236,14 +238,14 @@
|
||||
"hash": "sha256-k7OxHaltUXiIDfjFBT8Yz8eByv8Nnd9LPGRyRKnRws8="
|
||||
},
|
||||
{
|
||||
"mvn-path": "net/java/dev/jna/jna/5.14.0/jna-5.14.0.jar",
|
||||
"mvn-path": "net/java/dev/jna/jna/3.2.2/jna-3.2.2.jar",
|
||||
"mvn-repo": "https://repo1.maven.org/maven2/",
|
||||
"hash": "sha256-NO0eHyf6iWvKUNvE6ZzzcylnzsOHp6DV40hsCWc/6MY="
|
||||
"hash": "sha256-/o8Xb7+sDBzy+M19M2EPp8VI9VFy0C0gLjJwF02p2HU="
|
||||
},
|
||||
{
|
||||
"mvn-path": "net/java/dev/jna/jna/5.14.0/jna-5.14.0.pom",
|
||||
"mvn-path": "net/java/dev/jna/jna/3.2.2/jna-3.2.2.pom",
|
||||
"mvn-repo": "https://repo1.maven.org/maven2/",
|
||||
"hash": "sha256-4E4llRUB3yWtx7Hc22xTNzyUiXuE0+FJISknY+4Hrj0="
|
||||
"hash": "sha256-tIAbOJ/rE2ZJaYehnjKRtxAw2BnAmYRvjkF/ayD6xJQ="
|
||||
},
|
||||
{
|
||||
"mvn-path": "org/clojure/clojure/1.10.3/clojure-1.10.3.jar",
|
||||
@@ -564,6 +566,16 @@
|
||||
"mvn-path": "tigris/tigris/0.1.2/tigris-0.1.2.pom",
|
||||
"mvn-repo": "https://repo.clojars.org/",
|
||||
"hash": "sha256-H9VZA1l1INzUrnbmoz7/XjWmFUIrutKo7ZrDMqr75KA="
|
||||
},
|
||||
{
|
||||
"mvn-path": "weavejester/dependency/1.0.0/dependency-1.0.0.jar",
|
||||
"mvn-repo": "https://repo.clojars.org/",
|
||||
"hash": "sha256-XxpuyFNiyj3VVUj25hocVNmbLc0vv9/EdIBK5m0TbpI="
|
||||
},
|
||||
{
|
||||
"mvn-path": "weavejester/dependency/1.0.0/dependency-1.0.0.pom",
|
||||
"mvn-repo": "https://repo.clojars.org/",
|
||||
"hash": "sha256-5NIafyzna3LBsVI8OHFubwOWmDYklwz18sPRfLp9WzY="
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,6 +9,5 @@
|
||||
clj-rcon/clj-rcon {:mvn/version "0.1.1"}
|
||||
hawk/hawk {:mvn/version "0.2.11"}
|
||||
ch.qos.logback/logback-classic #:mvn{:version "1.1.3"}
|
||||
io.github.anteoas/hawkeye
|
||||
{:git/sha "5b3f5a99e9edfe482174258a864b577f7a4eeb47"}}
|
||||
integrant/integrant {:mvn/version "1.0.1"}}
|
||||
:paths ["src" "resources"]}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# For documentation on how to configure this file,
|
||||
# see https://diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "src/schema.rs"
|
||||
custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
|
||||
|
||||
[migrations_directory]
|
||||
dir = "migrations"
|
||||
Generated
+10
-104
@@ -1,115 +1,21 @@
|
||||
{
|
||||
"nodes": {
|
||||
"clj-nix": {
|
||||
"inputs": {
|
||||
"devshell": "devshell",
|
||||
"nix-fetcher-data": "nix-fetcher-data",
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1773151887,
|
||||
"narHash": "sha256-YUiehwe2iTlwYSrnJ1pcw7KDNX+U42xlTx/2k/mo0P8=",
|
||||
"owner": "jlesquembre",
|
||||
"repo": "clj-nix",
|
||||
"rev": "27dac4466c9d3939f6a4925bc09e0cb1d8f32d9c",
|
||||
"lastModified": 1785782307,
|
||||
"narHash": "sha256-MPaRdVkf6zZP5fCPxYCi8Dr4pZzgmXzg8T9nVEbp3Mw=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "2c71e194474d13de031d729b729c968ddbe3507f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "jlesquembre",
|
||||
"repo": "clj-nix",
|
||||
"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",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"lastModified": 1775095191,
|
||||
"narHash": "sha256-CsqRiYbgQyv01LS0NlC7shwzhDhjNDQSrhBX8VuD3nM=",
|
||||
@@ -127,8 +33,8 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"clj-nix": "clj-nix",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
"crane": "crane",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
clj-nix.url = "github:jlesquembre/clj-nix";
|
||||
crane.url = "github:ipetkov/crane";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, clj-nix, ... }@inputs:
|
||||
outputs = { self, nixpkgs, ... }@inputs:
|
||||
let
|
||||
supportedSystems = [
|
||||
"aarch64-darwin"
|
||||
@@ -16,9 +16,6 @@
|
||||
each-system = f: nixpkgs.lib.genAttrs supportedSystems (system: f rec {
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [
|
||||
clj-nix.overlays.default
|
||||
];
|
||||
};
|
||||
inherit (pkgs) lib;
|
||||
inherit system;
|
||||
@@ -27,20 +24,29 @@
|
||||
# Exposed as a REPL convenience.
|
||||
_pkgs = each-system ({ pkgs, ... }: pkgs);
|
||||
|
||||
packages = each-system ({ pkgs, ... }: {
|
||||
default = pkgs.callPackage ./default.nix {};
|
||||
});
|
||||
packages = each-system ({ pkgs, lib, ... }: (lib.fix (p: {
|
||||
default = p.wiktlarp;
|
||||
wiktlarp = pkgs.callPackage ./default.nix {
|
||||
crane-lib = inputs.crane.mkLib pkgs;
|
||||
};
|
||||
db = pkgs.callPackage ./db {};
|
||||
})));
|
||||
|
||||
devShells = each-system ({ pkgs, system, ... }: {
|
||||
default = pkgs.mkShell {
|
||||
inputsFrom = [
|
||||
self.packages.${system}.db
|
||||
self.packages.${system}.default
|
||||
];
|
||||
DATABASE_URL = "db/words.db";
|
||||
packages = with pkgs; [
|
||||
zprint
|
||||
clojure
|
||||
babashka
|
||||
python3
|
||||
(sqlite.override { interactive = true; })
|
||||
python314Packages.wiktextract
|
||||
sqlite-web
|
||||
jq
|
||||
rust-analyzer
|
||||
diesel-cli
|
||||
cargo-flamegraph
|
||||
];
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
drop table words
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Your SQL goes here
|
||||
create table words (
|
||||
id integer not null primary key autoincrement,
|
||||
word varchar not null,
|
||||
info text not null
|
||||
)
|
||||
@@ -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")))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 (())
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
pub mod models;
|
||||
pub mod schema;
|
||||
|
||||
use diesel::prelude::*;
|
||||
use diesel::connection::SimpleConnection;
|
||||
use diesel::r2d2::{ConnectionManager, CustomizeConnection, Pool};
|
||||
use std::env;
|
||||
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<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) {}
|
||||
}
|
||||
|
||||
pub fn get_pool () -> Pool<ConnectionManager<SqliteConnection>> {
|
||||
let url = env::var ("DATABASE_URL")
|
||||
.expect ("DATABASE_URL must be set");
|
||||
let manager = ConnectionManager::<SqliteConnection>::new (url);
|
||||
Pool::builder ()
|
||||
.connection_customizer (Box::new (Fuck ()))
|
||||
.test_on_check_out (true)
|
||||
.build (manager)
|
||||
.expect ("Could not build connection pool")
|
||||
}
|
||||
|
||||
pub fn connect () -> SqliteConnection {
|
||||
let database_url = env::var ("DATABASE_URL")
|
||||
.expect ("DATABASE_URL must be set");
|
||||
let mut conn = SqliteConnection::establish (&database_url)
|
||||
.unwrap_or_else (|_| panic! ("Error connecting to {}", database_url));
|
||||
init_conn (&mut conn).unwrap ();
|
||||
conn
|
||||
}
|
||||
|
||||
pub fn get_word (w : &str) -> Vec<Word> {
|
||||
let conn = &mut connect ();
|
||||
use schema::words::dsl::*;
|
||||
words
|
||||
.filter (word.eq (w))
|
||||
.select (Word::as_select ())
|
||||
.load (conn)
|
||||
.expect ("error loading words")
|
||||
}
|
||||
|
||||
pub fn entry_as_word (entry : &serde_json::Value) -> Option<&str> {
|
||||
entry.get ("word")?.as_str ()
|
||||
}
|
||||
|
||||
pub fn insert_word (
|
||||
conn : &mut SqliteConnection,
|
||||
info : &serde_json::Value,
|
||||
) -> Option<()> {
|
||||
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 (
|
||||
|e| {
|
||||
println! ("error for {}: {}", word, e);
|
||||
None
|
||||
},
|
||||
Some
|
||||
)?;
|
||||
Some (())
|
||||
}
|
||||
@@ -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 ()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// @generated automatically by Diesel CLI.
|
||||
|
||||
diesel::table! {
|
||||
words (id) {
|
||||
id -> Integer,
|
||||
word -> Text,
|
||||
info -> Json,
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
(ns wiktionary-tf2.main
|
||||
(:require [clojure.java.io :as io]
|
||||
[babashka.fs :as fs]
|
||||
[babashka.process :as p]
|
||||
[clj-rcon.core :as rcon]
|
||||
[clojure.tools.logging :as l]
|
||||
[clojure.string :as str]
|
||||
[hawkeye.core :as hawk])
|
||||
(:gen-class))
|
||||
|
||||
(def log-file
|
||||
(-> "~/.local/share/Steam/steamapps/common/Team Fortress 2/tf/console.log"
|
||||
fs/expand-home
|
||||
fs/file))
|
||||
|
||||
(def words-file "words.gz")
|
||||
|
||||
(def ^:dynamic *rcon*)
|
||||
|
||||
(defn lookup-word [word]
|
||||
(->> (p/shell {:out :string}
|
||||
"zgrep"
|
||||
(format "^%s\t" word)
|
||||
words-file)
|
||||
:out
|
||||
str/split-lines
|
||||
(map #(nth (re-matches #"([^\t]*)\t(.*)\n?" %)
|
||||
2 nil))
|
||||
(filter #(not (empty? %))) ; remove nil and ""
|
||||
first))
|
||||
|
||||
(defn parse-chat-message [x]
|
||||
(when-let [[_ _dead? author body]
|
||||
(re-matches #"(\*사망\* )?(.*?) : (.*)" x)]
|
||||
{:author author :body body}))
|
||||
|
||||
(defn say-word [word]
|
||||
(->> (format "%s: %s" word (or (lookup-word word)
|
||||
"no entry found }:("))
|
||||
(take 128)
|
||||
(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)
|
||||
(say-word w)))
|
||||
|
||||
(defn rcon-connect [host port password]
|
||||
(l/info "attempting rcon connection...")
|
||||
(or (try (let [c @(rcon/connect host port password)]
|
||||
@(rcon/exec c "echo \"WIKTIONARY 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)))
|
||||
|
||||
(defonce prev-size
|
||||
(atom (if (fs/exists? log-file)
|
||||
(-> log-file slurp count)
|
||||
0)))
|
||||
|
||||
(defn handler []
|
||||
(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)))))
|
||||
|
||||
(defn start-watcher! []
|
||||
(hawk/watch (str (fs/parent log-file))
|
||||
(bound-fn* (fn [ev]
|
||||
(when (= (str (fs/file-name log-file))
|
||||
(:file ev))
|
||||
(handler))))
|
||||
(fn [e ctx]
|
||||
(l/error e "error in watcher"))))
|
||||
|
||||
(defn start-logger! []
|
||||
@(rcon/exec *rcon* (format "con_logfile %s"
|
||||
(fs/file-name log-file)))
|
||||
@(rcon/exec *rcon* "echo \"wiktionary logging now\"")
|
||||
(Thread/sleep 400)
|
||||
(l/info "logfile has been set up!"))
|
||||
|
||||
(defn -main []
|
||||
(binding [*rcon* (rcon-connect "127.0.0.1" 27015 "monitor")]
|
||||
(start-logger!)
|
||||
(start-watcher!)
|
||||
(read-line)))
|
||||
Reference in New Issue
Block a user