+17
-265
Cargo.lock
+17
-265
Cargo.lock
···
33
33
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
34
34
35
35
[[package]]
36
+
name = "android-tzdata"
37
+
version = "0.1.1"
38
+
source = "registry+https://github.com/rust-lang/crates.io-index"
39
+
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
40
+
41
+
[[package]]
36
42
name = "android_system_properties"
37
43
version = "0.1.5"
38
44
source = "registry+https://github.com/rust-lang/crates.io-index"
···
142
148
version = "0.1.0"
143
149
dependencies = [
144
150
"bsky-sdk",
145
-
"chrono",
146
-
"cron-lite",
147
-
"futures",
148
151
"glob",
149
152
"grep",
150
-
"kameo",
151
153
"rand",
152
154
"redis",
153
155
"tokio",
···
267
269
268
270
[[package]]
269
271
name = "chrono"
270
-
version = "0.4.42"
272
+
version = "0.4.40"
271
273
source = "registry+https://github.com/rust-lang/crates.io-index"
272
-
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
274
+
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
273
275
dependencies = [
276
+
"android-tzdata",
274
277
"iana-time-zone",
275
278
"js-sys",
276
279
"num-traits",
···
280
283
]
281
284
282
285
[[package]]
283
-
name = "chrono-tz"
284
-
version = "0.10.4"
285
-
source = "registry+https://github.com/rust-lang/crates.io-index"
286
-
checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3"
287
-
dependencies = [
288
-
"chrono",
289
-
"phf",
290
-
]
291
-
292
-
[[package]]
293
286
name = "cid"
294
287
version = "0.11.1"
295
288
source = "registry+https://github.com/rust-lang/crates.io-index"
···
358
351
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
359
352
dependencies = [
360
353
"cfg-if",
361
-
]
362
-
363
-
[[package]]
364
-
name = "cron-lite"
365
-
version = "0.3.0"
366
-
source = "registry+https://github.com/rust-lang/crates.io-index"
367
-
checksum = "7b1c9e28df18340148b754969b7b66ed3c7f1242d10f4a4840391624333b589c"
368
-
dependencies = [
369
-
"chrono",
370
-
"futures",
371
-
"pin-project",
372
354
]
373
355
374
356
[[package]]
375
357
name = "croner"
376
-
version = "3.0.1"
358
+
version = "2.1.0"
377
359
source = "registry+https://github.com/rust-lang/crates.io-index"
378
-
checksum = "4aa42bcd3d846ebf66e15bd528d1087f75d1c6c1c66ebff626178a106353c576"
360
+
checksum = "38fd53511eaf0b00a185613875fee58b208dfce016577d0ad4bb548e1c4fb3ee"
379
361
dependencies = [
380
362
"chrono",
381
-
"derive_builder",
382
-
"strum",
383
363
]
384
364
385
365
[[package]]
···
407
387
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
408
388
409
389
[[package]]
410
-
name = "darling"
411
-
version = "0.20.11"
412
-
source = "registry+https://github.com/rust-lang/crates.io-index"
413
-
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
414
-
dependencies = [
415
-
"darling_core",
416
-
"darling_macro",
417
-
]
418
-
419
-
[[package]]
420
-
name = "darling_core"
421
-
version = "0.20.11"
422
-
source = "registry+https://github.com/rust-lang/crates.io-index"
423
-
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
424
-
dependencies = [
425
-
"fnv",
426
-
"ident_case",
427
-
"proc-macro2",
428
-
"quote",
429
-
"strsim",
430
-
"syn",
431
-
]
432
-
433
-
[[package]]
434
-
name = "darling_macro"
435
-
version = "0.20.11"
436
-
source = "registry+https://github.com/rust-lang/crates.io-index"
437
-
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
438
-
dependencies = [
439
-
"darling_core",
440
-
"quote",
441
-
"syn",
442
-
]
443
-
444
-
[[package]]
445
390
name = "dashmap"
446
391
version = "6.1.0"
447
392
source = "registry+https://github.com/rust-lang/crates.io-index"
···
482
427
]
483
428
484
429
[[package]]
485
-
name = "derive_builder"
486
-
version = "0.20.2"
487
-
source = "registry+https://github.com/rust-lang/crates.io-index"
488
-
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
489
-
dependencies = [
490
-
"derive_builder_macro",
491
-
]
492
-
493
-
[[package]]
494
-
name = "derive_builder_core"
495
-
version = "0.20.2"
496
-
source = "registry+https://github.com/rust-lang/crates.io-index"
497
-
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
498
-
dependencies = [
499
-
"darling",
500
-
"proc-macro2",
501
-
"quote",
502
-
"syn",
503
-
]
504
-
505
-
[[package]]
506
-
name = "derive_builder_macro"
507
-
version = "0.20.2"
508
-
source = "registry+https://github.com/rust-lang/crates.io-index"
509
-
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
510
-
dependencies = [
511
-
"derive_builder_core",
512
-
"syn",
513
-
]
514
-
515
-
[[package]]
516
430
name = "displaydoc"
517
431
version = "0.2.5"
518
432
source = "registry+https://github.com/rust-lang/crates.io-index"
···
522
436
"quote",
523
437
"syn",
524
438
]
525
-
526
-
[[package]]
527
-
name = "downcast-rs"
528
-
version = "2.0.2"
529
-
source = "registry+https://github.com/rust-lang/crates.io-index"
530
-
checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc"
531
-
532
-
[[package]]
533
-
name = "dyn-clone"
534
-
version = "1.0.20"
535
-
source = "registry+https://github.com/rust-lang/crates.io-index"
536
-
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
537
439
538
440
[[package]]
539
441
name = "encoding_rs"
···
643
545
]
644
546
645
547
[[package]]
646
-
name = "futures"
647
-
version = "0.3.31"
648
-
source = "registry+https://github.com/rust-lang/crates.io-index"
649
-
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
650
-
dependencies = [
651
-
"futures-channel",
652
-
"futures-core",
653
-
"futures-executor",
654
-
"futures-io",
655
-
"futures-sink",
656
-
"futures-task",
657
-
"futures-util",
658
-
]
659
-
660
-
[[package]]
661
548
name = "futures-channel"
662
549
version = "0.3.31"
663
550
source = "registry+https://github.com/rust-lang/crates.io-index"
664
551
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
665
552
dependencies = [
666
553
"futures-core",
667
-
"futures-sink",
668
554
]
669
555
670
556
[[package]]
···
674
560
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
675
561
676
562
[[package]]
677
-
name = "futures-executor"
678
-
version = "0.3.31"
679
-
source = "registry+https://github.com/rust-lang/crates.io-index"
680
-
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
681
-
dependencies = [
682
-
"futures-core",
683
-
"futures-task",
684
-
"futures-util",
685
-
]
686
-
687
-
[[package]]
688
-
name = "futures-io"
689
-
version = "0.3.31"
690
-
source = "registry+https://github.com/rust-lang/crates.io-index"
691
-
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
692
-
693
-
[[package]]
694
563
name = "futures-macro"
695
564
version = "0.3.31"
696
565
source = "registry+https://github.com/rust-lang/crates.io-index"
···
719
588
source = "registry+https://github.com/rust-lang/crates.io-index"
720
589
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
721
590
dependencies = [
722
-
"futures-channel",
723
591
"futures-core",
724
-
"futures-io",
725
592
"futures-macro",
726
593
"futures-sink",
727
594
"futures-task",
728
-
"memchr",
729
595
"pin-project-lite",
730
596
"pin-utils",
731
597
"slab",
···
878
744
]
879
745
880
746
[[package]]
881
-
name = "heck"
882
-
version = "0.5.0"
883
-
source = "registry+https://github.com/rust-lang/crates.io-index"
884
-
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
885
-
886
-
[[package]]
887
747
name = "http"
888
748
version = "1.2.0"
889
749
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1119
979
]
1120
980
1121
981
[[package]]
1122
-
name = "ident_case"
1123
-
version = "1.0.1"
1124
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1125
-
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
1126
-
1127
-
[[package]]
1128
982
name = "idna"
1129
983
version = "1.0.3"
1130
984
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1189
1043
]
1190
1044
1191
1045
[[package]]
1192
-
name = "kameo"
1193
-
version = "0.17.2"
1194
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1195
-
checksum = "41a73be96f616ca2784f597b5b6635582f5a7b3ba73b1dbe7afa5d9667955d39"
1196
-
dependencies = [
1197
-
"downcast-rs",
1198
-
"dyn-clone",
1199
-
"futures",
1200
-
"kameo_macros",
1201
-
"once_cell",
1202
-
"serde",
1203
-
"tokio",
1204
-
"tracing",
1205
-
]
1206
-
1207
-
[[package]]
1208
-
name = "kameo_macros"
1209
-
version = "0.17.0"
1210
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1211
-
checksum = "b3f384b32bf6426ae93a8b37da62c85073b676a31a82a86d608ad86453878de0"
1212
-
dependencies = [
1213
-
"heck",
1214
-
"proc-macro2",
1215
-
"quote",
1216
-
"syn",
1217
-
"uuid",
1218
-
]
1219
-
1220
-
[[package]]
1221
1046
name = "langtag"
1222
1047
version = "0.3.4"
1223
1048
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1549
1374
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
1550
1375
1551
1376
[[package]]
1552
-
name = "phf"
1553
-
version = "0.12.1"
1554
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1555
-
checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7"
1556
-
dependencies = [
1557
-
"phf_shared",
1558
-
]
1559
-
1560
-
[[package]]
1561
-
name = "phf_shared"
1562
-
version = "0.12.1"
1563
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1564
-
checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981"
1565
-
dependencies = [
1566
-
"siphasher",
1567
-
]
1568
-
1569
-
[[package]]
1570
-
name = "pin-project"
1571
-
version = "1.1.10"
1572
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1573
-
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
1574
-
dependencies = [
1575
-
"pin-project-internal",
1576
-
]
1577
-
1578
-
[[package]]
1579
-
name = "pin-project-internal"
1580
-
version = "1.1.10"
1581
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1582
-
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
1583
-
dependencies = [
1584
-
"proc-macro2",
1585
-
"quote",
1586
-
"syn",
1587
-
]
1588
-
1589
-
[[package]]
1590
1377
name = "pin-project-lite"
1591
1378
version = "0.2.16"
1592
1379
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2003
1790
]
2004
1791
2005
1792
[[package]]
2006
-
name = "siphasher"
2007
-
version = "1.0.1"
2008
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2009
-
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
2010
-
2011
-
[[package]]
2012
1793
name = "slab"
2013
1794
version = "0.4.9"
2014
1795
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2040
1821
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
2041
1822
2042
1823
[[package]]
2043
-
name = "strsim"
2044
-
version = "0.11.1"
2045
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2046
-
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
2047
-
2048
-
[[package]]
2049
-
name = "strum"
2050
-
version = "0.27.2"
2051
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2052
-
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
2053
-
dependencies = [
2054
-
"strum_macros",
2055
-
]
2056
-
2057
-
[[package]]
2058
-
name = "strum_macros"
2059
-
version = "0.27.2"
2060
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2061
-
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
2062
-
dependencies = [
2063
-
"heck",
2064
-
"proc-macro2",
2065
-
"quote",
2066
-
"syn",
2067
-
]
2068
-
2069
-
[[package]]
2070
1824
name = "syn"
2071
1825
version = "2.0.99"
2072
1826
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2181
1935
"signal-hook-registry",
2182
1936
"socket2",
2183
1937
"tokio-macros",
2184
-
"tracing",
2185
1938
"windows-sys 0.52.0",
2186
1939
]
2187
1940
2188
1941
[[package]]
2189
1942
name = "tokio-cron-scheduler"
2190
-
version = "0.15.1"
1943
+
version = "0.13.0"
2191
1944
source = "registry+https://github.com/rust-lang/crates.io-index"
2192
-
checksum = "1f50e41f200fd8ed426489bd356910ede4f053e30cebfbd59ef0f856f0d7432a"
1945
+
checksum = "6a5597b569b4712cf78aa0c9ae29742461b7bda1e49c2a5fdad1d79bf022f8f0"
2193
1946
dependencies = [
2194
1947
"chrono",
2195
-
"chrono-tz",
2196
1948
"croner",
2197
1949
"num-derive",
2198
1950
"num-traits",
···
2275
2027
2276
2028
[[package]]
2277
2029
name = "tracing-attributes"
2278
-
version = "0.1.31"
2030
+
version = "0.1.28"
2279
2031
source = "registry+https://github.com/rust-lang/crates.io-index"
2280
-
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
2032
+
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
2281
2033
dependencies = [
2282
2034
"proc-macro2",
2283
2035
"quote",
···
2604
2356
2605
2357
[[package]]
2606
2358
name = "windows-link"
2607
-
version = "0.2.1"
2359
+
version = "0.1.0"
2608
2360
source = "registry+https://github.com/rust-lang/crates.io-index"
2609
-
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
2361
+
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
2610
2362
2611
2363
[[package]]
2612
2364
name = "windows-registry"
+3
-5
Cargo.toml
+3
-5
Cargo.toml
···
1
+
cargo-features = ["edition2024"] # For rust-analyzer to work
2
+
1
3
[package]
2
4
name = "audquotes"
3
5
version = "0.1.0"
···
6
8
7
9
[dependencies]
8
10
bsky-sdk = "0.1.16"
9
-
chrono = "0.4.42"
10
-
cron-lite = { version = "0.3.0", features = ["async"] }
11
-
futures = "0.3.31"
12
11
glob = "0.3.2"
13
12
grep = "0.3.2"
14
-
kameo = "0.17.2"
15
13
rand = "0.9.0"
16
14
redis = { version = "0.29.1", features = ["aio", "connection-manager", "tokio-comp"] }
17
15
tokio = { version = "1.44.0", features = ["full"] }
18
-
tokio-cron-scheduler = "0.15.1"
16
+
tokio-cron-scheduler = "0.13.0"
+11
quotes/deltarune/1225_you_couldnt_find.txt
+11
quotes/deltarune/1225_you_couldnt_find.txt
···
1
+
* You used the machine.
2
+
* ... The capsule came out.
3
+
* Inside was something hard like lacquer.
4
+
* It was a small, dark, triangle.
5
+
* You tried to take it...
6
+
* But, it slipped through your hand
7
+
* And you couldn't find it anymore.
8
+
9
+
* You couldn't find
10
+
11
+
your hand.
+16
quotes/deltarune/THE_HALFWAY_MARK.txt
+16
quotes/deltarune/THE_HALFWAY_MARK.txt
+4
quotes/deltarune/alvin_asgore_pray.txt
+4
quotes/deltarune/alvin_asgore_pray.txt
+1
quotes/deltarune/alvin_shelter.txt
+1
quotes/deltarune/alvin_shelter.txt
···
1
+
* Kris... stay away from the shelter.
+4
quotes/deltarune/alvin_tarnish_his_legacy.txt
+4
quotes/deltarune/alvin_tarnish_his_legacy.txt
···
1
+
* Thank you, Kris. Ha ha. Although I did see that it put you to sleep.
2
+
* I know. I do not exactly have a "flair" for entertainment.
3
+
* That's why I don't write my own sermons. Or... anything, anymore.
4
+
* I don't think my father could rest well knowing I was... tarnishing his legacy.
+6
quotes/deltarune/asgore_they_will_all_1.txt
+6
quotes/deltarune/asgore_they_will_all_1.txt
+5
quotes/deltarune/asgore_they_will_all_2.txt
+5
quotes/deltarune/asgore_they_will_all_2.txt
+1
quotes/deltarune/bell_of_justice.txt
+1
quotes/deltarune/bell_of_justice.txt
···
1
+
* The bell of justice is ringing... it's for you.
+6
quotes/deltarune/carol_kris_the_festival.txt
+6
quotes/deltarune/carol_kris_the_festival.txt
+3
quotes/deltarune/concert_just_for_me_1.txt
+3
quotes/deltarune/concert_just_for_me_1.txt
···
1
+
It's funny... there was a time when they were coming over almost every day.
2
+
We'd play, and we'd play... then after a while, they would suddenly get very still, like they were remembering something.
3
+
They'd go into the dining room to "get a snack," then after a few moments, I'd hear the piano.
+2
quotes/deltarune/concert_just_for_me_2.txt
+2
quotes/deltarune/concert_just_for_me_2.txt
···
1
+
The first few times, I went into watch them play, but when they realized I was looking, they'd always shut the piano and come back.
2
+
So over time, I just started staying on the couch in the living room. I'd lie there, listening to them play, sometimes for hours, sometimes even until I fell asleep.
+3
quotes/deltarune/concert_just_for_me_3.txt
+3
quotes/deltarune/concert_just_for_me_3.txt
+4
quotes/deltarune/concert_just_for_me_4.txt
+4
quotes/deltarune/concert_just_for_me_4.txt
+3
quotes/deltarune/d_how_did_i.txt
+3
quotes/deltarune/d_how_did_i.txt
+8
quotes/deltarune/d_that_nightmare.txt
+8
quotes/deltarune/d_that_nightmare.txt
+9
quotes/deltarune/felt_it_shining.txt
+9
quotes/deltarune/felt_it_shining.txt
+9
quotes/deltarune/fountain_glowing_smile.txt
+9
quotes/deltarune/fountain_glowing_smile.txt
+6
quotes/deltarune/gerson_as_youre_green.txt
+6
quotes/deltarune/gerson_as_youre_green.txt
+2
quotes/deltarune/gerson_bench_dream.txt
+2
quotes/deltarune/gerson_bench_dream.txt
+6
quotes/deltarune/gerson_between_the_lines.txt
+6
quotes/deltarune/gerson_between_the_lines.txt
···
1
+
* Well now, a fairytale is a pretty little thing.
2
+
* Ain't it nice to believe a glimmer here and there...?
3
+
* I jus' think, those words shine a bit too bright.
4
+
* A path so blue, it's all you can see.
5
+
* So I say... why don't we go between the lines?
6
+
* It's darker there... Geheh... geheheh!
+8
quotes/deltarune/gerson_burnin_up_everything.txt
+8
quotes/deltarune/gerson_burnin_up_everything.txt
+7
quotes/deltarune/gerson_chapter_1.txt
+7
quotes/deltarune/gerson_chapter_1.txt
+7
quotes/deltarune/gerson_chapter_2.txt
+7
quotes/deltarune/gerson_chapter_2.txt
+8
quotes/deltarune/gerson_chapter_3.txt
+8
quotes/deltarune/gerson_chapter_3.txt
+7
quotes/deltarune/gerson_chapter_4.txt
+7
quotes/deltarune/gerson_chapter_4.txt
+11
quotes/deltarune/gerson_chapter_5.txt
+11
quotes/deltarune/gerson_chapter_5.txt
+9
quotes/deltarune/gerson_choose_eternity.txt
+9
quotes/deltarune/gerson_choose_eternity.txt
+5
quotes/deltarune/gerson_count_on.txt
+5
quotes/deltarune/gerson_count_on.txt
+17
quotes/deltarune/gerson_heroes.txt
+17
quotes/deltarune/gerson_heroes.txt
···
1
+
Now, I don't know
2
+
much about "heroes"...
3
+
... But I know that
4
+
whatever ya call 'em,
5
+
there is someone.
6
+
7
+
Someone who'll never
8
+
give up trying to
9
+
do the right thing,
10
+
no matter what.
11
+
12
+
There's no prophecy
13
+
or legend 'bout anyone
14
+
like that.
15
+
16
+
It's just something
17
+
I know is true.
+9
quotes/deltarune/gerson_how_it_all_ends.txt
+9
quotes/deltarune/gerson_how_it_all_ends.txt
+9
quotes/deltarune/gerson_ocean_of_ink.txt
+9
quotes/deltarune/gerson_ocean_of_ink.txt
+8
quotes/deltarune/gerson_old_tale.txt
+8
quotes/deltarune/gerson_old_tale.txt
+9
quotes/deltarune/gerson_pick_up_the_pen.txt
+9
quotes/deltarune/gerson_pick_up_the_pen.txt
+9
quotes/deltarune/gerson_susie_pen_of_hope_1.txt
+9
quotes/deltarune/gerson_susie_pen_of_hope_1.txt
+9
quotes/deltarune/gerson_susie_pen_of_hope_2.txt
+9
quotes/deltarune/gerson_susie_pen_of_hope_2.txt
+7
quotes/deltarune/gerson_susie_quitting_1.txt
+7
quotes/deltarune/gerson_susie_quitting_1.txt
+6
quotes/deltarune/gerson_susie_quitting_2.txt
+6
quotes/deltarune/gerson_susie_quitting_2.txt
+7
quotes/deltarune/gerson_susie_quitting_3.txt
+7
quotes/deltarune/gerson_susie_quitting_3.txt
···
1
+
* What if you practiced? Put a little shell into it.
2
+
3
+
* I mean, I kind of... When people aren't looking...
4
+
* I was kind of trying to practice a little, but...
5
+
* I sorta realized, y'know?
6
+
* I'll never be as good as Ralsei, so what's the point?
7
+
* So everyone can see how... bad I am?
+6
quotes/deltarune/gerson_susie_quitting_4.txt
+6
quotes/deltarune/gerson_susie_quitting_4.txt
+6
quotes/deltarune/gerson_susie_the_prophecy.txt
+6
quotes/deltarune/gerson_susie_the_prophecy.txt
+13
quotes/deltarune/gerson_swallowed_up.txt
+13
quotes/deltarune/gerson_swallowed_up.txt
+3
quotes/deltarune/gerson_the_motions_1.txt
+3
quotes/deltarune/gerson_the_motions_1.txt
+3
quotes/deltarune/gerson_the_motions_2.txt
+3
quotes/deltarune/gerson_the_motions_2.txt
+7
quotes/deltarune/gerson_what_happens_next.txt
+7
quotes/deltarune/gerson_what_happens_next.txt
···
1
+
* Ain't no better story than one told with sparkling eyes.
2
+
* She ain't got no fear, that one. Doubt, irony, that's what poisons your story...
3
+
* That, and too much predictability.
4
+
* So, young man... What do you think happens next?
5
+
6
+
* H... huh?
7
+
* I... I guess we'll... just have to see.
+5
quotes/deltarune/hand_pushed_you.txt
+5
quotes/deltarune/hand_pushed_you.txt
+2
quotes/deltarune/knight_beat.txt
+2
quotes/deltarune/knight_beat.txt
+1
quotes/deltarune/knight_heart.txt
+1
quotes/deltarune/knight_heart.txt
···
1
+
* Your heartbeat becomes twisted.
+4
quotes/deltarune/knight_kris_analysis.txt
+4
quotes/deltarune/knight_kris_analysis.txt
+3
quotes/deltarune/knight_kris_breath_quicken.txt
+3
quotes/deltarune/knight_kris_breath_quicken.txt
+3
quotes/deltarune/knight_kris_breath_smile.txt
+3
quotes/deltarune/knight_kris_breath_smile.txt
+1
quotes/deltarune/knight_kris_kneel.txt
+1
quotes/deltarune/knight_kris_kneel.txt
···
1
+
* Kris kneeled in silence.
+1
quotes/deltarune/knight_ralsei_swoon.txt
+1
quotes/deltarune/knight_ralsei_swoon.txt
···
1
+
* Ralsei became a pile of fluff.
+7
quotes/deltarune/knight_ralsei_talk.txt
+7
quotes/deltarune/knight_ralsei_talk.txt
+1
quotes/deltarune/knight_roaring.txt
+1
quotes/deltarune/knight_roaring.txt
···
1
+
* The Roaring Knight appeared.
+1
quotes/deltarune/knight_susie_swoon.txt
+1
quotes/deltarune/knight_susie_swoon.txt
···
1
+
* Susie was hurt and beaten.
+7
quotes/deltarune/knight_susie_talk.txt
+7
quotes/deltarune/knight_susie_talk.txt
+1
quotes/deltarune/knight_susie_warn.txt
+1
quotes/deltarune/knight_susie_warn.txt
···
1
+
* Susie struggled to give some kind of warning.
+3
quotes/deltarune/kris_never_wash_it_all_away.txt
+3
quotes/deltarune/kris_never_wash_it_all_away.txt
+3
quotes/deltarune/kris_pencil_1.txt
+3
quotes/deltarune/kris_pencil_1.txt
+3
quotes/deltarune/kris_pencil_2.txt
+3
quotes/deltarune/kris_pencil_2.txt
+2
quotes/deltarune/kris_pencil_3.txt
+2
quotes/deltarune/kris_pencil_3.txt
+3
quotes/deltarune/kris_pencil_4.txt
+3
quotes/deltarune/kris_pencil_4.txt
+2
quotes/deltarune/kris_susie_be_okay.txt
+2
quotes/deltarune/kris_susie_be_okay.txt
+5
quotes/deltarune/kris_taken_enough.txt
+5
quotes/deltarune/kris_taken_enough.txt
+5
quotes/deltarune/kris_unseen_shape.txt
+5
quotes/deltarune/kris_unseen_shape.txt
+7
quotes/deltarune/noelle_alone_with_kris.txt
+7
quotes/deltarune/noelle_alone_with_kris.txt
+4
quotes/deltarune/noelle_everything_important_1.txt
+4
quotes/deltarune/noelle_everything_important_1.txt
+4
quotes/deltarune/noelle_everything_important_2.txt
+4
quotes/deltarune/noelle_everything_important_2.txt
+7
quotes/deltarune/noelle_kris_ThornRing.txt
+7
quotes/deltarune/noelle_kris_ThornRing.txt
···
1
+
* K... Kris? Kris, what did you say?
2
+
* K... Kris? Kris, what is that?
3
+
* Kris... that's... not the thorn, is it?
4
+
* That's not... the... ThornRing, is it...?
5
+
* K... Kris... c'mon... you... you still...
6
+
* What about... what about last night? What about...
7
+
* Didn't you... want to... protect me...?
+8
quotes/deltarune/noelle_kris_dont_mention.txt
+8
quotes/deltarune/noelle_kris_dont_mention.txt
···
1
+
* Kris, there... there was something else you did.
2
+
* Something else... I have to ask you about.
3
+
* After you said all of that... you...
4
+
* Took my hand... and...
5
+
* Pulled... a thorn out of my finger.
6
+
* Then you said...
7
+
* You said... you said that when I saw you again...
8
+
* You said... not to mention you did any of this... or...
+5
quotes/deltarune/noelle_kris_its_just_us.txt
+5
quotes/deltarune/noelle_kris_its_just_us.txt
+4
quotes/deltarune/noelle_kris_piano.txt
+4
quotes/deltarune/noelle_kris_piano.txt
+3
quotes/deltarune/noelle_kris_talk_later.txt
+3
quotes/deltarune/noelle_kris_talk_later.txt
+5
quotes/deltarune/noelle_kris_voice_1.txt
+5
quotes/deltarune/noelle_kris_voice_1.txt
+4
quotes/deltarune/noelle_kris_voice_2A.txt
+4
quotes/deltarune/noelle_kris_voice_2A.txt
+4
quotes/deltarune/noelle_kris_voice_2B.txt
+4
quotes/deltarune/noelle_kris_voice_2B.txt
+5
quotes/deltarune/noelle_kris_your_voice.txt
+5
quotes/deltarune/noelle_kris_your_voice.txt
+7
quotes/deltarune/noelle_nightmare.txt
+7
quotes/deltarune/noelle_nightmare.txt
+4
quotes/deltarune/plaque_gerson_hope.txt
+4
quotes/deltarune/plaque_gerson_hope.txt
+4
quotes/deltarune/ralsei_dark_not_real.txt
+4
quotes/deltarune/ralsei_dark_not_real.txt
+10
quotes/deltarune/ralsei_dont_need_a_room.txt
+10
quotes/deltarune/ralsei_dont_need_a_room.txt
+5
quotes/deltarune/ralsei_forget_us_1.txt
+5
quotes/deltarune/ralsei_forget_us_1.txt
+3
quotes/deltarune/ralsei_forget_us_2.txt
+3
quotes/deltarune/ralsei_forget_us_2.txt
+7
quotes/deltarune/ralsei_friendship_far_greater.txt
+7
quotes/deltarune/ralsei_friendship_far_greater.txt
+2
quotes/deltarune/ralsei_healing_the_outside.txt
+2
quotes/deltarune/ralsei_healing_the_outside.txt
+7
quotes/deltarune/ralsei_i_am_smiling.txt
+7
quotes/deltarune/ralsei_i_am_smiling.txt
+5
quotes/deltarune/ralsei_i_do_for_you_1.txt
+5
quotes/deltarune/ralsei_i_do_for_you_1.txt
+7
quotes/deltarune/ralsei_i_do_for_you_2.txt
+7
quotes/deltarune/ralsei_i_do_for_you_2.txt
+8
quotes/deltarune/ralsei_i_just.txt
+8
quotes/deltarune/ralsei_i_just.txt
+3
quotes/deltarune/ralsei_knight_check_susie.txt
+3
quotes/deltarune/ralsei_knight_check_susie.txt
+7
quotes/deltarune/ralsei_knight_the_prophecy.txt
+7
quotes/deltarune/ralsei_knight_the_prophecy.txt
+4
quotes/deltarune/ralsei_knight_will_be_fine.txt
+4
quotes/deltarune/ralsei_knight_will_be_fine.txt
+3
quotes/deltarune/ralsei_kris_thank_susie.txt
+3
quotes/deltarune/ralsei_kris_thank_susie.txt
+3
quotes/deltarune/ralsei_kris_you_were_cool.txt
+3
quotes/deltarune/ralsei_kris_you_were_cool.txt
+7
quotes/deltarune/ralsei_let_it_just_be.txt
+7
quotes/deltarune/ralsei_let_it_just_be.txt
+6
quotes/deltarune/ralsei_my_purpose.txt
+6
quotes/deltarune/ralsei_my_purpose.txt
+5
quotes/deltarune/ralsei_noelle_bad.txt
+5
quotes/deltarune/ralsei_noelle_bad.txt
+4
quotes/deltarune/ralsei_not_filling.txt
+4
quotes/deltarune/ralsei_not_filling.txt
+3
quotes/deltarune/ralsei_pain_i_can_take.txt
+3
quotes/deltarune/ralsei_pain_i_can_take.txt
+8
quotes/deltarune/ralsei_scared.txt
+8
quotes/deltarune/ralsei_scared.txt
+10
quotes/deltarune/ralsei_should_i_even.txt
+10
quotes/deltarune/ralsei_should_i_even.txt
+8
quotes/deltarune/ralsei_something_even_worse.txt
+8
quotes/deltarune/ralsei_something_even_worse.txt
+7
quotes/deltarune/ralsei_sorry_i_say_nothing.txt
+7
quotes/deltarune/ralsei_sorry_i_say_nothing.txt
+8
quotes/deltarune/ralsei_susie_DONT_PLEASE.txt
+8
quotes/deltarune/ralsei_susie_DONT_PLEASE.txt
+9
quotes/deltarune/ralsei_susie_changed.txt
+9
quotes/deltarune/ralsei_susie_changed.txt
+3
quotes/deltarune/ralsei_susie_hog_all_the.txt
+3
quotes/deltarune/ralsei_susie_hog_all_the.txt
+9
quotes/deltarune/ralsei_susie_never_cake.txt
+9
quotes/deltarune/ralsei_susie_never_cake.txt
+6
quotes/deltarune/ralsei_susie_noogie_to_cry.txt
+6
quotes/deltarune/ralsei_susie_noogie_to_cry.txt
+9
quotes/deltarune/ralsei_susie_not_the_old_man_1.txt
+9
quotes/deltarune/ralsei_susie_not_the_old_man_1.txt
+8
quotes/deltarune/ralsei_susie_not_the_old_man_2.txt
+8
quotes/deltarune/ralsei_susie_not_the_old_man_2.txt
+5
quotes/deltarune/ralsei_susie_so_kind_how.txt
+5
quotes/deltarune/ralsei_susie_so_kind_how.txt
+10
quotes/deltarune/ralsei_susie_sounding_weird.txt
+10
quotes/deltarune/ralsei_susie_sounding_weird.txt
+7
quotes/deltarune/ralsei_susies_infectuous_hope.txt
+7
quotes/deltarune/ralsei_susies_infectuous_hope.txt
+6
quotes/deltarune/ralsei_tenna_empathy_1.txt
+6
quotes/deltarune/ralsei_tenna_empathy_1.txt
···
1
+
* Mr. Tenna... I... understand how you feel.
2
+
* To want to be... important. To be... useful.
3
+
* Perhaps... you might not be watched much anymore...
4
+
* But... that doesn't make you a failure, Tenna!
5
+
* You've brought smiles, light into Lightner's lives...
6
+
* ... to Kris's family and friends, for so long.
+6
quotes/deltarune/ralsei_tenna_empathy_2.txt
+6
quotes/deltarune/ralsei_tenna_empathy_2.txt
+6
quotes/deltarune/ralsei_the_one_thing_i_like.txt
+6
quotes/deltarune/ralsei_the_one_thing_i_like.txt
···
1
+
* Kris, I'm sorry, but...
2
+
* This is... my face.
3
+
* And if there's one thing I like about myself, it's this...
4
+
* So, even if you think it's boring, or too similar to...
5
+
* D-don't laugh, Kris. If you think it's the same as...
6
+
* OK, I get it! You don't think it's too similar! But...
+6
quotes/deltarune/ralsei_the_titan.txt
+6
quotes/deltarune/ralsei_the_titan.txt
+4
quotes/deltarune/ralsei_too_much_fun.txt
+4
quotes/deltarune/ralsei_too_much_fun.txt
+4
quotes/deltarune/ramb_i_saw_you.txt
+4
quotes/deltarune/ramb_i_saw_you.txt
+3
quotes/deltarune/ramb_just_play_your_games.txt
+3
quotes/deltarune/ramb_just_play_your_games.txt
+4
quotes/deltarune/ramb_was_it_even.txt
+4
quotes/deltarune/ramb_was_it_even.txt
+2
quotes/deltarune/ramb_whyd_you_do.txt
+2
quotes/deltarune/ramb_whyd_you_do.txt
+5
quotes/deltarune/rudy_carol_frozen.txt
+5
quotes/deltarune/rudy_carol_frozen.txt
+2
quotes/deltarune/rudy_locked_out.txt
+2
quotes/deltarune/rudy_locked_out.txt
+3
quotes/deltarune/rudy_noelle_snow_angels.txt
+3
quotes/deltarune/rudy_noelle_snow_angels.txt
+2
quotes/deltarune/savepoint_actors.txt
+2
quotes/deltarune/savepoint_actors.txt
+2
quotes/deltarune/savepoint_cold.txt
+2
quotes/deltarune/savepoint_cold.txt
+2
quotes/deltarune/savepoint_wind.txt
+2
quotes/deltarune/savepoint_wind.txt
+7
quotes/deltarune/shall_we_hasten.txt
+7
quotes/deltarune/shall_we_hasten.txt
+7
quotes/deltarune/shelter_lock.txt
+7
quotes/deltarune/shelter_lock.txt
+5
quotes/deltarune/spamtom_mere_puppet.txt
+5
quotes/deltarune/spamtom_mere_puppet.txt
+9
quotes/deltarune/susie_as_a_team.txt
+9
quotes/deltarune/susie_as_a_team.txt
···
1
+
* Hey... It's okay, man.
2
+
* That Last Prophecy thing... I don't need to see it.
3
+
* So... if it helps, whenever you gotta...
4
+
* You can go ahead alone.
5
+
* But everything else?
6
+
* We're doing it together. As a team.
7
+
* And NOTHING'S gonna change that.
8
+
* NOTHING!
9
+
* Whether you like it or not, toothpaste boy!
+4
quotes/deltarune/susie_asriel_difference.txt
+4
quotes/deltarune/susie_asriel_difference.txt
+5
quotes/deltarune/susie_be_nice_to_mom.txt
+5
quotes/deltarune/susie_be_nice_to_mom.txt
+8
quotes/deltarune/susie_broken_toy_1.txt
+8
quotes/deltarune/susie_broken_toy_1.txt
···
1
+
* I've never been able to... make friends.
2
+
* I was always... the scary girl. The bad kid.
3
+
* The only times anyone ever got close,
4
+
* ... were as a joke.
5
+
* And even if I did start to make a real friend...
6
+
* ... I'd always end up...
7
+
* Moving away.
8
+
* When I moved here, I thought it would be the same.
+4
quotes/deltarune/susie_broken_toy_2.txt
+4
quotes/deltarune/susie_broken_toy_2.txt
+7
quotes/deltarune/susie_broken_toy_3.txt
+7
quotes/deltarune/susie_broken_toy_3.txt
···
1
+
* Saw me sitting on the bench in the graveyard, crying.
2
+
* She asked me what was wrong, and...
3
+
* Told me everything was going to be okay.
4
+
* Took me to the diner. Bought me a hot chocolate.
5
+
* Talked to me. Told me I'd make friends.
6
+
* That... gave me hope. Even just a little.
7
+
* ...
+7
quotes/deltarune/susie_broken_toy_4.txt
+7
quotes/deltarune/susie_broken_toy_4.txt
+5
quotes/deltarune/susie_broken_toy_5.txt
+5
quotes/deltarune/susie_broken_toy_5.txt
+11
quotes/deltarune/susie_choose_eternity.txt
+11
quotes/deltarune/susie_choose_eternity.txt
+3
quotes/deltarune/susie_dark_world_real.txt
+3
quotes/deltarune/susie_dark_world_real.txt
+6
quotes/deltarune/susie_gerson_dragon_blazers.txt
+6
quotes/deltarune/susie_gerson_dragon_blazers.txt
···
1
+
* Isn't that what Dragon Blazers was based on?
2
+
3
+
* Yep, yep. And so they changed parts of the story.
4
+
* Of course, the biggest fans got mad... but, isn't it interesting?
5
+
* The book was already an interpretation of something else.
6
+
* Stories can be retold. They can be changed... That's what I believe.
+8
quotes/deltarune/susie_gerson_remind_me.txt
+8
quotes/deltarune/susie_gerson_remind_me.txt
+8
quotes/deltarune/susie_gerson_teaching.txt
+8
quotes/deltarune/susie_gerson_teaching.txt
···
1
+
* You were... trying to teach me something earlier, weren't you.
2
+
3
+
* That? Naw, you did all that yourself.
4
+
5
+
* C'mon man, you... you're the only person I've met...
6
+
* Who teaching me made me actually feel LESS stupid.
7
+
8
+
* Geheheh, well, it helps that I'm losing my mind, don't it?
+10
quotes/deltarune/susie_gerson_the_doragon.txt
+10
quotes/deltarune/susie_gerson_the_doragon.txt
+4
quotes/deltarune/susie_heal_no.txt
+4
quotes/deltarune/susie_heal_no.txt
+6
quotes/deltarune/susie_hope_crossed.txt
+6
quotes/deltarune/susie_hope_crossed.txt
+3
quotes/deltarune/susie_kris_doggy_ears.txt
+3
quotes/deltarune/susie_kris_doggy_ears.txt
+7
quotes/deltarune/susie_kris_giant_bloodstain.txt
+7
quotes/deltarune/susie_kris_giant_bloodstain.txt
+1
quotes/deltarune/susie_kris_hug_the_freezer.txt
+1
quotes/deltarune/susie_kris_hug_the_freezer.txt
···
1
+
* Stop hugging the fridge, freezer freak.
+7
quotes/deltarune/susie_kris_just_hear_you_play.txt
+7
quotes/deltarune/susie_kris_just_hear_you_play.txt
···
1
+
* ... I... don't even really know if I broke it, but...
2
+
* I hit it as hard as I could. Hard enough to do some damage.
3
+
* ... and I ran.
4
+
* ... A week ago, if I knew you did piano...
5
+
* ... I probably would've just hated that about you.
6
+
* But now...
7
+
* I kinda... just wanna hear you play.
+6
quotes/deltarune/susie_kris_mouth_closed.txt
+6
quotes/deltarune/susie_kris_mouth_closed.txt
+3
quotes/deltarune/susie_kris_my_prize.txt
+3
quotes/deltarune/susie_kris_my_prize.txt
+10
quotes/deltarune/susie_kris_no_need.txt
+10
quotes/deltarune/susie_kris_no_need.txt
+2
quotes/deltarune/susie_kris_or_something.txt
+2
quotes/deltarune/susie_kris_or_something.txt
+7
quotes/deltarune/susie_kris_plink.txt
+7
quotes/deltarune/susie_kris_plink.txt
+7
quotes/deltarune/susie_kris_we_are_friends.txt
+7
quotes/deltarune/susie_kris_we_are_friends.txt
+6
quotes/deltarune/susie_kris_wont_you_play_1.txt
+6
quotes/deltarune/susie_kris_wont_you_play_1.txt
+10
quotes/deltarune/susie_kris_wont_you_play_2.txt
+10
quotes/deltarune/susie_kris_wont_you_play_2.txt
···
1
+
* ... Kris, you should play.
2
+
3
+
* (You thought about Susie's words and took a deep breath.)
4
+
* (Your hands began to move on their own...)
5
+
6
+
* Hey, why'd you stop? That was cool.
7
+
* ... You should, uh, play more.
8
+
* Seriously, you're an idiot if you quit playing.
9
+
10
+
* (... Susie... is nice, isn't she, Kris.)
+6
quotes/deltarune/susie_kris_your_piano.txt
+6
quotes/deltarune/susie_kris_your_piano.txt
+8
quotes/deltarune/susie_letter_alvin.txt
+8
quotes/deltarune/susie_letter_alvin.txt
+9
quotes/deltarune/susie_ralsei_bad_bum.txt
+9
quotes/deltarune/susie_ralsei_bad_bum.txt
···
1
+
* Before... you were all stuck up about being good, right?
2
+
* Now... you're actually kinda normal.
3
+
4
+
* I suppose I just... started appreciating you more, Susie.
5
+
* For example, I thought your Rude Buster was scary...
6
+
* But now, I think it's really b--
7
+
* Umm, bad-bum!
8
+
9
+
* Just say "bad ass", idiot!
+10
quotes/deltarune/susie_ralsei_cant_go_1.txt
+10
quotes/deltarune/susie_ralsei_cant_go_1.txt
+9
quotes/deltarune/susie_ralsei_cant_go_2.txt
+9
quotes/deltarune/susie_ralsei_cant_go_2.txt
+11
quotes/deltarune/susie_ralsei_ill_do_it_all.txt
+11
quotes/deltarune/susie_ralsei_ill_do_it_all.txt
···
1
+
* What, that... Last Prophecy thing...?
2
+
3
+
* Susie... y-you didn't...
4
+
5
+
* No, I mean, we almost did, but--
6
+
7
+
* Please. Please let me go ahead from now on.
8
+
* I'll do every area. I'll do all the work.
9
+
* Next time, too.
10
+
* You two can just hang out and... have fun!
11
+
* I could do all the puzzles, I could...
+7
quotes/deltarune/susie_ralsei_no_discards.txt
+7
quotes/deltarune/susie_ralsei_no_discards.txt
+8
quotes/deltarune/susie_ralsei_real_feelings.txt
+8
quotes/deltarune/susie_ralsei_real_feelings.txt
···
1
+
* But Ralsei, how can you say you're not real!?
2
+
* I can see you... I can feel you... I can hear you...
3
+
* "Normal objects" don't have... feelings and stuff!
4
+
5
+
* That's right, Susie.
6
+
* They... shouldn't.
7
+
* That's why... I don't want you and Kris...
8
+
* ... to worry too much about us, OK?
+11
quotes/deltarune/susie_ralsei_say_anything.txt
+11
quotes/deltarune/susie_ralsei_say_anything.txt
···
1
+
* ... hey, Ralsei. If you know the whole prophecy already...
2
+
* ... why didn't you just say how to do the piano?
3
+
4
+
* H-huh...? Well, I just thought...
5
+
* I just thought, isn't it better if I don't... say anything?
6
+
7
+
* ...
8
+
* Why?
9
+
10
+
* Um...
11
+
* H-Hey, I think I found a light!
+11
quotes/deltarune/susie_ralsei_so_you_wont.txt
+11
quotes/deltarune/susie_ralsei_so_you_wont.txt
···
1
+
* Ralsei...
2
+
* Is that... why you've been acting... so weird today?
3
+
* Why you keep...
4
+
5
+
* ... I... I've just been trying to stay ahead of you two.
6
+
* So you don't have to see it. So you won't...
7
+
* Because it would...
8
+
9
+
* See... see what?
10
+
11
+
* The ending... of the prophecy.
+8
quotes/deltarune/susie_ralsei_the_whole_deal.txt
+8
quotes/deltarune/susie_ralsei_the_whole_deal.txt
+12
quotes/deltarune/susie_ralsei_wont_forget_1.txt
+12
quotes/deltarune/susie_ralsei_wont_forget_1.txt
···
1
+
* ... Ralsei... you...
2
+
* You idiot! How can you SAY that?!
3
+
* You think I could just FORGET you!?
4
+
* Forget Lancer!? Forget everyone!?
5
+
* I don't care what you say you are...
6
+
* You're real to me, OK!?
7
+
8
+
* ...
9
+
* Susie, I...
10
+
* I'm happy to hear that, but...
11
+
12
+
* SHUT UP and listen to me!
+7
quotes/deltarune/susie_ralsei_wont_forget_2.txt
+7
quotes/deltarune/susie_ralsei_wont_forget_2.txt
+7
quotes/deltarune/susie_ralsei_your_room_1.txt
+7
quotes/deltarune/susie_ralsei_your_room_1.txt
+6
quotes/deltarune/susie_ralsei_your_room_2.txt
+6
quotes/deltarune/susie_ralsei_your_room_2.txt
+8
quotes/deltarune/susie_something_important_1.txt
+8
quotes/deltarune/susie_something_important_1.txt
···
1
+
* Gotta be honest, Kris. I've never...
2
+
* I've never really gotten picked...
3
+
* ... for anything before.
4
+
* No one ever wanted to be my partner in class.
5
+
* ... Even got chosen last in gym.
6
+
* But this...?
7
+
* This is like... Something else, y'know?
8
+
* Something... important.
+6
quotes/deltarune/susie_something_important_2.txt
+6
quotes/deltarune/susie_something_important_2.txt
···
1
+
* Kris... before, I didn't think I could be a hero.
2
+
* I wasn't good enough. I mean, I was... bad.
3
+
* I still kinda am bad. In some ways, I guess.
4
+
* But now, I... I got hope crossed on my heart.
5
+
* Hope, written in truth. Written somewhere...
6
+
* Written somewhere no one can erase!
+11
quotes/deltarune/susie_stupid_dream_1.txt
+11
quotes/deltarune/susie_stupid_dream_1.txt
+12
quotes/deltarune/susie_stupid_dream_2.txt
+12
quotes/deltarune/susie_stupid_dream_2.txt
+8
quotes/deltarune/susie_tea_for_wimps.txt
+8
quotes/deltarune/susie_tea_for_wimps.txt
···
1
+
* It's kinda funny... I always thought tea parties were for wimps.
2
+
* I mean, don't get me wrong. They still are. But...
3
+
* Just feels like when I'm with you guys...
4
+
* I don't even... need to think about that.
5
+
6
+
* Aww, Susie!
7
+
8
+
* Guess your stupid attitude's been rubbing off on me.
+3
quotes/deltarune/susie_tea_got_options.txt
+3
quotes/deltarune/susie_tea_got_options.txt
+10
quotes/deltarune/susie_tenna_gaming.txt
+10
quotes/deltarune/susie_tenna_gaming.txt
···
1
+
* Just, uh...
2
+
* Wanted to say the games were pretty fun, I guess.
3
+
4
+
* They... are?
5
+
6
+
* I mean, you SAW when you interrupted us, but... Ralsei...
7
+
* He was being kind of... weird, earlier.
8
+
* I feel like this cheered him up.
9
+
* And... me too.
10
+
* It's just been taking my mind off... everything... I guess.
+4
quotes/deltarune/susie_tried_piano_1.txt
+4
quotes/deltarune/susie_tried_piano_1.txt
+7
quotes/deltarune/susie_tried_piano_2.txt
+7
quotes/deltarune/susie_tried_piano_2.txt
+9
quotes/deltarune/susie_tried_piano_3.txt
+9
quotes/deltarune/susie_tried_piano_3.txt
+8
quotes/deltarune/susie_wont_let_it_happen.txt
+8
quotes/deltarune/susie_wont_let_it_happen.txt
···
1
+
* The hell are you apologizing for?
2
+
* You're worried about THAT!? Seriously!?
3
+
* ... this stupid prophecy?
4
+
* Like something like that would happen.
5
+
* I wouldn't let it happen... Kris wouldn't let it happen...
6
+
* And obviously YOU wouldn't let it happen.
7
+
* So...
8
+
* Why wouldn't you laugh?
+3
quotes/deltarune/tenna_beautiful_princess_1.txt
+3
quotes/deltarune/tenna_beautiful_princess_1.txt
+6
quotes/deltarune/tenna_beautiful_princess_2.txt
+6
quotes/deltarune/tenna_beautiful_princess_2.txt
+7
quotes/deltarune/tenna_holidays_1.txt
+7
quotes/deltarune/tenna_holidays_1.txt
+6
quotes/deltarune/tenna_holidays_2.txt
+6
quotes/deltarune/tenna_holidays_2.txt
+10
quotes/deltarune/tenna_holidays_3.txt
+10
quotes/deltarune/tenna_holidays_3.txt
+3
quotes/deltarune/tenna_kris_december_1.txt
+3
quotes/deltarune/tenna_kris_december_1.txt
+3
quotes/deltarune/tenna_kris_december_2.txt
+3
quotes/deltarune/tenna_kris_december_2.txt
+12
quotes/deltarune/tenna_susie_kris_1.txt
+12
quotes/deltarune/tenna_susie_kris_1.txt
···
1
+
* I... can't lie. I've been a bit, alone, until recently.
2
+
3
+
* I... can relate, I guess.
4
+
5
+
* But hosting you and lil' Ralsei has been an HONOR! Even if you're a tad sassy!
6
+
7
+
* Umm, Kris was having fun, too.
8
+
9
+
* Oh... yeah, Kris!
10
+
* Haha, Kris! Good old Kris! What a stinker!
11
+
12
+
* ... don't you like Kris?
+7
quotes/deltarune/tenna_susie_kris_2.txt
+7
quotes/deltarune/tenna_susie_kris_2.txt
···
1
+
* What the - are you kidding!? I LOVE Kris! They were one of my original viewers!
2
+
* I'm just not sure if, heh, they... uhh, love TV.
3
+
4
+
* Huh? Why wouldn't they...
5
+
6
+
* HEY would you look at that!! The NEXT BOARD's ready!!!
7
+
* It was GREAT talking to you, Susie!! Good luck out there!!
+5
quotes/deltarune/tree_painted_over.txt
+5
quotes/deltarune/tree_painted_over.txt
+10
quotes/deltarune/yet_you_persist.txt
+10
quotes/deltarune/yet_you_persist.txt
+10
quotes/deltarune/you_promised.txt
+10
quotes/deltarune/you_promised.txt
+3
quotes/xenoblade-chronicles-3/monica_in_the_mud.txt
+3
quotes/xenoblade-chronicles-3/monica_in_the_mud.txt
+3
-2
redis_random_push.py
+3
-2
redis_random_push.py
···
14
14
import redis
15
15
import typer
16
16
17
-
def main(source_key, destination_key, rename_key: bool = False, password: str = "", host: str = "localhost", port: int = 16379, quotes_pattern: str = "quotes/**/*.txt", log_level: str = "INFO") -> None:
17
+
def main(source_key, destination_key, rename_key: bool = False, password: str = "", host: str = "localhost", port: int = 16379, quotes_pattern: str = "quotes/**/*.txt", quotes_list_file: Path | None = None, log_level: str = "INFO") -> None:
18
18
logging.getLogger().setLevel(os.getenv("LOGGING") or log_level)
19
19
20
-
new_quotes = tuple(iglob(quotes_pattern, recursive=True))
20
+
new_quotes = tuple(iglob(quotes_pattern, recursive=True)) if not quotes_list_file else tuple(line for line in quotes_list_file.read_text().split("\n") if line)
21
+
logging.info("Quotes to add: `%s`", new_quotes)
21
22
r = redis.Redis(host=host, port=port, decode_responses=True, password=password)
22
23
23
24
logging.info("Fetching existing list at key `%s`.", source_key)
-24
src/data.rs
-24
src/data.rs
···
1
-
/// A newtype over [String] to represent a single quote.
2
-
/// [Quote] does not implement [PartialEq] or [Eq] as, on principle, two
3
-
/// quotes may be comprised of the same contents whilst still
4
-
/// representing distinct quotes in practice (e.g. two different lines of dialogue which happen to be the same).
5
-
#[derive(Debug, Clone)]
6
-
pub struct Quote(String);
7
-
8
-
impl<S: AsRef<str>> From<S> for Quote {
9
-
fn from(value: S) -> Self {
10
-
Self(value.as_ref().to_owned())
11
-
}
12
-
}
13
-
14
-
impl From<Quote> for String {
15
-
fn from(value: Quote) -> Self {
16
-
value.0
17
-
}
18
-
}
19
-
20
-
impl Quote {
21
-
pub fn get(&self) -> &str {
22
-
&self.0
23
-
}
24
-
}
-94
src/lib.rs
-94
src/lib.rs
···
1
-
pub mod data;
2
-
pub mod sink;
3
-
pub mod storage;
4
-
5
-
pub mod run {
6
-
use std::time::Duration;
7
-
8
-
use crate::sink::{BskySink, PostQuote, SinkManager, StdoutSink};
9
-
use crate::storage::{
10
-
FetchQuote, QuoteCycle, queue::MemoryQueueStorage, source::FsFilterSourceManager,
11
-
};
12
-
use cron_lite::CronEvent;
13
-
use futures::StreamExt;
14
-
use kameo::prelude::*;
15
-
use tokio::time::timeout;
16
-
17
-
pub async fn entrypoint() -> Result<(), Box<dyn std::error::Error>> {
18
-
// TODO: Clean up this function's internals.
19
-
// The current structure is alright, but it was stitched together
20
-
// quickly just to confirm that everything is functioning as it should.
21
-
let use_bsky = std::env::var("USE_BLUESKY").unwrap_or("0".to_string()) == "1";
22
-
let bsky = if use_bsky {
23
-
Some(BskySink::spawn(
24
-
BskySink::new_session(
25
-
std::env::var("BLUESKY_USERNAME").expect("Bluesky username not supplied"),
26
-
std::env::var("BLUESKY_PASSWORD")
27
-
.expect("Bluesky application password not supplied"),
28
-
)
29
-
.await
30
-
.expect("Could not connect to Bluesky with supplied credentials"),
31
-
))
32
-
} else {
33
-
None
34
-
};
35
-
36
-
let sink = {
37
-
let stdout = StdoutSink::spawn(StdoutSink);
38
-
SinkManager::spawn(SinkManager::new(Some(stdout), bsky))
39
-
};
40
-
41
-
let cycle = {
42
-
let source = FsFilterSourceManager::spawn(FsFilterSourceManager::default());
43
-
let queue = MemoryQueueStorage::spawn(MemoryQueueStorage::new());
44
-
45
-
QuoteCycle::spawn(QuoteCycle::with_thread_rng(source, queue))
46
-
};
47
-
48
-
use cron_lite::Schedule;
49
-
const POSTING_TIMEOUT: Duration = Duration::from_secs(60);
50
-
const POSTING_INTERVAL: &str = "*/10 * * * * * *";
51
-
let schedule =
52
-
Schedule::new(POSTING_INTERVAL).expect("Schedule should be a valid cron expression");
53
-
let now = chrono::Utc::now();
54
-
55
-
let mut tick_stream = schedule.stream(&now);
56
-
57
-
while let Some(tick) = tick_stream.next().await {
58
-
if let CronEvent::Missed(missed_at) = tick {
59
-
eprintln!(
60
-
"Missed event tick at {}. Current time: {}. Skipping post.",
61
-
missed_at,
62
-
chrono::Utc::now()
63
-
);
64
-
continue;
65
-
}
66
-
67
-
// We store the code to perform the next posting iteration as one atomic future which we wrap with a timeout.
68
-
// This means that, if we miss a posting window due to the timeout, we will not get multiple consecutive or late posts.
69
-
let next_post_iteration = async || -> Result<(), Box<dyn std::error::Error>> {
70
-
let next_quote = cycle
71
-
.ask(FetchQuote)
72
-
.await
73
-
.map_err(|_| "fetch quote should always succeed")?;
74
-
75
-
// Note: By using `tell`, we don't know when each sink's code will have completed.
76
-
// If any sink uses, say, a file or stdout, that resource may well be contested between
77
-
// consecutive iterations of this loop.
78
-
sink.tell(PostQuote(next_quote)).await?;
79
-
println!();
80
-
81
-
Ok(())
82
-
};
83
-
84
-
if let Err(e) = timeout(POSTING_TIMEOUT, next_post_iteration()).await {
85
-
eprintln!(
86
-
"Could not submit post in time to all sinks. Timeout error: {}",
87
-
e
88
-
);
89
-
}
90
-
}
91
-
92
-
Ok(())
93
-
}
94
-
}
+310
-1
src/main.rs
+310
-1
src/main.rs
···
1
+
use bsky_sdk::api::app::bsky::feed::post;
2
+
use bsky_sdk::api::types::string::Datetime;
3
+
use bsky_sdk::{BskyAgent, api::types::Object};
4
+
5
+
use glob::glob;
6
+
use grep::{regex, searcher::sinks};
7
+
use rand::seq::SliceRandom;
8
+
use redis::aio::ConnectionManagerConfig;
9
+
10
+
use std::{sync::Arc, time::Duration};
11
+
use tokio_cron_scheduler::{Job, JobScheduler};
12
+
13
+
use redis::AsyncCommands;
14
+
15
+
const DEFAULT_QUEUE: &str = "queue:default";
16
+
const EVENT_QUEUE: &str = "queue:event";
17
+
18
+
// See https://cron.help for what these strings mean
19
+
const POSTING_INTERVAL_CRON: &str = "0 0,30 * * * *";
20
+
const POSTING_INTERVAL_DEBUG: &str = "1/10 * * * * *";
21
+
const EVENT_UPDATE_INTERVAL: &str = "55 23 * * *";
22
+
23
+
const POSTING_RETRIES: i32 = 5;
24
+
25
+
fn prepare_post<I: Into<String>>(text: I) -> post::RecordData {
26
+
post::RecordData {
27
+
text: text.into(),
28
+
created_at: Datetime::now(),
29
+
embed: None,
30
+
entities: None,
31
+
facets: None,
32
+
labels: None,
33
+
langs: None,
34
+
reply: None,
35
+
tags: None,
36
+
}
37
+
}
38
+
39
+
#[derive(Clone, Debug)]
40
+
struct QuoteFilter {
41
+
path: String,
42
+
content: String,
43
+
dates: Vec<String>,
44
+
}
45
+
46
+
impl QuoteFilter {
47
+
pub async fn get_quote(
48
+
&self,
49
+
mut con: impl redis::aio::ConnectionLike + AsyncCommands + Clone,
50
+
) -> Result<String, ()> {
51
+
// 1: Attempt to read from the event (priority) queue
52
+
let event_quote: Option<String> = con.lpop(EVENT_QUEUE, None).await.ok();
53
+
if let Some(quote) = event_quote {
54
+
return Ok(quote);
55
+
}
56
+
57
+
// 2: Otherwise, we read from the regular queue, repopulating it if it's empty
58
+
self.reshuffle_quotes(con.clone(), DEFAULT_QUEUE).await?;
59
+
con.lpop(DEFAULT_QUEUE, None).await.map_err(|_| ())
60
+
}
61
+
62
+
async fn reshuffle_quotes(
63
+
&self,
64
+
mut con: impl redis::aio::ConnectionLike + AsyncCommands,
65
+
output_queue: &str,
66
+
) -> Result<(), ()> {
67
+
let len: u64 = con.llen(output_queue).await.map_err(|_| ())?;
68
+
// NOTE: The following assumes the queue hasn't been repopulated by any other client
69
+
// in-between the call to llen and the execution of the pipeline.
70
+
// Hopefully won't be a problem :)
71
+
if len == 0 {
72
+
let mut file_contents = self.read_files();
73
+
74
+
{
75
+
let mut rand = rand::rng();
76
+
file_contents.shuffle(&mut rand);
77
+
}
78
+
79
+
let mut pipeline = redis::pipe();
80
+
for file_contents in file_contents.into_iter() {
81
+
pipeline.lpush(output_queue, file_contents.as_str());
82
+
}
83
+
let _: () = pipeline.query_async(&mut con).await.map_err(|_| ())?;
84
+
}
85
+
86
+
Ok(())
87
+
}
88
+
89
+
fn read_files(&self) -> Vec<String> {
90
+
let matcher = regex::RegexMatcher::new(&self.content).unwrap();
91
+
let mut searcher = grep::searcher::Searcher::new();
92
+
let mut results = Vec::new();
93
+
94
+
for file in glob(&self.path).unwrap() {
95
+
let file = match file {
96
+
Ok(file) => file,
97
+
Err(_) => continue,
98
+
};
99
+
100
+
let mut matched = false;
101
+
let sink = sinks::Lossy(|_lnum, _line| {
102
+
matched = true;
103
+
Ok(false)
104
+
});
105
+
106
+
let search_result = searcher.search_path(&matcher, &file, sink);
107
+
if !matched || search_result.is_err() {
108
+
continue;
109
+
}
110
+
111
+
let contents = std::fs::read_to_string(file).unwrap();
112
+
results.push(contents.trim().to_string());
113
+
}
114
+
115
+
results
116
+
}
117
+
}
118
+
119
+
#[derive(Clone)]
120
+
struct RedisState {
121
+
con_manager: redis::aio::ConnectionManager,
122
+
}
123
+
124
+
impl RedisState {
125
+
pub async fn new(url: String) -> Result<Self, ()> {
126
+
let redis = redis::Client::open(url).map_err(|_| ())?;
127
+
let config = ConnectionManagerConfig::new()
128
+
.set_response_timeout(std::time::Duration::from_secs(10))
129
+
.set_number_of_retries(3);
130
+
let con_manager = redis::aio::ConnectionManager::new_with_config(redis, config)
131
+
.await
132
+
.map_err(|_| ())?;
133
+
134
+
Ok(RedisState { con_manager })
135
+
}
136
+
137
+
pub async fn fetch_quote(&self, filter: &QuoteFilter) -> Result<String, ()> {
138
+
loop {
139
+
match filter.get_quote(self.con_manager.clone()).await {
140
+
Ok(text) => return Ok(text),
141
+
Err(_) => eprintln!("Error fetching quote from redis storage. Retrying..."),
142
+
};
143
+
}
144
+
}
145
+
}
146
+
147
+
#[derive(Clone)]
148
+
struct BlueskyState {
149
+
bsky_agent: BskyAgent,
150
+
bsky_session: Object<bsky_sdk::api::com::atproto::server::create_session::OutputData>,
151
+
}
152
+
153
+
impl BlueskyState {
154
+
pub async fn new_session(username: String, password: String) -> Result<Self, ()> {
155
+
let agent = BskyAgent::builder().build().await.map_err(|_| ())?;
156
+
let session = agent.login(username, password).await.map_err(|_| ())?;
157
+
158
+
Ok(Self {
159
+
bsky_agent: agent,
160
+
bsky_session: session,
161
+
})
162
+
}
163
+
164
+
pub async fn submit_post(self, post: String) -> Result<(), ()> {
165
+
let post = prepare_post(post.as_str());
166
+
167
+
for current_try in 0..POSTING_RETRIES {
168
+
if let Err(e) = self.bsky_agent.create_record(post.clone()).await {
169
+
eprintln!("Could not post quote: `{e}`");
170
+
eprintln!("Attempting to refresh login...");
171
+
172
+
if let Err(e) = self
173
+
.bsky_agent
174
+
.resume_session(self.bsky_session.clone())
175
+
.await
176
+
{
177
+
eprintln!("Failed to resume sessions due to following error: {e}")
178
+
}
179
+
} else {
180
+
if current_try > 0 {
181
+
eprintln!("Successfully posted quote on retry #{current_try}");
182
+
}
183
+
return Ok(());
184
+
}
185
+
}
186
+
187
+
Err(())
188
+
}
189
+
}
190
+
191
+
#[derive(Clone)]
192
+
struct State {
193
+
redis: RedisState,
194
+
bsky_session: Option<BlueskyState>,
195
+
}
196
+
197
+
impl State {
198
+
pub fn redis(&self) -> &RedisState {
199
+
&self.redis
200
+
}
201
+
202
+
pub fn bsky(&self) -> Option<&BlueskyState> {
203
+
self.bsky_session.as_ref()
204
+
}
205
+
}
206
+
1
207
#[tokio::main]
2
208
async fn main() -> Result<(), Box<dyn std::error::Error>> {
3
-
audquotes::run::entrypoint().await
209
+
let debug_mode = std::env::var("DEBUG").unwrap_or("0".to_string()) == "1";
210
+
let use_bsky = std::env::var("USE_BLUESKY").unwrap_or("0".to_string()) == "1";
211
+
212
+
let redis_state =
213
+
RedisState::new(std::env::var("REDIS_URL").unwrap_or("redis://localhost".to_string()))
214
+
.await
215
+
.expect("Initial redis connection failure");
216
+
let bsky_state = if use_bsky {
217
+
Some(
218
+
BlueskyState::new_session(
219
+
std::env::var("BLUESKY_USERNAME").expect("Bluesky username not supplied"),
220
+
std::env::var("BLUESKY_PASSWORD")
221
+
.expect("Bluesky application password not supplied"),
222
+
)
223
+
.await
224
+
.expect("Could not connect to Bluesky with supplied credentials"),
225
+
)
226
+
} else {
227
+
None
228
+
};
229
+
230
+
let app_state = Arc::new(State {
231
+
redis: redis_state,
232
+
bsky_session: bsky_state,
233
+
});
234
+
235
+
let sched = JobScheduler::new().await?;
236
+
237
+
/*
238
+
let event_filter = Arc::new(QuoteFilter {
239
+
content: r"\b(?i:mother|mommy|mama|mom)\b".to_string(),
240
+
path: "test/**/
241
+
*.txt".to_string(),
242
+
dates: vec![],
243
+
});
244
+
*/
245
+
246
+
let regular_filter = Arc::new(QuoteFilter {
247
+
content: r".*".to_string(),
248
+
path: if !debug_mode {
249
+
"quotes/**/*.txt".to_string()
250
+
} else {
251
+
"test/**/*.txt".to_string()
252
+
},
253
+
dates: vec![],
254
+
});
255
+
256
+
let posting_interval = if !debug_mode {
257
+
POSTING_INTERVAL_CRON
258
+
} else {
259
+
POSTING_INTERVAL_DEBUG
260
+
};
261
+
262
+
let post_job = Job::new_async(posting_interval, move |_uuid, _| {
263
+
let filter = regular_filter.clone();
264
+
let app_state = app_state.clone();
265
+
266
+
Box::pin(async move {
267
+
// We try fetching a new quote from our redis storage until we succeed
268
+
let text = match app_state.redis().fetch_quote(&filter).await {
269
+
Ok(text) => text,
270
+
Err(_) => {
271
+
eprintln!("Error fetching quote from redis storage.");
272
+
return;
273
+
}
274
+
};
275
+
276
+
if let Some(bsky) = app_state.bsky() {
277
+
if let Err(_) = bsky.clone().submit_post(text).await {
278
+
eprintln!("Error posting to bluesky.");
279
+
return;
280
+
}
281
+
} else {
282
+
// Let's just print the quote!
283
+
println!("{}\n", text);
284
+
}
285
+
})
286
+
})?;
287
+
288
+
// Add async job
289
+
sched.add(post_job).await?;
290
+
291
+
// sched
292
+
// .add(Job::new_async(EVENT_UPDATE_INTERVAL, move |_uuid, _| {
293
+
// let filter = event_filter.clone();
294
+
// let con = con_event_monitor.clone();
295
+
// let _agent = agent_event_monitor.clone(); // Can be used later to e.g. update profile
296
+
297
+
// Box::pin(async move {
298
+
// // For testing purposes, let's always upload events
299
+
// reshuffle_quotes(&filter, con.clone(), EVENT_QUEUE)
300
+
// .await
301
+
// .unwrap();
302
+
// })
303
+
// })?)
304
+
// .await?;
305
+
306
+
sched
307
+
.start()
308
+
.await
309
+
.expect("Error starting tokio scheduler. Shutting down...");
310
+
loop {
311
+
tokio::time::sleep(Duration::from_secs(10)).await;
312
+
}
4
313
}
-192
src/sink.rs
-192
src/sink.rs
···
1
-
use crate::data::Quote;
2
-
use bsky_sdk::{BskyAgent, api::types::Object};
3
-
use kameo::prelude::*;
4
-
5
-
/// A newtype over [Quote] used to prompt the [SinkManager] to
6
-
/// submit a new quote to all its configured sinks.
7
-
#[derive(Debug, Clone)]
8
-
pub struct PostQuote(pub Quote);
9
-
10
-
/// Error type for internal communication between
11
-
/// the [SinkManager] and its sinks.
12
-
/// The error reporting performed internally does not necessarily match the
13
-
/// behavior which other modules will observe.
14
-
#[derive(Debug, Clone)]
15
-
pub enum PostFailure {
16
-
/// Indicates that a given quote could not be posted to a sink,
17
-
/// but that it *may* be retried. The `reinitialize` boolean signals
18
-
/// whether the sink should be reinitialized before further attempts.
19
-
Retry { reinitialize: bool },
20
-
21
-
/// Indicates that a given quote could not be posted to a sink,
22
-
/// as it is unsupported by it in some way (e.g. quote exceeds the sink's length limit).
23
-
Unsupported,
24
-
25
-
/// Indicates that a given quote could not be posted to a sink
26
-
/// due to the occurrence of some unrecoverable error.
27
-
/// It is thus unlikely that the sink will work in the future, even if
28
-
/// reinitialized.
29
-
Unrecoverable,
30
-
}
31
-
32
-
pub type PostResult = Result<(), PostFailure>;
33
-
34
-
/// Represents internal implementation details of the interactions between
35
-
/// the [SinkManager] and its sinks.
36
-
pub trait QuoteSink: Actor + Message<PostQuote, Reply = PostResult> {}
37
-
38
-
/// A [QuoteSink] which will output the contents of each quote
39
-
/// over Stdout. Is primarily meant for testing and observing sink behavior.
40
-
#[derive(Actor)]
41
-
pub struct StdoutSink;
42
-
43
-
impl Message<PostQuote> for StdoutSink {
44
-
type Reply = PostResult;
45
-
46
-
async fn handle(
47
-
&mut self,
48
-
PostQuote(quote): PostQuote,
49
-
_ctx: &mut Context<Self, Self::Reply>,
50
-
) -> Self::Reply {
51
-
println!("{}", quote.get());
52
-
Ok(())
53
-
}
54
-
}
55
-
56
-
/// A [QuoteSink] which will post the contents of each quote to Bluesky.
57
-
#[derive(Actor)]
58
-
pub struct BskySink {
59
-
bsky_agent: BskyAgent,
60
-
bsky_session: Object<bsky_sdk::api::com::atproto::server::create_session::OutputData>,
61
-
}
62
-
63
-
impl BskySink {
64
-
pub async fn new_session(username: String, password: String) -> Result<Self, ()> {
65
-
let agent = BskyAgent::builder().build().await.map_err(|_| ())?;
66
-
let session = agent.login(username, password).await.map_err(|_| ())?;
67
-
68
-
Ok(Self {
69
-
bsky_agent: agent,
70
-
bsky_session: session,
71
-
})
72
-
}
73
-
74
-
async fn submit_post(&mut self, quote: Quote) -> Result<(), ()> {
75
-
let post = bsky_sdk::api::app::bsky::feed::post::RecordData {
76
-
text: quote.into(),
77
-
created_at: bsky_sdk::api::types::string::Datetime::now(),
78
-
embed: None,
79
-
entities: None,
80
-
facets: None,
81
-
labels: None,
82
-
langs: None,
83
-
reply: None,
84
-
tags: None,
85
-
};
86
-
87
-
if let Err(e) = self
88
-
.bsky_agent
89
-
.resume_session(self.bsky_session.clone())
90
-
.await
91
-
{
92
-
eprintln!("Failed to resume sessions due to following error: {e}");
93
-
return Err(());
94
-
}
95
-
96
-
match self.bsky_agent.create_record(post.clone()).await {
97
-
Ok(_) => Ok(()),
98
-
Err(_) => Err(()),
99
-
}
100
-
}
101
-
}
102
-
103
-
impl Message<PostQuote> for BskySink {
104
-
type Reply = PostResult;
105
-
106
-
async fn handle(
107
-
&mut self,
108
-
PostQuote(quote): PostQuote,
109
-
_ctx: &mut Context<Self, Self::Reply>,
110
-
) -> Self::Reply {
111
-
match self.submit_post(quote).await {
112
-
Ok(_) => Ok(()),
113
-
Err(_) => Err(PostFailure::Unrecoverable),
114
-
}
115
-
}
116
-
}
117
-
118
-
/// Supervises all [QuoteSink] actors within the program, forwarding
119
-
/// [PostQuote] messages to them as they are received.
120
-
/// The SinkManager will attempt to reinitialize failed sinks upon
121
-
/// encountering recoverable errors.
122
-
#[derive(Actor)]
123
-
pub struct SinkManager {
124
-
// Uh oh. As the [Actor] trait is *not* dyn-compatible,
125
-
// and I do not own its definition, I'm fairly certain that I cannot
126
-
// do asynchronous dynamic dispatch for it here.
127
-
// I've decided I'll limit this to one sink per implementation right now.
128
-
stdout_sink: Option<ActorRef<StdoutSink>>,
129
-
bsky_sink: Option<ActorRef<BskySink>>,
130
-
// ...
131
-
}
132
-
133
-
impl SinkManager {
134
-
pub fn new(
135
-
stdout_sink: Option<ActorRef<StdoutSink>>,
136
-
bsky_sink: Option<ActorRef<BskySink>>,
137
-
) -> Self {
138
-
Self {
139
-
stdout_sink,
140
-
bsky_sink,
141
-
}
142
-
}
143
-
}
144
-
145
-
pub type SinkReplies = Vec<Result<(), ()>>;
146
-
147
-
impl Message<PostQuote> for SinkManager {
148
-
type Reply = SinkReplies;
149
-
150
-
async fn handle(
151
-
&mut self,
152
-
msg: PostQuote,
153
-
_ctx: &mut Context<Self, Self::Reply>,
154
-
) -> Self::Reply {
155
-
use futures::future::join_all;
156
-
157
-
let stdout_result = self
158
-
.stdout_sink
159
-
.as_ref()
160
-
.map(|s| s.ask(msg.clone()).into_future());
161
-
162
-
let bsky_result = self
163
-
.bsky_sink
164
-
.as_ref()
165
-
.map(|s| s.ask(msg.clone()).into_future());
166
-
167
-
let futures = [stdout_result, bsky_result].into_iter().flatten();
168
-
let results = join_all(futures).await;
169
-
170
-
results.iter().map(|r| r.clone().or(Err(()))).collect()
171
-
}
172
-
}
173
-
174
-
mod test {
175
-
#[tokio::test]
176
-
async fn stdout_sink() {
177
-
use super::*;
178
-
179
-
let stdout = StdoutSink::spawn(StdoutSink);
180
-
let manager = SinkManager::spawn(SinkManager::new(Some(stdout), None));
181
-
182
-
let messages = ["First test!", "Second test.", "Third..."];
183
-
for msg in messages {
184
-
manager.tell(PostQuote(msg.into())).await.unwrap();
185
-
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
186
-
}
187
-
188
-
// Hopefully we don't crash...!
189
-
// TODO: Sink that actually stores every quote it "posts"?
190
-
// Could help in verifying everything was sent correctly.
191
-
}
192
-
}
-424
src/storage.rs
-424
src/storage.rs
···
1
-
use kameo::prelude::*;
2
-
3
-
use crate::storage::{
4
-
queue::{DequeueQuote, EnqueueQuotes},
5
-
source::SourceQuotes,
6
-
};
7
-
8
-
use crate::data::Quote;
9
-
10
-
mod rng {
11
-
use rand::SeedableRng;
12
-
13
-
pub struct PrngState {
14
-
rng: rand::rngs::SmallRng,
15
-
}
16
-
17
-
impl PrngState {
18
-
pub fn from_thread_rng() -> Self {
19
-
Self {
20
-
rng: rand::rngs::SmallRng::from_rng(&mut rand::rng()),
21
-
}
22
-
}
23
-
24
-
pub fn from_seed(seed: u64) -> Self {
25
-
Self {
26
-
rng: rand::rngs::SmallRng::seed_from_u64(seed),
27
-
}
28
-
}
29
-
30
-
pub fn shuffle_slice<T>(&mut self, slice: &mut [T]) {
31
-
use rand::seq::SliceRandom;
32
-
slice.shuffle(&mut self.rng);
33
-
}
34
-
}
35
-
36
-
mod test {
37
-
#[tokio::test]
38
-
async fn shuffle_slice() {
39
-
use super::*;
40
-
41
-
let mut data = vec![1, 2, 3, 4];
42
-
let mut rng = PrngState::from_thread_rng();
43
-
44
-
rng.shuffle_slice(&mut data);
45
-
println!("{:?}", data);
46
-
}
47
-
}
48
-
}
49
-
50
-
pub mod source {
51
-
use super::*;
52
-
53
-
// TODO: Should the quote source filters be
54
-
// generic over the exact manager implementation being used?
55
-
/// Message to request that a SourceManager source its quotes once again.
56
-
pub struct SourceQuotes;
57
-
pub type SourceReply = Result<Vec<Quote>, ()>;
58
-
59
-
/// Subtrait of Actor which specifically
60
-
/// denotes actors that can handle all relevant source messages.
61
-
pub trait SourceManager: Actor + Message<SourceQuotes, Reply = SourceReply> {}
62
-
63
-
impl<T> SourceManager for T where T: Message<SourceQuotes, Reply = SourceReply> {}
64
-
65
-
/// Implementation of [`SourceManager`] which sources quotes from a Vec
66
-
/// that it holds in memory, without accessing external services.
67
-
/// Its main purpose is to be used for testing.
68
-
#[derive(Actor)]
69
-
pub struct MemorySourceManager {
70
-
quotes: Vec<Quote>,
71
-
}
72
-
73
-
impl MemorySourceManager {
74
-
pub fn new(quotes: impl IntoIterator<Item = impl Into<Quote>>) -> Self {
75
-
Self {
76
-
quotes: quotes.into_iter().map(Into::into).collect(),
77
-
}
78
-
}
79
-
}
80
-
81
-
impl Message<SourceQuotes> for MemorySourceManager {
82
-
type Reply = SourceReply;
83
-
84
-
async fn handle(
85
-
&mut self,
86
-
_msg: SourceQuotes,
87
-
_ctx: &mut Context<Self, Self::Reply>,
88
-
) -> Self::Reply {
89
-
// We just clone the quotes we've been holding onto since startup
90
-
Ok(self.quotes.clone())
91
-
}
92
-
}
93
-
94
-
/// Uses a [QuoteFilter] to source quotes from the local filesystem
95
-
/// at the beginning of each cycle.
96
-
#[derive(Actor)]
97
-
pub struct FsFilterSourceManager {
98
-
filter: QuoteFilter,
99
-
}
100
-
101
-
impl FsFilterSourceManager {
102
-
pub fn new(filter: QuoteFilter) -> Self {
103
-
Self { filter }
104
-
}
105
-
}
106
-
107
-
impl Default for FsFilterSourceManager {
108
-
fn default() -> Self {
109
-
Self {
110
-
filter: QuoteFilter {
111
-
content: r".*".to_string(),
112
-
// TODO: Maybe make this a compile-time constant for debugging
113
-
path: "test/**/*.txt".to_string(),
114
-
_dates: vec![],
115
-
},
116
-
}
117
-
}
118
-
}
119
-
120
-
impl Message<SourceQuotes> for FsFilterSourceManager {
121
-
type Reply = SourceReply;
122
-
123
-
async fn handle(
124
-
&mut self,
125
-
_msg: SourceQuotes,
126
-
_ctx: &mut Context<Self, Self::Reply>,
127
-
) -> Self::Reply {
128
-
self.filter.read_files()
129
-
}
130
-
}
131
-
132
-
#[derive(Clone, Debug)]
133
-
pub struct QuoteFilter {
134
-
path: String,
135
-
content: String,
136
-
_dates: Vec<String>,
137
-
}
138
-
139
-
impl QuoteFilter {
140
-
// TODO: actually leverage async I/O
141
-
fn read_files(&self) -> Result<Vec<Quote>, ()> {
142
-
use glob::glob;
143
-
use grep::{regex, searcher::sinks};
144
-
145
-
let matcher = regex::RegexMatcher::new(&self.content).map_err(|_| ())?;
146
-
let mut searcher = grep::searcher::Searcher::new();
147
-
let mut results = Vec::new();
148
-
149
-
for file in glob(&self.path).map_err(|_| ())? {
150
-
let file = match file {
151
-
Ok(file) => file,
152
-
Err(_) => continue,
153
-
};
154
-
155
-
let mut matched = false;
156
-
let sink = sinks::Lossy(|_lnum, _line| {
157
-
matched = true;
158
-
Ok(false)
159
-
});
160
-
161
-
let search_result = searcher.search_path(&matcher, &file, sink);
162
-
if !matched || search_result.is_err() {
163
-
continue;
164
-
}
165
-
166
-
let contents = std::fs::read_to_string(file).map_err(|_| ())?;
167
-
results.push(contents.trim().into());
168
-
}
169
-
170
-
Ok(results)
171
-
}
172
-
}
173
-
}
174
-
175
-
pub mod queue {
176
-
use std::collections::VecDeque;
177
-
178
-
use super::*;
179
-
180
-
// Messages to interact with the quote queue
181
-
pub struct DequeueQuote;
182
-
pub type DequeueReply = Result<Option<Quote>, ()>;
183
-
184
-
pub struct EnqueueQuotes(pub Vec<Quote>);
185
-
pub type EnqueueReply = Result<(), ()>;
186
-
187
-
/// Subtrait of Actor which specifically
188
-
/// denotes actors that can handle all relevant queue messages.
189
-
pub trait QueueManager:
190
-
Actor
191
-
+ Message<EnqueueQuotes, Reply = EnqueueReply>
192
-
+ Message<DequeueQuote, Reply = DequeueReply>
193
-
{
194
-
}
195
-
196
-
impl<T> QueueManager for T where
197
-
T: Message<EnqueueQuotes, Reply = EnqueueReply>
198
-
+ Message<DequeueQuote, Reply = DequeueReply>
199
-
{
200
-
}
201
-
202
-
/// A basic implementation of an in-memory queue of quotes.
203
-
/// Its contents are *not* persisted across application restarts, so it
204
-
/// is only suited for testing purposes.
205
-
#[derive(Actor, Default)]
206
-
pub struct MemoryQueueStorage {
207
-
quotes: VecDeque<Quote>,
208
-
}
209
-
210
-
impl MemoryQueueStorage {
211
-
pub fn new() -> Self {
212
-
Self {
213
-
quotes: VecDeque::new(),
214
-
}
215
-
}
216
-
}
217
-
218
-
impl Message<EnqueueQuotes> for MemoryQueueStorage {
219
-
/// We only need to signal success or failure in this instance,
220
-
/// with no added metadata in either case.
221
-
type Reply = EnqueueReply;
222
-
223
-
async fn handle(
224
-
&mut self,
225
-
msg: EnqueueQuotes,
226
-
_ctx: &mut Context<Self, Self::Reply>,
227
-
) -> Self::Reply {
228
-
for q in msg.0 {
229
-
self.quotes.push_back(q);
230
-
}
231
-
232
-
Ok(())
233
-
}
234
-
}
235
-
236
-
impl Message<DequeueQuote> for MemoryQueueStorage {
237
-
type Reply = DequeueReply;
238
-
239
-
async fn handle(
240
-
&mut self,
241
-
_msg: DequeueQuote,
242
-
_ctx: &mut Context<Self, Self::Reply>,
243
-
) -> Self::Reply {
244
-
// Note: this can never fail, since the quotes are stored in memory
245
-
Ok(self.quotes.pop_front())
246
-
}
247
-
}
248
-
}
249
-
250
-
#[derive(Actor)]
251
-
pub struct QuoteCycle<S: source::SourceManager, Q: queue::QueueManager> {
252
-
rng: rng::PrngState,
253
-
source_manager: ActorRef<S>,
254
-
queue_manager: ActorRef<Q>,
255
-
}
256
-
257
-
impl<S: source::SourceManager, Q: queue::QueueManager> QuoteCycle<S, Q> {
258
-
pub fn new(
259
-
rng: rng::PrngState,
260
-
source_manager: ActorRef<S>,
261
-
queue_manager: ActorRef<Q>,
262
-
) -> Self {
263
-
Self {
264
-
rng,
265
-
source_manager,
266
-
queue_manager,
267
-
}
268
-
}
269
-
270
-
pub fn with_thread_rng(source_manager: ActorRef<S>, queue_manager: ActorRef<Q>) -> Self {
271
-
Self {
272
-
rng: rng::PrngState::from_thread_rng(),
273
-
source_manager,
274
-
queue_manager,
275
-
}
276
-
}
277
-
}
278
-
279
-
/// A message to [QuoteCycle] to fetch one more quote from its storage.
280
-
pub struct FetchQuote;
281
-
282
-
impl<S, Q> Message<FetchQuote> for QuoteCycle<S, Q>
283
-
where
284
-
S: source::SourceManager,
285
-
Q: queue::QueueManager,
286
-
{
287
-
type Reply = Result<Quote, ()>;
288
-
289
-
async fn handle(
290
-
&mut self,
291
-
_msg: FetchQuote,
292
-
_ctx: &mut Context<Self, Self::Reply>,
293
-
) -> Self::Reply {
294
-
// 1. We query our queue storage for the next quote
295
-
if let Some(next_quote) = self.queue_manager.ask(DequeueQuote).await.map_err(|_| ())? {
296
-
// if there is a quote, we simply return it and move on
297
-
return Ok(next_quote);
298
-
}
299
-
300
-
// 2. Otherwise, we must repopulate the queue through our source
301
-
let mut refreshed_quotes = self
302
-
.source_manager
303
-
.ask(SourceQuotes)
304
-
.await
305
-
.map_err(|_| ())?;
306
-
307
-
// 3. We shuffle the newly-sourced quotes
308
-
self.rng.shuffle_slice(&mut refreshed_quotes);
309
-
let refreshed_quotes = refreshed_quotes; // No longer mutable
310
-
311
-
// TODO: Perhaps we should assert that the new quotes are non-empty?
312
-
// 4. We enqueue the newly-sourced quotes...
313
-
self.queue_manager
314
-
.ask(EnqueueQuotes(refreshed_quotes))
315
-
.await
316
-
.map_err(|_| ())?;
317
-
318
-
// 5. and, finally, we return the first among them.
319
-
match self.queue_manager.ask(DequeueQuote).await {
320
-
Ok(Some(q)) => Ok(q),
321
-
Ok(None) => panic!("Newly-enqueued quotes should never be empty"),
322
-
Err(_) => Err(()),
323
-
}
324
-
}
325
-
}
326
-
327
-
mod test {
328
-
#[tokio::test]
329
-
async fn memory_queue() {
330
-
use super::Quote;
331
-
use super::queue::*;
332
-
use kameo::prelude::*;
333
-
334
-
let queue_manager = MemoryQueueStorage::spawn(MemoryQueueStorage::new());
335
-
336
-
let sample_quotes = ["Test no.1", "Test no.2", "Test no.3"];
337
-
338
-
queue_manager
339
-
.ask(EnqueueQuotes(
340
-
sample_quotes.iter().cloned().map(Quote::from).collect(),
341
-
))
342
-
.await
343
-
.expect("In-memory quote queue storage should be valid for insertion");
344
-
345
-
for text in sample_quotes.iter() {
346
-
assert_eq!(
347
-
*text,
348
-
queue_manager
349
-
.ask(DequeueQuote)
350
-
.await
351
-
.expect("In-memory queue storage should never panic on dequeue")
352
-
.expect("In-memory queue storage should never be initialized as empty")
353
-
.get()
354
-
);
355
-
}
356
-
}
357
-
358
-
#[tokio::test]
359
-
async fn memory_source() {
360
-
use super::source::*;
361
-
use kameo::prelude::*;
362
-
363
-
let sample_quotes = ["Minie", "Miney", "Moe", "and", "some", "more"];
364
-
365
-
let source_manager = MemorySourceManager::spawn(MemorySourceManager::new(sample_quotes));
366
-
367
-
let quotes = source_manager
368
-
.ask(SourceQuotes)
369
-
.await
370
-
.expect("In-memory quote queue storage should be valid for insertion");
371
-
372
-
assert_eq!(
373
-
sample_quotes.as_slice(),
374
-
quotes
375
-
.into_iter()
376
-
// Since [Quote] doesn't implement any Equality trait,
377
-
// we map the strings into quotes instead
378
-
.map(String::from)
379
-
.collect::<Vec<_>>()
380
-
.as_slice(),
381
-
);
382
-
}
383
-
384
-
#[tokio::test]
385
-
async fn memory_cycle() {
386
-
use std::{collections::HashMap, ops::AddAssign};
387
-
388
-
use super::FetchQuote;
389
-
use super::QuoteCycle;
390
-
use super::queue::*;
391
-
use super::source::*;
392
-
use kameo::prelude::*;
393
-
394
-
let sample_quotes = ["Minie", "Miney", "Moe"];
395
-
let cycle = {
396
-
let source = MemorySourceManager::spawn(MemorySourceManager::new(sample_quotes));
397
-
let queue = MemoryQueueStorage::spawn(MemoryQueueStorage::new());
398
-
399
-
QuoteCycle::spawn(QuoteCycle::with_thread_rng(source, queue))
400
-
};
401
-
402
-
// We loop over `sample_quotes` twice to simulate the queue being exhausted fully, then re-sourced
403
-
// Since the `cycle` manager will shuffle the quote sequence, we will verify that each
404
-
// quote appears *exactly* `LOOPS` times throughout these iterations.
405
-
const LOOPS: usize = 3;
406
-
let mut quote_counts = HashMap::new();
407
-
for _ in 0..(sample_quotes.len() * LOOPS) {
408
-
let next_quote = cycle.ask(FetchQuote).await.unwrap();
409
-
quote_counts
410
-
.entry(next_quote.get().to_owned())
411
-
.or_insert(0)
412
-
.add_assign(1);
413
-
}
414
-
415
-
let quote_counts = quote_counts; // no longer mut
416
-
assert!(
417
-
// Note: technically speaking, different quotes could contain equivalent strings,
418
-
// which would make this test fail; a "more proper" invariant check would ensure
419
-
// verify that all counts are a multiple of the amount of times `sample_quotes` was chained,
420
-
// and that the sum of all counts equals the total number of times a `FetchQuote` message was sent.
421
-
quote_counts.into_values().into_iter().all(|c| c == LOOPS)
422
-
);
423
-
}
424
-
}