├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ └── tox.yml ├── .gitignore ├── LICENSE ├── README.md ├── arrays.py ├── arrays_test.py ├── config.example.py ├── gen.py ├── requirements.txt └── tox.ini /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behaviour: 15 | 1. meow 16 | 2. awoo 17 | 3. nyaa 18 | 4. See error 19 | 20 | **Expected behaviour** 21 | A clear and concise description of what you expected to happen. 22 | 23 | 24 | **Additional context** 25 | Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **As a** [*contributor who is adding folx*], **I would like** [*a tox check for duplicates*], **so that** [*I don't accidently include a duplicate in my PR*] 11 | 12 | **Additional context** 13 | Add any other context or screenshots about the feature request here. 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/tox.yml: -------------------------------------------------------------------------------- 1 | name: tox 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [ "main" ] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: ["3.9", "3.10"] 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v3 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install dependencies 21 | run: | 22 | cp config.example.py config.py 23 | python -m pip install --upgrade pip 24 | pip install -r requirements.txt 25 | - name: Run tox 26 | run: tox 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | .tox/ 3 | __pycache__/ 4 | config.py 5 | .vscode/ 6 | last_* 7 | used_* 8 | *.log 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sammy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # You can have a README, as a treat 2 | 3 | ## What is? 4 | It's a silly fedi bot (currently at https://fox.nexus/@treats). 5 | 6 | ### Possible combinations 7 | An up-to-date calculation of the possible combinations can be found in [the bot's bio on fedi](https://fox.nexus/@treats). 8 | 9 | ## Contributing 10 | ### Setting up 11 | ```bash 12 | # Clone the repo and cd into it, then: 13 | python3 -m venv venv 14 | source ./venv/bin/activate 15 | pip install -r requirements.txt 16 | ``` 17 | 18 | ### Editing [arrays.py](arrays.py) 19 | The format is "*{folx} can have {treats}, as a treat*", so add items to `FOLX` and/or `TREATS`: 20 | 21 | ```python 22 | FOLX = [ 23 | "Transfems", 24 | "Foxgirls", 25 | [...], 26 | ] 27 | 28 | TREATS = [ 29 | "a headpat", 30 | "an anti-trust lawsuit", 31 | [...], 32 | ] 33 | ``` 34 | 35 | ### Tox 36 | I use `tox` to run tests, check code style and fix formatting. It's a good idea to run it before pushing changes. A GitHub action will also run it on PRs. 37 | ```bash 38 | # To run tests: 39 | tox 40 | 41 | # To fix formatting etc: 42 | tox -e fix 43 | ``` 44 | 45 | ### Gotchas 46 | - `gen.py` will fail if `config.py` isn't present — you can just copy `config.example.py` to `config.py` while testing. Note that the example config doesn't contain the necessary credentials to actually make a post. 47 | - I <3 trailing commas, and `tox` will get very upset if you forget this :3 48 | -------------------------------------------------------------------------------- /arrays.py: -------------------------------------------------------------------------------- 1 | # Format: {folx} can have {treats}, as a treat 2 | 3 | # The case of these will not be changed 4 | FOLX = [ 5 | "Transfems", 6 | "Foxgirls", 7 | "Foxes", 8 | "Transmascs", 9 | "Catgirls", 10 | "Catboys", 11 | "Wolfgirls", 12 | "Wolfboys", 13 | "Puppygirls", 14 | "Dogboys", 15 | "Furries", 16 | "Ratgirls", 17 | "Wyverns", 18 | "Deergirls", 19 | "Cryptids", 20 | "I", 21 | "The Maus", 22 | "Bees", 23 | "Wikipedians", 24 | "Mathematicians", 25 | "CS undergrads", 26 | "Rust developers", 27 | "Javascript developers", 28 | "Python developers", 29 | "PHP developers", 30 | "C developers", 31 | "C++ developers", 32 | "C# developers", 33 | "Java developers", 34 | "Arch Linux users", 35 | "Alpine Linux users", 36 | "Gentoo users", 37 | "Nix users", 38 | "Debian users", 39 | "Windows users", 40 | "Mac users", 41 | "Fedora users", 42 | "You", 43 | "Fedibots", 44 | "Robotgirls", 45 | "Dolls", 46 | "labyrinth.zone users", 47 | "fox.nexus users", 48 | "honeycomb.engineer users", 49 | "Yassie", 50 | "Yaseenists", 51 | "Folx that use they/them", 52 | "Open source maintainers", 53 | "Package maintainers", 54 | "Mastodon users", 55 | "Akkoma users", 56 | "Sharkey users", 57 | "Glitch-soc users", 58 | "GoToSocial users", 59 | "Firefish users", 60 | "Iceshrimp users", 61 | "Dagns", 62 | "Good pets", 63 | "Gamers", 64 | "Doms", 65 | "The Kat", 66 | "Polycules", 67 | "FFXIV players", 68 | "Cuties", 69 | "Boykissers", 70 | "Girlkissers", 71 | "Enbykissers", 72 | "Femboys", 73 | "Useless lesbians", 74 | "Orange cats", 75 | "Void kitties", 76 | "Everyone", 77 | "Nyanbinary folx", 78 | "Friend-shaped creatures", 79 | "Neurospicy people", 80 | "Sammy", 81 | "Xenia", 82 | "Livvy", 83 | "Luna", 84 | "Julia", 85 | "Kris", 86 | "Fuxle", 87 | "Cal", 88 | "transfem.social users", 89 | "Lexi", 90 | "catgirl-engine users", 91 | "Cute dorks", 92 | "The BOFH", 93 | "Wikimedians", 94 | "Tim Sweeney", 95 | "Pastafarians", 96 | "Discordians", 97 | "Neurotypicals", 98 | "Vim users", 99 | "Emacs users", 100 | "Nano users", 101 | "Large Language Models", 102 | "Lost robots", 103 | "Scientists", 104 | "Subscribers", 105 | "Software supply chains", 106 | "Infosec mavens", 107 | "Rapid response furries", 108 | "Robos", 109 | "Drones", 110 | "We", 111 | "Omegas", 112 | "Alphas", 113 | "Queer people", 114 | "Debian Maintainers", 115 | "Debian Developers, uploading", 116 | "Debian Developers, non-uploading", 117 | "Humanoids", 118 | "Canids", 119 | "Vulpines", 120 | "Felines", 121 | "Kreechurs", 122 | "Optimists", 123 | "Pessimists", 124 | "Realists", 125 | "Grouchy sysadmins", 126 | "Poets", 127 | "The Scout from TF2", 128 | "The Soldier from TF2", 129 | "The Pyro from TF2", 130 | "The Demoman from TF2", 131 | "The Heavy from TF2", 132 | "The Engineer from TF2", 133 | "The Medic from TF2", 134 | "The Sniper from TF2", 135 | "The Spy from TF2", 136 | "We and our 847 partners", 137 | "Xbox players", 138 | "Playstation players", 139 | "PC players", 140 | "Nintendo players", 141 | "Dani", 142 | "Mara", 143 | "Rail", 144 | "Taavi", 145 | "Herobrine", 146 | "Sackboy", 147 | "Robloxians", 148 | "Everyone in the closet", 149 | "Disabled people", 150 | "That kind person at the grocery store", 151 | "Critters", 152 | "Non-human entities", 153 | "Therians", 154 | "No one", 155 | "Nerds", 156 | "Dorks", 157 | "Cat lovers", 158 | "Dog lovers", 159 | "Gardeners", 160 | "Claire", 161 | "A random MediaWiki extension", 162 | "A random MediaWiki skin", 163 | "MediaWiki", 164 | "Skin:Citizen", 165 | "Extension:Cargo", 166 | "Miraheze", 167 | "Starfleet captains", 168 | "Klingons", 169 | "Romulans", 170 | "Vulcans", 171 | "Ferengi", 172 | "The Borg", 173 | "Soong-type androids", 174 | "Extension:DataDump", 175 | "Extension:CSS", 176 | "Extension:CreateWiki", 177 | "Extension:WikiDiscover", 178 | "Neocats", 179 | "Neofoxes", 180 | "donotsta.re users", 181 | "Cheese gobblers", 182 | "Disney adults", 183 | "IRC users", 184 | "ChanServ", 185 | "NickServ", 186 | "API crime committers", 187 | "Dragonfoxes", 188 | "Yinglets", 189 | "My (25M) subs (23F, 26F, 22M, 28M, 28M)", 190 | "Slenderman", 191 | "K&R", 192 | "Vampires", 193 | "D&D players", 194 | "The DM", 195 | "Madeline", 196 | "Part of You", 197 | "Readers", 198 | "The ouroboros", 199 | "Fedi users", 200 | "New fedi users", 201 | "Elf-friends", 202 | "Chell", 203 | "GLaDOS", 204 | "Wheatley", 205 | "The BDFL", 206 | "Lucas", 207 | "Hobbits", 208 | "Fedi meta", 209 | "Medi feta", 210 | "The Steam catgirl", 211 | "XMPP users", 212 | "Hackers", 213 | "OpenStreetMap contributors", 214 | "Discordian popes", 215 | "Chat", 216 | "Moths", 217 | "Grown-ups", 218 | "People who work from home", 219 | "The bots", 220 | "That one", 221 | "Blåhajar", 222 | "liv", 223 | "Your Innie", 224 | "Your Outie", 225 | "The Smol", 226 | "Hackerspaces", 227 | "Beepers", 228 | ] 229 | 230 | # The case of these will not be changed 231 | TREATS = [ 232 | "a headpat", 233 | "nothing", 234 | "an anti-trust lawsuit", 235 | "some gunpowder", 236 | "a misskey fork", 237 | "a normal fork", 238 | "some poorly maintained code", 239 | "some MediaWiki", 240 | "some chicken nuggets", 241 | "a ride on a Sprinter 158/9", 242 | "a ride on a Class 218", 243 | "a ride on an ICE 4", 244 | "a ride on a Stadler KISS", 245 | "an undefined variable", 246 | "a new spinny skirt", 247 | "a new set of thigh highs", 248 | "a new checked shirt", 249 | "new pronouns", 250 | "a cheemsborgar", 251 | "a copy of Celeste", 252 | "Play of the Game", 253 | "a cheeky Nandos", 254 | "a cuddle of the Yassie plushie", 255 | "a fediblock", 256 | "a broken migration", 257 | "some new cat ears", 258 | "a LEGO UCS Millennium Falcon 75192", 259 | "a `random.choice(TREATS)`", 260 | "a new OLED steamdeck", 261 | "a barely used Nvidia RTX 4090 Ti", 262 | "a LEGO UCS Death Star 75159", 263 | "a shiny Gardevoir", 264 | "an iced latte", 265 | "a docker container", 266 | "an invalid IPv4 address", 267 | "an IPv6 in the RFC 4193 block", 268 | "a **BLÅHAJ**", 269 | "a malformed BIRD config", 270 | "a *click*", 271 | "belly rubs", 272 | "working code", 273 | ":3", 274 | "a biiiiig stretch", 275 | "a cute frog", 276 | "a neofox", 277 | "a neocat", 278 | "a trip to IKEA", 279 | "some extra spoons", 280 | "a little kiss on the forehead", 281 | "a flatpak", 282 | "a cool rock", 283 | "some cool moss", 284 | "a redundant backup", 285 | "a fluffy tail", 286 | "a snoot boop", 287 | "a creative mode Minecraft server", 288 | "a hit of dopamine", 289 | "root access", 290 | "a boiled pizza", 291 | "a Eurasian collared dove", 292 | "a Monster Ultra White", 293 | "a Monster Ultra Rosa", 294 | "some bepis", 295 | "an unmaintained npm package", 296 | "a hacky python script", 297 | "a shitpost", 298 | "a Thinkpad T440p", 299 | "a Nix flake", 300 | "a Warhammer Age of Sigmar Warrior Starter Set", 301 | "temporary use of the single brain cell", 302 | "a Twitch Prime subscription", 303 | "a comfy blanket", 304 | "a hassle-free git merge", 305 | "a stack overflow", 306 | "federated biting", 307 | "nibbles from a maus", 308 | "small bits of cheese", 309 | "a furnished kennel", 310 | "a new 3D printer", 311 | "an extra hydrated spool of PLA", 312 | "a new 60% mechanical keyboard", 313 | "a failing wireguard tunnel", 314 | "broken Path MTU Discovery", 315 | "a 100 MB Iomega Zip Disk", 316 | "a nat 20", 317 | "an ear wiggle", 318 | "another fedi drama", 319 | "a Windows 98 VM", 320 | "a Torment Nexus", 321 | "little a Salami", 322 | "a little break", 323 | "a way to exit vim", 324 | "a giant pride flag", 325 | "spam from mastodon dot social", 326 | "a dependabot PR", 327 | "a 1996 Subaru Outback", 328 | "a request to agree to the GPL", 329 | "some extra RAM", 330 | "a 7200 RPM hard drive", 331 | "a zero-day exploit", 332 | "a cute little collar", 333 | "*this* subpost", 334 | "a little nap", 335 | "trailing commas", 336 | "paw beans", 337 | "brushy brushy", 338 | "walkies", 339 | "ear scritches", 340 | "a bowl of kibble", 341 | "a game of fetch", 342 | "an hour-long video about a fridge", 343 | "an infodump", 344 | "a parallel play session", 345 | "a new stim toy", 346 | "some peace and quiet", 347 | "a pipe bomb", 348 | "warm sheets fresh out of the dryer", 349 | "a cookie", 350 | "a bag of Percy Pigs", 351 | "a long lie-in", 352 | "documentation stored in a discord server", 353 | "SSO", 354 | "an LDAP server", 355 | "a lightly-chewed CAT6a cable", 356 | "a combined DisplayPort/HDMI port", 357 | "yet another micro-USB cable", 358 | "a proprietary charging port", 359 | "a Y2K bug", 360 | "a Year 2038 bug", 361 | "an integer overflow", 362 | "a keysmash", 363 | "free downloadable RAM", 364 | "a random excuse from the BOFH's excuse generator", 365 | "fully automated luxury gay space communism", 366 | "a barnstar", 367 | "the British Rail Advanced Passenger Train", 368 | "a smooth, quiet, and altogether delightful experience", 369 | "railway electrification", 370 | "a Grafana dashboard", 371 | "a memory leak", 372 | "a kernel panic", 373 | "a sequence from the OEIS", 374 | "a cuddle with a plushie", 375 | "a visit from a TV Licensing Enforcement Officer", 376 | "a nice relaxing bath", 377 | "a Get Out of Jail Free card", 378 | "a bottle of sparkly nail polish", 379 | "the last slice of pizza", 380 | "Tim Sweeney", 381 | "a reminder that this bot [accepts contributions](https://github.com/theresnotime/as-a-treat)", 382 | "a successful EU261 claim", 383 | "an unoccupied middle seat", 384 | "a Code-Review +2 vote", 385 | "a PGP key signature", 386 | "a peering request", 387 | "a disk failure", 388 | "a last-minute aircraft swap", 389 | "a geodesic dome", 390 | "an all-nighter", 391 | "an unterminated regular expression literal", 392 | "an expired SSL certificate", 393 | "a solarpunk utopia", 394 | "a giant mechanical spider", 395 | "a lost robot", 396 | "an inflatable tentacle", 397 | "a hacky racer", 398 | "several tshunks", 399 | "a hallucinating Large Language Model", 400 | "an unexpected delay repayment", 401 | "a weighted blanket", 402 | "a fully operational hoverport", 403 | "a juicy strawblbery", 404 | "a blatantly gerrymandered district", 405 | "a QSL request", 406 | "Jupyter Notebooks", 407 | "a Fortran code from 1976", 408 | "as-a-treat as a Service (aaaS)", 409 | "an Oxford comma", 410 | "a worn out VHS tape", 411 | "a slightly scratched laserdisc", 412 | "a write-protected 3½-inch floppy", 413 | "a suspicious USB drive", 414 | "a demagnetised cassette tape", 415 | "a spicy GitHub tarball", 416 | "an sshd backdoor", 417 | "an orphan source", 418 | "a new neo* emoji pack", 419 | "a timezone change", 420 | "a Potion of Invisibility", 421 | "Universal Basic Income", 422 | "an awawawawa", 423 | "the GitHub consequences", 424 | "an etherkiller", 425 | "a USB condom", 426 | "a loafing cat", 427 | "UTF-8", 428 | "UTF-32", 429 | "a hug", 430 | "this", 431 | "frozen yogurt", 432 | "a light mode theme", 433 | "a genuine compliment", 434 | "a free upgrade to premium economy", 435 | "a ride in the front seat of the DLR", 436 | "the window seat", 437 | "an empty table seat on the train", 438 | "melted frozen yoghurt", 439 | "a meowing backpack", 440 | "a Non-Maintainer Upload", 441 | "an FTBFS bug", 442 | "a source-only upload", 443 | "a stable point release", 444 | "a package in NEW", 445 | "a new release of the Debian Policy Manual", 446 | "a library transition", 447 | "a failed autopkgtest", 448 | "an inconveniently timed freeze", 449 | "a broken reverse dependency", 450 | "DM upload rights", 451 | "a Hetzner takedown", 452 | "some petty corruption", 453 | "a little bribery", 454 | "a small kickback", 455 | "a minor scandal", 456 | "corporate embezzlement", 457 | "tax fraud", 458 | "LF OS installed on their main computer", 459 | "a vote of support", 460 | "a rigged random number generator", 461 | "a smoking 18650 battery", 462 | "a long-running Jenkins job", 463 | "a 5-star rating and a big tip", 464 | "a slushie with all the flavours mixed together", 465 | "some gender euphoria", 466 | "in-cab signalling", 467 | "a movement authority", 468 | "an eurobalise", 469 | "variable gauge rolling stock", 470 | "a section of dual-gauge track", 471 | "an oddly-specific hyperactive interest in old, outdated software", 472 | "an IBM PC/AT", 473 | "a giant pile of dead hardware platforms", 474 | "corporate in-fighting", 475 | "a full development lifecycle reset", 476 | "a Blåhaj-colored computer", 477 | "a sentence that is false", 478 | "a fully-functional Commodore Amiga 1200", 479 | "a disk box", 480 | "a golden joystick", 481 | "negative zero", 482 | "a fun new meme generator", 483 | "the unbearable lightness of being", 484 | "a camping trip to Isla Sorna", 485 | "some mojibake", 486 | "one moment of perfect beauty", 487 | "a novel compiler bug", 488 | "some highland cattle", 489 | "an Übercharge", 490 | "the intelligence", 491 | "a briefcase", 492 | "the capture point", 493 | "a payload cart", 494 | "the hill", 495 | "hats", 496 | "crate keys", 497 | "an unusual", 498 | "a golden frying pan", 499 | "a 1.5TB LTO-5 storage tape", 500 | "an unsubstantiated copyright strike", 501 | "a sea view room", 502 | "an all-you-can-eat buffet", 503 | "a comfy hoodie", 504 | "a 55-gallon barrel of lube", 505 | "a 150kW DC rapid charger", 506 | "a little pot of tea", 507 | "a platinum trophy", 508 | "a custom plushie", 509 | "a bowl of Skittles and M&Ms mixed together", 510 | "the last Rolo", 511 | "an inaccurate commit message", 512 | "robux", 513 | "tix", 514 | "a singular robuck", 515 | "a singular ticket", 516 | "score bubbles", 517 | "prize bubbles", 518 | "vbucks", 519 | "collectabells", 520 | "free alternative", 521 | "linux rice", 522 | "more ram", 523 | "more arrays", 524 | "yawn detection", 525 | "a light switch that doesn't seem to do anything", 526 | "an out of control Markov bot", 527 | "an HR-mandated diversity and inclusion training video", 528 | "some new laptop stickers", 529 | "an unexpected package delivery", 530 | "a day off", 531 | "a vase of pretty flowers", 532 | "me", 533 | "a cute blushy face", 534 | "an egg", 535 | "a wink", 536 | "some new plants", 537 | "a shiny thing", 538 | "capitalism's downfall", 539 | "the right to grumble at leisure all day long", 540 | "a bite", 541 | "carcinisation", 542 | "a brand new box of floppy disks", 543 | "a picture frame containing a projection of everything in the universe", 544 | "a back rub", 545 | "all those meetings cancelled", 546 | "breakfast in bed", 547 | "alt text", 548 | "a cat making biscuits", 549 | "a piece of helpful advice", 550 | "an unexpected long weekend", 551 | "a ride in the front seat upstairs on a double-decker bus", 552 | "a CloudStrike", 553 | "rail renationalisation", 554 | "spammy push notifications", 555 | "a poorly described Jira ticket", 556 | "a squeaky toy", 557 | "a surprise tax rebate", 558 | "a little bit of pud", 559 | "an extra fluffy pillow", 560 | "a discount code for 15% off", 561 | "tone indicators", 562 | "control of the aux", 563 | "a reminder to like and subscribe", 564 | "a sponsorship from PCBWay", 565 | "an indecipherable compiler error", 566 | "a cloned Yubikey", 567 | "some copper from Ea-nāṣir", 568 | "a live service game shutdown", 569 | "a very non-suspicious giant wooden horse", 570 | "an arrow to the knee", 571 | "a security vulnerability", 572 | "a patch", 573 | "an XSS", 574 | "a SQL injection", 575 | "a CSRF vulnerability", 576 | "a fun cat fact", 577 | "a warp core breach", 578 | "a new Monster of the Week", 579 | "membership in the Federation", 580 | "subspace interference", 581 | "a bowl of Gagh", 582 | "an emotion chip", 583 | "36 bars of gold-pressed Latinum", 584 | "a path traversal", 585 | "a Framework", 586 | "estrogen", 587 | "testosterone", 588 | "Extra Magic Hours", 589 | "a Lightning Lane Multi Pass", 590 | "a souvenir bucket of popcorn", 591 | "a Loungefly mini backpack", 592 | "a mouse ear headband", 593 | "a baked Camembert", 594 | "a rubber ducky", 595 | "a '; DROP TABLE users; --", 596 | "an in-show exit", 597 | "a segmentation fault", 598 | "a friday afternoon PagerDuty alert", 599 | "some freshly baked bread", 600 | "a 642 episode let's play series", 601 | "a 210 minute queue for a roller coaster", 602 | "double pepperoni", 603 | "a quip", 604 | "op", 605 | "voice", 606 | "a ThinkPad X230", 607 | "a pointless button", 608 | "Speedy Boarding", 609 | "the next turn on the Xbox", 610 | "updated terms and conditions", 611 | "the front row on a roller coaster", 612 | "the back row on a roller coaster", 613 | "a 6€ hot chocolate", 614 | "a jigsaw puzzle with a piece missing", 615 | "my home assistant login", 616 | "my mastodon password", 617 | "a home assistant integration", 618 | "a working backup", 619 | "a broken backup", 620 | "a full disk", 621 | "an empty disk", 622 | "posixly correct coreutils", 623 | "a PNG corrupted in an interesting way", 624 | "a major version upgrade", 625 | "a bugfix", 626 | "a bug", 627 | "a useless machine", 628 | "a proof of the Riemann hypothesis", 629 | "a proof that P≠NP", 630 | "a proof that P=NP", 631 | "three positive integers a, b, c such that a^n + b^n = c^n for an n > 2", 632 | "a glass of choccy milk", 633 | "a union", 634 | "a union job", 635 | "a NullPointerException", 636 | "[object Object]", 637 | "a Protogen", 638 | "a cutie", 639 | "a pizza", 640 | "some free candy", 641 | "a printer that just works", 642 | "a reminder to follow this bot", 643 | "a pair of programming socks", 644 | "an HO scale LNER A4 class 4-6-2 locomotive", 645 | "a slack message that just says hi", 646 | "a 7-day global Interrail pass", 647 | "a mysterious envelope", 648 | "a pair of woolly socks", 649 | "some Python 2 code", 650 | "some Java 7 code", 651 | "another jq implementation", 652 | "a pair of programmer socks", 653 | "an LED strip", 654 | "a LED strip", 655 | "an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp", 656 | "an IPv4 block", 657 | "a Rubik's Cube", 658 | "a warm fuzzy feeling", 659 | "a configure script", 660 | "cow tools", 661 | "a copy of K&R", 662 | "a forwarded meme", 663 | "a chain email", 664 | "a new browser engine", 665 | "a stroopwafel", 666 | "a public piano", 667 | "a conlang", 668 | "a pile of unread books", 669 | "a hello world program", 670 | "a quine", 671 | "a polyglot program", 672 | "a fun task", 673 | "an introduction post", 674 | "a man page that points to an info page", 675 | "some manually written troff markup", 676 | "an unmaintained tool", 677 | "tasty leftovers", 678 | "a smolhaj", 679 | "a portal gun", 680 | "an Aperture Science Handheld Portal Device", 681 | "a Companion Cube", 682 | "a god object", 683 | "an eepy cat", 684 | "a Silmaril", 685 | "a messenger from Godot", 686 | "second breakfast", 687 | "a Safe and Sophie Germain prime number", 688 | "a cellular automata", 689 | "a prime number p greater than 127, such that r=2ᵖ-1 and p=2ʳ-1 are both prime", 690 | "a proof to the Collatz Conjecture", 691 | "1 (one) virus", 692 | "a TARDIS key", 693 | "a beesechurger", 694 | "beans", 695 | "a trout", 696 | "a Ferrero Rocher", 697 | "Nutella", 698 | "a Juicero Press", 699 | "a cardboard box", 700 | "a copy of Euro Truck Simulator 2", 701 | "the gender fluid", 702 | "a gender", 703 | "catgirl HRT", 704 | "catboy HRT", 705 | "a scented candle", 706 | "one more lane", 707 | "a gTLD", 708 | "a forklift certification", 709 | "a postage stamp", 710 | "an IKEA pencil", 711 | "some Toblerone", 712 | "an insecure VNC server", 713 | "a little schadenfreude", 714 | "a cookie banner", 715 | "a fursuit", 716 | "an on-time train", 717 | "a unit test", 718 | "a seal of approval", 719 | "an idle fancy", 720 | "an SSD", 721 | "a retro computer", 722 | "a layer of untouched snow", 723 | "Half-Life 3", 724 | "a lava lamp", 725 | "a snow day", 726 | "a new furry mascot", 727 | "exactly 3 Scritches", 728 | "an ASN", 729 | "a car running gotosocial", 730 | "a 9.8 CVE", 731 | "an artisinally crafted ice cream sundae", 732 | "a super intelligent immortal snail", 733 | "a LEGO GBC module", 734 | "an RMC conversion", 735 | "the exit row", 736 | "the tenth pitch drop", 737 | "a flip dot display", 738 | "DOOM running on a random device", 739 | "an asteroid impact", 740 | "a puppy", 741 | "a flower from a sweetie", 742 | "a large correctly-formatted data file", 743 | "floor time", 744 | "two Ns", 745 | "the Pizza Planet truck", 746 | "a cameo from someone that no one has ever heard of", 747 | "a line of foxes waiting for chocolate", 748 | "plot armour", 749 | "a deus ex machina", 750 | "Chekhov's gun", 751 | "a surprise twist", 752 | "subtle foreshadowing", 753 | "a girlfriend", 754 | "a partner", 755 | "a boyfriend", 756 | "a stable Arch install", 757 | "🔑 Unable to decrypt message", 758 | "a Wellness Session", 759 | "a hall pass", 760 | "some scary numbers", 761 | "reintegration", 762 | "a Music Dance Experience", 763 | "an ORTBO", 764 | "a banger hypercore compilation", 765 | "a filthy af 199-bpm track", 766 | "mediawiki vulnerabilities", 767 | "a random podium inspection", 768 | "trains", 769 | "a ppc64el-specific bug", 770 | "an armhf-specific bug", 771 | "a s390x-specific bug", 772 | "a single /64 IPv6 prefix delegation", 773 | "/48 IPv6 address block", 774 | "a fox onesie", 775 | "50 GiB of garbage in /nix/store", 776 | ] 777 | -------------------------------------------------------------------------------- /arrays_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | def test_unique(): 5 | seen = set() 6 | for line in Path("arrays.py").read_text().split("\n"): 7 | if line.strip() == line: 8 | # No leading indentation, not a list entry 9 | if line == "]": 10 | # This is the end of a list. Clear seen, so same entries 11 | # can be on both lists. 12 | seen = set() 13 | continue 14 | assert line not in seen 15 | seen.add(line) 16 | -------------------------------------------------------------------------------- /config.example.py: -------------------------------------------------------------------------------- 1 | API_URL = "" 2 | ACCESS_TOKEN = "" 3 | FTP_HOST = "" 4 | FTP_USER = "" 5 | FTP_PASS = "" 6 | DONT_UPLOAD_LOGS = True 7 | -------------------------------------------------------------------------------- /gen.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import config 3 | import ftplib 4 | import logging 5 | import os 6 | import random 7 | import sys 8 | from arrays import FOLX, TREATS 9 | from datetime import datetime, timezone 10 | from enum import Enum 11 | from mastodon import Mastodon 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | # The chance for the treat to become a threat 16 | THREAT_PROBABILITY = 1 / 100 17 | 18 | 19 | class Visibility(Enum): 20 | """The possible visibilities for a post according to the mastodon client""" 21 | 22 | private = "private" 23 | direct = "direct" 24 | unlisted = "unlisted" 25 | public = "public" 26 | 27 | def __str__(self: "Visibility") -> str: 28 | return self.value 29 | 30 | 31 | def get_log_level(no_log: bool, verbose: bool) -> int: 32 | if no_log: 33 | return logging.ERROR 34 | elif verbose: 35 | return logging.DEBUG 36 | else: 37 | return logging.INFO 38 | 39 | 40 | def count_combinations() -> None: 41 | """Calculate the number of possible outputs""" 42 | num_folx = len(FOLX) 43 | num_treats = len(TREATS) 44 | combinations = num_folx * num_treats 45 | output = f"There are {num_folx} folx and {num_treats} treats, resulting in {combinations:,} possible combinations." 46 | log.info(output) 47 | print(output) 48 | 49 | 50 | def update_bio(dry_run: bool = False) -> None: 51 | """Update the bot's bio with the number of possible combinations""" 52 | num_folx = len(FOLX) 53 | num_treats = len(TREATS) 54 | combinations = num_folx * num_treats 55 | last_update = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") 56 | bio = f"You can have another bot, as a treat.\n\nI can choose from {num_folx} folx and {num_treats} treats, so there are {combinations:,} possible combinations.\n\nI last updated this bio on {last_update} (UTC)." 57 | 58 | if dry_run is False: 59 | mastodon = Mastodon( 60 | access_token=config.ACCESS_TOKEN, api_base_url=config.API_URL 61 | ) 62 | mastodon.account_update_credentials(note=bio) 63 | log.info("Updated bio to: %s", bio) 64 | print(f"Updated bio to: {bio}") 65 | else: 66 | print(f"Dry run, would have updated bio to: {bio}") 67 | log.info("Dry run, would have updated bio to: %s", bio) 68 | 69 | 70 | def write_status( 71 | status: str, dry_run: bool = False, visibility: Visibility = Visibility("unlisted") 72 | ) -> None: 73 | """Write a status to Mastodon""" 74 | if dry_run is False: 75 | # Post 76 | mastodon = Mastodon( 77 | access_token=config.ACCESS_TOKEN, api_base_url=config.API_URL 78 | ) 79 | mastodon.status_post(status=status, visibility=str(visibility)) 80 | log.info('Posted "%s"', status) 81 | print(f"Posted {status}") 82 | else: 83 | print(f"Dry run, would have posted {status}") 84 | log.info('Dry run, would have posted "%s"', status) 85 | 86 | 87 | def should_be_threat(): 88 | """Use THREAT_PROBABILITY to determine if this treat should be a threat""" 89 | range_max = int(1 / THREAT_PROBABILITY) 90 | chosen_value = random.randint(1, range_max) 91 | log.debug("Treat/Threat value %d (threat requires %d)", chosen_value, range_max) 92 | 93 | is_threat = chosen_value == range_max 94 | if is_threat: 95 | log.debug("Post will be a threat") 96 | else: 97 | log.debug("Post will be a treat") 98 | 99 | return is_threat 100 | 101 | 102 | def get_used_filename(thing: str) -> str: 103 | """Get the filename for the file containing used _things_""" 104 | return f"used_{thing}" 105 | 106 | 107 | def save_used(thing: str, value: str) -> None: 108 | """Add an entry to the used _things_ list""" 109 | filename = get_used_filename(thing) 110 | with open(filename, "a") as f: 111 | f.write(value + "\n") 112 | 113 | 114 | def get_used(thing: str) -> list[str]: 115 | """Get the list of used _things_""" 116 | filename = get_used_filename(thing) 117 | if os.path.isfile(filename): 118 | with open(filename, "r") as f: 119 | return f.read().splitlines() 120 | else: 121 | return [] 122 | 123 | 124 | def clear_used(thing: str) -> None: 125 | """Clear the list of used _things_""" 126 | filename = get_used_filename(thing) 127 | with open(filename, "w") as f: 128 | f.write("") 129 | log.info("Cleared used %s list", thing) 130 | 131 | 132 | def upload_logs(filename: str) -> None: 133 | """Upload a file to the FTP server""" 134 | # Check if filename exists 135 | if not os.path.isfile(filename): 136 | log.error(f"File {filename} does not exist") 137 | return 138 | session = ftplib.FTP(config.FTP_HOST, config.FTP_USER, config.FTP_PASS) 139 | ftplib.FTP.cwd(session, "as-a-treat") 140 | file = open(filename, "rb") 141 | session.storbinary(f"STOR {filename}", file) 142 | file.close() 143 | session.quit() 144 | log.info(f"Uploaded {filename}") 145 | 146 | 147 | if __name__ == "__main__": 148 | parser = argparse.ArgumentParser( 149 | description='Generate a string in the format "{folx} can have {treats}, as a treat" and post it to fedi' 150 | ) 151 | parser.add_argument( 152 | "-d", 153 | "--dry-run", 154 | action="store_true", 155 | help="Generate output, but do not post it", 156 | ) 157 | parser.add_argument( 158 | "-c", 159 | "--count", 160 | action="store_true", 161 | help="Count the number of possible outputs and exit", 162 | ) 163 | parser.add_argument( 164 | "-u", 165 | "--update-bio", 166 | action="store_true", 167 | help="Update the bot's bio with the number of possible combinations", 168 | ) 169 | parser.add_argument( 170 | "--visibility", 171 | type=Visibility, 172 | choices=list(Visibility), 173 | action="store", 174 | default="unlisted", 175 | ) 176 | parser.add_argument("--no-log", action="store_true", help="Disable logging") 177 | parser.add_argument( 178 | "-v", "--verbose", action="store_true", help="Enable verbose logging" 179 | ) 180 | args = parser.parse_args() 181 | 182 | log_level = get_log_level(args.no_log, args.verbose) 183 | log_format = "%(asctime)s %(levelname)-5s %(message)s" 184 | logging.basicConfig(filename="as-a-treat.log", level=log_level, format=log_format) 185 | 186 | if args.count: 187 | count_combinations() 188 | sys.exit(0) 189 | 190 | if args.update_bio: 191 | update_bio(args.dry_run) 192 | sys.exit(0) 193 | 194 | used_folx = get_used("folx") 195 | used_treats = get_used("treats") 196 | 197 | # Remove previously used folx 198 | available_folx = [item for item in FOLX if item not in used_folx] 199 | log.debug("%d unused folx remaining", len(available_folx)) 200 | if len(available_folx) == 0: 201 | available_folx = FOLX 202 | clear_used("folx") 203 | 204 | # Remove previously used treats 205 | available_treats = [item for item in TREATS if item not in used_treats] 206 | log.debug("%d unused treats remaining", len(available_treats)) 207 | if len(available_treats) == 0: 208 | available_treats = TREATS 209 | clear_used("treats") 210 | 211 | # Choose a random folx and treat from the remaining available options 212 | folx = random.choice(available_folx) 213 | treat = random.choice(available_treats) 214 | log.debug('Chose folx "%s" and treat "%s"', folx, treat) 215 | 216 | # Save the chosen folx and treat so they can't be picked again 217 | save_used("folx", folx) 218 | save_used("treats", treat) 219 | 220 | treat_or_threat = "threat" if should_be_threat() else "treat" 221 | 222 | status = f"{folx} can have {treat}, as a {treat_or_threat}" 223 | write_status(status, args.dry_run, args.visibility) 224 | 225 | # Upload logs 226 | if config.DONT_UPLOAD_LOGS: 227 | print("Not uploading logs as DONT_UPLOAD_LOGS is True") 228 | else: 229 | log.info("Uploading logs...") 230 | upload_logs("used_folx") 231 | upload_logs("used_treats") 232 | upload_logs("as-a-treat.log") 233 | log.info("Finished uploading logs") 234 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Mastodon.py==1.8.1 2 | tox==4.23.2 -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipsdist = True 3 | envlist = flake8, black, isort, test 4 | 5 | [testenv:fix] 6 | commands = 7 | black . 8 | isort --profile black . 9 | deps = 10 | black==23.3.0 11 | isort==5.12.0 12 | 13 | [testenv:flake8] 14 | # Example usage: 15 | # 16 | # tox -e flake8 -- --statistics 17 | # 18 | commands = flake8 {posargs} 19 | deps = flake8==6.0.0 20 | 21 | [testenv:black] 22 | commands = black --check --diff . 23 | deps = black==23.3.0 24 | 25 | [testenv:isort] 26 | commands = isort --profile black --check --diff . 27 | deps = 28 | isort==5.12.0 29 | black==23.3.0 30 | 31 | [testenv:test] 32 | commands = pytest 33 | deps = pytest 34 | 35 | [flake8] 36 | exclude = 37 | venv, 38 | .venv, 39 | .tox, 40 | __pycache__, 41 | config.py 42 | max-line-length = 286 43 | ignore = W503, E722, E231 44 | 45 | [isort] 46 | profile = black 47 | multi_line_output = 3 48 | no_sections = true 49 | --------------------------------------------------------------------------------