Source code for my personal quote bot project.

Compare changes

Choose any two refs to compare.

Changed files
+1527 -1007
quotes
deltarune
xenoblade-chronicles-3
src
+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
··· 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
··· 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
··· 1 + WELL DONE. 2 + 3 + THE HALFWAY 4 + MARK HAS 5 + BEEN ATTAINED. 6 + 7 + BUT 8 + IT 9 + IS STILL 10 + WAITING. 11 + 12 + HOW MUCH 13 + LONGER NOW...? 14 + 15 + MY 16 + DELTARUNE.
+4
quotes/deltarune/alvin_asgore_pray.txt
··· 1 + * Ah, your father? He typically comes here at night. 2 + * He prays much for you, Kris... for Asriel, Toriel, Rudy... and his flowers, too. 3 + * You should join your mother in praying for him, as well. 4 + * The poor man. It seems the Angel has made a difficult path for him...
+1
quotes/deltarune/alvin_shelter.txt
··· 1 + * Kris... stay away from the shelter.
+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
··· 1 + * We're almost there, aren't we, old friend? 2 + * This time for sure... Tori will finally see. 3 + * ... see what really happened. 4 + * ... that I just wanted to... protect everyone... 5 + * And this time, she'll have to believe me. 6 + * ... they all will.
+5
quotes/deltarune/asgore_they_will_all_2.txt
··· 1 + * Then... 2 + * We'll all be a happy family again... won't we? 3 + * ... 4 + * It sure is beautiful, isn't it...? 5 + * ... this black shard.
+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
··· 1 + * Kris... are you there? 2 + * ... Then, I'll just leave a message. 3 + * You are going to the festival tomorrow, aren't you? 4 + * ... I just wanted to confirm. 5 + * Noelle said... 6 + * She's really looking forward to seeing you.
+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
··· 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
··· 1 + Even then, what were they thinking about me? 2 + Maybe they weren't thinking about me at all. 3 + They didn't have a piano at their house, so they probably just came over to use mine.
+4
quotes/deltarune/concert_just_for_me_4.txt
··· 1 + Even then, with my eyes open, there were times when I wasn't even sure if we were friends. 2 + 3 + But when I closed my eyes, 4 + it felt like a concert just for me.
+3
quotes/deltarune/d_how_did_i.txt
··· 1 + What is this place, anyway...? 2 + How did I get here? 3 + ... as if I haven't asked that question a billion times already.
+8
quotes/deltarune/d_that_nightmare.txt
··· 1 + I... 2 + I always did have that nightmare. 3 + Walking into the darkness, 4 + With the light shining from the doorway... 5 + Then the door slams behind me. 6 + And everything goes black. 7 + ... is this that nightmare? 8 + ... or was everything else a dream?
+9
quotes/deltarune/felt_it_shining.txt
··· 1 + INCREDIBLE. 2 + 3 + I FELT IT THERE 4 + 5 + SHINING. 6 + 7 + YOUR POWER. 8 + 9 + A LITTLE FURTHER.
+9
quotes/deltarune/fountain_glowing_smile.txt
··· 1 + * (Susie's smile, which 2 + seemed to defeat everything) 3 + * (Shined on in front of you, 4 + and for a moment) 5 + * (It was as if all 6 + the darkness had been 7 + blown away.) 8 + * (As if in imitation, 9 + your SOUL began to glow...)
+6
quotes/deltarune/gerson_as_youre_green.txt
··· 1 + As long as you're GREEN, 2 + you can't run away! 3 + 4 + Unless you FACE DANGER 5 + HEAD ON... These bullets 6 + will do you in!
+2
quotes/deltarune/gerson_bench_dream.txt
··· 1 + * "Throughout my career, some of my best ideas came from dreams." 2 + * "Take a rest here. If anyone asks -- you're writing!"
+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
··· 1 + But you, my dear? 2 + I see a story lit 3 + up in your eyes... 4 + 5 + Burnin' bright... 6 + Burnin' black. 7 + Burnin' up 8 + everything.
+7
quotes/deltarune/gerson_chapter_1.txt
··· 1 + Chapter 1 2 + The March of 3 + the Dark King. 4 + 5 + The heroes defeat 6 + the king and 7 + stop the dragon.
+7
quotes/deltarune/gerson_chapter_2.txt
··· 1 + Chapter 2 2 + The City of 3 + the Shining. 4 + 5 + The heroes do 6 + battle in chariots 7 + to save the Queen.
+8
quotes/deltarune/gerson_chapter_3.txt
··· 1 + Chapter 3, The 2 + Isles of 3 + Northernlight. 4 + 5 + The heroes travel 6 + among the islands 7 + and catch a glimpse 8 + of a lost land.
+7
quotes/deltarune/gerson_chapter_4.txt
··· 1 + Chapter 4, The 2 + Trials of the 3 + Holy Hammer. 4 + 5 + A great smith gives 6 + the heroes a 7 + terrible weapon.
+11
quotes/deltarune/gerson_chapter_5.txt
··· 1 + Chapter 5, The 2 + Field of Pink 3 + and Gold. 4 + 5 + The vast garden is 6 + charred in an inferno 7 + of jealousy. 8 + 9 + ...What happens next? 10 + 11 + Geheheh! Who knows?
+9
quotes/deltarune/gerson_choose_eternity.txt
··· 1 + Gah ha ha ha 2 + ha ha ha!!! Well 3 + ain't that somethin'. 4 + 5 + At this late hour, 6 + with the bells ringing 7 + out their justice. 8 + 9 + You choose eternity.
+5
quotes/deltarune/gerson_count_on.txt
··· 1 + * (You sipped the tea. ... it's very sweet.) 2 + * (You drank it slowly, feeling a warmth spread across your body...) 3 + * (Until, only a thin, sickly syrup was left cold at the bottom.) 4 + 5 + * That's the nice thing about tea. You can always count on it, geheheh...
+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
··· 1 + The bell's ringin' now. 2 + Seems we've reached 3 + the final rounds. 4 + 5 + So... how do you 6 + think it all ends? 7 + 8 + No, how do you want 9 + it to end?
+9
quotes/deltarune/gerson_ocean_of_ink.txt
··· 1 + Miss, hold on 2 + dearly to that thought. 3 + 4 + For the tides of fate 5 + are drawing close. 6 + 7 + And soon, an ocean 8 + of ink shall wash 9 + across the pages.
+8
quotes/deltarune/gerson_old_tale.txt
··· 1 + Did you ever hear 2 + that old tale? 3 + 4 + Yes, the old tale, 5 + based upon the 6 + prophecy... 7 + 8 + Lord of the Hammer.
+9
quotes/deltarune/gerson_pick_up_the_pen.txt
··· 1 + The ones who could write 2 + the next, the youth, 3 + the pen was lying 4 + there for them to 5 + pick up. 6 + 7 + To make the next page. 8 + 9 + ...they never did.
+9
quotes/deltarune/gerson_susie_pen_of_hope_1.txt
··· 1 + But... there is one 2 + thing that can 3 + overwrite the dark. 4 + 5 + A white pen, 6 + known as hope. 7 + 8 + Miss! I believe... 9 + this is what you hold.
+9
quotes/deltarune/gerson_susie_pen_of_hope_2.txt
··· 1 + "Me? Nah, Kris 2 + has the pen. 3 + 4 + My weapon's like a 5 + hairbrush or something." 6 + 7 + "Geh-hahahahaha! 8 + Is that so? 9 + Is that SO???"
+7
quotes/deltarune/gerson_susie_quitting_1.txt
··· 1 + * ... Shouldn't you be helping that pumpkin fella? 2 + 3 + * Nah, Ralsei's got it covered. I... I quit. 4 + 5 + * Quit? Quit what? 6 + 7 + * You know. Healing. That green sparkle thing.
+6
quotes/deltarune/gerson_susie_quitting_2.txt
··· 1 + * Hmm. So you don't like doing it? 2 + 3 + * Well, uh... I dunno, actually. 4 + * At first, I was really... I was really excited I could do it. 5 + * But like... I sort of realized... 6 + * I just suck at it. Heh.
+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
··· 1 + * Hm. Is that so. 2 + * Didn't take you for a purple bellied coward. 3 + 4 + * Coward...? Hey, what do you mean by that!? 5 + 6 + * You're afraid of making some little green sparkles.
+6
quotes/deltarune/gerson_susie_the_prophecy.txt
··· 1 + * Oopsy daisy! Darn my clumsiness, geheh... 2 + 3 + * Damn it! You broke it again, didn't you...? 4 + 5 + * Well, we were following that Prophecy so closely... 6 + * I couldn't see quite where I was going, geh-heh!!
+13
quotes/deltarune/gerson_swallowed_up.txt
··· 1 + There was only one more 2 + chapter... After that, 3 + 4 + It all stopped. 5 + 6 + That next book, it 7 + never did get written. 8 + 9 + The story, it became so 10 + grand, so overwhelming, 11 + 12 + Some say it swallowed up 13 + the author himself.
+3
quotes/deltarune/gerson_the_motions_1.txt
··· 1 + * ... You're really just going through the motions, huh? 2 + * Something tough's going on with you. ... I can tell that sorta thing. 3 + * And now, you still gotta go through your day. Still gotta talk, fight, do homework...
+3
quotes/deltarune/gerson_the_motions_2.txt
··· 1 + * I bet it all feels pointless, don't it. It's almost an insult that the earth's still turnin'. 2 + * ... but don't you give up. Even if you gotta do those motions... 3 + * Trying to find something, anything in them, that means a little something... ya hear?
+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
··· 1 + * (It seems the way is blocked.) 2 + * (You put out your hand...) 3 + * (And, just out of view, just perfectly out of view, 4 + * Another hand pressed against yours 5 + * And pushed you back.)
+2
quotes/deltarune/knight_beat.txt
··· 1 + * Kris coughed. 2 + * The enemy slowly tilted its head...
+1
quotes/deltarune/knight_heart.txt
··· 1 + * Your heartbeat becomes twisted.
+4
quotes/deltarune/knight_kris_analysis.txt
··· 1 + * Kris analyzed the enemy! 2 + 3 + * But Kris 4 + couldn't learn anything.
+3
quotes/deltarune/knight_kris_breath_quicken.txt
··· 1 + * Kris held their breath. 2 + * Their heartbeat quickened. 3 + * The SOUL now moves faster.
+3
quotes/deltarune/knight_kris_breath_smile.txt
··· 1 + * Kris held their breath... 2 + * Kris smiled. 3 + * Nothing happened.
+1
quotes/deltarune/knight_kris_kneel.txt
··· 1 + * Kris kneeled in silence.
+1
quotes/deltarune/knight_ralsei_swoon.txt
··· 1 + * Ralsei became a pile of fluff.
+7
quotes/deltarune/knight_ralsei_talk.txt
··· 1 + * Ralsei tried talking... 2 + 3 + * Please... please, don't do this... 4 + * If the Roaring happens, then... then... 5 + * Please... stop...! 6 + 7 + * (... but nothing happened.)
+1
quotes/deltarune/knight_roaring.txt
··· 1 + * The Roaring Knight appeared.
+1
quotes/deltarune/knight_susie_swoon.txt
··· 1 + * Susie was hurt and beaten.
+7
quotes/deltarune/knight_susie_talk.txt
··· 1 + * I don't know what the hell you are, but... 2 + * Leave Toriel alone! You hear me!? 3 + * ... 4 + * ... Fine, you don't wanna listen? 5 + * Then we'll just. Have to do things the hard way. 6 + 7 + * (Susie will not ACT any more.)
+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
··· 1 + * (You ran water over your hands, and dried them.) 2 + * (Between your fingers, a faint grey crease glittered stubbornly.) 3 + * (... You can never wash it all away.)
+3
quotes/deltarune/kris_pencil_1.txt
··· 1 + * (Kris picked up a pencil.) 2 + * (They began by sketching what they had seen behind the tree.) 3 + * (But, as they sketched, the line began to err, as if the pencil had lost its way.)
+3
quotes/deltarune/kris_pencil_2.txt
··· 1 + * (So they started again, drawing over the lines they had already made.) 2 + * (Many times, they would suddenly halt, or the pencil drifted into a new drawing...) 3 + * (So, they started over, over, over again.)
+2
quotes/deltarune/kris_pencil_3.txt
··· 1 + * (Through many repetitions, their movements became faster and smoother...) 2 + * (... until without looking, they had completed a sketch in a single, wild line.)
+3
quotes/deltarune/kris_pencil_4.txt
··· 1 + * (...) 2 + * (In the end, as a result of layering the sketches on top of each other...) 3 + * (... The surface of the canvas was nothing more than a monochrome smear.)
+2
quotes/deltarune/kris_susie_be_okay.txt
··· 1 + * (It's hard to put into words, but for some reason, seeing Susie next to you...) 2 + * (... You felt like, whatever you were, for just right now, it might be okay.)
+5
quotes/deltarune/kris_taken_enough.txt
··· 1 + * There's 5 dollars in your brother's drawer. Take it? 2 + 3 + Do not 4 + 5 + * (You have already taken enough.)
+5
quotes/deltarune/kris_unseen_shape.txt
··· 1 + * (...) 2 + * (Kris got up. It seemed the painting was complete.) 3 + * (...) 4 + * (But, their hand, smudged with graphite...) 5 + * (... was still moving, as if to trace some unseen shape.)
+7
quotes/deltarune/noelle_alone_with_kris.txt
··· 1 + * To be honest, I've kind of been waiting for... 2 + * A chance for us to be alone all day. 3 + * ... 4 + * There's... something I want to talk to you about, Kris. 5 + * ... 6 + * I want to talk to you about... 7 + * ... last night.
+4
quotes/deltarune/noelle_everything_important_1.txt
··· 1 + * Huh...? You mean, um... how I... got locked out of my house? 2 + * That's why I... well, I used to end up at your place sometimes. 3 + * Y'know, you'd help me... faha, break into my own room. 4 + * Even then, my mom... never gave your family a spare key.
+4
quotes/deltarune/noelle_everything_important_2.txt
··· 1 + * She never even kept anything at City Hall, either. 2 + * No keys, no documents, no... handmade gifts, anything. 3 + * Everything important... she always... 4 + * ... keeps locked down at home.
+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
··· 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
··· 1 + * ? 2 + * Kris, it's okay! It's just us! 3 + * You just said not to mention any of that... 4 + * Because it'd get... heard. 5 + * ... Kris... Kris, it's okay, it's just us.
+4
quotes/deltarune/noelle_kris_piano.txt
··· 1 + * H... huh? Thanks! Where'd that come from? 2 + * I don't think you've ever... complimented me, before. Fahaha. 3 + * You know, if you wanted to join the choir, you could... 4 + * I always missed, um... liked hearing you play the piano.
+3
quotes/deltarune/noelle_kris_talk_later.txt
··· 1 + * ... Kris? You came over to... check on me? 2 + * That's really nice of you, I... 3 + * ... Do you... think I could talk you later?
+5
quotes/deltarune/noelle_kris_voice_1.txt
··· 1 + * That's when I felt it. 2 + * A hand on my wrist. 3 + * I froze. 4 + * And then I heard your voice. 5 + * Kris's voice.
+4
quotes/deltarune/noelle_kris_voice_2A.txt
··· 1 + * You said, sorry for taking your watch, Noelle. 2 + * You said, it was all a stupid prank. 3 + * You said, Berdly is going to get better. 4 + * And you put the watch back on my wrist.
+4
quotes/deltarune/noelle_kris_voice_2B.txt
··· 1 + * You said, sorry for being weird at the hospital, Noelle. 2 + * You said, sorry for being weird yesterday, Noelle. 3 + * You said, it was all a stupid prank. 4 + * You said, Berdly was going to get better.
+5
quotes/deltarune/noelle_kris_your_voice.txt
··· 1 + * ... Kris, your voice, your deadpan, mumbly voice... 2 + * Even if you sounded kind of weak and shaky... 3 + * I don't know why, but it felt so long since I heard it. 4 + * I even started sniffling! Haha! 5 + * Kris, Kris, you're back...!
+7
quotes/deltarune/noelle_nightmare.txt
··· 1 + * Last night, I... I couldn't sleep. 2 + * Every time I closed my eyes... 3 + * All I could see 4 + was snow. 5 + * I was terrified, Kris. 6 + * Terrified it was all real. 7 + * I went out on the porch to breathe, and...
+4
quotes/deltarune/plaque_gerson_hope.txt
··· 1 + * (It's a plaque bearing the words of a famous writer.) 2 + * ("Hope comes to those who believe. And for those that cannot...") 3 + * ("... May our hope shine so brightly...") 4 + * ("... That they, too, may keep shelter from the dark.")
+4
quotes/deltarune/ralsei_dark_not_real.txt
··· 1 + In other words... when the lights come back on... 2 + This world is no longer "real". 3 + The places you go, the people you meet... none of them are. 4 + Even I, the one telling you this... I too, am...
+10
quotes/deltarune/ralsei_dont_need_a_room.txt
··· 1 + * It's... 2 + * It's... empty...? 3 + 4 + * ... 5 + * My own room? ... I... I don't need a room! 6 + * ... 7 + * ...at least... that's what I thought. 8 + * So... So I never... 9 + 10 + * ... You... you LIVE here. Why do WE have rooms and not you?
+5
quotes/deltarune/ralsei_forget_us_1.txt
··· 1 + * It's funny, I was... overjoyed to make friends. 2 + * Then, selfishly, I... started feeling sad, too. 3 + * Sad I couldn't be your friend in real life, Susie. 4 + * That I CAN'T do your homework. That I can't go with you. 5 + * That I... can't be there to help you when you really need me.
+3
quotes/deltarune/ralsei_forget_us_2.txt
··· 1 + * I'm sorry. That's why I... If, you know, me, Lancer... 2 + * We can't do it all. So... if you ever feel like we're not enough... 3 + * Just forget about us and make some real friends, okay?
+7
quotes/deltarune/ralsei_friendship_far_greater.txt
··· 1 + * I 2 + * Being with you two... 3 + * I... 4 + * The more I'm with you two, I realize... 5 + * Friendship... isn't just... 6 + * About being happy all the time. 7 + * It's something far, far greater than I could've imagined...
+2
quotes/deltarune/ralsei_healing_the_outside.txt
··· 1 + * I was wondering why my healing wasn't working... 2 + * I suppose... I was only healing the outside.
+7
quotes/deltarune/ralsei_i_am_smiling.txt
··· 1 + * ... 2 + * S-sorry... don't look at me... I... 3 + * I don't mean to always... 4 + * Make everything about me... 5 + * I'm sorry. I... 6 + * Everything's OK, Kris. Look, I... 7 + * I'm smiling!
+5
quotes/deltarune/ralsei_i_do_for_you_1.txt
··· 1 + * Kris, Susie is so nice... 2 + * It... makes my heart hurt. 3 + * I don't really have any hobbies, or interests. 4 + * Baking, sewing, singing... those are all just... 5 + * Things I thought to do... for you two.
+7
quotes/deltarune/ralsei_i_do_for_you_2.txt
··· 1 + * ... 2 + * But recently... 3 + * I'm starting to feel like... 4 + * Like I'm developing my own opinions. 5 + * My own likes. 6 + * My own dislikes. 7 + * My own desires. My own... fears.
+8
quotes/deltarune/ralsei_i_just.txt
··· 1 + * I just 2 + * I thought if I said something different 3 + * If we did something different 4 + * If we were just kind enough 5 + * Perhaps by the time we got here... it would change. 6 + * But... but no matter what we do... 7 + * Our fate... 8 + * ... is already decided.
+3
quotes/deltarune/ralsei_knight_check_susie.txt
··· 1 + * Kris, I... I'll be fine. 2 + * But... if I'm hurt this badly, then Susie must be... 3 + * Kris... you'll... make sure she's alright, won't you?
+7
quotes/deltarune/ralsei_knight_the_prophecy.txt
··· 1 + * Somehow, I thought we... had won... for a moment... 2 + * That... we would be able to end the battle... here and now... 3 + * ... you... 4 + * You were so brave... and yet... 5 + * In the end... our struggle... 6 + * It's only beginning, isn't it...? 7 + * ... Isn't it, Kris...?
+4
quotes/deltarune/ralsei_knight_will_be_fine.txt
··· 1 + * K... Kris... I... 2 + * ... 3 + * I'll be... fine. 4 + * You do... whatever you need to.
+3
quotes/deltarune/ralsei_kris_thank_susie.txt
··· 1 + * Kris, at least as long as you're there... 2 + * I can know she won't be lonely, or hungry. 3 + * ... Thank you, Kris.
+3
quotes/deltarune/ralsei_kris_you_were_cool.txt
··· 1 + * Umm... Kris? 2 + * You were 3 + * You were cool when you played the piano earlier!
+7
quotes/deltarune/ralsei_let_it_just_be.txt
··· 1 + * I didn't ask to know, but I do. 2 + * I... I'm sorry if it creeps you out! 3 + * I'm sorry if I... don't tell you everything. 4 + * It's just... sometimes, knowing things... 5 + * It hurts. 6 + * And if anyone's going to hurt... 7 + * Let it... just be me, okay?
+6
quotes/deltarune/ralsei_my_purpose.txt
··· 1 + * Making you two happy... 2 + * Is... my purpose. 3 + * I... 4 + * If I think about it... 5 + * There's no reason... 6 + * For me... to have... anything.
+5
quotes/deltarune/ralsei_noelle_bad.txt
··· 1 + * N... Noelle? 2 + * Kris, I... 3 + * I think that's... 4 + * ... 5 + * A bad choice.
+4
quotes/deltarune/ralsei_not_filling.txt
··· 1 + * I realized... being friends is... a responsibility. 2 + * A responsibility... I can't... fully complete. 3 + * The cakes I bake, the cotton candy we eat, it might be yummy. 4 + * ... But, when you return to the Light World, you won't be full.
+3
quotes/deltarune/ralsei_pain_i_can_take.txt
··· 1 + * Susie... Kris... as long as... 2 + * There's any pain I can take in your place... 3 + * I'll be there, okay?
+8
quotes/deltarune/ralsei_scared.txt
··· 1 + * K... Kris? Kris, you... 2 + * Haha... 3 + * I... I'm scared... 4 + * It's getting harder and harder 5 + * To just stand there smiling all the time. 6 + * To be the one 7 + * Pretending I'm not afraid, too. 8 + * But
+10
quotes/deltarune/ralsei_should_i_even.txt
··· 1 + * ... please... tell me... 2 + * Should I... 3 + * Should a... Darkner... be feeling like this? 4 + 5 + Please be 6 + yourself 7 + 8 + * ... I... 9 + * I... 10 + * K... Kris, hearing that from you, I might just...
+8
quotes/deltarune/ralsei_something_even_worse.txt
··· 1 + * I... I want to. I want to believe again. 2 + * I want to believe... it can change! 3 + * That there isn't just one ending! 4 + * But... 5 + * What would that be...? 6 + * If there was something else, what would it be...? 7 + * And how do we know 8 + * It wouldn't be something even worse?
+7
quotes/deltarune/ralsei_sorry_i_say_nothing.txt
··· 1 + * I 2 + * I'm 3 + * I'm sorry, I 4 + * I know so many things, I 5 + * I know so many things I, don't know. When to say them. 6 + * I don't want... to say them. To... worry... you two. 7 + * So I end up... I end up not saying... anything.
+8
quotes/deltarune/ralsei_susie_DONT_PLEASE.txt
··· 1 + * No... 2 + * SUSIE, STOP!!! 3 + * SUSIE, DON'T LOOK-- 4 + * S-Susie, I'm sorry, I-- 5 + * I, I'm sorry, I-- 6 + * I'm sorry, I-- 7 + 8 + * Shut up.
+9
quotes/deltarune/ralsei_susie_changed.txt
··· 1 + * Aww, Susie! 2 + * You've changed so tremendously since I met you. 3 + * No, rather, you've finally just been able to... 4 + * Be... yourself. 5 + * I feel... haha, I just feel so proud of you, Susie. 6 + 7 + * Hey, you've changed too, dude. 8 + 9 + * I... I have?
+3
quotes/deltarune/ralsei_susie_hog_all_the.txt
··· 1 + * S... Susie...! What are you doing...!? 2 + 3 + * ... Like I'm going to let Kris hog all the bullets.
+9
quotes/deltarune/ralsei_susie_never_cake.txt
··· 1 + * Oh! Th... this is really yummy! 2 + 3 + * Why the heck are you so surprised? 4 + 5 + * I... I've actually never eaten cake before... 6 + * Since, I was making it for you two, after all. 7 + * ... I can't believe it tastes this good! 8 + 9 + * Haha, dude, you're such a weirdo sometimes.
+6
quotes/deltarune/ralsei_susie_noogie_to_cry.txt
··· 1 + * Susie...! 2 + 3 + * Uhh, first noogie to make someone cry? 4 + 5 + * H-hahaha! Sorry! 6 + * I'm just... glad you're friends with me.
+9
quotes/deltarune/ralsei_susie_not_the_old_man_1.txt
··· 1 + * Susie... 2 + * That wasn't the old man. 3 + 4 + * H... huh? 5 + 6 + * One of the statues in his shape from his study... 7 + * It must have begun moving when you made this Dark World. 8 + 9 + * A... a statue...? Then... the old man...?
+8
quotes/deltarune/ralsei_susie_not_the_old_man_2.txt
··· 1 + * He's... not in this world, sadly. 2 + * Darkners patterned after the memory of a loved one... 3 + * Tend to only appear in very specific darknesses. 4 + * ... even if we tried to bring him to Castle Town, he... 5 + 6 + * THEN WHY DIDN'T YOU SAY SOMETHING EARLIER!? 7 + 8 + * H... huh?
+5
quotes/deltarune/ralsei_susie_so_kind_how.txt
··· 1 + * Susie... 2 + * ... she's... 3 + * She's a really really nice person, Kris. 4 + * How can she be so kind...? 5 + * ... how?
+10
quotes/deltarune/ralsei_susie_sounding_weird.txt
··· 1 + * I'll be more honest, Susie. I promise. 2 + * Even if it means... sounding... weird. 3 + 4 + * ... Ralsei? 5 + * You couldn't get any weirder if you tried. 6 + 7 + * You too, Susie! 8 + 9 + * ... uh, thanks. 10 + * ... well, let's get out of here, Kris.
+7
quotes/deltarune/ralsei_susies_infectuous_hope.txt
··· 1 + * I... I want to. I want to believe again. 2 + * I want to believe... it can change! 3 + * That there isn't just one ending! 4 + * Susie's hope... Her naรฏve hope... 5 + * It's... infectious, isn't it, Kris? 6 + * ... So, until we see fate with our own eyes... 7 + * Let's believe, too.
+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
··· 1 + * So, there's nothing to be ashamed of. 2 + * If... that ever comes to an end. 3 + * Darkners... all become obsolete eventually. 4 + * But we aren't "real", Tenna. 5 + * We shouldn't make Lightners worry about what happens to us. 6 + * It'd just... make them unhappy, wouldn't it?
+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
··· 1 + * A Titan. 2 + * It's the fear-of-dark. 3 + * It's the bump-in-the-night. 4 + * It's the shadow of the backside of your mind. 5 + * It has no consciousness. 6 + * It only exists to destroy.
+4
quotes/deltarune/ralsei_too_much_fun.txt
··· 1 + * Huh? How I'm doing? Umm... 2 + * Those games were a lot of fun, weren't they? 3 + * I've never played such a game before... I kind of... 4 + * I kind of got so wrapped up...
+4
quotes/deltarune/ramb_i_saw_you.txt
··· 1 + * Kris... I'm an ignorant plug. Can't say I know your motivation. 2 + * But... I saw. I saw you make it, you know. The Fountain. 3 + * Could it be... you just wanted to have fun again? 4 + * Heh heh heh... some REAL fun, just like the old days.
+3
quotes/deltarune/ramb_just_play_your_games.txt
··· 1 + * Your mother. Your poor mother, what will happen if she wakes up...? 2 + * ... ah, what am I doing? A power strip, giving YOU a lecture... 3 + * ... just play your games, luv.
+4
quotes/deltarune/ramb_was_it_even.txt
··· 1 + * ... 2 + * That's been my purpose, y'know. Letting you play your games. 3 + * But, heh, now that the games are almost over... 4 + * Makes you think, was it really all such a good idea?
+2
quotes/deltarune/ramb_whyd_you_do.txt
··· 1 + * Kris... if not for fun, why'd you do it? 2 + * ... mum's the word, innit. Fair play, fair play.
+5
quotes/deltarune/rudy_carol_frozen.txt
··· 1 + * (Like, when Noelle was a kid...) 2 + * (She made paper snowflakes, for Carol to hang up at work...) 3 + * (But Carol just... wouldn't take 'em there.) 4 + * (Because she wanted to keep the damn things safe.) 5 + * (She preserved 'em, and stuck 'em up on the wall at home...)
+2
quotes/deltarune/rudy_locked_out.txt
··· 1 + * (Noelle... wasn't stuck outside the gate again, was she, Kris...?) 2 + * (... Carol... damn, I keep telling her to keep a key at City Hall...)
+3
quotes/deltarune/rudy_noelle_snow_angels.txt
··· 1 + * (... course, you know Noelle, she had to cry over that.) 2 + * (Until she went and made snow angels with you guys...) 3 + * (... After that, she forgot she was sad at all.)
+2
quotes/deltarune/savepoint_actors.txt
··· 1 + * (After the performance, when all the actors have gone home...) 2 + * (You feel a sense of emptiness.)
+2
quotes/deltarune/savepoint_cold.txt
··· 1 + * (A draft blows, and you feel the cold night's presence.) 2 + * (Still, this never bothered you. In fact...)
+2
quotes/deltarune/savepoint_wind.txt
··· 1 + * (The wind blows fiercely, as if it fears the night itself.) 2 + * (The final act draws near.)
+7
quotes/deltarune/shall_we_hasten.txt
··· 1 + IF YOU ARE SO 2 + DETERMINED 3 + TO TRY ONCE MORE 4 + 5 + THEN 6 + 7 + SHALL WE HASTEN?
+7
quotes/deltarune/shelter_lock.txt
··· 1 + * (On the shelter was a strange panel.) 2 + * (It looked like a digital security lock...) 3 + * (There were three entry fields, and three symbols besides them.) 4 + * (One had a pine tree beside it.) 5 + * (One had an officer's badge beside it.) 6 + 7 + * (And one had the DELTA RUNE.)
+5
quotes/deltarune/spamtom_mere_puppet.txt
··· 1 + * It seems after all I couldn't be anything more than a simple puppet. 2 + * But you three... You're strong. 3 + * With a power like that... 4 + * Maybe you three can break your own strings. 5 + * Let me become your strength.
+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
··· 1 + * HAHAHA! Why did you photoshop Ralsei to be... 2 + * ... What? That's your brother? 3 + * ... Yeah, now that I look at it, he looks, uh, different. 4 + * I SAID I get it! You can stop pointing out the differences!
+5
quotes/deltarune/susie_be_nice_to_mom.txt
··· 1 + * Heh, what are those little horns you're wearing? 2 + * It's almost like you were trying to... 3 + * ... 4 + * ... heh. 5 + * ... Kris, you better be nice to your mom.
+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
··· 1 + * I felt like you. 2 + * I felt like a broken toy no one wanted. 3 + * ... 4 + * Kris's mom saw me. Toriel.
+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
··· 1 + * And recently. That hope's been growing. 2 + * Because... now I have friends. 3 + * Real friends. 4 + * Friends I wanna keep. 5 + * Kris. Ralsei. Lancer... Toriel, too. 6 + * Even though I thought I was broken... 7 + * I just needed to find the right people, y'know?
+5
quotes/deltarune/susie_broken_toy_5.txt
··· 1 + * So... 2 + * Don't worry, dude. 3 + * Someone out there wants you. Promise. 4 + * Kris, you'll help me ask around town, right? 5 + * Someone's gotta want a TV.
+11
quotes/deltarune/susie_choose_eternity.txt
··· 1 + An ending, huh... 2 + 3 + ...If I could chooseโ€ฆ 4 + I guess... 5 + 6 + I wouldn't have an 7 + ending! 8 + 9 + It's just better if 10 + stuff just... 11 + goes on forever, right?
+3
quotes/deltarune/susie_dark_world_real.txt
··· 1 + * ... But yeah, maybe it's not real. The dark world. 2 + * (I just wish Ralsei would realize...) 3 + * (That doesn't matter to me so much.)
+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
··· 1 + * Why'd you... help me so much? 2 + * You didn't have to. 3 + 4 + * You remind me a bit of someone I used to know. 5 + 6 + * ... Who? 7 + 8 + * Just a character from that old story...
+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
··· 1 + * The words on a wall called you a hero. 2 + * ... Whatever you end up being, I'm sure it'll be tremendous. 3 + 4 + * ... 5 + * ... anyway, which Dragon Blazers character am I? 6 + * Gahahaha, right! 7 + 8 + * You are... the Dragon. 9 + 10 + * Heheh. That's what I was hoping.
+4
quotes/deltarune/susie_heal_no.txt
··· 1 + ??% Heal 2 + 3 + It seems the user doesn't 4 + want to use this spell.
+6
quotes/deltarune/susie_hope_crossed.txt
··· 1 + * Kris! Kris...! 2 + * Hey, look! It's me!! 3 + * "The girl with hope crossed on her heart." 4 + * Man, that's... That's bad ass, isn't it!? 5 + * You think there's anything else about us? 6 + * Don't just stand there, let's keep looking!
+3
quotes/deltarune/susie_kris_doggy_ears.txt
··· 1 + * Arright, Kris. If Ralsei can't come to the Light World... 2 + * Then YOU'RE gonna wear doggy ears and do my homework. 3 + * ... stop howling! Man, you're such a pain in the ass. Heh.
+7
quotes/deltarune/susie_kris_giant_bloodstain.txt
··· 1 + * ... what the hell is this giant bloodstain? 2 + * Just use VINEGAR, dumbass. Vinegar and hot water. Cleans it up. 3 + * ... look, we don't have time to... 4 + 5 + * (Somehow, you and Susie made time to clean up the stain.) 6 + 7 + * Dammit. We're gonna be late. Dammit.
+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
··· 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
··· 1 + * ... huh? 2 + * ... Why are you talking... with your mouth closed? 3 + * ... "Nothing"? 4 + * ... then why'd you wait so long to say it? 5 + * ... 6 + * Guess we should go.
+3
quotes/deltarune/susie_kris_my_prize.txt
··· 1 + * Hey, the hell does your brother have all the prizes? 2 + * Wait a sec... 3 + * There you go. The Susie prize. For having me over your house.
+10
quotes/deltarune/susie_kris_no_need.txt
··· 1 + * ... 2 + * You don't have 3 + to say anything back, 4 + Kris. 5 + * I... 6 + * ... 7 + * I know... 8 + * You're thinking the 9 + same thing 10 + * ... aren't you, dumbass?
+2
quotes/deltarune/susie_kris_or_something.txt
··· 1 + * Hey, didn't know you owned a bird. Or, uh... something. 2 + * ... don't really like how you nodded at "or something."
+7
quotes/deltarune/susie_kris_plink.txt
··· 1 + * Woah, Kris, you can play the piano? 2 + 3 + * (Plink...) 4 + 5 + * Hahaha! Almost thought you were serious for a sec! 6 + * (... what's with that pissed off look?) 7 + * ... damn. Got kinda hyped for a sec.
+7
quotes/deltarune/susie_kris_we_are_friends.txt
··· 1 + * Haha, like you're one to talk, idiot! 2 + * You got so few friends... 3 + * Your mom was even happy you, heh... 4 + * ... uh, made friends with me, I guess. 5 + * ... 6 + * ... man, we could've been friends way before. 7 + * Isn't that stupid...?
+6
quotes/deltarune/susie_kris_wont_you_play_1.txt
··· 1 + * Ralsei, wait a sec. 2 + * Kris... today in church, when you were asleep... 3 + * Your mom... said you used to play good. 4 + * I dunno why you're pretending you can't now. 5 + * Maybe you're like... embarrassed or something? 6 + * But like... y'know...
+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
··· 1 + * ... Y'know, when you played piano earlier... it was... 2 + * Okay, it was actually kinda cool. I mean that. 3 + * I'm not saying you should join the choir or whatever but... 4 + * If you were playing every week. 5 + * ... I might actually wanna go to church. 6 + * ...
+8
quotes/deltarune/susie_letter_alvin.txt
··· 1 + * "Alvin 2 + * Your old man was proud of you. 3 + * The reason he didnt come to church alot 4 + * is beacause he wanted you to follow your dream. 5 + * Hes sorry he never said that right. 6 + * So write your own story's. 7 + * Its okay if even if there not perfect. 8 + * Sinserely a friend"
+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
··· 1 + * Just... uh... 2 + * I mean, we don't even have to play any games. 3 + * We could... just make a mess. Cause some trouble. 4 + * Just... 5 + * I dunno, could be... fun if you... 6 + * ... just went, you know? 7 + 8 + * No, Susie... I... 9 + * I... want to, I just... 10 + * I can't go.
+9
quotes/deltarune/susie_ralsei_cant_go_2.txt
··· 1 + * ... 2 + * ... um, did I say something weird? 3 + 4 + * No! No, not at all, Susie! 5 + * I'm happy to be invited by you, Susie, I am... 6 + * It's just... 7 + * I'm a Darkner. 8 + * I can't go to the Light World. 9 + * Even if I... wanted to.
+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
··· 1 + * Hear that, Ralsei? 2 + * No one's gonna get thrown away. 3 + * Not you. Not me. Not Kris. 4 + * Got it? 5 + 6 + * ... 7 + * ... got it, Susie.
+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
··· 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
··· 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
··· 1 + * ... man. 2 + * There's nothing that pisses me off more... 3 + * ... than people who don't tell you the whole deal. 4 + 5 + * ... You mean, Tenna...? 6 + 7 + * Yeah. I dunno what, but... he's hiding something. 8 + * ... Let's go take a look around, Kris.
+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
··· 1 + * No matter what you say... 2 + * No matter what you are... 3 + * Me... you... Kris... We're a TEAM, alright? 4 + * None of us are gonna forget each other! 5 + * So please... 6 + * Just... 7 + * ... just...
+7
quotes/deltarune/susie_ralsei_your_room_1.txt
··· 1 + * That's the stupidest goddamn thing I ever heard! 2 + * You see this stupid excuse for a room? 3 + * I don't care what the reason is. 4 + * Me and Kris are gonna fill it up. Wall to wall. 5 + * Until you have the most badass room in the castle. 6 + 7 + * Susie...
+6
quotes/deltarune/susie_ralsei_your_room_2.txt
··· 1 + * Look. You can't be the only one with an empty room. 2 + * It's... 3 + * It's too sad, y'know? 4 + * Wherever we go, we'll keep an eye out for furniture, 5 + * Right, Kris? 6 + * C'mon.
+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
··· 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
··· 1 + * ... 2 + * Hey, Kris... 3 + * ... 4 + * Can I tell you 5 + * ... a stupid dream 6 + I have? 7 + * ... 8 + * No matter what 9 + * I... want to keep 10 + being friends 11 + with you and Ralsei.
+12
quotes/deltarune/susie_stupid_dream_2.txt
··· 1 + * When the sun 2 + comes up again... 3 + * I want tomorrow 4 + to be the same 5 + as yesterday. 6 + * And the next day 7 + * To be just the 8 + same as that. 9 + * That in the end 10 + * We can always go back 11 + to the way things were 12 + before.
+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
··· 1 + * Like... I don't want to say I've been getting soft. But... 2 + * I just always... HAD to be tough, you know? 3 + * Now it just... feels like I... have options.
+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
··· 1 + * ... That... Kinda reminds me of something. 2 + * So... In one of the places I used to live, there was like... 3 + * There was this plaza... and they put this public piano in it. 4 + * So I started playing it. Thinking, y'know, it might be fun.
+7
quotes/deltarune/susie_tried_piano_2.txt
··· 1 + * But... I guess... 2 + * Maybe because I was... so bad... 3 + * Or because I just looked like I shouldn't be playing... 4 + * They told me I wasn't... allowed to play it. 5 + * So I backed off. 6 + * And then... They let somebody else play. 7 + * ... someone better.
+9
quotes/deltarune/susie_tried_piano_3.txt
··· 1 + * ... 2 + * I don't know... I don't know why I expected I'd be able to play it. 3 + * ... Is what I thought... then. 4 + * ... 5 + * That night, when no one else was around... 6 + * ... I came back. 7 + * And you know what I did? 8 + * ... I smashed it. 9 + * Yeah... I smashed the piano.
+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
··· 1 + * I... I just wanted to be watched... 2 + * I just wanted you to look at me... 3 + * One last time..
+6
quotes/deltarune/tenna_beautiful_princess_2.txt
··· 1 + * ... 2 + * Since... since a little while ago... 3 + * All that I've been broadcasting, is the same reruns, over and over... 4 + * I... I don't have anything anyone wants to watch anymore... 5 + * I'm irrelevant... I'm junk... 6 + * I don't wanna be thrown away...
+7
quotes/deltarune/tenna_holidays_1.txt
··· 1 + * ENOUGH of your dumb games!! 2 + * Why the hell do you want us to play so bad anyway!? 3 + 4 + * Kris. 5 + * Kris knows why. 6 + * Do you remember it, Kris? 7 + * When everyone came over for the holidays?
+6
quotes/deltarune/tenna_holidays_2.txt
··· 1 + Back when I was just a little old TV. 2 + Everyone used to gather around my glow to watch the specials, 3 + Smiling. Laughing. Eating butterscotch pie. 4 + Wasn't that just so much... fun? 5 + To watch me. 6 + I... was so happy.
+10
quotes/deltarune/tenna_holidays_3.txt
··· 1 + Then... 2 + People stopped coming. 3 + Everyone disappeared... one by one... 4 + No one wanted to watch TV anymore. 5 + No one wanted to play games anymore. 6 + 7 + I've been all alone... 8 + Until now. 9 + When the KNIGHT appeared... 10 + The ROARING KNIGHT.
+3
quotes/deltarune/tenna_kris_december_1.txt
··· 1 + * Oh, Kris! Remember DECEMBER? Oh, that little musical prodigy... 2 + * Remember when she'd grab the remote and swap it from cartoons... 3 + * ... to that WILD music video channel Toriel FORBID you to see?
+3
quotes/deltarune/tenna_kris_december_2.txt
··· 1 + * Dess, rocking her guitar and singing along with those nasty, nasty songs! 2 + * Didn't you all think she was just so... COOL? 3 + * HECK! Maybe you could get her to come back and watch again sometime!?
+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
··· 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
··· 1 + * (...) 2 + * (Next, a tree was painted over it...) 3 + * (... painted over everything...) 4 + * (... leaving nothing but a simple still life.) 5 + * (... After all, there wasn't anything to look at in the room besides a single tree.)
+10
quotes/deltarune/yet_you_persist.txt
··· 1 + VERY 2 + 3 + INTERESTING. 4 + 5 + YOUR LOSS HERE 6 + IS ALL 7 + BUT GUARANTEED. 8 + 9 + AND YET 10 + YOU PERSIST...
+10
quotes/deltarune/you_promised.txt
··· 1 + ... 2 + Kris... 3 + 4 + ... 5 + don't 6 + forget, 7 + Kris... 8 + 9 + ... 10 + you promised.
+3
quotes/xenoblade-chronicles-3/monica_in_the_mud.txt
··· 1 + Whether it ends in a lie or not is up to you. 2 + We all have to crawl face down in the mud. 3 + Believing...we'll come out smiling.
+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
··· 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
··· 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
··· 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
··· 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
··· 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 - }