tangled
alpha
login
or
join now
bwc9876.dev
/
bingus-bot
0
fork
atom
The world's most clever kitty cat
0
fork
atom
overview
issues
pulls
pipelines
Message replying and learning
bwc9876.dev
1 day ago
5e0be501
deecaccd
verified
This commit was signed with the committer's
known signature
.
bwc9876.dev
SSH Key Fingerprint:
SHA256:DanMEP/RNlSC7pAVbnXO6wzQV00rqyKj053tz4uH5gQ=
+553
-34
6 changed files
expand all
collapse all
unified
split
Cargo.lock
Cargo.toml
src
brain.rs
main.rs
on_message.rs
status.rs
+229
Cargo.lock
···
3
version = 4
4
5
[[package]]
0
0
0
0
0
0
0
0
0
6
name = "alloc-no-stdlib"
7
version = "2.0.4"
8
source = "registry+https://github.com/rust-lang/crates.io-index"
···
18
]
19
20
[[package]]
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
21
name = "anyhow"
22
version = "1.0.102"
23
source = "registry+https://github.com/rust-lang/crates.io-index"
···
68
version = "0.1.0"
69
dependencies = [
70
"anyhow",
0
0
71
"fastrand",
0
72
"rmp-serde",
73
"rustls",
74
"serde",
···
86
version = "2.11.0"
87
source = "registry+https://github.com/rust-lang/crates.io-index"
88
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
0
0
0
0
0
0
0
0
0
0
0
89
90
[[package]]
91
name = "brotli-decompressor"
···
137
]
138
139
[[package]]
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
140
name = "combine"
141
version = "4.6.7"
142
source = "registry+https://github.com/rust-lang/crates.io-index"
···
198
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
199
200
[[package]]
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
201
name = "equivalent"
202
version = "1.0.2"
203
source = "registry+https://github.com/rust-lang/crates.io-index"
204
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
0
0
0
0
0
0
0
0
0
0
205
206
[[package]]
207
name = "fastrand"
···
428
]
429
430
[[package]]
0
0
0
0
0
0
431
name = "itoa"
432
version = "1.0.17"
433
source = "registry+https://github.com/rust-lang/crates.io-index"
434
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
435
436
[[package]]
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
437
name = "jni"
438
version = "0.21.1"
439
source = "registry+https://github.com/rust-lang/crates.io-index"
···
525
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
526
527
[[package]]
0
0
0
0
0
0
528
name = "openssl-probe"
529
version = "0.2.1"
530
source = "registry+https://github.com/rust-lang/crates.io-index"
···
577
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
578
579
[[package]]
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
580
name = "powerfmt"
581
version = "0.2.0"
582
source = "registry+https://github.com/rust-lang/crates.io-index"
···
616
]
617
618
[[package]]
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
619
name = "ring"
620
version = "0.17.14"
621
source = "registry+https://github.com/rust-lang/crates.io-index"
···
847
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
848
849
[[package]]
0
0
0
0
0
0
0
0
0
0
850
name = "simdutf8"
851
version = "0.1.5"
852
source = "registry+https://github.com/rust-lang/crates.io-index"
···
951
"libc",
952
"mio",
953
"pin-project-lite",
0
954
"socket2",
955
"tokio-macros",
956
"windows-sys 0.61.2",
···
1199
version = "0.9.0"
1200
source = "registry+https://github.com/rust-lang/crates.io-index"
1201
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
0
0
0
0
0
0
1202
1203
[[package]]
1204
name = "walkdir"
···
3
version = 4
4
5
[[package]]
6
+
name = "aho-corasick"
7
+
version = "1.1.4"
8
+
source = "registry+https://github.com/rust-lang/crates.io-index"
9
+
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
10
+
dependencies = [
11
+
"memchr",
12
+
]
13
+
14
+
[[package]]
15
name = "alloc-no-stdlib"
16
version = "2.0.4"
17
source = "registry+https://github.com/rust-lang/crates.io-index"
···
27
]
28
29
[[package]]
30
+
name = "anstream"
31
+
version = "0.6.21"
32
+
source = "registry+https://github.com/rust-lang/crates.io-index"
33
+
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
34
+
dependencies = [
35
+
"anstyle",
36
+
"anstyle-parse",
37
+
"anstyle-query",
38
+
"anstyle-wincon",
39
+
"colorchoice",
40
+
"is_terminal_polyfill",
41
+
"utf8parse",
42
+
]
43
+
44
+
[[package]]
45
+
name = "anstyle"
46
+
version = "1.0.13"
47
+
source = "registry+https://github.com/rust-lang/crates.io-index"
48
+
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
49
+
50
+
[[package]]
51
+
name = "anstyle-parse"
52
+
version = "0.2.7"
53
+
source = "registry+https://github.com/rust-lang/crates.io-index"
54
+
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
55
+
dependencies = [
56
+
"utf8parse",
57
+
]
58
+
59
+
[[package]]
60
+
name = "anstyle-query"
61
+
version = "1.1.5"
62
+
source = "registry+https://github.com/rust-lang/crates.io-index"
63
+
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
64
+
dependencies = [
65
+
"windows-sys 0.61.2",
66
+
]
67
+
68
+
[[package]]
69
+
name = "anstyle-wincon"
70
+
version = "3.0.11"
71
+
source = "registry+https://github.com/rust-lang/crates.io-index"
72
+
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
73
+
dependencies = [
74
+
"anstyle",
75
+
"once_cell_polyfill",
76
+
"windows-sys 0.61.2",
77
+
]
78
+
79
+
[[package]]
80
name = "anyhow"
81
version = "1.0.102"
82
source = "registry+https://github.com/rust-lang/crates.io-index"
···
127
version = "0.1.0"
128
dependencies = [
129
"anyhow",
130
+
"brotli",
131
+
"colog",
132
"fastrand",
133
+
"log",
134
"rmp-serde",
135
"rustls",
136
"serde",
···
148
version = "2.11.0"
149
source = "registry+https://github.com/rust-lang/crates.io-index"
150
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
151
+
152
+
[[package]]
153
+
name = "brotli"
154
+
version = "8.0.2"
155
+
source = "registry+https://github.com/rust-lang/crates.io-index"
156
+
checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
157
+
dependencies = [
158
+
"alloc-no-stdlib",
159
+
"alloc-stdlib",
160
+
"brotli-decompressor",
161
+
]
162
163
[[package]]
164
name = "brotli-decompressor"
···
210
]
211
212
[[package]]
213
+
name = "colog"
214
+
version = "1.4.0"
215
+
source = "registry+https://github.com/rust-lang/crates.io-index"
216
+
checksum = "df62599ba6adc9c6c04a54278c8209125343dc4775f57b9d76c9a4287e58f2bd"
217
+
dependencies = [
218
+
"colored",
219
+
"env_logger",
220
+
"log",
221
+
]
222
+
223
+
[[package]]
224
+
name = "colorchoice"
225
+
version = "1.0.4"
226
+
source = "registry+https://github.com/rust-lang/crates.io-index"
227
+
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
228
+
229
+
[[package]]
230
+
name = "colored"
231
+
version = "3.1.1"
232
+
source = "registry+https://github.com/rust-lang/crates.io-index"
233
+
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
234
+
dependencies = [
235
+
"windows-sys 0.61.2",
236
+
]
237
+
238
+
[[package]]
239
name = "combine"
240
version = "4.6.7"
241
source = "registry+https://github.com/rust-lang/crates.io-index"
···
297
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
298
299
[[package]]
300
+
name = "env_filter"
301
+
version = "1.0.0"
302
+
source = "registry+https://github.com/rust-lang/crates.io-index"
303
+
checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f"
304
+
dependencies = [
305
+
"log",
306
+
"regex",
307
+
]
308
+
309
+
[[package]]
310
+
name = "env_logger"
311
+
version = "0.11.9"
312
+
source = "registry+https://github.com/rust-lang/crates.io-index"
313
+
checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d"
314
+
dependencies = [
315
+
"anstream",
316
+
"anstyle",
317
+
"env_filter",
318
+
"jiff",
319
+
"log",
320
+
]
321
+
322
+
[[package]]
323
name = "equivalent"
324
version = "1.0.2"
325
source = "registry+https://github.com/rust-lang/crates.io-index"
326
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
327
+
328
+
[[package]]
329
+
name = "errno"
330
+
version = "0.3.14"
331
+
source = "registry+https://github.com/rust-lang/crates.io-index"
332
+
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
333
+
dependencies = [
334
+
"libc",
335
+
"windows-sys 0.52.0",
336
+
]
337
338
[[package]]
339
name = "fastrand"
···
560
]
561
562
[[package]]
563
+
name = "is_terminal_polyfill"
564
+
version = "1.70.2"
565
+
source = "registry+https://github.com/rust-lang/crates.io-index"
566
+
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
567
+
568
+
[[package]]
569
name = "itoa"
570
version = "1.0.17"
571
source = "registry+https://github.com/rust-lang/crates.io-index"
572
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
573
574
[[package]]
575
+
name = "jiff"
576
+
version = "0.2.23"
577
+
source = "registry+https://github.com/rust-lang/crates.io-index"
578
+
checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359"
579
+
dependencies = [
580
+
"jiff-static",
581
+
"log",
582
+
"portable-atomic",
583
+
"portable-atomic-util",
584
+
"serde_core",
585
+
]
586
+
587
+
[[package]]
588
+
name = "jiff-static"
589
+
version = "0.2.23"
590
+
source = "registry+https://github.com/rust-lang/crates.io-index"
591
+
checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4"
592
+
dependencies = [
593
+
"proc-macro2",
594
+
"quote",
595
+
"syn",
596
+
]
597
+
598
+
[[package]]
599
name = "jni"
600
version = "0.21.1"
601
source = "registry+https://github.com/rust-lang/crates.io-index"
···
687
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
688
689
[[package]]
690
+
name = "once_cell_polyfill"
691
+
version = "1.70.2"
692
+
source = "registry+https://github.com/rust-lang/crates.io-index"
693
+
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
694
+
695
+
[[package]]
696
name = "openssl-probe"
697
version = "0.2.1"
698
source = "registry+https://github.com/rust-lang/crates.io-index"
···
745
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
746
747
[[package]]
748
+
name = "portable-atomic"
749
+
version = "1.13.1"
750
+
source = "registry+https://github.com/rust-lang/crates.io-index"
751
+
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
752
+
753
+
[[package]]
754
+
name = "portable-atomic-util"
755
+
version = "0.2.5"
756
+
source = "registry+https://github.com/rust-lang/crates.io-index"
757
+
checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5"
758
+
dependencies = [
759
+
"portable-atomic",
760
+
]
761
+
762
+
[[package]]
763
name = "powerfmt"
764
version = "0.2.0"
765
source = "registry+https://github.com/rust-lang/crates.io-index"
···
799
]
800
801
[[package]]
802
+
name = "regex"
803
+
version = "1.12.3"
804
+
source = "registry+https://github.com/rust-lang/crates.io-index"
805
+
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
806
+
dependencies = [
807
+
"aho-corasick",
808
+
"memchr",
809
+
"regex-automata",
810
+
"regex-syntax",
811
+
]
812
+
813
+
[[package]]
814
+
name = "regex-automata"
815
+
version = "0.4.14"
816
+
source = "registry+https://github.com/rust-lang/crates.io-index"
817
+
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
818
+
dependencies = [
819
+
"aho-corasick",
820
+
"memchr",
821
+
"regex-syntax",
822
+
]
823
+
824
+
[[package]]
825
+
name = "regex-syntax"
826
+
version = "0.8.10"
827
+
source = "registry+https://github.com/rust-lang/crates.io-index"
828
+
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
829
+
830
+
[[package]]
831
name = "ring"
832
version = "0.17.14"
833
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1059
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
1060
1061
[[package]]
1062
+
name = "signal-hook-registry"
1063
+
version = "1.4.8"
1064
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1065
+
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
1066
+
dependencies = [
1067
+
"errno",
1068
+
"libc",
1069
+
]
1070
+
1071
+
[[package]]
1072
name = "simdutf8"
1073
version = "0.1.5"
1074
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1173
"libc",
1174
"mio",
1175
"pin-project-lite",
1176
+
"signal-hook-registry",
1177
"socket2",
1178
"tokio-macros",
1179
"windows-sys 0.61.2",
···
1422
version = "0.9.0"
1423
source = "registry+https://github.com/rust-lang/crates.io-index"
1424
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
1425
+
1426
+
[[package]]
1427
+
name = "utf8parse"
1428
+
version = "0.2.2"
1429
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1430
+
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
1431
1432
[[package]]
1433
name = "walkdir"
+9
-1
Cargo.toml
···
5
6
[dependencies]
7
anyhow = "1.0.102"
0
0
8
fastrand = "2.3.0"
0
9
rmp-serde = "1.3.1"
10
rustls = "0.23.37"
11
serde = { version = "1.0.228", features = ["derive"] }
12
-
tokio = { version = "1.50.0", features = ["macros", "rt-multi-thread"] }
0
0
0
0
0
13
twilight-cache-inmemory = "0.17.1"
14
twilight-gateway = "0.17.1"
15
twilight-http = "0.17.1"
···
5
6
[dependencies]
7
anyhow = "1.0.102"
8
+
brotli = "8.0.2"
9
+
colog = "1.4.0"
10
fastrand = "2.3.0"
11
+
log = "0.4.29"
12
rmp-serde = "1.3.1"
13
rustls = "0.23.37"
14
serde = { version = "1.0.228", features = ["derive"] }
15
+
tokio = { version = "1.50.0", features = [
16
+
"macros",
17
+
"rt-multi-thread",
18
+
"fs",
19
+
"signal",
20
+
] }
21
twilight-cache-inmemory = "0.17.1"
22
twilight-gateway = "0.17.1"
23
twilight-http = "0.17.1"
+37
-7
src/brain.rs
···
0
0
1
use std::collections::HashMap;
2
0
3
use serde::{Deserialize, Serialize};
0
4
5
/// Some = Word, None = End Message
6
pub type Token = Option<String>;
···
11
12
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
13
pub struct Brain(HashMap<Token, Edges>);
0
0
0
14
15
pub fn format_token(tok: &Token) -> String {
16
if let Some(w) = tok {
···
60
}
61
}
62
0
0
63
impl Brain {
64
fn normalize_token(word: &str) -> Token {
65
let w = if word.starts_with("http://") || word.starts_with("https://") {
···
84
}
85
86
fn should_reply(rand: &mut fastrand::Rng, is_self: bool) -> bool {
87
-
let chance = if is_self { 80 } else { 45 };
88
let roll = rand.u8(0..=100);
89
90
-
cfg!(test) || roll <= chance
91
}
92
93
fn extract_final_token(msg: &str) -> Option<Token> {
···
106
}
107
}
108
109
-
pub fn ingest(&mut self, msg: &str) {
0
110
// This is a silly way to do windows rust ppl :sob:
111
let _ = Self::parse(msg)
112
.map_windows(|[from, to]| {
113
-
eprintln!("{from:?} {to:?}");
114
if let Some(edge) = self.0.get_mut(from) {
115
edge.increment_token(to);
116
} else {
117
let new = Edges(HashMap::from_iter([(to.clone(), 1)]), 1);
118
self.0.insert(from.clone(), new);
0
119
}
120
})
121
.collect::<Vec<_>>();
0
0
122
}
123
124
pub fn merge_from(&mut self, other: Self) {
···
131
}
132
}
133
134
-
pub fn respond(&self, msg: &str, is_self: bool) -> Option<String> {
0
0
0
0
0
135
const MAX_TOKENS: usize = 20;
136
137
let mut rng = fastrand::Rng::new();
138
139
// Roll if we should reply
140
if !Self::should_reply(&mut rng, is_self) {
0
141
return None;
142
}
143
···
147
Self::extract_final_token(msg).or_else(|| self.random_token(&mut rng))?;
148
149
let mut chain = Vec::with_capacity(MAX_TOKENS);
0
150
151
while let Some(tok) = current_token
152
&& chain.len() <= MAX_TOKENS
···
155
let next = edges.sample(&mut rng).flatten();
156
if let Some(ref s) = next {
157
chain.push(s.clone());
0
0
0
158
}
159
current_token = next;
160
} else {
···
162
}
163
}
164
0
0
0
0
165
Some(chain.join(" "))
0
0
0
0
166
}
167
168
pub fn get_weights(&self, tok: &str) -> Option<&Edges> {
···
211
hello_edges.0,
212
HashMap::from_iter([(Some("world".to_string()), 1)])
213
);
214
-
let reply = brain.respond("hello", false);
215
assert_eq!(reply, Some("world".to_string()));
216
}
217
···
225
.join(" ");
226
let mut brain = Brain::default();
227
brain.ingest(&msg);
228
-
let reply = brain.respond("a", false);
229
let expected = LETTERS
230
.chars()
231
.skip(1)
···
1
+
#![allow(unused)]
2
+
3
use std::collections::HashMap;
4
5
+
use log::debug;
6
use serde::{Deserialize, Serialize};
7
+
use tokio::sync::oneshot;
8
9
/// Some = Word, None = End Message
10
pub type Token = Option<String>;
···
15
16
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
17
pub struct Brain(HashMap<Token, Edges>);
18
+
19
+
pub type TypingSender = oneshot::Sender<bool>;
20
+
pub type TypingReceiver = oneshot::Receiver<bool>;
21
22
pub fn format_token(tok: &Token) -> String {
23
if let Some(w) = tok {
···
67
}
68
}
69
70
+
const FORCE_REPLIES: bool = cfg!(test) || (option_env!("BINGUS_FORCE_REPLY").is_some());
71
+
72
impl Brain {
73
fn normalize_token(word: &str) -> Token {
74
let w = if word.starts_with("http://") || word.starts_with("https://") {
···
93
}
94
95
fn should_reply(rand: &mut fastrand::Rng, is_self: bool) -> bool {
96
+
let chance = if is_self { 45 } else { 80 };
97
let roll = rand.u8(0..=100);
98
99
+
(FORCE_REPLIES && !is_self) || roll <= chance
100
}
101
102
fn extract_final_token(msg: &str) -> Option<Token> {
···
115
}
116
}
117
118
+
pub fn ingest(&mut self, msg: &str) -> bool {
119
+
let mut learned_new_word = false;
120
// This is a silly way to do windows rust ppl :sob:
121
let _ = Self::parse(msg)
122
.map_windows(|[from, to]| {
0
123
if let Some(edge) = self.0.get_mut(from) {
124
edge.increment_token(to);
125
} else {
126
let new = Edges(HashMap::from_iter([(to.clone(), 1)]), 1);
127
self.0.insert(from.clone(), new);
128
+
learned_new_word = true;
129
}
130
})
131
.collect::<Vec<_>>();
132
+
133
+
learned_new_word
134
}
135
136
pub fn merge_from(&mut self, other: Self) {
···
143
}
144
}
145
146
+
pub fn respond(
147
+
&self,
148
+
msg: &str,
149
+
is_self: bool,
150
+
mut typing_oneshot: Option<TypingSender>,
151
+
) -> Option<String> {
152
const MAX_TOKENS: usize = 20;
153
154
let mut rng = fastrand::Rng::new();
155
156
// Roll if we should reply
157
if !Self::should_reply(&mut rng, is_self) {
158
+
debug!("Failed roll");
159
return None;
160
}
161
···
165
Self::extract_final_token(msg).or_else(|| self.random_token(&mut rng))?;
166
167
let mut chain = Vec::with_capacity(MAX_TOKENS);
168
+
let mut has_triggered_typing = false;
169
170
while let Some(tok) = current_token
171
&& chain.len() <= MAX_TOKENS
···
174
let next = edges.sample(&mut rng).flatten();
175
if let Some(ref s) = next {
176
chain.push(s.clone());
177
+
if !has_triggered_typing && let Some(typ) = typing_oneshot.take() {
178
+
typ.send(true).ok();
179
+
}
180
}
181
current_token = next;
182
} else {
···
184
}
185
}
186
187
+
if let Some(typ) = typing_oneshot.take() {
188
+
typ.send(false).ok();
189
+
}
190
+
191
Some(chain.join(" "))
192
+
}
193
+
194
+
pub fn word_count(&self) -> usize {
195
+
self.0.len()
196
}
197
198
pub fn get_weights(&self, tok: &str) -> Option<&Edges> {
···
241
hello_edges.0,
242
HashMap::from_iter([(Some("world".to_string()), 1)])
243
);
244
+
let reply = brain.respond("hello", false, None);
245
assert_eq!(reply, Some("world".to_string()));
246
}
247
···
255
.join(" ");
256
let mut brain = Brain::default();
257
brain.ingest(&msg);
258
+
let reply = brain.respond("a", false, None);
259
let expected = LETTERS
260
.chars()
261
.skip(1)
+165
-26
src/main.rs
···
1
#![feature(iter_map_windows)]
2
-
#![allow(unused)]
3
4
mod brain;
0
0
5
6
pub mod prelude {
7
pub use anyhow::Context;
···
9
pub type Result<T = (), E = anyhow::Error> = StdResult<T, E>;
10
}
11
12
-
use std::{collections::HashSet, sync::Arc};
0
0
0
0
0
0
0
0
13
0
0
14
use prelude::*;
0
0
0
0
15
use twilight_cache_inmemory::{DefaultInMemoryCache, ResourceType};
16
-
use twilight_gateway::{Event, EventTypeFlags, Intents, Shard, ShardId, StreamExt};
0
0
17
use twilight_http::Client as HttpClient;
0
0
0
0
0
18
19
#[derive(Debug)]
20
-
struct BotContext {
21
http: HttpClient,
22
-
reply_channels: HashSet<String>,
0
0
0
0
0
23
}
24
25
-
async fn handle_discord_event(event: Event, _ctx: Arc<BotContext>) -> Result {
26
match event {
27
-
Event::MessageCreate(msg) => {
28
-
let channel_id = msg.channel_id.to_string();
29
-
eprintln!("id: {channel_id}");
30
-
}
31
Event::Ready(ev) => {
32
-
eprintln!("Connected to gateway as {}", ev.user.name);
0
0
33
}
34
-
_ => {}
0
0
35
}
36
37
Ok(())
38
}
39
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
40
#[tokio::main]
41
async fn main() -> Result {
0
0
0
0
0
0
0
0
0
0
0
0
0
42
// Config
43
let token_file = std::env::var("TOKEN_FILE").context("Missing TOKEN_FILE env var")?;
44
-
let reply_channels: HashSet<String> = HashSet::from_iter(
45
-
std::env::var("REPLY_CHANNELS")
46
-
.context("Missing REPLY_CHANNELS env var")?
47
-
.split(",")
48
-
.map(|s| s.trim().to_string()),
49
-
);
0
0
50
let intents = Intents::GUILD_MESSAGES | Intents::MESSAGE_CONTENT;
51
52
// Read token
53
let token = std::fs::read_to_string(token_file).context("Failed to read bot token")?;
54
let token = token.trim();
55
0
0
0
0
0
0
0
0
0
0
56
// Init
57
let mut shard = Shard::new(ShardId::ONE, token.to_string(), intents);
58
let http = HttpClient::new(token.to_string());
···
65
)
66
.build();
67
0
0
0
0
0
0
0
0
0
0
0
68
let context = Arc::new(BotContext {
69
http,
0
70
reply_channels,
0
0
0
0
71
});
72
73
-
// Event Loop
74
-
while let Some(res) = shard.next_event(EventTypeFlags::all()).await {
75
-
match res {
76
-
Ok(event) => {
77
-
cache.update(&event);
78
-
tokio::spawn(handle_discord_event(event, Arc::clone(&context)));
0
0
0
0
0
0
0
0
0
0
0
0
79
}
80
-
Err(why) => {
81
-
eprintln!("Failed to receive event: {why:?}");
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
82
}
83
}
84
}
0
0
0
0
0
0
85
86
Ok(())
87
}
···
1
#![feature(iter_map_windows)]
0
2
3
mod brain;
4
+
mod on_message;
5
+
mod status;
6
7
pub mod prelude {
8
pub use anyhow::Context;
···
10
pub type Result<T = (), E = anyhow::Error> = StdResult<T, E>;
11
}
12
13
+
use std::{
14
+
collections::HashSet,
15
+
fs::File,
16
+
path::{Path, PathBuf},
17
+
sync::{
18
+
Arc,
19
+
atomic::{AtomicBool, Ordering},
20
+
},
21
+
};
22
23
+
use brotli::enc::BrotliEncoderParams;
24
+
use log::{debug, error, info, warn};
25
use prelude::*;
26
+
use tokio::{
27
+
sync::Mutex,
28
+
time::{self, Duration},
29
+
};
30
use twilight_cache_inmemory::{DefaultInMemoryCache, ResourceType};
31
+
use twilight_gateway::{
32
+
CloseFrame, Event, EventTypeFlags, Intents, MessageSender, Shard, ShardId, StreamExt,
33
+
};
34
use twilight_http::Client as HttpClient;
35
+
use twilight_model::id::{Id, marker::UserMarker};
36
+
37
+
use crate::{brain::Brain, on_message::handle_discord_message, status::update_status};
38
+
39
+
pub type BrainHandle = Mutex<Brain>;
40
41
#[derive(Debug)]
42
+
pub struct BotContext {
43
http: HttpClient,
44
+
self_id: Id<UserMarker>,
45
+
brain_file_path: PathBuf,
46
+
reply_channels: HashSet<u64>,
47
+
brain_handle: BrainHandle,
48
+
shard_sender: MessageSender,
49
+
pending_save: AtomicBool,
50
}
51
52
+
async fn handle_discord_event(event: Event, ctx: Arc<BotContext>) -> Result {
53
match event {
54
+
Event::MessageCreate(msg) => handle_discord_message(msg, ctx).await?,
0
0
0
55
Event::Ready(ev) => {
56
+
info!("Connected to gateway as {}", ev.user.name);
57
+
let brain = ctx.brain_handle.lock().await;
58
+
update_status(&*brain, &ctx.shard_sender).context("Failed to update status")?;
59
}
60
+
_ => {
61
+
debug!("Ev: {event:?}");
62
+
}
63
}
64
65
Ok(())
66
}
67
68
+
fn load_brain(path: &Path) -> Result<Option<Brain>> {
69
+
if path.exists() {
70
+
let mut file = File::open(path).context("Failed to open brain file")?;
71
+
let mut brotli_stream = brotli::Decompressor::new(&mut file, 4096);
72
+
rmp_serde::from_read(&mut brotli_stream)
73
+
.map(|b| Some(b))
74
+
.context("Failed to decode brain file")
75
+
} else {
76
+
Ok(None)
77
+
}
78
+
}
79
+
80
+
async fn save_brain(ctx: Arc<BotContext>) -> Result {
81
+
let mut file = File::create(&ctx.brain_file_path).context("Failed to open brain file")?;
82
+
let params = BrotliEncoderParams::default();
83
+
let mut brotli_writer = brotli::CompressorWriter::with_params(&mut file, 4096, ¶ms);
84
+
let brain = ctx.brain_handle.lock().await;
85
+
rmp_serde::encode::write(&mut brotli_writer, &*brain)
86
+
.context("Failed to write serialized brain")?;
87
+
debug!("Saved brain file");
88
+
Ok(())
89
+
}
90
+
91
#[tokio::main]
92
async fn main() -> Result {
93
+
let mut clog = colog::default_builder();
94
+
clog.filter(
95
+
None,
96
+
if cfg!(debug_assertions) {
97
+
log::LevelFilter::Debug
98
+
} else {
99
+
log::LevelFilter::Info
100
+
},
101
+
);
102
+
clog.try_init().context("Failed to initialize colog")?;
103
+
104
+
info!("Start of bingus-bot {}", env!("CARGO_PKG_VERSION"));
105
+
106
// Config
107
let token_file = std::env::var("TOKEN_FILE").context("Missing TOKEN_FILE env var")?;
108
+
let reply_channels: HashSet<u64> = std::env::var("REPLY_CHANNELS")
109
+
.context("Missing REPLY_CHANNELS env var")?
110
+
.split(",")
111
+
.map(|s| s.trim().parse::<u64>())
112
+
.collect::<Result<_, _>>()
113
+
.context("Invalid channel IDs for REPLY_CHANNELS")?;
114
+
let brain_file_path =
115
+
PathBuf::from(std::env::var("BRAIN_FILE").unwrap_or_else(|_| "brain.msgpackz".to_string()));
116
let intents = Intents::GUILD_MESSAGES | Intents::MESSAGE_CONTENT;
117
118
// Read token
119
let token = std::fs::read_to_string(token_file).context("Failed to read bot token")?;
120
let token = token.trim();
121
122
+
// Read Brain
123
+
let brain = if let Some(brain) = load_brain(&brain_file_path)? {
124
+
info!("Loading brain from {brain_file_path:?}");
125
+
brain
126
+
} else {
127
+
info!("Creating new brain file at {brain_file_path:?}");
128
+
Brain::default()
129
+
};
130
+
let brain_handle = Mutex::new(brain);
131
+
132
// Init
133
let mut shard = Shard::new(ShardId::ONE, token.to_string(), intents);
134
let http = HttpClient::new(token.to_string());
···
141
)
142
.build();
143
144
+
let self_id = http
145
+
.current_user_application()
146
+
.await
147
+
.context("Failed to get current App")?
148
+
.model()
149
+
.await
150
+
.context("Failed to deserialize")?
151
+
.bot
152
+
.context("App is not a bot!")?
153
+
.id;
154
+
155
let context = Arc::new(BotContext {
156
http,
157
+
self_id,
158
reply_channels,
159
+
brain_file_path,
160
+
brain_handle,
161
+
shard_sender: shard.sender(),
162
+
pending_save: AtomicBool::new(false),
163
});
164
165
+
info!("Ensuring brain is writable...");
166
+
save_brain(context.clone())
167
+
.await
168
+
.context("Brain file is not writable")?;
169
+
info!("Brain file saved");
170
+
171
+
let mut interval = time::interval(Duration::from_secs(60));
172
+
interval.tick().await;
173
+
tokio::pin!(interval);
174
+
175
+
info!("Connecting to gateway...");
176
+
177
+
loop {
178
+
tokio::select! {
179
+
Ok(()) = tokio::signal::ctrl_c() => {
180
+
info!("SIGINT: Closing connection and saving");
181
+
shard.close(CloseFrame::NORMAL);
182
+
break;
183
}
184
+
_ = interval.tick() => {
185
+
debug!("Save Interval");
186
+
if context.pending_save.load(Ordering::Relaxed) {
187
+
let ctx = context.clone();
188
+
tokio::spawn(async move {
189
+
if let Err(why) = save_brain(ctx.clone()).await {
190
+
error!("Failed to save brain file:\n{why:?}");
191
+
}
192
+
ctx.pending_save.store(true, Ordering::Relaxed);
193
+
});
194
+
}
195
+
},
196
+
opt = shard.next_event(EventTypeFlags::all()) => {
197
+
match opt {
198
+
Some(Ok(event)) => {
199
+
cache.update(&event);
200
+
let ctx = context.clone();
201
+
tokio::spawn(async move {
202
+
if let Err(why) = handle_discord_event(event, ctx).await {
203
+
error!("Error while processing Discord event:\n{why:?}");
204
+
}
205
+
});
206
+
}
207
+
Some(Err(why)) => {
208
+
warn!("Failed to receive event:\n{why:?}");
209
+
}
210
+
None => {
211
+
info!("Disconnected from Discord: Saving brain and exiting");
212
+
break;
213
+
}
214
+
}
215
}
216
}
217
}
218
+
219
+
save_brain(context)
220
+
.await
221
+
.context("Failed to write brain file on exit")?;
222
+
223
+
info!("Save Complete, Exiting");
224
225
Ok(())
226
}
+72
src/on_message.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
use std::{
2
+
boxed::Box,
3
+
sync::{Arc, atomic::Ordering},
4
+
};
5
+
6
+
use log::warn;
7
+
use twilight_model::{
8
+
channel::message::{AllowedMentions, MessageFlags, MessageType},
9
+
gateway::payload::incoming::MessageCreate,
10
+
};
11
+
12
+
use crate::{BotContext, prelude::*, status::update_status};
13
+
14
+
pub async fn handle_discord_message(msg: Box<MessageCreate>, ctx: Arc<BotContext>) -> Result {
15
+
let channel_id = msg.channel_id.get();
16
+
let is_self = msg.author.id == ctx.self_id;
17
+
let is_normal_message = matches!(msg.kind, MessageType::Regular | MessageType::Reply);
18
+
let is_ephemeral = msg
19
+
.flags
20
+
.is_some_and(|flags| flags.contains(MessageFlags::EPHEMERAL));
21
+
let is_dm = msg.guild_id.is_none();
22
+
23
+
// Should Ingest Message?
24
+
if is_self || !is_normal_message || is_ephemeral || is_dm {
25
+
return Ok(());
26
+
}
27
+
28
+
let mut brain = ctx.brain_handle.lock().await;
29
+
let learned_new_word = brain.ingest(&msg.content);
30
+
ctx.pending_save.store(true, Ordering::Relaxed);
31
+
32
+
if learned_new_word {
33
+
update_status(&*brain, &ctx.shard_sender).context("Failed to update status")?;
34
+
}
35
+
36
+
// Should Reply to Message?
37
+
if !ctx.reply_channels.contains(&channel_id) {
38
+
return Ok(());
39
+
}
40
+
41
+
let (typ_tx, typ_rx) = tokio::sync::oneshot::channel();
42
+
let (done_tx, done_rx) = tokio::sync::oneshot::channel();
43
+
44
+
let ctx_typ = ctx.clone();
45
+
let typ_id = msg.channel_id;
46
+
tokio::spawn(async move {
47
+
if typ_rx.await.ok().is_some_and(|start| start) {
48
+
if let Err(why) = ctx_typ.http.create_typing_trigger(typ_id).await {
49
+
warn!("Failed to set typing indicator:\n{why:?}");
50
+
}
51
+
}
52
+
done_tx.send(()).ok();
53
+
});
54
+
55
+
if let Some(reply_text) = brain
56
+
.respond(&msg.content, is_self, Some(typ_tx))
57
+
.filter(|s| !s.trim().is_empty())
58
+
{
59
+
drop(brain);
60
+
done_rx.await.ok();
61
+
ctx.http
62
+
.create_message(msg.channel_id)
63
+
.content(&reply_text)
64
+
.reply(msg.id)
65
+
.fail_if_not_exists(false)
66
+
.allowed_mentions(Some(&AllowedMentions::default()))
67
+
.await
68
+
.context("Failed to send message")?;
69
+
}
70
+
71
+
Ok(())
72
+
}
+41
src/status.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
use log::debug;
2
+
use twilight_gateway::MessageSender;
3
+
use twilight_model::gateway::{
4
+
payload::outgoing::UpdatePresence,
5
+
presence::{Activity, ActivityType, Status},
6
+
};
7
+
8
+
use crate::{brain::Brain, prelude::*};
9
+
10
+
pub fn update_status(brain: &Brain, sender: &MessageSender) -> Result {
11
+
let words = brain.word_count();
12
+
13
+
let activity = Activity {
14
+
application_id: None,
15
+
assets: None,
16
+
buttons: Vec::new(),
17
+
created_at: None,
18
+
details: None,
19
+
emoji: None,
20
+
flags: None,
21
+
id: None,
22
+
instance: None,
23
+
kind: ActivityType::Custom,
24
+
name: "Bingus".to_string(),
25
+
party: None,
26
+
secrets: None,
27
+
state: Some(format!("I know {words} words!")),
28
+
timestamps: None,
29
+
url: None,
30
+
};
31
+
32
+
let status = UpdatePresence::new(vec![activity], false, None, Status::Online)
33
+
.context("Failed to make status")?;
34
+
35
+
sender
36
+
.command(&status)
37
+
.context("Failed to send to gateway")?;
38
+
39
+
debug!("Sent status update");
40
+
Ok(())
41
+
}