Compare commits

...

272 Commits

Author SHA1 Message Date
WTMike24 0aaffbf5f0 Added quick and dirty escape to asterisks.
3 years ago
bobloy 771d1457e5 Bump to latest version of Chatterbot
3 years ago
bobloy d191abf6bd Only update if it's enabled (not all keys)
3 years ago
bobloy aba9840bcb Less verbose debug
3 years ago
bobloy 0064e6e9b5 More verbose debug mode
3 years ago
bobloy 955624ad1a Use greedy converter, no more commas
3 years ago
bobloy a6e0646b85
Merge pull request #186 from ScaredDonut/master
4 years ago
bobloy 269266ce04
Merge pull request #189 from aleclol/master
4 years ago
bobloy db3ce30122 Add back embed description, now required by discord
4 years ago
bobloy 1e87eacf83 Add edited timestamp
4 years ago
bobloy 61fa006e33 QoL fixes
4 years ago
bobloy 4183607372 Add option for skipping bots
4 years ago
bobloy 2421c4e9bf Add sm and md to requirements
4 years ago
bobloy b2e843e781 Invalid timestring instead of error
4 years ago
aleclol a7ce815e14
Merge branch 'bobloy:master' into master
4 years ago
bobloy 1e535c2f3e
Merge pull request #193 from XargsUK/master
4 years ago
bobloy fd80819618 Merge branch 'master' into XargsUK_master
4 years ago
bobloy a5ff888f4c black reformat
4 years ago
bobloy 3eb499bf0e black reformat
4 years ago
bobloy 698dafade4 Add chatter initialization
4 years ago
Brad Duncan 15ecf72c64 Update recyclingplant.py
4 years ago
Brad Duncan 1d514f80c6 adding no response exit condition
4 years ago
bobloy b752bfd153 Stop looking at DMs
4 years ago
bobloy 47269ba8f4 fix has_table, move age and minutes to trainset
4 years ago
bobloy e0a361b952 remove en_core_web_sm as dependency
4 years ago
bobloy 2a18760a83
Merge pull request #191 from bobloy/chatter_fox
4 years ago
bobloy b26afdf2db Merge branch 'master' into chatter_fox
4 years ago
bobloy 86cc1fa35a Don't specify pyaml
4 years ago
aleclol b331238c1c
Move .get_text() to after None check
4 years ago
bobloy 6f0c88b1ac change default models, fix requirements
4 years ago
bobloy c165313031 no reply errors in cache
4 years ago
bobloy 9c63c12656 Don't need requirements file
4 years ago
bobloy a55ae8a511 Use Bobloy's chatterbot fork
4 years ago
Alexander Soloviev 1ffac638ed Update infochannel.py
4 years ago
Alexander Soloviev 5b75002c88 Update infochannel.py
4 years ago
Alexander Soloviev 7c08a5de0c Update infochannel.py
4 years ago
bobloy bc12aa866e Fix formatting
4 years ago
bobloy dbafd6ffd7
Merge pull request #183 from aleclol/master
4 years ago
aleclol 52ca2f6a45
Update lovecalculator.py
4 years ago
bobloy 2937b6ac92 List of dicts
4 years ago
bobloy dbf84c8b81
Merge pull request #180 from phenom4n4n/patch-1
4 years ago
PhenoM4n4n 2f21de6a97 lovecalc attributeerror
4 years ago
bobloy ed6cc433c8 Remove pycountry
4 years ago
bobloy e1a30359d8
Merge pull request #148 from BogdanWDK/master
4 years ago
bobloy 506a79c6d6 Removing debugging print
4 years ago
bobloy 1ddcd98078 Implement languages
4 years ago
bobloy 7bd004c78e Merge branch 'master' into BogdanWDK_master
4 years ago
bobloy 184833421a
Merge pull request #178 from Kreusada/patch-1
4 years ago
Kreusada f04ff6886b
[SCP] Remove double setup
4 years ago
bobloy 28edcc1fdd deliminate on space not newline
4 years ago
bobloy 10ed1f9b9f pagify lots of stolen emojis
4 years ago
bobloy 93b403b35f Better progress logging
4 years ago
bobloy 5f30bc1234 Add multiple channel training
4 years ago
bobloy 9f22dfb790 Swap learning to global config
4 years ago
bobloy ea126db0c5
Merge pull request #175 from bobloy/chatter_develop
4 years ago
bobloy e1297a4dca Return success value
4 years ago
bobloy 87187abbb3 Fix logging
4 years ago
bobloy db24bb4db4 No differences
4 years ago
bobloy 1319d98972 Less often, still writing too much.
4 years ago
bobloy 802929d757 better wording
4 years ago
bobloy 59fd96fc5a add save_every for less disk intensive work.
4 years ago
bobloy b4f20dd7d2 Don't print everything, use log
4 years ago
bobloy ac9cf1e589 Implement movie trainer, guild cache, and learning toggle
4 years ago
bobloy 8feb21e34b Add new kaggle trainers
4 years ago
bobloy 04ccb435f8 Implement `check_same_thread` = False storage adapter.
4 years ago
bobloy eac7aee82c Save every 50 instead of all at once, so it can be cancelled
4 years ago
bobloy 8200cd9af1 Run futures correctly
4 years ago
bobloy ff9610ff77 Merge branch 'master' into chatter_develop
4 years ago
bobloy 95931d24f3
Merge pull request #174 from sourcery-ai-bot/master
4 years ago
bobloy dad14fe972 black reformatting
4 years ago
bobloy ea88addc42 black refactoring
4 years ago
Sourcery AI 0475b18437 'Refactored by Sourcery'
4 years ago
bobloy 7811c71edb Use is_reply to train
4 years ago
bobloy 8acbc5d964 Whatever this commit is
4 years ago
bobloy 59090b9eaa Merge branch 'master' into chatter_develop
4 years ago
bobloy 11eb4a9dbf
Merge pull request #172 from Lifeismana/master
4 years ago
bobloy d32de1586f Reply enabled by default cause it's cool.
4 years ago
bobloy 578ea4a555 Consistently avoid guild.icon
4 years ago
bobloy 920f8817d7 Use guild.id and author.id for file name, support using jpg
4 years ago
Antoine Rybacki 42bdc64028 Black format fix
4 years ago
Antoine Rybacki f7dad0aa3f [Chatter] Bot will respond to reply
4 years ago
bobloy cc199c395d Discord file now takes a path, so just give em a path.
4 years ago
Antoine Rybacki 5a26b48fda [Chatter] Allow bot to reply to maintain conversation continuity
4 years ago
bobloy bf9115e13c Update for new channel
4 years ago
bobloy a5eda8ca2a Forgot how to use regex apparently
4 years ago
bobloy 221ca4074b Update launchlib to version 2
4 years ago
bobloy 8b1ac18609 Merge remote-tracking branch 'origin/master'
4 years ago
bobloy ee8f6bbf57 Fix docstrings
4 years ago
bobloy af3de08da2
Merge pull request #169 from Obi-Wan3/master
4 years ago
Obi-Wan3 92957bcb1f
implement same logic for skipping further checks
4 years ago
Obi-Wan3 7ad6b15641
Edit to satisfy style requirement
4 years ago
Obi-Wan3 6363f5eadc
[StealEmoji] update to use guild.emoji_limit
4 years ago
bobloy 0e034d83ef Bad idea to steal these, set the empty by default instead
4 years ago
bobloy 337def2fa3 Some progress on updated ubuntu trainer
4 years ago
bobloy 14f8b825d8 Fix bad learning and checks
4 years ago
bobloy dbf6ba5a4b More precise message imitation
4 years ago
bobloy bf16630573
Merge pull request #168 from bobloy/fifo_finetuned_message
4 years ago
bobloy 6233db2272 FakeMessage is subclass and the implications
4 years ago
bobloy 3f997fa804 Fix to pickle error and Nonetype comparison
4 years ago
bobloy 837cff7a26
Merge pull request #167 from bobloy/fifo_develop
4 years ago
bobloy 796edb4d35 Corrected expired triggers
4 years ago
bobloy 6c669dd170 Change typing
4 years ago
bobloy 9bdaf73944 Add __str__ and TODO for better "seeing"
4 years ago
bobloy 3b50785c5b Docs update, better delete
4 years ago
bobloy d14db16746 Small doc update
4 years ago
bobloy 2c9f3838da Update info to include install instructions
4 years ago
bobloy 9f10ea262d Semantic change
4 years ago
bobloy 320f729cc9 Print expired triggers separately
4 years ago
bobloy 9c9b46dc76 Print expired triggers separately
4 years ago
bobloy d5bc5993ea Nevermind, bad idea. Just add the checks
4 years ago
bobloy ce41c80c3b Remove `fetch_message`, channel history is just better
4 years ago
bobloy c603e4b326 Merge branch 'master' into fifo_develop
4 years ago
bobloy d13fd39cfc Require python-dateutil
4 years ago
bobloy 8dc81808e6 Merge remote-tracking branch 'origin/master'
4 years ago
bobloy b2c8268c9b Update labeler
4 years ago
bobloy f3dab0f0c6 Fix construction of set
4 years ago
bobloy bae50f6a7a
Merge pull request #164 from bobloy/werewolf_develop
4 years ago
bobloy 5892bed5b9 Didn't do werewolf label right
4 years ago
bobloy 36826a44e7 Mostly line separators
4 years ago
bobloy 0a0e8650e4 Merge branch 'master' into werewolf_develop
4 years ago
bobloy d36493f5a8
Merge pull request #163 from ASSASSIN0831/master
4 years ago
bobloy fc8e465c33 Remove excess comments
4 years ago
bobloy 087b10deb2 Merge branch 'master' into ASSASSIN0831_master
4 years ago
bobloy bf3c292fee Black formatting
4 years ago
bobloy c7820ec40c No asyncio here
4 years ago
bobloy b566b58e1a Infochannel rewrite
4 years ago
ASSASSIN0831 69e2e5acb3
Black formatting
4 years ago
ASSASSIN0831 bce07f069f
Update infochannel.py
4 years ago
ASSASSIN0831 9ac89aa369
The big update
4 years ago
bobloy 624e8863b1 Additional expired trigger handling
4 years ago
bobloy 51dc2e62d4 Use customdate, check expired before scheduling
4 years ago
bobloy d85f166062 Custom Date that doesn't do past dates
4 years ago
bobloy 5ecb8dc826 Don't schedule jobs without a trigger
4 years ago
bobloy 9411fff5e8 Clear old code, shutdown manually, AsyncIter steps
4 years ago
bobloy 0ff56d933b Make relative times better, add fifo wakeup
4 years ago
bobloy 8ab6c50625 HOTFIX: Don't be crzy with pytz timezones
4 years ago
bobloy 19ee6e6f24 Add and remove comments
4 years ago
bobloy b2ebddc825 I forgot to add the bot object for some reason.
4 years ago
bobloy 907fb76574
Merge pull request #159 from bobloy/fifo_develop
4 years ago
bobloy 3c3dd2d6cd Correctly handle backwards compatibility
4 years ago
bobloy a946b1b83b Catch error better
4 years ago
bobloy ca8c762e69 FIFO resturcture
4 years ago
bobloy 721316a14e
Merge pull request #158 from bobloy/fifo_add_relativetrigger
4 years ago
bobloy 40b01cff26 Black formatting
4 years ago
bobloy 2ea077bb0c Add relative trigger, better error handling
4 years ago
bobloy 99ab9fc1b4 [HOTFIX] Fix not applying the similarity threshold
4 years ago
bobloy a2948322f9 Download ubuntu data to the cog data directory
4 years ago
bobloy 419863b07a Better error logging.
4 years ago
bobloy 8015e4a46d Hotfix scheduling snowflake issue
4 years ago
bobloy 1f1d116a56 Docstring update
4 years ago
bobloy db538f7530 Hotfix cause I didn't test this
4 years ago
bobloy 3bb6af9b9b
Merge pull request #156 from bobloy/fifo_fixmultitrigger
4 years ago
bobloy fc0870af68 Fix multi-triggers, add `checktask` to see more scheduled executions
4 years ago
bobloy 5fffaf4893 cog_unload, not __unload, whoops.
4 years ago
bobloy 477364f9bf
Merge pull request #154 from bobloy/chatter_checks
4 years ago
bobloy 6e2c62d897 Add appropriate checks
4 years ago
bobloy 675e9b82c8
Update README.md
4 years ago
bobloy 4634210960 Add automatic install option
4 years ago
bobloy a6ebe02233 Back to basics
4 years ago
bobloy 26234e3b18 Alternate dependencies attempt
4 years ago
bobloy 37c699eeee Merge branch 'master' into chatter_develop
4 years ago
bobloy 5f58d1d658
Merge pull request #152 from bobloy/stealemoji_develop
4 years ago
bobloy 5ddafff59f Add ability to delete autobanked guildbanks
4 years ago
bobloy d71e3afb86 Fix re-adding roles bug
4 years ago
bobloy 5611f7abe7 Don't commit before testing
4 years ago
bobloy 1e8d1efb57 Respect embed color
4 years ago
bobloy a92c373b49 New gitignore
4 years ago
bobloy 60806fb19c
Merge pull request #151 from bobloy/timerole_develop
4 years ago
bobloy 4c1cd86930 Better time keepking doesn't include tick
4 years ago
bobloy 10767da507 Clear the reapply logic if the role is deleted
4 years ago
bobloy c63a4923e7 Actually do the logic right
4 years ago
bobloy b141accbd9 Timerole rewrite WIP
4 years ago
bobloy 44035b78f7 Timerole rewrite
4 years ago
bobloy 815cfcb031 Add member role WIP
4 years ago
bobloy 8e0105355c fix ww_stop bug when no game is running
4 years ago
bobloy ffbed8cb9a
Merge pull request #150 from bobloy/lovecalc_image
4 years ago
bobloy 5752ba6056 Black formatting
4 years ago
bobloy 479b23f0f3 Get love image right (when cert is fixed)
4 years ago
bobloy 54be5addb5
Merge pull request #149 from bobloy/audiotrivia_update
4 years ago
bobloy 9440f34669 lovecalculator hotfix ssl error
4 years ago
bobloy 7c95bd4c0f Black formatting
4 years ago
bobloy 3fceea634b Audiotrivia updates from lessons learned attempting core
4 years ago
bobloy b210f4a9ff Uppercase key
4 years ago
bobloy da754e3cb2 Update to latest version of labeler
4 years ago
BogdanWDK 960b66a5b8 Language Support Added
4 years ago
bobloy 20d8acc800
Merge pull request #147 from bobloy/werewolf-develop
4 years ago
bobloy 266b0a485d Alpha ready changes
4 years ago
bobloy d0445f41c7 Black format update
4 years ago
bobloy 7f8d0f13f7 Black format
4 years ago
bobloy c529d792e6 Fix double game_end
4 years ago
bobloy 62a70c52c6 Some weird error with dm-ing keeps happening, add better log to catch it
4 years ago
bobloy 19104241d7 Don't await task cancels
4 years ago
bobloy 8a4893c5f5 Forgot the f in fstring
4 years ago
bobloy 9ca5d37f7e Fixed a variable reuse, channel naming, bot's can't play, less bold, object deletion error catching
4 years ago
bobloy f3965b73d8 Mostly messaging adjustments, fix for failing to talley votes
4 years ago
bobloy e27cfba763 Move to italics
4 years ago
bobloy 211df56e1b Add repr
4 years ago
bobloy 443c84ccab Fix `night_messages` to `night_results`
4 years ago
bobloy c7d320ccaa WIP Player converter
4 years ago
bobloy 2ab87866dd Adjust to italics, fix generate targets to be more obvious and readable, fix `night_messages` confusion with `night_results`
4 years ago
bobloy a691b2b85a Merge branch 'master' into werewolf-develop
4 years ago
bobloy 8c0a1db06f v2 checkout
4 years ago
bobloy cd89bd87e9 Only pull requests
4 years ago
bobloy 31c2e77be6 Merge branch 'master' into werewolf-develop
4 years ago
bobloy 3a6d3df374 More WIP progress
4 years ago
bobloy 94aceb32e8 Update issue templates
4 years ago
bobloy 693964183c Update issue templates
4 years ago
bobloy 8a42b87bd6 black
4 years ago
bobloy a36a800b45 named black
4 years ago
bobloy af41d079d3 Add black check
4 years ago
bobloy ad66d171d4 Forgot the contents
4 years ago
bobloy b9d8be397c Add workflow labeler
4 years ago
bobloy 70d50c5e97
Merge pull request #146 from bobloy/audiotrivia_newvideogames
4 years ago
bobloy b27b252e6f Add new videogames trivia list
4 years ago
bobloy ab1b069ee9 Move games to non-functional
4 years ago
bobloy af4cd92488 Correctly reset channels
4 years ago
bobloy 03f0ef17be Fix aggressive refactor
4 years ago
bobloy db1d64ae3e More async iters
4 years ago
bobloy 029b6a51b1 black
4 years ago
bobloy 61049c2343 Adding constants
4 years ago
bobloy eb0c79ef1d Introduction of The Blob
4 years ago
bobloy a0c645bd28 Precarious import order
4 years ago
bobloy 762b0fd320 WIP Twitter training
4 years ago
bobloy 224ff93531 black and __all__
4 years ago
bobloy f263f97cc2 Update builder to accept any number of roles
4 years ago
bobloy 61d1313411 Switch game to handle daytime smoother allowing cancellation
4 years ago
bobloy cb0a7f1041 Add priority and parameters
4 years ago
bobloy 39801aada9 Missed priority
4 years ago
bobloy eaa3e0a2f7 Fix listener parameters and priority
4 years ago
bobloy 8ffc8cc707 Missed the listener update
4 years ago
bobloy 84ed2728e7 switch to log
4 years ago
bobloy 596865e49d Dad hotfix. Don't listen to bots
4 years ago
bobloy 5940ab1af9
Merge pull request #145 from bobloy/fifo_develop
4 years ago
bobloy 9f17bca226 Change imports
4 years ago
bobloy bdcb74587e Bump to BETA, change requirements
4 years ago
bobloy 8531ff5f91 Timezone support and Aik Timezone cog integration
4 years ago
bobloy bed6cf8bb7 Merge branch 'master' into fifo_develop
4 years ago
bobloy d13331d52d black
4 years ago
bobloy 9ea57ee99a
Merge pull request #143 from bobloy/fifo_long_lists
4 years ago
bobloy 608f425965 Fix very long lists
4 years ago
bobloy b04e82fa1d
Merge pull request #142 from bobloy/audiotrivia_develop
4 years ago
bobloy f05a8bf4f6 *some* games fixed
4 years ago
bobloy bf81d7c157 Embeds and track variable
4 years ago
bobloy e0042780a1 Better detection of bad questions in trivia
4 years ago
bobloy 98ae481d14 Show valid settings correctly
4 years ago
bobloy 29aa493033 Better checking of valid settings
4 years ago
bobloy 7109471c35 WIP listeners, switch to f strings, and overall rewrite
4 years ago
bobloy a2eaf55515 Priority update for listeners
4 years ago
bobloy 06af229a62 non-relative imports
4 years ago
bobloy 8a3f45bdc1 Listener structure major change
4 years ago
bobloy ec5d713fa0 Correct listeners
4 years ago
bobloy 1723dc381d More listener
4 years ago
bobloy c428fa3131
Merge pull request #138 from bobloy/audiotrivia_develop
4 years ago
bobloy f69e8fdb1a Handle track errors gracefully
4 years ago
bobloy 28bf2a73e1 Still going on events
4 years ago
bobloy a046102549 Hotfix maybe_send_embed in lseen
4 years ago
bobloy fe1f11b2eb Maybe dispatch? WIP
4 years ago
bobloy 7e1a6e108e Logs mainly
4 years ago
bobloy 339492d6d9 Black formatting
4 years ago
bobloy 7a9fb922bd Merge branch 'master' into werewolf-develop
4 years ago
bobloy 18e5cc12ff Black formatting
4 years ago
bobloy 8ecdf45fa7 One more error handler
4 years ago
bobloy 88ef475339 Bring reactrestrict into the modern discord era
4 years ago
bobloy 55656ea672
Merge pull request #136 from jack1142/patch-1
4 years ago
jack1142 eddac5b8b2
[ccrole] Specify an audit log reason
4 years ago
bobloy 58054c7a92 Rate limits are for NERDS
4 years ago
bobloy 2e000b1190 "tools" is more common
4 years ago
bobloy d377461602 WIP adding timezone to Cron triggers
4 years ago
bobloy 849262969c forgot some await's
6 years ago
bobloy 53d817756a Merge branch 'master' into werewolf-develop
6 years ago
bobloy 0ee0199b11 role `__init__.py` experiment
6 years ago
bobloy 0d59a5220f ctx.me instead of ctx.guild.me to work in dm's
6 years ago

@ -0,0 +1,26 @@
---
name: Bug report
about: Create an issue to report a bug
title: ''
labels: bug
assignees: bobloy
---
**Describe the bug**
<!--A clear and concise description of what the bug is.-->
**To Reproduce**
<!--Steps to reproduce the behavior:-->
1. Load cog '...'
2. Run command '....'
3. See error
**Expected behavior**
<!--A clear and concise description of what you expected to happen.-->
**Screenshots or Error Messages**
<!--If applicable, add screenshots to help explain your problem.-->
**Additional context**
<!--Add any other context about the problem here.-->

@ -0,0 +1,14 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature Request]"
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
<!--A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]-->
**Describe the solution you'd like**
<!--A clear and concise description of what you want to happen. Include which cog or cogs this would interact with-->

@ -0,0 +1,26 @@
---
name: New AudioTrivia List
about: Submit a new AudioTrivia list to be added
title: "[AudioTrivia Submission]"
labels: 'cog: audiotrivia'
assignees: bobloy
---
**What is this trivia list?**
<!--What's in the list? What kind of category is?-->
**Number of Questions**
<!--Rough estimate at the number of question in this list-->
**Original Content?**
<!--Did you come up with this list yourself or did you get it from some else's work?-->
<!--If no, be sure to include the source-->
- [ ] Yes
- [ ] No
**Did I test the list?**
<!--Did you already try out the list and find no bugs?-->
- [ ] Yes
- [ ] No

@ -0,0 +1,62 @@
'cog: announcedaily':
- announcedaily/*
'cog: audiotrivia':
- audiotrivia/*
'cog: ccrole':
- ccrole/*
'cog: chatter':
- chatter/*
'cog: conquest':
- conquest/*
'cog: dad':
- dad/*
'cog: exclusiverole':
- exclusiverole/*
'cog: fifo':
- fifo/*
'cog: firstmessage':
- firstmessage/*
'cog: flag':
- flag/*
'cog: forcemention':
- forcemention/*
'cog: hangman':
- hangman
'cog: infochannel':
- infochannel/*
'cog: isitdown':
- isitdown/*
'cog: launchlib':
- launchlib/*
'cog: leaver':
- leaver/*
'cog: lovecalculator':
- lovecalculator/*
'cog: lseen':
- lseen/*
'cog: nudity':
- nudity/*
'cog: planttycoon':
- planttycoon/*
'cog: qrinvite':
- qrinvite/*
'cog: reactrestrict':
- reactrestrict/*
'cog: recyclingplant':
- recyclingplant/*
'cog: rpsls':
- rpsls/*
'cog: sayurl':
- sayurl/*
'cog: scp':
- scp/*
'cog: stealemoji':
- stealemoji/*
'cog: timerole':
- timerole/*
'cog: tts':
- tts/*
'cog: unicode':
- unicode/*
'cog: werewolf':
- werewolf/*

@ -0,0 +1,20 @@
# GitHub Action that uses Black to reformat the Python code in an incoming pull request.
# If all Python code in the pull request is compliant with Black then this Action does nothing.
# Othewrwise, Black is run and its changes are committed back to the incoming pull request.
# https://github.com/cclauss/autoblack
name: black
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: '3.8'
- name: Install Black
run: pip install --upgrade --no-cache-dir black
- name: Run black --check .
run: black --check --diff -l 99 .

@ -0,0 +1,19 @@
# This workflow will triage pull requests and apply a label based on the
# paths that are modified in the pull request.
#
# To use this workflow, you will need to set up a .github/labeler.yml
# file with configuration. For more information, see:
# https://github.com/actions/labeler
name: Labeler
on: [pull_request_target]
jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@2.2.0
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

1
.gitignore vendored

@ -4,3 +4,4 @@ venv/
v-data/
database.sqlite3
/venv3.4/
/.venv/

@ -53,7 +53,7 @@ Check out *Deprecated* my V2 cogs at [Fox-Cogs v2](https://github.com/bobloy/Fox
# Contact
Get support on the [Third Party Cog Server](https://discord.gg/GET4DVk)
Feel free to @ me in the #support_othercogs channel
Feel free to @ me in the #support_fox-v3 channel
Discord: Bobloy#6513

@ -54,8 +54,7 @@ class AnnounceDaily(Cog):
Do `[p]help annd <subcommand>` for more details
"""
if ctx.invoked_subcommand is None:
pass
pass
@commands.command()
@checks.guildowner()

@ -1,21 +1,25 @@
"""Module to manage audio trivia sessions."""
import asyncio
import logging
import lavalink
from redbot.cogs.trivia import TriviaSession
from redbot.cogs.trivia.session import _parse_answers
from redbot.core.utils.chat_formatting import bold
log = logging.getLogger("red.fox_v3.audiotrivia.audiosession")
class AudioSession(TriviaSession):
"""Class to run a session of audio trivia"""
def __init__(self, ctx, question_list: dict, settings: dict, player: lavalink.Player):
def __init__(self, ctx, question_list: dict, settings: dict, audio=None):
super().__init__(ctx, question_list, settings)
self.player = player
self.audio = audio
@classmethod
def start(cls, ctx, question_list, settings, player: lavalink.Player = None):
session = cls(ctx, question_list, settings, player)
def start(cls, ctx, question_list, settings, audio=None):
session = cls(ctx, question_list, settings, audio)
loop = ctx.bot.loop
session._task = loop.create_task(session.run())
return session
@ -23,52 +27,95 @@ class AudioSession(TriviaSession):
async def run(self):
"""Run the audio trivia session.
In order for the trivia session to be stopped correctly, this should
only be called internally by `TriviaSession.start`.
"""
In order for the trivia session to be stopped correctly, this should
only be called internally by `TriviaSession.start`.
"""
await self._send_startup_msg()
max_score = self.settings["max_score"]
delay = self.settings["delay"]
audio_delay = self.settings["audio_delay"]
timeout = self.settings["timeout"]
for question, answers in self._iter_questions():
if self.audio is not None:
import lavalink
player = lavalink.get_player(self.ctx.guild.id)
player.store("channel", self.ctx.channel.id) # What's this for? I dunno
await self.audio.set_player_settings(self.ctx)
else:
lavalink = None
player = False
for question, answers, audio_url in self._iter_questions():
async with self.ctx.typing():
await asyncio.sleep(3)
self.count += 1
await self.player.stop()
msg = "**Question number {}!**\n\nName this audio!".format(self.count)
await self.ctx.send(msg)
# print("Audio question: {}".format(question))
# await self.ctx.invoke(self.audio.play(ctx=self.ctx, query=question))
# ctx_copy = copy(self.ctx)
msg = bold(f"Question number {self.count}!") + f"\n\n{question}"
if player:
await player.stop()
if audio_url:
if not player:
log.debug("Got an audio question in a non-audio trivia session")
continue
# await self.ctx.invoke(self.player.play, query=question)
query = question.strip("<>")
tracks = await self.player.get_tracks(query)
seconds = tracks[0].length / 1000
load_result = await player.load_tracks(audio_url)
if (
load_result.has_error
or load_result.load_type != lavalink.enums.LoadType.TRACK_LOADED
):
await self.ctx.maybe_send_embed(
"Audio Track has an error, skipping. See logs for details"
)
log.info(f"Track has error: {load_result.exception_message}")
continue
tracks = load_result.tracks
track = tracks[0]
seconds = track.length / 1000
track.uri = "" # Hide the info from `now`
if self.settings["repeat"] and seconds < audio_delay:
# Append it until it's longer than the delay
tot_length = seconds + 0
while tot_length < audio_delay:
player.add(self.ctx.author, track)
tot_length += seconds
else:
player.add(self.ctx.author, track)
if self.settings["repeat"] and seconds < delay:
tot_length = seconds + 0
while tot_length < delay:
self.player.add(self.ctx.author, tracks[0])
tot_length += seconds
else:
self.player.add(self.ctx.author, tracks[0])
if not self.player.current:
await self.player.play()
if not player.current:
await player.play()
await self.ctx.maybe_send_embed(msg)
log.debug(f"Audio question: {question}")
continue_ = await self.wait_for_answer(answers, delay, timeout)
continue_ = await self.wait_for_answer(
answers, audio_delay if audio_url else delay, timeout
)
if continue_ is False:
break
if any(score >= max_score for score in self.scores.values()):
await self.end_game()
break
else:
await self.ctx.send("There are no more questions!")
await self.ctx.maybe_send_embed("There are no more questions!")
await self.end_game()
async def end_game(self):
await super().end_game()
await self.player.disconnect()
if self.audio is not None:
await self.ctx.invoke(self.audio.command_disconnect)
def _iter_questions(self):
"""Iterate over questions and answers for this session.
Yields
------
`tuple`
A tuple containing the question (`str`) and the answers (`tuple` of
`str`).
"""
for question, q_data in self.question_list:
answers = _parse_answers(q_data["answers"])
_audio = q_data["audio"]
if _audio:
yield _audio, answers, question.strip("<>")
else:
yield question, answers, _audio

@ -1,21 +1,22 @@
import datetime
import logging
import pathlib
from typing import List
from typing import List, Optional
import discord
import lavalink
import yaml
from redbot.cogs.audio import Audio
from redbot.cogs.trivia import LOG
from redbot.cogs.trivia.trivia import InvalidListError, Trivia
from redbot.cogs.trivia.trivia import InvalidListError, Trivia, get_core_lists
from redbot.core import Config, checks, commands
from redbot.core.bot import Red
from redbot.core.data_manager import cog_data_path
from redbot.core.utils.chat_formatting import box
from redbot.core.utils.chat_formatting import bold, box
from .audiosession import AudioSession
# from redbot.cogs.audio.utils import userlimit
log = logging.getLogger("red.fox_v3.audiotrivia")
class AudioTrivia(Trivia):
@ -27,12 +28,11 @@ class AudioTrivia(Trivia):
def __init__(self, bot: Red):
super().__init__()
self.bot = bot
self.audio = None
self.audioconf = Config.get_conf(
self, identifier=651171001051118411410511810597, force_registration=True
)
self.audioconf.register_guild(delay=30.0, repeat=True)
self.audioconf.register_guild(audio_delay=30.0, repeat=True)
@commands.group()
@commands.guild_only()
@ -43,122 +43,112 @@ class AudioTrivia(Trivia):
settings_dict = await audioset.all()
msg = box(
"**Audio settings**\n"
"Answer time limit: {delay} seconds\n"
"Answer time limit: {audio_delay} seconds\n"
"Repeat Short Audio: {repeat}"
"".format(**settings_dict),
lang="py",
)
await ctx.send(msg)
@atriviaset.command(name="delay")
async def atriviaset_delay(self, ctx: commands.Context, seconds: float):
@atriviaset.command(name="timelimit")
async def atriviaset_timelimit(self, ctx: commands.Context, seconds: float):
"""Set the maximum seconds permitted to answer a question."""
if seconds < 4.0:
await ctx.send("Must be at least 4 seconds.")
return
settings = self.audioconf.guild(ctx.guild)
await settings.delay.set(seconds)
await ctx.send("Done. Maximum seconds to answer set to {}.".format(seconds))
await settings.audo_delay.set(seconds)
await ctx.maybe_send_embed(f"Done. Maximum seconds to answer set to {seconds}.")
@atriviaset.command(name="repeat")
async def atriviaset_repeat(self, ctx: commands.Context, true_or_false: bool):
"""Set whether or not short audio will be repeated"""
settings = self.audioconf.guild(ctx.guild)
await settings.repeat.set(true_or_false)
await ctx.send("Done. Repeating short audio is now set to {}.".format(true_or_false))
await ctx.maybe_send_embed(f"Done. Repeating short audio is now set to {true_or_false}.")
@commands.group(invoke_without_command=True)
@commands.guild_only()
async def audiotrivia(self, ctx: commands.Context, *categories: str):
"""Start trivia session on the specified category.
"""Start trivia session on the specified category or categories.
Includes Audio categories.
You may list multiple categories, in which case the trivia will involve
questions from all of them.
"""
if not categories and ctx.invoked_subcommand is None:
await ctx.send_help()
return
if self.audio is None:
self.audio: Audio = self.bot.get_cog("Audio")
if self.audio is None:
await ctx.send("Audio is not loaded. Load it and try again")
return
categories = [c.lower() for c in categories]
session = self._get_trivia_session(ctx.channel)
if session is not None:
await ctx.send("There is already an ongoing trivia session in this channel.")
return
status = await self.audio.config.status()
notify = await self.audio.config.guild(ctx.guild).notify()
if status:
await ctx.send(
f"It is recommended to disable audio status with `{ctx.prefix}audioset status`"
await ctx.maybe_send_embed(
"There is already an ongoing trivia session in this channel."
)
if notify:
await ctx.send(
f"It is recommended to disable audio notify with `{ctx.prefix}audioset notify`"
)
if not self.audio._player_check(ctx):
try:
if not ctx.author.voice.channel.permissions_for(
ctx.me
).connect or self.audio.is_vc_full(ctx.author.voice.channel):
return await ctx.send("I don't have permission to connect to your channel.")
await lavalink.connect(ctx.author.voice.channel)
lavaplayer = lavalink.get_player(ctx.guild.id)
lavaplayer.store("connect", datetime.datetime.utcnow())
except AttributeError:
return await ctx.send("Connect to a voice channel first.")
lavaplayer = lavalink.get_player(ctx.guild.id)
lavaplayer.store("channel", ctx.channel.id) # What's this for? I dunno
await self.audio.set_player_settings(ctx)
if not ctx.author.voice or ctx.author.voice.channel != lavaplayer.channel:
return await ctx.send(
"You must be in the voice channel to use the audiotrivia command."
)
return
trivia_dict = {}
authors = []
any_audio = False
for category in reversed(categories):
# We reverse the categories so that the first list's config takes
# priority over the others.
try:
dict_ = self.get_audio_list(category)
except FileNotFoundError:
await ctx.send(
"Invalid category `{0}`. See `{1}audiotrivia list`"
await ctx.maybe_send_embed(
f"Invalid category `{category}`. See `{ctx.prefix}audiotrivia list`"
" for a list of trivia categories."
"".format(category, ctx.prefix)
)
except InvalidListError:
await ctx.send(
await ctx.maybe_send_embed(
"There was an error parsing the trivia list for"
" the `{}` category. It may be formatted"
" incorrectly.".format(category)
f" the `{category}` category. It may be formatted"
" incorrectly."
)
else:
trivia_dict.update(dict_)
authors.append(trivia_dict.pop("AUTHOR", None))
is_audio = dict_.pop("AUDIO", False)
authors.append(dict_.pop("AUTHOR", None))
trivia_dict.update(
{_q: {"audio": is_audio, "answers": _a} for _q, _a in dict_.items()}
)
any_audio = any_audio or is_audio
continue
return
if not trivia_dict:
await ctx.send(
await ctx.maybe_send_embed(
"The trivia list was parsed successfully, however it appears to be empty!"
)
return
if not any_audio:
audio = None
else:
audio: Optional["Audio"] = self.bot.get_cog("Audio")
if audio is None:
await ctx.send("Audio lists were parsed but Audio is not loaded!")
return
status = await audio.config.status()
notify = await audio.config.guild(ctx.guild).notify()
if status:
await ctx.maybe_send_embed(
f"It is recommended to disable audio status with `{ctx.prefix}audioset status`"
)
if notify:
await ctx.maybe_send_embed(
f"It is recommended to disable audio notify with `{ctx.prefix}audioset notify`"
)
failed = await ctx.invoke(audio.command_summon)
if failed:
return
lavaplayer = lavalink.get_player(ctx.guild.id)
lavaplayer.store("channel", ctx.channel.id) # What's this for? I dunno
settings = await self.config.guild(ctx.guild).all()
audiosettings = await self.audioconf.guild(ctx.guild).all()
config = trivia_dict.pop("CONFIG", None)
config = trivia_dict.pop("CONFIG", {"answer": None})["answer"]
if config and settings["allow_override"]:
settings.update(config)
settings["lists"] = dict(zip(categories, reversed(authors)))
@ -166,22 +156,33 @@ class AudioTrivia(Trivia):
# Delay in audiosettings overwrites delay in settings
combined_settings = {**settings, **audiosettings}
session = AudioSession.start(
ctx=ctx, question_list=trivia_dict, settings=combined_settings, player=lavaplayer,
ctx,
trivia_dict,
combined_settings,
audio,
)
self.trivia_sessions.append(session)
LOG.debug("New audio trivia session; #%s in %d", ctx.channel, ctx.guild.id)
log.debug("New audio trivia session; #%s in %d", ctx.channel, ctx.guild.id)
@audiotrivia.command(name="list")
@commands.guild_only()
async def audiotrivia_list(self, ctx: commands.Context):
"""List available trivia categories."""
lists = set(p.stem for p in self._audio_lists())
msg = box("**Available trivia lists**\n\n{}".format(", ".join(sorted(lists))))
if len(msg) > 1000:
await ctx.author.send(msg)
return
await ctx.send(msg)
"""List available trivia including audio categories."""
lists = {p.stem for p in self._all_audio_lists()}
if await ctx.embed_requested():
await ctx.send(
embed=discord.Embed(
title="Available trivia lists",
colour=await ctx.embed_colour(),
description=", ".join(sorted(lists)),
)
)
else:
msg = box(bold("Available trivia lists") + "\n\n" + ", ".join(sorted(lists)))
if len(msg) > 1000:
await ctx.author.send(msg)
else:
await ctx.send(msg)
def get_audio_list(self, category: str) -> dict:
"""Get the audiotrivia list corresponding to the given category.
@ -198,7 +199,7 @@ class AudioTrivia(Trivia):
"""
try:
path = next(p for p in self._audio_lists() if p.stem == category)
path = next(p for p in self._all_audio_lists() if p.stem == category)
except StopIteration:
raise FileNotFoundError("Could not find the `{}` category.".format(category))
@ -210,13 +211,15 @@ class AudioTrivia(Trivia):
else:
return dict_
def _audio_lists(self) -> List[pathlib.Path]:
def _all_audio_lists(self) -> List[pathlib.Path]:
# Custom trivia lists uploaded with audiotrivia. Not necessarily audio lists
personal_lists = [p.resolve() for p in cog_data_path(self).glob("*.yaml")]
return personal_lists + get_core_lists()
# Add to that custom lists uploaded with trivia and core lists
return personal_lists + get_core_audio_lists() + self._all_lists()
def get_core_lists() -> List[pathlib.Path]:
def get_core_audio_lists() -> List[pathlib.Path]:
"""Return a list of paths for all trivia lists packaged with the bot."""
core_lists_path = pathlib.Path(__file__).parent.resolve() / "data/lists"
return list(core_lists_path.glob("*.yaml"))

@ -1,4 +1,5 @@
AUTHOR: Plab
AUDIO: "[Audio] Identify this Anime!"
https://www.youtube.com/watch?v=2uq34TeWEdQ:
- 'Hagane no Renkinjutsushi (2009)'
- '(2009) الخيميائي المعدني الكامل'

@ -1,4 +1,5 @@
AUTHOR: Lazar
AUDIO: "[Audio] Identify this NHL Team by their goal horn"
https://youtu.be/6OejNXrGkK0:
- Anaheim Ducks
- Anaheim

File diff suppressed because it is too large Load Diff

@ -1,13 +1,14 @@
AUTHOR: Plab
https://www.youtube.com/watch?v=--bWm9hhoZo:
NEEDS: New links for all songs.
https://www.youtube.com/watch?v=f9O2Rjn1azc:
- Transistor
https://www.youtube.com/watch?v=-4nCbgayZNE:
https://www.youtube.com/watch?v=PgUhYFkVdSY:
- Dark Cloud 2
- Dark Cloud II
https://www.youtube.com/watch?v=-64NlME4lJU:
https://www.youtube.com/watch?v=1T1RZttyMwU:
- Mega Man 7
- Mega Man VII
https://www.youtube.com/watch?v=-AesqnudNuw:
https://www.youtube.com/watch?v=AdDbbzuq1vY:
- Mega Man 9
- Mega Man IX
https://www.youtube.com/watch?v=-BmGDtP2t7M:

@ -3,24 +3,49 @@ import logging
import re
import discord
from discord.ext.commands import RoleConverter, Greedy, CommandError, ArgumentParsingError
from discord.ext.commands.view import StringView
from redbot.core import Config, checks, commands
from redbot.core.bot import Red
from redbot.core.utils.chat_formatting import box, pagify
from redbot.core.utils.mod import get_audit_reason
log = logging.getLogger("red.fox_v3.ccrole")
async def _get_roles_from_content(ctx, content):
content_list = content.split(",")
try:
role_list = [
discord.utils.get(ctx.guild.roles, name=role.strip(" ")).id for role in content_list
]
except (discord.HTTPException, AttributeError): # None.id is attribute error
return None
else:
return role_list
# greedy = Greedy[RoleConverter]
view = StringView(content)
rc = RoleConverter()
# "Borrowed" from discord.ext.commands.Command._transform_greedy_pos
result = []
while not view.eof:
# for use with a manual undo
previous = view.index
view.skip_ws()
try:
argument = view.get_quoted_word()
value = await rc.convert(ctx, argument)
except (CommandError, ArgumentParsingError):
view.index = previous
break
else:
result.append(value)
return [r.id for r in result]
# Old method
# content_list = content.split(",")
# try:
# role_list = [
# discord.utils.get(ctx.guild.roles, name=role.strip(" ")).id for role in content_list
# ]
# except (discord.HTTPException, AttributeError): # None.id is attribute error
# return None
# else:
# return role_list
class CCRole(commands.Cog):
@ -47,8 +72,7 @@ class CCRole(commands.Cog):
"""Custom commands management with roles
Highly customizable custom commands with role management."""
if not ctx.invoked_subcommand:
pass
pass
@ccrole.command(name="add")
@checks.mod_or_permissions(administrator=True)
@ -84,7 +108,7 @@ class CCRole(commands.Cog):
# Roles to add
await ctx.send(
"What roles should it add? (Must be **comma separated**)\n"
"What roles should it add?\n"
"Say `None` to skip adding roles"
)
@ -106,7 +130,7 @@ class CCRole(commands.Cog):
# Roles to remove
await ctx.send(
"What roles should it remove? (Must be comma separated)\n"
"What roles should it remove?\n"
"Say `None` to skip removing roles"
)
try:
@ -124,7 +148,7 @@ class CCRole(commands.Cog):
# Roles to use
await ctx.send(
"What roles are allowed to use this command? (Must be comma separated)\n"
"What roles are allowed to use this command?\n"
"Say `None` to allow all roles"
)
@ -142,8 +166,9 @@ class CCRole(commands.Cog):
return
# Selfrole
await ctx.send("Is this a targeted command?(yes/no)\n"
"No will make this a selfrole command")
await ctx.send(
"Is this a targeted command?(yes/no)\n" "No will make this a selfrole command"
)
try:
answer = await self.bot.wait_for("message", timeout=120, check=check)
@ -226,7 +251,7 @@ class CCRole(commands.Cog):
if not role_list:
return "None"
return ", ".join(
[discord.utils.get(ctx.guild.roles, id=roleid).name for roleid in role_list]
discord.utils.get(ctx.guild.roles, id=roleid).name for roleid in role_list
)
embed.add_field(name="Text", value="```{}```".format(cmd["text"]), inline=False)
@ -250,7 +275,7 @@ class CCRole(commands.Cog):
)
return
cmd_list = ", ".join([ctx.prefix + c for c in sorted(cmd_list.keys())])
cmd_list = ", ".join(ctx.prefix + c for c in sorted(cmd_list.keys()))
cmd_list = "Custom commands:\n\n" + cmd_list
if (
@ -290,13 +315,13 @@ class CCRole(commands.Cog):
# Thank you Cog-Creators
cmd = ctx.invoked_with
cmd = cmd.lower() # Continues the proud case_insentivity tradition of ccrole
cmd = cmd.lower() # Continues the proud case-insensitivity tradition of ccrole
guild = ctx.guild
# message = ctx.message # Unneeded since switch to `on_message_without_command` from `on_command_error`
cmdlist = self.config.guild(guild).cmdlist
cmd_list = self.config.guild(guild).cmdlist
# cmd = message.content[len(prefix) :].split()[0].lower()
cmd = await cmdlist.get_raw(cmd, default=None)
cmd = await cmd_list.get_raw(cmd, default=None)
if cmd is not None:
await self.eval_cc(cmd, message, ctx)
@ -323,9 +348,7 @@ class CCRole(commands.Cog):
async def eval_cc(self, cmd, message: discord.Message, ctx: commands.Context):
"""Does all the work"""
if cmd["proles"] and not (
set(role.id for role in message.author.roles) & set(cmd["proles"])
):
if cmd["proles"] and not {role.id for role in message.author.roles} & set(cmd["proles"]):
log.debug(f"{message.author} missing required role to execute {ctx.invoked_with}")
return # Not authorized, do nothing
@ -358,23 +381,24 @@ class CCRole(commands.Cog):
else:
target = message.author
reason = get_audit_reason(message.author)
if cmd["aroles"]:
arole_list = [
discord.utils.get(message.guild.roles, id=roleid) for roleid in cmd["aroles"]
]
try:
await target.add_roles(*arole_list)
await target.add_roles(*arole_list, reason=reason)
except discord.Forbidden:
log.exception(f"Permission error: Unable to add roles")
await ctx.send("Permission error: Unable to add roles")
await asyncio.sleep(1)
if cmd["rroles"]:
rrole_list = [
discord.utils.get(message.guild.roles, id=roleid) for roleid in cmd["rroles"]
]
try:
await target.remove_roles(*rrole_list)
await target.remove_roles(*rrole_list, reason=reason)
except discord.Forbidden:
log.exception(f"Permission error: Unable to remove roles")
await ctx.send("Permission error: Unable to remove roles")

@ -59,62 +59,50 @@ Install these on your windows machine before attempting the installation:
[Pandoc - Universal Document Converter](https://pandoc.org/installing.html)
## Methods
### Windows - Manually
#### Step 1: Built-in Downloader
### Automatic
This method requires some luck to pull off.
You need to get a copy of the requirements.txt provided with chatter, I recommend this method.
#### Step 1: Add repo and install cog
```
[p]repo add Fox https://github.com/bobloy/Fox-V3
[p]cog install Fox chatter
```
#### Step 2: Install Requirements
If you get an error at this step, stop and skip to one of the manual methods below.
Make sure you have your virtual environment that you installed Red on activated before starting this step. See the Red Docs for details on how.
#### Step 2: Install additional dependencies
In a terminal running as an admin, navigate to the directory containing this repo.
Here you need to decide which training models you want to have available to you.
I've used my install directory as an example.
Shutdown the bot and run any number of these in the console:
```
cd C:\Users\Bobloy\AppData\Local\Red-DiscordBot\Red-DiscordBot\data\bobbot\cogs\RepoManager\repos\Fox\chatter
pip install -r requirements.txt
pip install --no-deps "chatterbot>=1.1"
```
#### Step 3: Load Chatter
python -m spacy download en_core_web_sm # ~15 MB
```
[p]repo add Fox https://github.com/bobloy/Fox-V3 # If you didn't already do this in step 1
[p]cog install Fox chatter
[p]load chatter
```
python -m spacy download en_core_web_md # ~50 MB
### Linux - Manually
python -m spacy download en_core_web_lg # ~750 MB (CPU Optimized)
#### Step 1: Built-in Downloader
```
[p]cog install <Fox> Chatter
python -m spacy download en_core_web_trf # ~500 MB (GPU Optimized)
```
#### Step 2: Install Requirements
In your console with your virtual environment activated:
#### Step 3: Load the cog and get started
```
pip install --no-deps "chatterbot>=1.1"
[p]load chatter
```
### Step 3: Load Chatter
### Windows - Manually
Deprecated
```
[p]load chatter
```
### Linux - Manually
Deprecated
# Configuration
Chatter works out the the box without any training by learning as it goes,
Chatter works out the box without any training by learning as it goes,
but will have very poor and repetitive responses at first.
Initial training is recommended to speed up its learning.

@ -1,8 +1,10 @@
from .chat import Chatter
def setup(bot):
bot.add_cog(Chatter(bot))
async def setup(bot):
cog = Chatter(bot)
await cog.initialize()
bot.add_cog(cog)
# __all__ = (

@ -2,19 +2,24 @@ import asyncio
import logging
import os
import pathlib
from collections import defaultdict
from datetime import datetime, timedelta
from typing import Optional
from functools import partial
from typing import Dict, List, Optional
import discord
from chatterbot import ChatBot
from chatterbot.comparisons import JaccardSimilarity, LevenshteinDistance, SpacySimilarity
from chatterbot.response_selection import get_random_response
from chatterbot.trainers import ChatterBotCorpusTrainer, ListTrainer, UbuntuCorpusTrainer
from redbot.core import Config, commands
from redbot.core import Config, checks, commands
from redbot.core.commands import Cog
from redbot.core.data_manager import cog_data_path
from redbot.core.utils.predicates import MessagePredicate
from chatter.trainers import MovieTrainer, TwitterCorpusTrainer, UbuntuCorpusTrainer2
chatterbot_log = logging.getLogger("red.fox_v3.chatterbot")
log = logging.getLogger("red.fox_v3.chatter")
@ -25,6 +30,12 @@ def my_local_get_prefix(prefixes, content):
return None
class ENG_TRF:
ISO_639_1 = "en_core_web_trf"
ISO_639 = "eng"
ENGLISH_NAME = "English"
class ENG_LG:
ISO_639_1 = "en_core_web_lg"
ISO_639 = "eng"
@ -48,50 +59,77 @@ class Chatter(Cog):
This cog trains a chatbot that will talk like members of your Guild
"""
models = [ENG_SM, ENG_MD, ENG_LG, ENG_TRF]
algos = [SpacySimilarity, JaccardSimilarity, LevenshteinDistance]
def __init__(self, bot):
super().__init__()
self.bot = bot
self.config = Config.get_conf(self, identifier=6710497116116101114)
default_global = {}
default_guild = {"whitelist": None, "days": 1, "convo_delta": 15, "chatchannel": None}
default_global = {"learning": True, "model_number": 0, "algo_number": 0, "threshold": 0.90}
self.default_guild = {
"whitelist": None,
"days": 1,
"convo_delta": 15,
"chatchannel": None,
"reply": True,
}
path: pathlib.Path = cog_data_path(self)
self.data_path = path / "database.sqlite3"
# TODO: Move training_model and similarity_algo to config
# TODO: Add an option to see current settings
self.tagger_language = ENG_MD
self.tagger_language = ENG_SM
self.similarity_algo = SpacySimilarity
self.similarity_threshold = 0.90
self.chatbot = self._create_chatbot()
self.chatbot = None
# self.chatbot.set_trainer(ListTrainer)
# self.trainer = ListTrainer(self.chatbot)
self.config.register_global(**default_global)
self.config.register_guild(**default_guild)
self.config.register_guild(**self.default_guild)
self.loop = asyncio.get_event_loop()
self._guild_cache = defaultdict(dict)
self._global_cache = {}
self._last_message_per_channel: Dict[Optional[discord.Message]] = defaultdict(lambda: None)
async def red_delete_data_for_user(self, **kwargs):
"""Nothing to delete"""
return
async def initialize(self):
all_config = dict(self.config.defaults["GLOBAL"])
all_config.update(await self.config.all())
model_number = all_config["model_number"]
algo_number = all_config["algo_number"]
threshold = all_config["threshold"]
self.tagger_language = self.models[model_number]
self.similarity_algo = self.algos[algo_number]
self.similarity_threshold = threshold
self.chatbot = self._create_chatbot()
def _create_chatbot(self):
return ChatBot(
"ChatterBot",
storage_adapter="chatterbot.storage.SQLStorageAdapter",
# storage_adapter="chatterbot.storage.SQLStorageAdapter",
storage_adapter="chatter.storage_adapters.MyDumbSQLStorageAdapter",
database_uri="sqlite:///" + str(self.data_path),
statement_comparison_function=self.similarity_algo,
response_selection_method=get_random_response,
logic_adapters=["chatterbot.logic.BestMatch"],
maximum_similarity_threshold=self.similarity_threshold,
tagger_language=self.tagger_language,
logger=log,
logger=chatterbot_log,
)
async def _get_conversation(self, ctx, in_channel: discord.TextChannel = None):
async def _get_conversation(self, ctx, in_channels: List[discord.TextChannel]):
"""
Compiles all conversation in the Guild this bot can get it's hands on
Currently takes a stupid long time
@ -105,20 +143,12 @@ class Chatter(Cog):
return msg.clean_content
def new_conversation(msg, sent, out_in, delta):
# if sent is None:
# return False
# Don't do "too short" processing here. Sometimes people don't respond.
# if len(out_in) < 2:
# return False
# print(msg.created_at - sent)
# Should always be positive numbers
return msg.created_at - sent >= delta
for channel in ctx.guild.text_channels:
if in_channel:
channel = in_channel
for channel in in_channels:
# if in_channel:
# channel = in_channel
await ctx.maybe_send_embed("Gathering {}".format(channel.mention))
user = None
i = 0
@ -153,16 +183,47 @@ class Chatter(Cog):
except discord.HTTPException:
pass
if in_channel:
break
# if in_channel:
# break
return out
def _train_twitter(self, *args, **kwargs):
trainer = TwitterCorpusTrainer(self.chatbot)
trainer.train(*args, **kwargs)
return True
def _train_ubuntu(self):
trainer = UbuntuCorpusTrainer(self.chatbot)
trainer = UbuntuCorpusTrainer(
self.chatbot, ubuntu_corpus_data_directory=cog_data_path(self) / "ubuntu_data"
)
trainer.train()
return True
async def _train_movies(self):
trainer = MovieTrainer(self.chatbot, cog_data_path(self))
return await trainer.asynctrain()
async def _train_ubuntu2(self, intensity):
train_kwarg = {}
if intensity == 196:
train_kwarg["train_dialogue"] = False
train_kwarg["train_196"] = True
elif intensity == 301:
train_kwarg["train_dialogue"] = False
train_kwarg["train_301"] = True
elif intensity == 497:
train_kwarg["train_dialogue"] = False
train_kwarg["train_196"] = True
train_kwarg["train_301"] = True
elif intensity >= 9000: # NOT 9000!
train_kwarg["train_dialogue"] = True
train_kwarg["train_196"] = True
train_kwarg["train_301"] = True
trainer = UbuntuCorpusTrainer2(self.chatbot, cog_data_path(self))
return await trainer.asynctrain(**train_kwarg)
def _train_english(self):
trainer = ChatterBotCorpusTrainer(self.chatbot)
# try:
@ -174,13 +235,10 @@ class Chatter(Cog):
def _train(self, data):
trainer = ListTrainer(self.chatbot)
total = len(data)
# try:
for c, convo in enumerate(data, 1):
log.info(f"{c} / {total}")
if len(convo) > 1: # TODO: Toggleable skipping short conversations
print(f"{c} / {total}")
trainer.train(convo)
# except:
# return False
return True
@commands.group(invoke_without_command=False)
@ -188,9 +246,10 @@ class Chatter(Cog):
"""
Base command for this cog. Check help for the commands list.
"""
if ctx.invoked_subcommand is None:
pass
self._guild_cache[ctx.guild.id] = {} # Clear cache when modifying values
self._global_cache = {}
@commands.admin()
@chatter.command(name="channel")
async def chatter_channel(
self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None
@ -210,12 +269,55 @@ class Chatter(Cog):
await self.config.guild(ctx.guild).chatchannel.set(channel.id)
await ctx.maybe_send_embed(f"Chat channel is now {channel.mention}")
@commands.admin()
@chatter.command(name="reply")
async def chatter_reply(self, ctx: commands.Context, toggle: Optional[bool] = None):
"""
Toggle bot reply to messages if conversation continuity is not present
"""
reply = await self.config.guild(ctx.guild).reply()
if toggle is None:
toggle = not reply
await self.config.guild(ctx.guild).reply.set(toggle)
if toggle:
await ctx.maybe_send_embed(
"I will now respond to you if conversation continuity is not present"
)
else:
await ctx.maybe_send_embed(
"I will not reply to your message if conversation continuity is not present, anymore"
)
@commands.is_owner()
@chatter.command(name="learning")
async def chatter_learning(self, ctx: commands.Context, toggle: Optional[bool] = None):
"""
Toggle the bot learning from its conversations.
This is a global setting.
This is on by default.
"""
learning = await self.config.learning()
if toggle is None:
toggle = not learning
await self.config.learning.set(toggle)
if toggle:
await ctx.maybe_send_embed("I will now learn from conversations.")
else:
await ctx.maybe_send_embed("I will no longer learn from conversations.")
@commands.is_owner()
@chatter.command(name="cleardata")
async def chatter_cleardata(self, ctx: commands.Context, confirm: bool = False):
"""
This command will erase all training data and reset your configuration settings
This command will erase all training data and reset your configuration settings.
This applies to all guilds.
Use `[p]chatter cleardata True`
Use `[p]chatter cleardata True` to confirm.
"""
if not confirm:
@ -242,20 +344,18 @@ class Chatter(Cog):
await ctx.tick()
@commands.is_owner()
@chatter.command(name="algorithm", aliases=["algo"])
async def chatter_algorithm(
self, ctx: commands.Context, algo_number: int, threshold: float = None
):
"""
Switch the active logic algorithm to one of the three. Default after reload is Spacy
Switch the active logic algorithm to one of the three. Default is Spacy
0: Spacy
1: Jaccard
2: Levenshtein
"""
algos = [SpacySimilarity, JaccardSimilarity, LevenshteinDistance]
if algo_number < 0 or algo_number > 2:
await ctx.send_help()
return
@ -267,31 +367,33 @@ class Chatter(Cog):
)
return
else:
self.similarity_algo = threshold
self.similarity_threshold = threshold
await self.config.threshold.set(self.similarity_threshold)
self.similarity_algo = self.algos[algo_number]
await self.config.algo_number.set(algo_number)
self.similarity_algo = algos[algo_number]
async with ctx.typing():
self.chatbot = self._create_chatbot()
await ctx.tick()
@commands.is_owner()
@chatter.command(name="model")
async def chatter_model(self, ctx: commands.Context, model_number: int):
"""
Switch the active model to one of the three. Default after reload is Medium
Switch the active model to one of the three. Default is Small
0: Small
1: Medium
1: Medium (Requires additional setup)
2: Large (Requires additional setup)
3. Accurate (Requires additional setup)
"""
models = [ENG_SM, ENG_MD, ENG_LG]
if model_number < 0 or model_number > 2:
if model_number < 0 or model_number > 3:
await ctx.send_help()
return
if model_number == 2:
if model_number >= 0:
await ctx.maybe_send_embed(
"Additional requirements needed. See guide before continuing.\n" "Continue?"
)
@ -304,7 +406,8 @@ class Chatter(Cog):
if not pred.result:
return
self.tagger_language = models[model_number]
self.tagger_language = self.models[model_number]
await self.config.model_number.set(model_number)
async with ctx.typing():
self.chatbot = self._create_chatbot()
@ -312,7 +415,14 @@ class Chatter(Cog):
f"Model has been switched to {self.tagger_language.ISO_639_1}"
)
@chatter.command(name="minutes")
@commands.is_owner()
@chatter.group(name="trainset")
async def chatter_trainset(self, ctx: commands.Context):
"""Commands for configuring training"""
pass
@commands.is_owner()
@chatter_trainset.command(name="minutes")
async def minutes(self, ctx: commands.Context, minutes: int):
"""
Sets the number of minutes the bot will consider a break in a conversation during training
@ -323,11 +433,12 @@ class Chatter(Cog):
await ctx.send_help()
return
await self.config.guild(ctx.guild).convo_length.set(minutes)
await self.config.guild(ctx.guild).convo_delta.set(minutes)
await ctx.tick()
@chatter.command(name="age")
@commands.is_owner()
@chatter_trainset.command(name="age")
async def age(self, ctx: commands.Context, days: int):
"""
Sets the number of days to look back
@ -341,6 +452,16 @@ class Chatter(Cog):
await self.config.guild(ctx.guild).days.set(days)
await ctx.tick()
@commands.is_owner()
@chatter.command(name="kaggle")
async def chatter_kaggle(self, ctx: commands.Context):
"""Register with the kaggle API to download additional datasets for training"""
if not await self.check_for_kaggle():
await ctx.maybe_send_embed(
"[Click here for instructions to setup the kaggle api](https://github.com/Kaggle/kaggle-api#api-credentials)"
)
@commands.is_owner()
@chatter.command(name="backup")
async def backup(self, ctx, backupname):
"""
@ -362,7 +483,71 @@ class Chatter(Cog):
else:
await ctx.maybe_send_embed("Error occurred :(")
@chatter.command(name="trainubuntu")
@commands.is_owner()
@chatter.group(name="train")
async def chatter_train(self, ctx: commands.Context):
"""Commands for training the bot"""
pass
@chatter_train.group(name="kaggle")
async def chatter_train_kaggle(self, ctx: commands.Context):
"""
Base command for kaggle training sets.
See `[p]chatter kaggle` for details on how to enable this option
"""
pass
@chatter_train_kaggle.command(name="ubuntu")
async def chatter_train_kaggle_ubuntu(
self, ctx: commands.Context, confirmation: bool = False, intensity=0
):
"""
WARNING: Large Download! Trains the bot using *NEW* Ubuntu Dialog Corpus data.
"""
if not confirmation:
await ctx.maybe_send_embed(
"Warning: This command downloads ~800MB and is CPU intensive during training\n"
"If you're sure you want to continue, run `[p]chatter train kaggle ubuntu True`"
)
return
async with ctx.typing():
future = await self._train_ubuntu2(intensity)
if future:
await ctx.maybe_send_embed("Training successful!")
else:
await ctx.maybe_send_embed("Error occurred :(")
@chatter_train_kaggle.command(name="movies")
async def chatter_train_kaggle_movies(self, ctx: commands.Context, confirmation: bool = False):
"""
WARNING: Language! Trains the bot using Cornell University's "Movie Dialog Corpus".
This training set contains dialog from a spread of movies with different MPAA.
This dialog includes racism, sexism, and any number of sensitive topics.
Use at your own risk.
"""
if not confirmation:
await ctx.maybe_send_embed(
"Warning: This command downloads ~29MB and is CPU intensive during training\n"
"If you're sure you want to continue, run `[p]chatter train kaggle movies True`"
)
return
async with ctx.typing():
future = await self._train_movies()
if future:
await ctx.maybe_send_embed("Training successful!")
else:
await ctx.maybe_send_embed("Error occurred :(")
@chatter_train.command(name="ubuntu")
async def chatter_train_ubuntu(self, ctx: commands.Context, confirmation: bool = False):
"""
WARNING: Large Download! Trains the bot using Ubuntu Dialog Corpus data.
@ -370,8 +555,8 @@ class Chatter(Cog):
if not confirmation:
await ctx.maybe_send_embed(
"Warning: This command downloads ~500MB then eats your CPU for training\n"
"If you're sure you want to continue, run `[p]chatter trainubuntu True`"
"Warning: This command downloads ~500MB and is CPU intensive during training\n"
"If you're sure you want to continue, run `[p]chatter train ubuntu True`"
)
return
@ -379,11 +564,11 @@ class Chatter(Cog):
future = await self.loop.run_in_executor(None, self._train_ubuntu)
if future:
await ctx.send("Training successful!")
await ctx.maybe_send_embed("Training successful!")
else:
await ctx.send("Error occurred :(")
await ctx.maybe_send_embed("Error occurred :(")
@chatter.command(name="trainenglish")
@chatter_train.command(name="english")
async def chatter_train_english(self, ctx: commands.Context):
"""
Trains the bot in english
@ -396,11 +581,32 @@ class Chatter(Cog):
else:
await ctx.maybe_send_embed("Error occurred :(")
@chatter.command()
async def train(self, ctx: commands.Context, channel: discord.TextChannel):
@chatter_train.command(name="list")
async def chatter_train_list(self, ctx: commands.Context):
"""Trains the bot based on an uploaded list.
Must be a file in the format of a python list: ['prompt', 'response1', 'response2']
"""
Trains the bot based on language in this guild
if not ctx.message.attachments:
await ctx.maybe_send_embed("You must upload a file when using this command")
return
attachment: discord.Attachment = ctx.message.attachments[0]
a_bytes = await attachment.read()
await ctx.send("Not yet implemented")
@chatter_train.command(name="channel")
async def chatter_train_channel(
self, ctx: commands.Context, channels: commands.Greedy[discord.TextChannel]
):
"""
Trains the bot based on language in this guild.
"""
if not channels:
await ctx.send_help()
return
await ctx.maybe_send_embed(
"Warning: The cog may use significant RAM or CPU if trained on large data sets.\n"
@ -409,7 +615,7 @@ class Chatter(Cog):
)
async with ctx.typing():
conversation = await self._get_conversation(ctx, channel)
conversation = await self._get_conversation(ctx, channels)
if not conversation:
await ctx.maybe_send_embed("Failed to gather training data")
@ -434,7 +640,7 @@ class Chatter(Cog):
else:
await ctx.maybe_send_embed("Error occurred :(")
@commands.Cog.listener()
@Cog.listener()
async def on_message_without_command(self, message: discord.Message):
"""
Credit to https://github.com/Twentysix26/26-Cogs/blob/master/cleverbot/cleverbot.py
@ -451,7 +657,7 @@ class Chatter(Cog):
guild: discord.Guild = getattr(message, "guild", None)
if await self.bot.cog_disabled_in_guild(self, guild):
if guild is None or await self.bot.cog_disabled_in_guild(self, guild):
return
ctx: commands.Context = await self.bot.get_context(message)
@ -463,7 +669,18 @@ class Chatter(Cog):
# Thank you Cog-Creators
channel: discord.TextChannel = message.channel
if guild is not None and channel.id == await self.config.guild(guild).chatchannel():
if not self._guild_cache[guild.id]:
self._guild_cache[guild.id] = await self.config.guild(guild).all()
is_reply = False # this is only useful with in_response_to
if (
message.reference is not None
and isinstance(message.reference.resolved, discord.Message)
and message.reference.resolved.author.id == self.bot.user.id
):
is_reply = True # this is only useful with in_response_to
pass # this is a reply to the bot, good to go
elif guild is not None and channel.id == self._guild_cache[guild.id]["chatchannel"]:
pass # good to go
else:
when_mentionables = commands.when_mentioned(self.bot, message)
@ -478,10 +695,57 @@ class Chatter(Cog):
text = message.clean_content
async with channel.typing():
future = await self.loop.run_in_executor(None, self.chatbot.get_response, text)
async with ctx.typing():
if is_reply:
in_response_to = message.reference.resolved.content
elif self._last_message_per_channel[ctx.channel.id] is not None:
last_m: discord.Message = self._last_message_per_channel[ctx.channel.id]
minutes = self._guild_cache[ctx.guild.id]["convo_delta"]
if (datetime.utcnow() - last_m.created_at).seconds > minutes * 60:
in_response_to = None
else:
in_response_to = last_m.content
else:
in_response_to = None
# Always use generate reponse
# Chatterbot tries to learn based on the result it comes up with, which is dumb
log.debug("Generating response")
Statement = self.chatbot.storage.get_object("statement")
future = await self.loop.run_in_executor(
None, self.chatbot.generate_response, Statement(text)
)
if not self._global_cache:
self._global_cache = await self.config.all()
if in_response_to is not None and self._global_cache["learning"]:
log.debug("learning response")
await self.loop.run_in_executor(
None,
partial(
self.chatbot.learn_response,
Statement(text),
previous_statement=in_response_to,
),
)
replying = None
if (
"reply" not in self._guild_cache[guild.id] and self.default_guild["reply"]
) or self._guild_cache[guild.id]["reply"]:
if message != ctx.channel.last_message:
replying = message
if future and str(future):
await channel.send(str(future))
self._last_message_per_channel[ctx.channel.id] = await channel.send(
str(future), reference=replying
)
else:
await channel.send(":thinking:")
await ctx.send(":thinking:")
async def check_for_kaggle(self):
"""Check whether Kaggle is installed and configured properly"""
# TODO: This
return False

@ -2,22 +2,15 @@
"author": [
"Bobloy"
],
"min_bot_version": "3.4.0",
"description": "Create an offline chatbot that talks like your average member using Machine Learning",
"min_bot_version": "3.4.6",
"description": "Create an offline chatbot that talks like your average member using Machine Learning. See setup instructions at https://github.com/bobloy/Fox-V3/tree/master/chatter",
"hidden": false,
"install_msg": "Thank you for installing Chatter! Please make sure you check the install instructions at https://github.com/bobloy/Fox-V3/blob/master/chatter/README.md\nAfter that, get started ith `[p]load chatter` and `[p]help Chatter`",
"requirements": [
"git+git://github.com/gunthercox/chatterbot-corpus@master#egg=chatterbot_corpus",
"mathparse>=0.1,<0.2",
"nltk>=3.2,<4.0",
"pint>=0.8.1",
"python-dateutil>=2.8,<2.9",
"pyyaml>=5.3,<5.4",
"sqlalchemy>=1.3,<1.4",
"pytz",
"https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-2.3.1/en_core_web_sm-2.3.1.tar.gz#egg=en_core_web_sm",
"https://github.com/explosion/spacy-models/releases/download/en_core_web_md-2.3.1/en_core_web_md-2.3.1.tar.gz#egg=en_core_web_md",
"spacy>=2.3,<2.4"
"git+git://github.com/bobloy/ChatterBot@fox#egg=ChatterBot>=1.1.0.dev4",
"kaggle",
"https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.1.0/en_core_web_sm-3.1.0.tar.gz#egg=en_core_web_sm",
"https://github.com/explosion/spacy-models/releases/download/en_core_web_md-3.1.0/en_core_web_md-3.1.0.tar.gz#egg=en_core_web_md"
],
"short": "Local Chatbot run on machine learning",
"end_user_data_statement": "This cog only stores anonymous conversations data; no End User Data is stored.",

@ -1,12 +0,0 @@
git+git://github.com/gunthercox/chatterbot-corpus@master#egg=chatterbot_corpus
mathparse>=0.1,<0.2
nltk>=3.2,<4.0
pint>=0.8.1
python-dateutil>=2.8,<2.9
pyyaml>=5.3,<5.4
sqlalchemy>=1.3,<1.4
pytz
spacy>=2.3,<2.4
https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-2.3.1/en_core_web_sm-2.3.1.tar.gz#egg=en_core_web_sm
https://github.com/explosion/spacy-models/releases/download/en_core_web_md-2.3.1/en_core_web_md-2.3.1.tar.gz#egg=en_core_web_md
# https://github.com/explosion/spacy-models/releases/download/en_core_web_lg-2.3.1/en_core_web_lg-2.3.1.tar.gz#egg=en_core_web_lg

@ -0,0 +1,71 @@
from chatterbot.storage import StorageAdapter, SQLStorageAdapter
class MyDumbSQLStorageAdapter(SQLStorageAdapter):
def __init__(self, **kwargs):
super(SQLStorageAdapter, self).__init__(**kwargs)
from sqlalchemy import create_engine, inspect
from sqlalchemy.orm import sessionmaker
self.database_uri = kwargs.get("database_uri", False)
# None results in a sqlite in-memory database as the default
if self.database_uri is None:
self.database_uri = "sqlite://"
# Create a file database if the database is not a connection string
if not self.database_uri:
self.database_uri = "sqlite:///db.sqlite3"
self.engine = create_engine(self.database_uri, connect_args={"check_same_thread": False})
if self.database_uri.startswith("sqlite://"):
from sqlalchemy.engine import Engine
from sqlalchemy import event
@event.listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
dbapi_connection.execute("PRAGMA journal_mode=WAL")
dbapi_connection.execute("PRAGMA synchronous=NORMAL")
if not inspect(self.engine).has_table("Statement"):
self.create_database()
self.Session = sessionmaker(bind=self.engine, expire_on_commit=True)
class AsyncSQLStorageAdapter(SQLStorageAdapter):
def __init__(self, **kwargs):
super(SQLStorageAdapter, self).__init__(**kwargs)
self.database_uri = kwargs.get("database_uri", False)
# None results in a sqlite in-memory database as the default
if self.database_uri is None:
self.database_uri = "sqlite://"
# Create a file database if the database is not a connection string
if not self.database_uri:
self.database_uri = "sqlite:///db.sqlite3"
async def initialize(self):
# from sqlalchemy import create_engine
from aiomysql.sa import create_engine
from sqlalchemy.orm import sessionmaker
self.engine = await create_engine(self.database_uri, convert_unicode=True)
if self.database_uri.startswith("sqlite://"):
from sqlalchemy.engine import Engine
from sqlalchemy import event
@event.listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
dbapi_connection.execute("PRAGMA journal_mode=WAL")
dbapi_connection.execute("PRAGMA synchronous=NORMAL")
if not self.engine.dialect.has_table(self.engine, "Statement"):
self.create_database()
self.Session = sessionmaker(bind=self.engine, expire_on_commit=True)

@ -0,0 +1,351 @@
import asyncio
import csv
import html
import logging
import os
import pathlib
import time
from functools import partial
from chatterbot import utils
from chatterbot.conversation import Statement
from chatterbot.tagging import PosLemmaTagger
from chatterbot.trainers import Trainer
from redbot.core.bot import Red
from dateutil import parser as date_parser
from redbot.core.utils import AsyncIter
log = logging.getLogger("red.fox_v3.chatter.trainers")
class KaggleTrainer(Trainer):
def __init__(self, chatbot, datapath: pathlib.Path, **kwargs):
super().__init__(chatbot, **kwargs)
self.data_directory = datapath / kwargs.get("downloadpath", "kaggle_download")
self.kaggle_dataset = kwargs.get(
"kaggle_dataset",
"Cornell-University/movie-dialog-corpus",
)
# Create the data directory if it does not already exist
if not os.path.exists(self.data_directory):
os.makedirs(self.data_directory)
def is_downloaded(self, file_path):
"""
Check if the data file is already downloaded.
"""
if os.path.exists(file_path):
self.chatbot.logger.info("File is already downloaded")
return True
return False
async def download(self, dataset):
import kaggle # This triggers the API token check
future = await asyncio.get_event_loop().run_in_executor(
None,
partial(
kaggle.api.dataset_download_files,
dataset=dataset,
path=self.data_directory,
quiet=False,
unzip=True,
),
)
def train(self, *args, **kwargs):
log.error("See asynctrain instead")
def asynctrain(self, *args, **kwargs):
raise self.TrainerInitializationException()
class SouthParkTrainer(KaggleTrainer):
def __init__(self, chatbot, datapath: pathlib.Path, **kwargs):
super().__init__(
chatbot,
datapath,
downloadpath="ubuntu_data_v2",
kaggle_dataset="tovarischsukhov/southparklines",
**kwargs,
)
class MovieTrainer(KaggleTrainer):
def __init__(self, chatbot, datapath: pathlib.Path, **kwargs):
super().__init__(
chatbot,
datapath,
downloadpath="kaggle_movies",
kaggle_dataset="Cornell-University/movie-dialog-corpus",
**kwargs,
)
async def run_movie_training(self):
dialogue_file = "movie_lines.tsv"
conversation_file = "movie_conversations.tsv"
log.info(f"Beginning dialogue training on {dialogue_file}")
start_time = time.time()
tagger = PosLemmaTagger(language=self.chatbot.storage.tagger.language)
# [lineID, characterID, movieID, character name, text of utterance]
# File parsing from https://www.kaggle.com/mushaya/conversation-chatbot
with open(self.data_directory / conversation_file, "r", encoding="utf-8-sig") as conv_tsv:
conv_lines = conv_tsv.readlines()
with open(self.data_directory / dialogue_file, "r", encoding="utf-8-sig") as lines_tsv:
dialog_lines = lines_tsv.readlines()
# trans_dict = str.maketrans({"<u>": "__", "</u>": "__", '""': '"'})
lines_dict = {}
for line in dialog_lines:
_line = line[:-1].strip('"').split("\t")
if len(_line) >= 5: # Only good lines
lines_dict[_line[0]] = (
html.unescape(("".join(_line[4:])).strip())
.replace("<u>", "__")
.replace("</u>", "__")
.replace('""', '"')
)
else:
log.debug(f"Bad line {_line}")
# collecting line ids for each conversation
conv = []
for line in conv_lines[:-1]:
_line = line[:-1].split("\t")[-1][1:-1].replace("'", "").replace(" ", ",")
conv.append(_line.split(","))
# conversations = csv.reader(conv_tsv, delimiter="\t")
#
# reader = csv.reader(lines_tsv, delimiter="\t")
#
#
#
# lines_dict = {}
# for row in reader:
# try:
# lines_dict[row[0].strip('"')] = row[4]
# except:
# log.exception(f"Bad line: {row}")
# pass
# else:
# # log.info(f"Good line: {row}")
# pass
#
# # lines_dict = {row[0].strip('"'): row[4] for row in reader_list}
statements_from_file = []
save_every = 300
count = 0
# [characterID of first, characterID of second, movieID, list of utterances]
async for lines in AsyncIter(conv):
previous_statement_text = None
previous_statement_search_text = ""
for line in lines:
text = lines_dict[line]
statement = Statement(
text=text,
in_response_to=previous_statement_text,
conversation="training",
)
for preprocessor in self.chatbot.preprocessors:
statement = preprocessor(statement)
statement.search_text = tagger.get_text_index_string(statement.text)
statement.search_in_response_to = previous_statement_search_text
previous_statement_text = statement.text
previous_statement_search_text = statement.search_text
statements_from_file.append(statement)
count += 1
if count >= save_every:
if statements_from_file:
self.chatbot.storage.create_many(statements_from_file)
statements_from_file = []
count = 0
if statements_from_file:
self.chatbot.storage.create_many(statements_from_file)
log.info(f"Training took {time.time() - start_time} seconds.")
async def asynctrain(self, *args, **kwargs):
extracted_lines = self.data_directory / "movie_lines.tsv"
extracted_lines: pathlib.Path
# Download and extract the Ubuntu dialog corpus if needed
if not extracted_lines.exists():
await self.download(self.kaggle_dataset)
else:
log.info("Movie dialog already downloaded")
if not extracted_lines.exists():
raise FileNotFoundError(f"{extracted_lines}")
await self.run_movie_training()
return True
# train_dialogue = kwargs.get("train_dialogue", True)
# train_196_dialogue = kwargs.get("train_196", False)
# train_301_dialogue = kwargs.get("train_301", False)
#
# if train_dialogue:
# await self.run_dialogue_training(extracted_dir, "dialogueText.csv")
#
# if train_196_dialogue:
# await self.run_dialogue_training(extracted_dir, "dialogueText_196.csv")
#
# if train_301_dialogue:
# await self.run_dialogue_training(extracted_dir, "dialogueText_301.csv")
class UbuntuCorpusTrainer2(KaggleTrainer):
def __init__(self, chatbot, datapath: pathlib.Path, **kwargs):
super().__init__(
chatbot,
datapath,
downloadpath="kaggle_ubuntu",
kaggle_dataset="rtatman/ubuntu-dialogue-corpus",
**kwargs,
)
async def asynctrain(self, *args, **kwargs):
extracted_dir = self.data_directory / "Ubuntu-dialogue-corpus"
# Download and extract the Ubuntu dialog corpus if needed
if not extracted_dir.exists():
await self.download(self.kaggle_dataset)
else:
log.info("Ubuntu dialogue already downloaded")
if not extracted_dir.exists():
raise FileNotFoundError("Did not extract in the expected way")
train_dialogue = kwargs.get("train_dialogue", True)
train_196_dialogue = kwargs.get("train_196", False)
train_301_dialogue = kwargs.get("train_301", False)
if train_dialogue:
await self.run_dialogue_training(extracted_dir, "dialogueText.csv")
if train_196_dialogue:
await self.run_dialogue_training(extracted_dir, "dialogueText_196.csv")
if train_301_dialogue:
await self.run_dialogue_training(extracted_dir, "dialogueText_301.csv")
return True
async def run_dialogue_training(self, extracted_dir, dialogue_file):
log.info(f"Beginning dialogue training on {dialogue_file}")
start_time = time.time()
tagger = PosLemmaTagger(language=self.chatbot.storage.tagger.language)
with open(extracted_dir / dialogue_file, "r", encoding="utf-8") as dg:
reader = csv.DictReader(dg)
next(reader) # Skip the header
last_dialogue_id = None
previous_statement_text = None
previous_statement_search_text = ""
statements_from_file = []
save_every = 50
count = 0
async for row in AsyncIter(reader):
dialogue_id = row["dialogueID"]
if dialogue_id != last_dialogue_id:
previous_statement_text = None
previous_statement_search_text = ""
last_dialogue_id = dialogue_id
count += 1
if count >= save_every:
if statements_from_file:
self.chatbot.storage.create_many(statements_from_file)
statements_from_file = []
count = 0
if len(row) > 0:
statement = Statement(
text=row["text"],
in_response_to=previous_statement_text,
conversation="training",
# created_at=date_parser.parse(row["date"]),
persona=row["from"],
)
for preprocessor in self.chatbot.preprocessors:
statement = preprocessor(statement)
statement.search_text = tagger.get_text_index_string(statement.text)
statement.search_in_response_to = previous_statement_search_text
previous_statement_text = statement.text
previous_statement_search_text = statement.search_text
statements_from_file.append(statement)
if statements_from_file:
self.chatbot.storage.create_many(statements_from_file)
log.info(f"Training took {time.time() - start_time} seconds.")
class TwitterCorpusTrainer(Trainer):
pass
# def train(self, *args, **kwargs):
# """
# Train the chat bot based on the provided list of
# statements that represents a single conversation.
# """
# import twint
#
# c = twint.Config()
# c.__dict__.update(kwargs)
# twint.run.Search(c)
#
#
# previous_statement_text = None
# previous_statement_search_text = ''
#
# statements_to_create = []
#
# for conversation_count, text in enumerate(conversation):
# if self.show_training_progress:
# utils.print_progress_bar(
# 'List Trainer',
# conversation_count + 1, len(conversation)
# )
#
# statement_search_text = self.chatbot.storage.tagger.get_text_index_string(text)
#
# statement = self.get_preprocessed_statement(
# Statement(
# text=text,
# search_text=statement_search_text,
# in_response_to=previous_statement_text,
# search_in_response_to=previous_statement_search_text,
# conversation='training'
# )
# )
#
# previous_statement_text = statement.text
# previous_statement_search_text = statement_search_text
#
# statements_to_create.append(statement)
#
# self.chatbot.storage.create_many(statements_to_create)

@ -58,11 +58,7 @@ class CogLint(Cog):
future = await self.bot.loop.run_in_executor(None, lint.py_run, path, "return_std=True")
if future:
(pylint_stdout, pylint_stderr) = future
else:
(pylint_stdout, pylint_stderr) = None, None
(pylint_stdout, pylint_stderr) = future or (None, None)
# print(pylint_stderr)
# print(pylint_stdout)

@ -1,5 +1,6 @@
import asyncio
import json
import logging
import os
import pathlib
from abc import ABC
@ -13,6 +14,8 @@ from redbot.core import Config, commands
from redbot.core.bot import Red
from redbot.core.data_manager import bundled_data_path, cog_data_path
log = logging.getLogger("red.fox_v3.conquest")
class Conquest(commands.Cog):
"""
@ -53,23 +56,28 @@ class Conquest(commands.Cog):
self.current_map = await self.config.current_map()
if self.current_map:
await self.current_map_load()
if not await self.current_map_load():
await self.config.current_map.clear()
async def current_map_load(self):
map_data_path = self.asset_path / self.current_map / "data.json"
if not map_data_path.exists():
log.warning(f"{map_data_path} does not exist. Clearing current map")
return False
with map_data_path.open() as mapdata:
self.map_data: dict = json.load(mapdata)
self.ext = self.map_data["extension"]
self.ext_format = "JPEG" if self.ext.upper() == "JPG" else self.ext.upper()
return True
@commands.group()
async def conquest(self, ctx: commands.Context):
"""
Base command for conquest cog. Start with `[p]conquest set map` to select a map.
"""
if ctx.invoked_subcommand is None:
if self.current_map is not None:
await self._conquest_current(ctx)
if ctx.invoked_subcommand is None and self.current_map is not None:
await self._conquest_current(ctx)
@conquest.command(name="list")
async def _conquest_list(self, ctx: commands.Context):
@ -80,14 +88,13 @@ class Conquest(commands.Cog):
with maps_json.open() as maps:
maps_json = json.load(maps)
map_list = "\n".join(map_name for map_name in maps_json["maps"])
map_list = "\n".join(maps_json["maps"])
await ctx.maybe_send_embed(f"Current maps:\n{map_list}")
@conquest.group(name="set")
async def conquest_set(self, ctx: commands.Context):
"""Base command for admin actions like selecting a map"""
if ctx.invoked_subcommand is None:
pass
pass
@conquest_set.command(name="resetzoom")
async def _conquest_set_resetzoom(self, ctx: commands.Context):
@ -159,7 +166,12 @@ class Conquest(commands.Cog):
self.data_path / self.current_map / f"current.{self.ext}", x, y, zoom
)
await ctx.send(file=discord.File(fp=zoomed_path, filename=f"current_zoomed.{self.ext}",))
await ctx.send(
file=discord.File(
fp=zoomed_path,
filename=f"current_zoomed.{self.ext}",
)
)
async def _create_zoomed_map(self, map_path, x, y, zoom, **kwargs):
current_map = Image.open(map_path)

@ -1,7 +1,7 @@
{
"maps": [
"simple_blank_map",
"test",
"test2"
"simple",
"ck2",
"HoI"
]
}

@ -30,8 +30,7 @@ class MapMaker(commands.Cog):
"""
Base command for managing current maps or creating new ones
"""
if ctx.invoked_subcommand is None:
pass
pass
@mapmaker.command(name="upload")
async def _mapmaker_upload(self, ctx: commands.Context, map_path=""):

@ -65,7 +65,7 @@ def floodfill(image, xy, value, border=None, thresh=0) -> set:
if border is None:
fill = _color_diff(p, background) <= thresh
else:
fill = p != value and p != border
fill = p not in [value, border]
if fill:
pixel[s, t] = value
new_edge.add((s, t))

@ -85,6 +85,8 @@ class Dad(Cog):
@commands.Cog.listener()
async def on_message_without_command(self, message: discord.Message):
if message.author.bot:
return
guild: discord.Guild = getattr(message, "guild", None)
if guild is None:
return

@ -27,8 +27,7 @@ class ExclusiveRole(Cog):
async def exclusive(self, ctx):
"""Base command for managing exclusive roles"""
if not ctx.invoked_subcommand:
pass
pass
@exclusive.command(name="add")
@checks.mod_or_permissions(administrator=True)
@ -85,7 +84,7 @@ class ExclusiveRole(Cog):
if role_set is None:
role_set = set(await self.config.guild(member.guild).role_list())
member_set = set([role.id for role in member.roles])
member_set = {role.id for role in member.roles}
to_remove = (member_set - role_set) - {member.guild.default_role.id}
if to_remove and member_set & role_set:
@ -103,7 +102,7 @@ class ExclusiveRole(Cog):
await asyncio.sleep(1)
role_set = set(await self.config.guild(after.guild).role_list())
member_set = set([role.id for role in after.roles])
member_set = {role.id for role in after.roles}
if role_set & member_set:
try:

@ -1,5 +1,15 @@
import sys
from .fifo import FIFO
# Applying fix from: https://github.com/Azure/azure-functions-python-worker/issues/640
# [Fix] Create a wrapper for importing imgres
from .date_trigger import *
from . import CustomDateTrigger
# [Fix] Register imgres into system modules
sys.modules["CustomDateTrigger"] = CustomDateTrigger
async def setup(bot):
cog = FIFO(bot)

@ -0,0 +1,10 @@
from apscheduler.triggers.date import DateTrigger
class CustomDateTrigger(DateTrigger):
def get_next_fire_time(self, previous_fire_time, now):
next_run = super().get_next_fire_time(previous_fire_time, now)
return next_run if next_run is not None and next_run >= now else None
def __getstate__(self):
return {"version": 1, "run_date": self.run_date}

@ -1,9 +1,10 @@
from datetime import datetime
from datetime import datetime, tzinfo
from typing import TYPE_CHECKING
from apscheduler.triggers.cron import CronTrigger
from dateutil import parser
from discord.ext.commands import BadArgument, Converter
from pytz import timezone
from fifo.timezones import assemble_timezones
@ -12,6 +13,18 @@ if TYPE_CHECKING:
CronConverter = str
else:
class TimezoneConverter(Converter):
async def convert(self, ctx, argument) -> tzinfo:
tzinfos = assemble_timezones()
if argument.upper() in tzinfos:
return tzinfos[argument.upper()]
timez = timezone(argument)
if timez is not None:
return timez
raise BadArgument()
class DatetimeConverter(Converter):
async def convert(self, ctx, argument) -> datetime:
dt = parser.parse(argument, fuzzy=True, tzinfos=assemble_timezones())

@ -1,8 +1,10 @@
import itertools
import logging
from datetime import datetime, timedelta
from datetime import MAXYEAR, datetime, timedelta, tzinfo
from typing import Optional, Union
import discord
import pytz
from apscheduler.job import Job
from apscheduler.jobstores.base import JobLookupError
from apscheduler.schedulers.asyncio import AsyncIOScheduler
@ -10,8 +12,9 @@ from apscheduler.schedulers.base import STATE_PAUSED, STATE_RUNNING
from redbot.core import Config, checks, commands
from redbot.core.bot import Red
from redbot.core.commands import TimedeltaConverter
from redbot.core.utils.chat_formatting import humanize_timedelta, pagify
from .datetime_cron_converters import CronConverter, DatetimeConverter
from .datetime_cron_converters import CronConverter, DatetimeConverter, TimezoneConverter
from .task import Task
schedule_log = logging.getLogger("red.fox_v3.fifo.scheduler")
@ -20,11 +23,12 @@ schedule_log.setLevel(logging.DEBUG)
log = logging.getLogger("red.fox_v3.fifo")
async def _execute_task(task_state):
log.info(f"Executing {task_state=}")
async def _execute_task(**task_state):
log.info(f"Executing {task_state.get('name')}")
task = Task(**task_state)
if await task.load_from_config():
return await task.execute()
log.warning(f"Failed to load data on {task_state=}")
return False
@ -36,6 +40,37 @@ def _disassemble_job_id(job_id: str):
return job_id.split("_")
def _get_run_times(job: Job, now: datetime = None):
"""
Computes the scheduled run times between ``next_run_time`` and ``now`` (inclusive).
Modified to be asynchronous and yielding instead of all-or-nothing
"""
if not job.next_run_time:
raise StopIteration()
if now is None:
now = datetime(MAXYEAR, 12, 31, 23, 59, 59, 999999, tzinfo=job.next_run_time.tzinfo)
yield from _get_run_times(job, now) # Recursion
raise StopIteration()
next_run_time = job.next_run_time
while next_run_time and next_run_time <= now:
yield next_run_time
next_run_time = job.trigger.get_next_fire_time(next_run_time, now)
class CapturePrint:
"""Silly little class to get `print` output"""
def __init__(self):
self.string = None
def write(self, string):
self.string = string if self.string is None else self.string + "\n" + string
class FIFO(commands.Cog):
"""
Simple Scheduling Cog
@ -54,9 +89,11 @@ class FIFO(commands.Cog):
self.config.register_global(**default_global)
self.config.register_guild(**default_guild)
self.scheduler = None
self.scheduler: Optional[AsyncIOScheduler] = None
self.jobstore = None
self.tz_cog = None
async def red_delete_data_for_user(self, **kwargs):
"""Nothing to delete"""
return
@ -68,17 +105,22 @@ class FIFO(commands.Cog):
async def initialize(self):
job_defaults = {"coalesce": False, "max_instances": 1}
job_defaults = {
"coalesce": True, # Multiple missed triggers within the grace time will only fire once
"max_instances": 5, # This is probably way too high, should likely only be one
"misfire_grace_time": 15, # 15 seconds ain't much, but it's honest work
"replace_existing": True, # Very important for persistent data
}
# executors = {"default": AsyncIOExecutor()}
# Default executor is already AsyncIOExecutor
self.scheduler = AsyncIOScheduler(job_defaults=job_defaults, logger=schedule_log)
from .redconfigjobstore import RedConfigJobStore
from .redconfigjobstore import RedConfigJobStore # Wait to import to prevent cyclic import
self.jobstore = RedConfigJobStore(self.config, self.bot)
await self.jobstore.load_from_config(self.scheduler, "default")
await self.jobstore.load_from_config()
self.scheduler.add_jobstore(self.jobstore, "default")
self.scheduler.start()
@ -101,35 +143,71 @@ class FIFO(commands.Cog):
await task.delete_self()
async def _process_task(self, task: Task):
job: Union[Job, None] = await self._get_job(task)
if job is not None:
job.reschedule(await task.get_combined_trigger())
return job
# None of this is necessar, we have `replace_existing` already
# job: Union[Job, None] = await self._get_job(task)
# if job is not None:
# combined_trigger_ = await task.get_combined_trigger()
# if combined_trigger_ is None:
# job.remove()
# else:
# job.reschedule(combined_trigger_)
# return job
return await self._add_job(task)
async def _get_job(self, task: Task) -> Job:
return self.scheduler.get_job(_assemble_job_id(task.name, task.guild_id))
async def _add_job(self, task: Task):
combined_trigger_ = await task.get_combined_trigger()
if combined_trigger_ is None:
return None
return self.scheduler.add_job(
_execute_task,
args=[task.__getstate__()],
kwargs=task.__getstate__(),
id=_assemble_job_id(task.name, task.guild_id),
trigger=await task.get_combined_trigger(),
trigger=combined_trigger_,
name=task.name,
replace_existing=True,
)
async def _resume_job(self, task: Task):
try:
job = self.scheduler.resume_job(job_id=_assemble_job_id(task.name, task.guild_id))
except JobLookupError:
job: Union[Job, None] = await self._get_job(task)
if job is not None:
job.resume()
else:
job = await self._process_task(task)
return job
async def _pause_job(self, task: Task):
return self.scheduler.pause_job(job_id=_assemble_job_id(task.name, task.guild_id))
try:
return self.scheduler.pause_job(job_id=_assemble_job_id(task.name, task.guild_id))
except JobLookupError:
return False
async def _remove_job(self, task: Task):
return self.scheduler.remove_job(job_id=_assemble_job_id(task.name, task.guild_id))
try:
self.scheduler.remove_job(job_id=_assemble_job_id(task.name, task.guild_id))
except JobLookupError:
pass
async def _get_tz(self, user: Union[discord.User, discord.Member]) -> Union[None, tzinfo]:
if self.tz_cog is None:
self.tz_cog = self.bot.get_cog("Timezone")
if self.tz_cog is None:
self.tz_cog = False # only try once to get the timezone cog
if not self.tz_cog:
return None
try:
usertime = await self.tz_cog.config.user(user).usertime()
except AttributeError:
return None
if usertime:
return await TimezoneConverter().convert(None, usertime)
else:
return None
@checks.is_owner()
@commands.guild_only()
@ -139,7 +217,7 @@ class FIFO(commands.Cog):
self.scheduler.remove_all_jobs()
await self.config.guild(ctx.guild).tasks.clear()
await self.config.jobs.clear()
await self.config.jobs_index.clear()
# await self.config.jobs_index.clear()
await ctx.tick()
@checks.is_owner() # Will be reduced when I figure out permissions later
@ -149,8 +227,42 @@ class FIFO(commands.Cog):
"""
Base command for handling scheduling of tasks
"""
if ctx.invoked_subcommand is None:
pass
pass
@fifo.command(name="wakeup")
async def fifo_wakeup(self, ctx: commands.Context):
"""Debug command to fix missed executions.
If you see a negative "Next run time" when adding a trigger, this may help resolve it.
Check the logs when using this command.
"""
self.scheduler.wakeup()
await ctx.tick()
@fifo.command(name="checktask", aliases=["checkjob", "check"])
async def fifo_checktask(self, ctx: commands.Context, task_name: str):
"""Returns the next 10 scheduled executions of the task"""
task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
await task.load_from_config()
if task.data is None:
await ctx.maybe_send_embed(
f"Task by the name of {task_name} is not found in this guild"
)
return
job = await self._get_job(task)
if job is None:
await ctx.maybe_send_embed("No job scheduled for this task")
return
now = datetime.now(job.next_run_time.tzinfo)
times = [
humanize_timedelta(timedelta=x - now)
for x in itertools.islice(_get_run_times(job), 10)
]
await ctx.maybe_send_embed("\n\n".join(times))
@fifo.command(name="set")
async def fifo_set(
@ -279,10 +391,14 @@ class FIFO(commands.Cog):
else:
embed.add_field(name="Server", value="Server not found", inline=False)
triggers, expired_triggers = await task.get_triggers()
trigger_str = "\n".join(str(t) for t in await task.get_triggers())
trigger_str = "\n".join(str(t) for t in triggers)
expired_str = "\n".join(str(t) for t in expired_triggers)
if trigger_str:
embed.add_field(name="Triggers", value=trigger_str, inline=False)
if expired_str:
embed.add_field(name="Expired Triggers", value=expired_str, inline=False)
job = await self._get_job(task)
if job and job.next_run_time:
@ -298,18 +414,44 @@ class FIFO(commands.Cog):
Do `[p]fifo list True` to see tasks from all guilds
"""
if all_guilds:
pass
pass # TODO: All guilds
else:
out = ""
all_tasks = await self.config.guild(ctx.guild).tasks()
for task_name, task_data in all_tasks.items():
out += f"{task_name}: {task_data}\n"
out += f"{task_name}: {task_data}\n\n"
if out:
await ctx.maybe_send_embed(out)
if len(out) > 2000:
for page in pagify(out):
await ctx.maybe_send_embed(page)
else:
await ctx.maybe_send_embed(out)
else:
await ctx.maybe_send_embed("No tasks to list")
@fifo.command(name="printschedule")
async def fifo_printschedule(self, ctx: commands.Context):
"""
Print the current schedule of execution.
Useful for debugging.
"""
cp = CapturePrint()
self.scheduler.print_jobs(out=cp)
out = cp.string
out=out.replace("*","\*")
if out:
if len(out) > 2000:
for page in pagify(out):
await ctx.maybe_send_embed(page)
else:
await ctx.maybe_send_embed(out)
else:
await ctx.maybe_send_embed("Failed to get schedule from scheduler")
@fifo.command(name="add")
async def fifo_add(self, ctx: commands.Context, task_name: str, *, command_to_execute: str):
"""
@ -369,6 +511,7 @@ class FIFO(commands.Cog):
return
await task.clear_triggers()
await self._remove_job(task)
await ctx.tick()
@fifo.group(name="addtrigger", aliases=["trigger"])
@ -376,8 +519,7 @@ class FIFO(commands.Cog):
"""
Add a new trigger for a task from the current guild.
"""
if ctx.invoked_subcommand is None:
pass
pass
@fifo_trigger.command(name="interval")
async def fifo_trigger_interval(
@ -388,7 +530,7 @@ class FIFO(commands.Cog):
"""
task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
await task.load_from_config()
await task.load_from_config() # Will set the channel and author
if task.data is None:
await ctx.maybe_send_embed(
@ -406,7 +548,41 @@ class FIFO(commands.Cog):
job: Job = await self._process_task(task)
delta_from_now: timedelta = job.next_run_time - datetime.now(job.next_run_time.tzinfo)
await ctx.maybe_send_embed(
f"Task `{task_name}` added interval of {interval_str} to its scheduled runtimes\n"
f"Task `{task_name}` added interval of {interval_str} to its scheduled runtimes\n\n"
f"Next run time: {job.next_run_time} ({delta_from_now.total_seconds()} seconds)"
)
@fifo_trigger.command(name="relative")
async def fifo_trigger_relative(
self, ctx: commands.Context, task_name: str, *, time_from_now: TimedeltaConverter
):
"""
Add a "run once" trigger at a time relative from now to the specified task
"""
task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
await task.load_from_config()
if task.data is None:
await ctx.maybe_send_embed(
f"Task by the name of {task_name} is not found in this guild"
)
return
time_to_run = datetime.now(pytz.utc) + time_from_now
result = await task.add_trigger("date", time_to_run, time_to_run.tzinfo)
if not result:
await ctx.maybe_send_embed(
"Failed to add a date trigger to this task, see console for logs"
)
return
await task.save_data()
job: Job = await self._process_task(task)
delta_from_now: timedelta = job.next_run_time - datetime.now(job.next_run_time.tzinfo)
await ctx.maybe_send_embed(
f"Task `{task_name}` added {time_to_run} to its scheduled runtimes\n"
f"Next run time: {job.next_run_time} ({delta_from_now.total_seconds()} seconds)"
)
@ -418,7 +594,7 @@ class FIFO(commands.Cog):
Add a "run once" datetime trigger to the specified task
"""
task = Task(task_name, ctx.guild.id, self.config)
task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
await task.load_from_config()
if task.data is None:
@ -427,7 +603,9 @@ class FIFO(commands.Cog):
)
return
result = await task.add_trigger("date", datetime_str)
maybe_tz = await self._get_tz(ctx.author)
result = await task.add_trigger("date", datetime_str, maybe_tz)
if not result:
await ctx.maybe_send_embed(
"Failed to add a date trigger to this task, see console for logs"
@ -444,14 +622,19 @@ class FIFO(commands.Cog):
@fifo_trigger.command(name="cron")
async def fifo_trigger_cron(
self, ctx: commands.Context, task_name: str, *, cron_str: CronConverter
self,
ctx: commands.Context,
task_name: str,
optional_tz: Optional[TimezoneConverter] = None,
*,
cron_str: CronConverter,
):
"""
Add a cron "time of day" trigger to the specified task
See https://crontab.guru/ for help generating the cron_str
"""
task = Task(task_name, ctx.guild.id, self.config)
task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
await task.load_from_config()
if task.data is None:
@ -460,7 +643,10 @@ class FIFO(commands.Cog):
)
return
result = await task.add_trigger("cron", cron_str)
if optional_tz is None:
optional_tz = await self._get_tz(ctx.author) # might still be None
result = await task.add_trigger("cron", cron_str, optional_tz)
if not result:
await ctx.maybe_send_embed(
"Failed to add a cron trigger to this task, see console for logs"

@ -3,19 +3,21 @@
"Bobloy"
],
"min_bot_version": "3.4.0",
"description": "[ALPHA] Schedule commands to be run at certain times or intervals",
"description": "[BETA] Schedule commands to be run at certain times or intervals",
"hidden": false,
"install_msg": "Thank you for installing FIFO.\nGet started with `[p]load fifo`, then `[p]help FIFO`",
"short": "[ALPHA] Schedule commands to be run at certain times or intervals",
"short": "[BETA] Schedule commands to be run at certain times or intervals",
"end_user_data_statement": "This cog does not store any End User Data",
"requirements": [
"apscheduler",
"pytz",
"python-dateutil"
],
"tags": [
"bobloy",
"utilities",
"tool",
"tools",
"roles",
"schedule",
"cron",
@ -23,6 +25,7 @@
"date",
"datetime",
"time",
"calendar"
"calendar",
"timezone"
]
}

@ -2,17 +2,14 @@ import asyncio
import base64
import logging
import pickle
from datetime import datetime
from typing import Tuple, Union
from apscheduler.job import Job
from apscheduler.jobstores.base import ConflictingIdError, JobLookupError
from apscheduler.jobstores.memory import MemoryJobStore
from apscheduler.schedulers.asyncio import run_in_event_loop
from apscheduler.util import datetime_to_utc_timestamp
from redbot.core import Config
# TODO: use get_lock on config
# TODO: use get_lock on config maybe
from redbot.core.bot import Red
from redbot.core.utils import AsyncIter
@ -28,44 +25,55 @@ class RedConfigJobStore(MemoryJobStore):
self.config = config
self.bot = bot
self.pickle_protocol = pickle.HIGHEST_PROTOCOL
self._eventloop = self.bot.loop
# TODO: self.config.jobs_index is never used,
# fine but maybe a sign of inefficient use of config
# task = asyncio.create_task(self.load_from_config())
# while not task.done():
# sleep(0.1)
# future = asyncio.ensure_future(self.load_from_config(), loop=self.bot.loop)
self._eventloop = self.bot.loop # Used for @run_in_event_loop
@run_in_event_loop
def start(self, scheduler, alias):
super().start(scheduler, alias)
for job, timestamp in self._jobs:
job._scheduler = self._scheduler
job._jobstore_alias = self._alias
async def load_from_config(self, scheduler, alias):
super().start(scheduler, alias)
async def load_from_config(self):
_jobs = await self.config.jobs()
self._jobs = [
(await self._decode_job(job), timestamp) async for (job, timestamp) in AsyncIter(_jobs)
]
# self._jobs = [
# (await self._decode_job(job), timestamp) async for (job, timestamp) in AsyncIter(_jobs)
# ]
async for job, timestamp in AsyncIter(_jobs, steps=5):
job = await self._decode_job(job)
index = self._get_job_index(timestamp, job.id)
self._jobs.insert(index, (job, timestamp))
self._jobs_index[job.id] = (job, timestamp)
async def save_to_config(self):
"""Yea that's basically it"""
await self.config.jobs.set(
[(self._encode_job(job), timestamp) for job, timestamp in self._jobs]
)
# self._jobs_index = await self.config.jobs_index.all() # Overwritten by next
self._jobs_index = {job.id: (job, timestamp) for job, timestamp in self._jobs}
# self._jobs_index = {job.id: (job, timestamp) for job, timestamp in self._jobs}
def _encode_job(self, job: Job):
job_state = job.__getstate__()
new_args = list(job_state["args"])
new_args[0]["config"] = None
new_args[0]["bot"] = None
job_state["args"] = tuple(new_args)
job_state["kwargs"]["config"] = None
job_state["kwargs"]["bot"] = None
# new_kwargs = job_state["kwargs"]
# new_kwargs["config"] = None
# new_kwargs["bot"] = None
# job_state["kwargs"] = new_kwargs
encoded = base64.b64encode(pickle.dumps(job_state, self.pickle_protocol))
out = {
"_id": job.id,
"next_run_time": datetime_to_utc_timestamp(job.next_run_time),
"job_state": encoded.decode("ascii"),
}
new_args = list(job_state["args"])
new_args[0]["config"] = self.config
new_args[0]["bot"] = self.bot
job_state["args"] = tuple(new_args)
job_state["kwargs"]["config"] = self.config
job_state["kwargs"]["bot"] = self.bot
# new_kwargs = job_state["kwargs"]
# new_kwargs["config"] = self.config
# new_kwargs["bot"] = self.bot
# job_state["kwargs"] = new_kwargs
# log.debug(f"Encoding job id: {job.id}\n"
# f"Encoded as: {out}")
@ -76,10 +84,15 @@ class RedConfigJobStore(MemoryJobStore):
return None
job_state = in_job["job_state"]
job_state = pickle.loads(base64.b64decode(job_state))
new_args = list(job_state["args"])
new_args[0]["config"] = self.config
new_args[0]["bot"] = self.bot
job_state["args"] = tuple(new_args)
if job_state["args"]: # Backwards compatibility on args to kwargs
job_state["kwargs"] = {**job_state["args"][0]}
job_state["args"] = []
job_state["kwargs"]["config"] = self.config
job_state["kwargs"]["bot"] = self.bot
# new_kwargs = job_state["kwargs"]
# new_kwargs["config"] = self.config
# new_kwargs["bot"] = self.bot
# job_state["kwargs"] = new_kwargs
job = Job.__new__(Job)
job.__setstate__(job_state)
job._scheduler = self._scheduler
@ -96,79 +109,6 @@ class RedConfigJobStore(MemoryJobStore):
return job
@run_in_event_loop
def add_job(self, job: Job):
if job.id in self._jobs_index:
raise ConflictingIdError(job.id)
# log.debug(f"Check job args: {job.args=}")
timestamp = datetime_to_utc_timestamp(job.next_run_time)
index = self._get_job_index(timestamp, job.id) # This is fine
self._jobs.insert(index, (job, timestamp))
self._jobs_index[job.id] = (job, timestamp)
asyncio.create_task(self._async_add_job(job, index, timestamp))
# log.debug(f"Added job: {self._jobs[index][0].args}")
async def _async_add_job(self, job, index, timestamp):
encoded_job = self._encode_job(job)
job_tuple = tuple([encoded_job, timestamp])
async with self.config.jobs() as jobs:
jobs.insert(index, job_tuple)
# await self.config.jobs_index.set_raw(job.id, value=job_tuple)
return True
@run_in_event_loop
def update_job(self, job):
old_tuple: Tuple[Union[Job, None], Union[datetime, None]] = self._jobs_index.get(
job.id, (None, None)
)
old_job = old_tuple[0]
old_timestamp = old_tuple[1]
if old_job is None:
raise JobLookupError(job.id)
# If the next run time has not changed, simply replace the job in its present index.
# Otherwise, reinsert the job to the list to preserve the ordering.
old_index = self._get_job_index(old_timestamp, old_job.id)
new_timestamp = datetime_to_utc_timestamp(job.next_run_time)
asyncio.create_task(
self._async_update_job(job, new_timestamp, old_index, old_job, old_timestamp)
)
async def _async_update_job(self, job, new_timestamp, old_index, old_job, old_timestamp):
encoded_job = self._encode_job(job)
if old_timestamp == new_timestamp:
self._jobs[old_index] = (job, new_timestamp)
async with self.config.jobs() as jobs:
jobs[old_index] = (encoded_job, new_timestamp)
else:
del self._jobs[old_index]
new_index = self._get_job_index(new_timestamp, job.id) # This is fine
self._jobs.insert(new_index, (job, new_timestamp))
async with self.config.jobs() as jobs:
del jobs[old_index]
jobs.insert(new_index, (encoded_job, new_timestamp))
self._jobs_index[old_job.id] = (job, new_timestamp)
# await self.config.jobs_index.set_raw(old_job.id, value=(encoded_job, new_timestamp))
log.debug(f"Async Updated {job.id=}")
log.debug(f"Check job args: {job.args=}")
@run_in_event_loop
def remove_job(self, job_id):
job, timestamp = self._jobs_index.get(job_id, (None, None))
if job is None:
raise JobLookupError(job_id)
index = self._get_job_index(timestamp, job_id)
del self._jobs[index]
del self._jobs_index[job.id]
asyncio.create_task(self._async_remove_job(index, job))
async def _async_remove_job(self, index, job):
async with self.config.jobs() as jobs:
del jobs[index]
# await self.config.jobs_index.clear_raw(job.id)
@run_in_event_loop
def remove_all_jobs(self):
super().remove_all_jobs()
@ -180,4 +120,9 @@ class RedConfigJobStore(MemoryJobStore):
def shutdown(self):
"""Removes all jobs without clearing config"""
super().remove_all_jobs()
asyncio.create_task(self.async_shutdown())
async def async_shutdown(self):
await self.save_to_config()
self._jobs = []
self._jobs_index = {}

@ -1,17 +1,19 @@
import logging
from datetime import datetime, timedelta
from typing import Dict, List, Union
from typing import Dict, List, Optional, Tuple, Union
import discord
import pytz
from apscheduler.triggers.base import BaseTrigger
from apscheduler.triggers.combining import OrTrigger
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger
from apscheduler.triggers.interval import IntervalTrigger
from discord.utils import time_snowflake
from redbot.core import Config, commands
from redbot.core.bot import Red
from fifo.date_trigger import CustomDateTrigger
log = logging.getLogger("red.fox_v3.fifo.task")
@ -25,28 +27,135 @@ def get_trigger(data):
return IntervalTrigger(days=parsed_time.days, seconds=parsed_time.seconds)
if data["type"] == "date":
return DateTrigger(data["time_data"])
return CustomDateTrigger(data["time_data"], timezone=data["tzinfo"])
if data["type"] == "cron":
return CronTrigger.from_crontab(data["time_data"])
return CronTrigger.from_crontab(data["time_data"], timezone=data["tzinfo"])
return False
def check_expired_trigger(trigger: BaseTrigger):
return trigger.get_next_fire_time(None, datetime.now(pytz.utc)) is None
def parse_triggers(data: Union[Dict, None]):
if data is None or not data.get("triggers", False): # No triggers
return None
if len(data["triggers"]) > 1: # Multiple triggers
return OrTrigger(get_trigger(t_data) for t_data in data["triggers"])
triggers_list = [get_trigger(t_data) for t_data in data["triggers"]]
triggers_list = [t for t in triggers_list if not check_expired_trigger(t)]
if not triggers_list:
return None
return OrTrigger(triggers_list)
else:
trigger = get_trigger(data["triggers"][0])
if check_expired_trigger(trigger):
return None
return trigger
# class FakeMessage:
# def __init__(self, message: discord.Message):
# d = {k: getattr(message, k, None) for k in dir(message)}
# self.__dict__.update(**d)
# Potential FakeMessage subclass of Message
# class DeleteSlots(type):
# @classmethod
# def __prepare__(metacls, name, bases):
# """Borrowed a bit from https://stackoverflow.com/q/56579348"""
# super_prepared = super().__prepare__(name, bases)
# print(super_prepared)
# return super_prepared
things_for_fakemessage_to_steal = [
"_state",
"id",
"webhook_id",
# "reactions",
# "attachments",
"embeds",
"application",
"activity",
"channel",
"_edited_time",
"type",
"pinned",
"flags",
"mention_everyone",
"tts",
"content",
"nonce",
"reference",
"_edited_timestamp" # New 7/23/21
]
things_fakemessage_sets_by_default = {
"attachments": [],
"reactions": [],
}
class FakeMessage(discord.Message):
def __init__(self, *args, message: discord.Message, **kwargs):
d = {k: getattr(message, k, None) for k in things_for_fakemessage_to_steal}
d.update(things_fakemessage_sets_by_default)
for k, v in d.items():
try:
# log.debug(f"{k=} {v=}")
setattr(self, k, v)
except TypeError:
# log.exception("This is fine")
pass
except AttributeError:
# log.exception("This is fine")
pass
self.id = time_snowflake(datetime.utcnow(), high=False) # Pretend to be now
self.type = discord.MessageType.default
def process_the_rest(
self,
author: discord.Member,
channel: discord.TextChannel,
content,
):
# self.content = content
# log.debug(self.content)
# for handler in ('author', 'member', 'mentions', 'mention_roles', 'call', 'flags'):
# try:
# getattr(self, '_handle_%s' % handler)(data[handler])
# except KeyError:
# continue
self.author = author
# self._handle_author(author._user._to_minimal_user_json())
# self._handle_member(author)
self._rebind_channel_reference(channel)
self._update(
{
"content": content,
}
)
self._update(
{
"mention_roles": self.raw_role_mentions,
"mentions": [{"id": _id} for _id in self.raw_mentions],
}
)
# self._handle_content(content)
# log.debug(self.content)
return get_trigger(data["triggers"][0])
self.mention_everyone = "@everyone" in self.content or "@here" in self.content
# self._handle_mention_roles(self.raw_role_mentions)
# self._handle_mentions(self.raw_mentions)
class FakeMessage:
def __init__(self, message: discord.Message):
d = {k: getattr(message, k, None) for k in dir(message)}
self.__dict__.update(**d)
# self.__dict__.update(**d)
def neuter_message(message: FakeMessage):
@ -65,11 +174,12 @@ def neuter_message(message: FakeMessage):
class Task:
default_task_data = {"triggers": [], "command_str": ""}
default_task_data = {"triggers": [], "command_str": "", "expired_triggers": []}
default_trigger = {
"type": "",
"time_data": None, # Used for Interval and Date Triggers
"time_data": None,
"tzinfo": None,
}
def __init__(
@ -85,9 +195,10 @@ class Task:
async def _encode_time_triggers(self):
if not self.data or not self.data.get("triggers", None):
return []
return [], []
triggers = []
expired_triggers = []
for t in self.data["triggers"]:
if t["type"] == "interval": # Convert into timedelta
td: timedelta = t["time_data"]
@ -99,47 +210,59 @@ class Task:
if t["type"] == "date": # Convert into datetime
dt: datetime = t["time_data"]
triggers.append({"type": t["type"], "time_data": dt.isoformat()})
# triggers.append(
# {
# "type": t["type"],
# "time_data": {
# "year": dt.year,
# "month": dt.month,
# "day": dt.day,
# "hour": dt.hour,
# "minute": dt.minute,
# "second": dt.second,
# "tzinfo": dt.tzinfo,
# },
# }
# )
data_to_append = {
"type": t["type"],
"time_data": dt.isoformat(),
"tzinfo": getattr(t["tzinfo"], "zone", None),
}
if dt < datetime.now(pytz.utc):
expired_triggers.append(data_to_append)
else:
triggers.append(data_to_append)
continue
if t["type"] == "cron":
triggers.append(t) # already a string, nothing to do
if t["tzinfo"] is None:
triggers.append(t) # already a string, nothing to do
else:
triggers.append(
{
"type": t["type"],
"time_data": t["time_data"],
"tzinfo": getattr(t["tzinfo"], "zone", None),
}
)
continue
raise NotImplemented
return triggers
return triggers, expired_triggers
async def _decode_time_triggers(self):
if not self.data or not self.data.get("triggers", None):
return
for n, t in enumerate(self.data["triggers"]):
for t in self.data["triggers"]:
# Backwards compatibility
if "tzinfo" not in t:
t["tzinfo"] = None
# First decode timezone if there is one
if t["tzinfo"] is not None:
t["tzinfo"] = pytz.timezone(t["tzinfo"])
if t["type"] == "interval": # Convert into timedelta
self.data["triggers"][n]["time_data"] = timedelta(**t["time_data"])
t["time_data"] = timedelta(**t["time_data"])
continue
if t["type"] == "date": # Convert into datetime
# self.data["triggers"][n]["time_data"] = datetime(**t["time_data"])
self.data["triggers"][n]["time_data"] = datetime.fromisoformat(t["time_data"])
t["time_data"] = datetime.fromisoformat(t["time_data"])
continue
if t["type"] == "cron":
continue # already a string
raise NotImplemented
# async def load_from_data(self, data: Dict):
@ -154,7 +277,7 @@ class Task:
return
self.author_id = data["author_id"]
self.guild_id = data["guild_id"]
self.guild_id = data["guild_id"] # Weird I'm doing this, since self.guild_id was just used
self.channel_id = data["channel_id"]
self.data = data["data"]
@ -162,14 +285,23 @@ class Task:
await self._decode_time_triggers()
return self.data
async def get_triggers(self) -> List[Union[IntervalTrigger, DateTrigger]]:
async def get_triggers(self) -> Tuple[List[BaseTrigger], List[BaseTrigger]]:
if not self.data:
await self.load_from_config()
if self.data is None or "triggers" not in self.data: # No triggers
return []
return [], []
return [get_trigger(t) for t in self.data["triggers"]]
trigs = []
expired_trigs = []
for t in self.data["triggers"]:
trig = get_trigger(t)
if check_expired_trigger(trig):
expired_trigs.append(t)
else:
trigs.append(t)
return trigs, expired_trigs
async def get_combined_trigger(self) -> Union[BaseTrigger, None]:
if not self.data:
@ -189,7 +321,10 @@ class Task:
data_to_save = self.default_task_data.copy()
if self.data:
data_to_save["command_str"] = self.get_command_str()
data_to_save["triggers"] = await self._encode_time_triggers()
(
data_to_save["triggers"],
data_to_save["expired_triggers"],
) = await self._encode_time_triggers()
to_save = {
"guild_id": self.guild_id,
@ -205,7 +340,10 @@ class Task:
return
data_to_save = self.data.copy()
data_to_save["triggers"] = await self._encode_time_triggers()
(
data_to_save["triggers"],
data_to_save["expired_triggers"],
) = await self._encode_time_triggers()
await self.config.guild_from_id(self.guild_id).tasks.set_raw(
self.name, "data", value=data_to_save
@ -213,63 +351,87 @@ class Task:
async def execute(self):
if not self.data or not self.get_command_str():
log.warning(f"Could not execute task due to data problem: {self.data=}")
log.warning(f"Could not execute Task[{self.name}] due to data problem: {self.data=}")
return False
guild: discord.Guild = self.bot.get_guild(self.guild_id) # used for get_prefix
if guild is None:
log.warning(f"Could not execute task due to missing guild: {self.guild_id}")
log.warning(
f"Could not execute Task[{self.name}] due to missing guild: {self.guild_id}"
)
return False
channel: discord.TextChannel = guild.get_channel(self.channel_id)
if channel is None:
log.warning(f"Could not execute task due to missing channel: {self.channel_id}")
log.warning(
f"Could not execute Task[{self.name}] due to missing channel: {self.channel_id}"
)
return False
author: discord.User = guild.get_member(self.author_id)
author: discord.Member = guild.get_member(self.author_id)
if author is None:
log.warning(f"Could not execute task due to missing author: {self.author_id}")
log.warning(
f"Could not execute Task[{self.name}] due to missing author: {self.author_id}"
)
return False
actual_message: discord.Message = channel.last_message
actual_message: Optional[discord.Message] = channel.last_message
# I'd like to present you my chain of increasingly desperate message fetching attempts
if actual_message is None:
# log.warning("No message found in channel cache yet, skipping execution")
# return
actual_message = await channel.fetch_message(channel.last_message_id)
if channel.last_message_id is not None:
try:
actual_message = await channel.fetch_message(channel.last_message_id)
except discord.NotFound:
actual_message = None
if actual_message is None: # last_message_id was an invalid message I guess
actual_message = await channel.history(limit=1).flatten()
if not actual_message: # Basically only happens if the channel has no messages
actual_message = await author.history(limit=1).flatten()
if not actual_message: # Okay, the *author* has never sent a message?
log.warning("No message found in channel cache yet, skipping execution")
return
return False
actual_message = actual_message[0]
message = FakeMessage(actual_message)
# message = FakeMessage2
message.author = author
message.guild = guild # Just in case we got desperate
message.channel = channel
message.id = time_snowflake(datetime.now()) # Pretend to be now
message = neuter_message(message)
# message._handle_author(author) # Option when message is subclass
# message._state = self.bot._get_state()
# Time to set the relevant attributes
# message.author = author
# Don't need guild with subclass, guild is just channel.guild
# message.guild = guild # Just in case we got desperate, see above
# message.channel = channel
# absolutely weird that this takes a message object instead of guild
prefixes = await self.bot.get_prefix(message)
prefixes = await self.bot.get_prefix(actual_message)
if isinstance(prefixes, str):
prefix = prefixes
else:
prefix = prefixes[0]
message.content = f"{prefix}{self.get_command_str()}"
new_content = f"{prefix}{self.get_command_str()}"
# log.debug(f"{new_content=}")
if not message.guild or not message.author or not message.content:
log.warning(f"Could not execute task due to message problem: {message}")
message = FakeMessage(message=actual_message)
message = neuter_message(message)
message.process_the_rest(author=author, channel=channel, content=new_content)
if (
not message.guild
or not message.author
or not message.content
or message.content == prefix
):
log.warning(
f"Could not execute Task[{self.name}] due to message problem: "
f"{message.guild=}, {message.author=}, {message.content=}"
)
return False
new_ctx: commands.Context = await self.bot.get_context(message)
new_ctx.assume_yes = True
if not new_ctx.valid:
log.warning(
f"Could not execute Task[{self.name}] due invalid context: {new_ctx.invoked_with}"
f"Could not execute Task[{self.name}] due invalid context: "
f"{new_ctx.invoked_with=} {new_ctx.prefix=} {new_ctx.command=}"
)
return False
@ -300,8 +462,16 @@ class Task:
self.data["command_str"] = command_str
return True
async def add_trigger(self, param, parsed_time: Union[timedelta, datetime, str]):
trigger_data = {"type": param, "time_data": parsed_time}
async def add_trigger(
self, param, parsed_time: Union[timedelta, datetime, str], timezone=None
):
# TODO: Save timezone separately for cron and date triggers
trigger_data = self.default_trigger.copy()
trigger_data["type"] = param
trigger_data["time_data"] = parsed_time
if timezone is not None:
trigger_data["tzinfo"] = timezone
if not get_trigger(trigger_data):
return False

@ -4,7 +4,10 @@ Timezone information for the dateutil parser
All credit to https://github.com/prefrontal/dateutil-parser-timezones
"""
from dateutil.tz import gettz
# from dateutil.tz import gettz
from datetime import datetime
from pytz import timezone
def assemble_timezones():
@ -14,182 +17,218 @@ def assemble_timezones():
"""
timezones = {}
timezones['ACDT'] = gettz('Australia/Darwin') # Australian Central Daylight Savings Time (UTC+10:30)
timezones['ACST'] = gettz('Australia/Darwin') # Australian Central Standard Time (UTC+09:30)
timezones['ACT'] = gettz('Brazil/Acre') # Acre Time (UTC05)
timezones['ADT'] = gettz('America/Halifax') # Atlantic Daylight Time (UTC03)
timezones['AEDT'] = gettz('Australia/Sydney') # Australian Eastern Daylight Savings Time (UTC+11)
timezones['AEST'] = gettz('Australia/Sydney') # Australian Eastern Standard Time (UTC+10)
timezones['AFT'] = gettz('Asia/Kabul') # Afghanistan Time (UTC+04:30)
timezones['AKDT'] = gettz('America/Juneau') # Alaska Daylight Time (UTC08)
timezones['AKST'] = gettz('America/Juneau') # Alaska Standard Time (UTC09)
timezones['AMST'] = gettz('America/Manaus') # Amazon Summer Time (Brazil)[1] (UTC03)
timezones['AMT'] = gettz('America/Manaus') # Amazon Time (Brazil)[2] (UTC04)
timezones['ART'] = gettz('America/Cordoba') # Argentina Time (UTC03)
timezones['AST'] = gettz('Asia/Riyadh') # Arabia Standard Time (UTC+03)
timezones['AWST'] = gettz('Australia/Perth') # Australian Western Standard Time (UTC+08)
timezones['AZOST'] = gettz('Atlantic/Azores') # Azores Summer Time (UTC±00)
timezones['AZOT'] = gettz('Atlantic/Azores') # Azores Standard Time (UTC01)
timezones['AZT'] = gettz('Asia/Baku') # Azerbaijan Time (UTC+04)
timezones['BDT'] = gettz('Asia/Brunei') # Brunei Time (UTC+08)
timezones['BIOT'] = gettz('Etc/GMT+6') # British Indian Ocean Time (UTC+06)
timezones['BIT'] = gettz('Pacific/Funafuti') # Baker Island Time (UTC12)
timezones['BOT'] = gettz('America/La_Paz') # Bolivia Time (UTC04)
timezones['BRST'] = gettz('America/Sao_Paulo') # Brasilia Summer Time (UTC02)
timezones['BRT'] = gettz('America/Sao_Paulo') # Brasilia Time (UTC03)
timezones['BST'] = gettz('Asia/Dhaka') # Bangladesh Standard Time (UTC+06)
timezones['BTT'] = gettz('Asia/Thimphu') # Bhutan Time (UTC+06)
timezones['CAT'] = gettz('Africa/Harare') # Central Africa Time (UTC+02)
timezones['CCT'] = gettz('Indian/Cocos') # Cocos Islands Time (UTC+06:30)
timezones['CDT'] = gettz('America/Chicago') # Central Daylight Time (North America) (UTC05)
timezones['CEST'] = gettz('Europe/Berlin') # Central European Summer Time (Cf. HAEC) (UTC+02)
timezones['CET'] = gettz('Europe/Berlin') # Central European Time (UTC+01)
timezones['CHADT'] = gettz('Pacific/Chatham') # Chatham Daylight Time (UTC+13:45)
timezones['CHAST'] = gettz('Pacific/Chatham') # Chatham Standard Time (UTC+12:45)
timezones['CHOST'] = gettz('Asia/Choibalsan') # Choibalsan Summer Time (UTC+09)
timezones['CHOT'] = gettz('Asia/Choibalsan') # Choibalsan Standard Time (UTC+08)
timezones['CHST'] = gettz('Pacific/Guam') # Chamorro Standard Time (UTC+10)
timezones['CHUT'] = gettz('Pacific/Chuuk') # Chuuk Time (UTC+10)
timezones['CIST'] = gettz('Etc/GMT-8') # Clipperton Island Standard Time (UTC08)
timezones['CIT'] = gettz('Asia/Makassar') # Central Indonesia Time (UTC+08)
timezones['CKT'] = gettz('Pacific/Rarotonga') # Cook Island Time (UTC10)
timezones['CLST'] = gettz('America/Santiago') # Chile Summer Time (UTC03)
timezones['CLT'] = gettz('America/Santiago') # Chile Standard Time (UTC04)
timezones['COST'] = gettz('America/Bogota') # Colombia Summer Time (UTC04)
timezones['COT'] = gettz('America/Bogota') # Colombia Time (UTC05)
timezones['CST'] = gettz('America/Chicago') # Central Standard Time (North America) (UTC06)
timezones['CT'] = gettz('Asia/Chongqing') # China time (UTC+08)
timezones['CVT'] = gettz('Atlantic/Cape_Verde') # Cape Verde Time (UTC01)
timezones['CXT'] = gettz('Indian/Christmas') # Christmas Island Time (UTC+07)
timezones['DAVT'] = gettz('Antarctica/Davis') # Davis Time (UTC+07)
timezones['DDUT'] = gettz('Antarctica/DumontDUrville') # Dumont d'Urville Time (UTC+10)
timezones['DFT'] = gettz('Europe/Berlin') # AIX equivalent of Central European Time (UTC+01)
timezones['EASST'] = gettz('Chile/EasterIsland') # Easter Island Summer Time (UTC05)
timezones['EAST'] = gettz('Chile/EasterIsland') # Easter Island Standard Time (UTC06)
timezones['EAT'] = gettz('Africa/Mogadishu') # East Africa Time (UTC+03)
timezones['ECT'] = gettz('America/Guayaquil') # Ecuador Time (UTC05)
timezones['EDT'] = gettz('America/New_York') # Eastern Daylight Time (North America) (UTC04)
timezones['EEST'] = gettz('Europe/Bucharest') # Eastern European Summer Time (UTC+03)
timezones['EET'] = gettz('Europe/Bucharest') # Eastern European Time (UTC+02)
timezones['EGST'] = gettz('America/Scoresbysund') # Eastern Greenland Summer Time (UTC±00)
timezones['EGT'] = gettz('America/Scoresbysund') # Eastern Greenland Time (UTC01)
timezones['EIT'] = gettz('Asia/Jayapura') # Eastern Indonesian Time (UTC+09)
timezones['EST'] = gettz('America/New_York') # Eastern Standard Time (North America) (UTC05)
timezones['FET'] = gettz('Europe/Minsk') # Further-eastern European Time (UTC+03)
timezones['FJT'] = gettz('Pacific/Fiji') # Fiji Time (UTC+12)
timezones['FKST'] = gettz('Atlantic/Stanley') # Falkland Islands Summer Time (UTC03)
timezones['FKT'] = gettz('Atlantic/Stanley') # Falkland Islands Time (UTC04)
timezones['FNT'] = gettz('Brazil/DeNoronha') # Fernando de Noronha Time (UTC02)
timezones['GALT'] = gettz('Pacific/Galapagos') # Galapagos Time (UTC06)
timezones['GAMT'] = gettz('Pacific/Gambier') # Gambier Islands (UTC09)
timezones['GET'] = gettz('Asia/Tbilisi') # Georgia Standard Time (UTC+04)
timezones['GFT'] = gettz('America/Cayenne') # French Guiana Time (UTC03)
timezones['GILT'] = gettz('Pacific/Tarawa') # Gilbert Island Time (UTC+12)
timezones['GIT'] = gettz('Pacific/Gambier') # Gambier Island Time (UTC09)
timezones['GMT'] = gettz('GMT') # Greenwich Mean Time (UTC±00)
timezones['GST'] = gettz('Asia/Muscat') # Gulf Standard Time (UTC+04)
timezones['GYT'] = gettz('America/Guyana') # Guyana Time (UTC04)
timezones['HADT'] = gettz('Pacific/Honolulu') # Hawaii-Aleutian Daylight Time (UTC09)
timezones['HAEC'] = gettz('Europe/Paris') # Heure Avancée d'Europe Centrale (CEST) (UTC+02)
timezones['HAST'] = gettz('Pacific/Honolulu') # Hawaii-Aleutian Standard Time (UTC10)
timezones['HKT'] = gettz('Asia/Hong_Kong') # Hong Kong Time (UTC+08)
timezones['HMT'] = gettz('Indian/Kerguelen') # Heard and McDonald Islands Time (UTC+05)
timezones['HOVST'] = gettz('Asia/Hovd') # Khovd Summer Time (UTC+08)
timezones['HOVT'] = gettz('Asia/Hovd') # Khovd Standard Time (UTC+07)
timezones['ICT'] = gettz('Asia/Ho_Chi_Minh') # Indochina Time (UTC+07)
timezones['IDT'] = gettz('Asia/Jerusalem') # Israel Daylight Time (UTC+03)
timezones['IOT'] = gettz('Etc/GMT+3') # Indian Ocean Time (UTC+03)
timezones['IRDT'] = gettz('Asia/Tehran') # Iran Daylight Time (UTC+04:30)
timezones['IRKT'] = gettz('Asia/Irkutsk') # Irkutsk Time (UTC+08)
timezones['IRST'] = gettz('Asia/Tehran') # Iran Standard Time (UTC+03:30)
timezones['IST'] = gettz('Asia/Kolkata') # Indian Standard Time (UTC+05:30)
timezones['JST'] = gettz('Asia/Tokyo') # Japan Standard Time (UTC+09)
timezones['KGT'] = gettz('Asia/Bishkek') # Kyrgyzstan time (UTC+06)
timezones['KOST'] = gettz('Pacific/Kosrae') # Kosrae Time (UTC+11)
timezones['KRAT'] = gettz('Asia/Krasnoyarsk') # Krasnoyarsk Time (UTC+07)
timezones['KST'] = gettz('Asia/Seoul') # Korea Standard Time (UTC+09)
timezones['LHST'] = gettz('Australia/Lord_Howe') # Lord Howe Standard Time (UTC+10:30)
timezones['LINT'] = gettz('Pacific/Kiritimati') # Line Islands Time (UTC+14)
timezones['MAGT'] = gettz('Asia/Magadan') # Magadan Time (UTC+12)
timezones['MART'] = gettz('Pacific/Marquesas') # Marquesas Islands Time (UTC09:30)
timezones['MAWT'] = gettz('Antarctica/Mawson') # Mawson Station Time (UTC+05)
timezones['MDT'] = gettz('America/Denver') # Mountain Daylight Time (North America) (UTC06)
timezones['MEST'] = gettz('Europe/Paris') # Middle European Summer Time Same zone as CEST (UTC+02)
timezones['MET'] = gettz('Europe/Berlin') # Middle European Time Same zone as CET (UTC+01)
timezones['MHT'] = gettz('Pacific/Kwajalein') # Marshall Islands (UTC+12)
timezones['MIST'] = gettz('Antarctica/Macquarie') # Macquarie Island Station Time (UTC+11)
timezones['MIT'] = gettz('Pacific/Marquesas') # Marquesas Islands Time (UTC09:30)
timezones['MMT'] = gettz('Asia/Rangoon') # Myanmar Standard Time (UTC+06:30)
timezones['MSK'] = gettz('Europe/Moscow') # Moscow Time (UTC+03)
timezones['MST'] = gettz('America/Denver') # Mountain Standard Time (North America) (UTC07)
timezones['MUT'] = gettz('Indian/Mauritius') # Mauritius Time (UTC+04)
timezones['MVT'] = gettz('Indian/Maldives') # Maldives Time (UTC+05)
timezones['MYT'] = gettz('Asia/Kuching') # Malaysia Time (UTC+08)
timezones['NCT'] = gettz('Pacific/Noumea') # New Caledonia Time (UTC+11)
timezones['NDT'] = gettz('Canada/Newfoundland') # Newfoundland Daylight Time (UTC02:30)
timezones['NFT'] = gettz('Pacific/Norfolk') # Norfolk Time (UTC+11)
timezones['NPT'] = gettz('Asia/Kathmandu') # Nepal Time (UTC+05:45)
timezones['NST'] = gettz('Canada/Newfoundland') # Newfoundland Standard Time (UTC03:30)
timezones['NT'] = gettz('Canada/Newfoundland') # Newfoundland Time (UTC03:30)
timezones['NUT'] = gettz('Pacific/Niue') # Niue Time (UTC11)
timezones['NZDT'] = gettz('Pacific/Auckland') # New Zealand Daylight Time (UTC+13)
timezones['NZST'] = gettz('Pacific/Auckland') # New Zealand Standard Time (UTC+12)
timezones['OMST'] = gettz('Asia/Omsk') # Omsk Time (UTC+06)
timezones['ORAT'] = gettz('Asia/Oral') # Oral Time (UTC+05)
timezones['PDT'] = gettz('America/Los_Angeles') # Pacific Daylight Time (North America) (UTC07)
timezones['PET'] = gettz('America/Lima') # Peru Time (UTC05)
timezones['PETT'] = gettz('Asia/Kamchatka') # Kamchatka Time (UTC+12)
timezones['PGT'] = gettz('Pacific/Port_Moresby') # Papua New Guinea Time (UTC+10)
timezones['PHOT'] = gettz('Pacific/Enderbury') # Phoenix Island Time (UTC+13)
timezones['PKT'] = gettz('Asia/Karachi') # Pakistan Standard Time (UTC+05)
timezones['PMDT'] = gettz('America/Miquelon') # Saint Pierre and Miquelon Daylight time (UTC02)
timezones['PMST'] = gettz('America/Miquelon') # Saint Pierre and Miquelon Standard Time (UTC03)
timezones['PONT'] = gettz('Pacific/Pohnpei') # Pohnpei Standard Time (UTC+11)
timezones['PST'] = gettz('America/Los_Angeles') # Pacific Standard Time (North America) (UTC08)
timezones['PYST'] = gettz('America/Asuncion') # Paraguay Summer Time (South America)[7] (UTC03)
timezones['PYT'] = gettz('America/Asuncion') # Paraguay Time (South America)[8] (UTC04)
timezones['RET'] = gettz('Indian/Reunion') # Réunion Time (UTC+04)
timezones['ROTT'] = gettz('Antarctica/Rothera') # Rothera Research Station Time (UTC03)
timezones['SAKT'] = gettz('Asia/Vladivostok') # Sakhalin Island time (UTC+11)
timezones['SAMT'] = gettz('Europe/Samara') # Samara Time (UTC+04)
timezones['SAST'] = gettz('Africa/Johannesburg') # South African Standard Time (UTC+02)
timezones['SBT'] = gettz('Pacific/Guadalcanal') # Solomon Islands Time (UTC+11)
timezones['SCT'] = gettz('Indian/Mahe') # Seychelles Time (UTC+04)
timezones['SGT'] = gettz('Asia/Singapore') # Singapore Time (UTC+08)
timezones['SLST'] = gettz('Asia/Colombo') # Sri Lanka Standard Time (UTC+05:30)
timezones['SRET'] = gettz('Asia/Srednekolymsk') # Srednekolymsk Time (UTC+11)
timezones['SRT'] = gettz('America/Paramaribo') # Suriname Time (UTC03)
timezones['SST'] = gettz('Asia/Singapore') # Singapore Standard Time (UTC+08)
timezones['SYOT'] = gettz('Antarctica/Syowa') # Showa Station Time (UTC+03)
timezones['TAHT'] = gettz('Pacific/Tahiti') # Tahiti Time (UTC10)
timezones['TFT'] = gettz('Indian/Kerguelen') # Indian/Kerguelen (UTC+05)
timezones['THA'] = gettz('Asia/Bangkok') # Thailand Standard Time (UTC+07)
timezones['TJT'] = gettz('Asia/Dushanbe') # Tajikistan Time (UTC+05)
timezones['TKT'] = gettz('Pacific/Fakaofo') # Tokelau Time (UTC+13)
timezones['TLT'] = gettz('Asia/Dili') # Timor Leste Time (UTC+09)
timezones['TMT'] = gettz('Asia/Ashgabat') # Turkmenistan Time (UTC+05)
timezones['TOT'] = gettz('Pacific/Tongatapu') # Tonga Time (UTC+13)
timezones['TVT'] = gettz('Pacific/Funafuti') # Tuvalu Time (UTC+12)
timezones['ULAST'] = gettz('Asia/Ulan_Bator') # Ulaanbaatar Summer Time (UTC+09)
timezones['ULAT'] = gettz('Asia/Ulan_Bator') # Ulaanbaatar Standard Time (UTC+08)
timezones['USZ1'] = gettz('Europe/Kaliningrad') # Kaliningrad Time (UTC+02)
timezones['UTC'] = gettz('UTC') # Coordinated Universal Time (UTC±00)
timezones['UYST'] = gettz('America/Montevideo') # Uruguay Summer Time (UTC02)
timezones['UYT'] = gettz('America/Montevideo') # Uruguay Standard Time (UTC03)
timezones['UZT'] = gettz('Asia/Tashkent') # Uzbekistan Time (UTC+05)
timezones['VET'] = gettz('America/Caracas') # Venezuelan Standard Time (UTC04)
timezones['VLAT'] = gettz('Asia/Vladivostok') # Vladivostok Time (UTC+10)
timezones['VOLT'] = gettz('Europe/Volgograd') # Volgograd Time (UTC+04)
timezones['VOST'] = gettz('Antarctica/Vostok') # Vostok Station Time (UTC+06)
timezones['VUT'] = gettz('Pacific/Efate') # Vanuatu Time (UTC+11)
timezones['WAKT'] = gettz('Pacific/Wake') # Wake Island Time (UTC+12)
timezones['WAST'] = gettz('Africa/Lagos') # West Africa Summer Time (UTC+02)
timezones['WAT'] = gettz('Africa/Lagos') # West Africa Time (UTC+01)
timezones['WEST'] = gettz('Europe/London') # Western European Summer Time (UTC+01)
timezones['WET'] = gettz('Europe/London') # Western European Time (UTC±00)
timezones['WIT'] = gettz('Asia/Jakarta') # Western Indonesian Time (UTC+07)
timezones['WST'] = gettz('Australia/Perth') # Western Standard Time (UTC+08)
timezones['YAKT'] = gettz('Asia/Yakutsk') # Yakutsk Time (UTC+09)
timezones['YEKT'] = gettz('Asia/Yekaterinburg') # Yekaterinburg Time (UTC+05)
timezones["ACDT"] = timezone(
"Australia/Darwin"
) # Australian Central Daylight Savings Time (UTC+10:30)
timezones["ACST"] = timezone(
"Australia/Darwin"
) # Australian Central Standard Time (UTC+09:30)
timezones["ACT"] = timezone("Brazil/Acre") # Acre Time (UTC05)
timezones["ADT"] = timezone("America/Halifax") # Atlantic Daylight Time (UTC03)
timezones["AEDT"] = timezone(
"Australia/Sydney"
) # Australian Eastern Daylight Savings Time (UTC+11)
timezones["AEST"] = timezone("Australia/Sydney") # Australian Eastern Standard Time (UTC+10)
timezones["AFT"] = timezone("Asia/Kabul") # Afghanistan Time (UTC+04:30)
timezones["AKDT"] = timezone("America/Juneau") # Alaska Daylight Time (UTC08)
timezones["AKST"] = timezone("America/Juneau") # Alaska Standard Time (UTC09)
timezones["AMST"] = timezone("America/Manaus") # Amazon Summer Time (Brazil)[1] (UTC03)
timezones["AMT"] = timezone("America/Manaus") # Amazon Time (Brazil)[2] (UTC04)
timezones["ART"] = timezone("America/Cordoba") # Argentina Time (UTC03)
timezones["AST"] = timezone("Asia/Riyadh") # Arabia Standard Time (UTC+03)
timezones["AWST"] = timezone("Australia/Perth") # Australian Western Standard Time (UTC+08)
timezones["AZOST"] = timezone("Atlantic/Azores") # Azores Summer Time (UTC±00)
timezones["AZOT"] = timezone("Atlantic/Azores") # Azores Standard Time (UTC01)
timezones["AZT"] = timezone("Asia/Baku") # Azerbaijan Time (UTC+04)
timezones["BDT"] = timezone("Asia/Brunei") # Brunei Time (UTC+08)
timezones["BIOT"] = timezone("Etc/GMT+6") # British Indian Ocean Time (UTC+06)
timezones["BIT"] = timezone("Pacific/Funafuti") # Baker Island Time (UTC12)
timezones["BOT"] = timezone("America/La_Paz") # Bolivia Time (UTC04)
timezones["BRST"] = timezone("America/Sao_Paulo") # Brasilia Summer Time (UTC02)
timezones["BRT"] = timezone("America/Sao_Paulo") # Brasilia Time (UTC03)
timezones["BST"] = timezone("Asia/Dhaka") # Bangladesh Standard Time (UTC+06)
timezones["BTT"] = timezone("Asia/Thimphu") # Bhutan Time (UTC+06)
timezones["CAT"] = timezone("Africa/Harare") # Central Africa Time (UTC+02)
timezones["CCT"] = timezone("Indian/Cocos") # Cocos Islands Time (UTC+06:30)
timezones["CDT"] = timezone(
"America/Chicago"
) # Central Daylight Time (North America) (UTC05)
timezones["CEST"] = timezone(
"Europe/Berlin"
) # Central European Summer Time (Cf. HAEC) (UTC+02)
timezones["CET"] = timezone("Europe/Berlin") # Central European Time (UTC+01)
timezones["CHADT"] = timezone("Pacific/Chatham") # Chatham Daylight Time (UTC+13:45)
timezones["CHAST"] = timezone("Pacific/Chatham") # Chatham Standard Time (UTC+12:45)
timezones["CHOST"] = timezone("Asia/Choibalsan") # Choibalsan Summer Time (UTC+09)
timezones["CHOT"] = timezone("Asia/Choibalsan") # Choibalsan Standard Time (UTC+08)
timezones["CHST"] = timezone("Pacific/Guam") # Chamorro Standard Time (UTC+10)
timezones["CHUT"] = timezone("Pacific/Chuuk") # Chuuk Time (UTC+10)
timezones["CIST"] = timezone("Etc/GMT-8") # Clipperton Island Standard Time (UTC08)
timezones["CIT"] = timezone("Asia/Makassar") # Central Indonesia Time (UTC+08)
timezones["CKT"] = timezone("Pacific/Rarotonga") # Cook Island Time (UTC10)
timezones["CLST"] = timezone("America/Santiago") # Chile Summer Time (UTC03)
timezones["CLT"] = timezone("America/Santiago") # Chile Standard Time (UTC04)
timezones["COST"] = timezone("America/Bogota") # Colombia Summer Time (UTC04)
timezones["COT"] = timezone("America/Bogota") # Colombia Time (UTC05)
timezones["CST"] = timezone(
"America/Chicago"
) # Central Standard Time (North America) (UTC06)
timezones["CT"] = timezone("Asia/Chongqing") # China time (UTC+08)
timezones["CVT"] = timezone("Atlantic/Cape_Verde") # Cape Verde Time (UTC01)
timezones["CXT"] = timezone("Indian/Christmas") # Christmas Island Time (UTC+07)
timezones["DAVT"] = timezone("Antarctica/Davis") # Davis Time (UTC+07)
timezones["DDUT"] = timezone("Antarctica/DumontDUrville") # Dumont d'Urville Time (UTC+10)
timezones["DFT"] = timezone(
"Europe/Berlin"
) # AIX equivalent of Central European Time (UTC+01)
timezones["EASST"] = timezone("Chile/EasterIsland") # Easter Island Summer Time (UTC05)
timezones["EAST"] = timezone("Chile/EasterIsland") # Easter Island Standard Time (UTC06)
timezones["EAT"] = timezone("Africa/Mogadishu") # East Africa Time (UTC+03)
timezones["ECT"] = timezone("America/Guayaquil") # Ecuador Time (UTC05)
timezones["EDT"] = timezone(
"America/New_York"
) # Eastern Daylight Time (North America) (UTC04)
timezones["EEST"] = timezone("Europe/Bucharest") # Eastern European Summer Time (UTC+03)
timezones["EET"] = timezone("Europe/Bucharest") # Eastern European Time (UTC+02)
timezones["EGST"] = timezone("America/Scoresbysund") # Eastern Greenland Summer Time (UTC±00)
timezones["EGT"] = timezone("America/Scoresbysund") # Eastern Greenland Time (UTC01)
timezones["EIT"] = timezone("Asia/Jayapura") # Eastern Indonesian Time (UTC+09)
timezones["EST"] = timezone(
"America/New_York"
) # Eastern Standard Time (North America) (UTC05)
timezones["FET"] = timezone("Europe/Minsk") # Further-eastern European Time (UTC+03)
timezones["FJT"] = timezone("Pacific/Fiji") # Fiji Time (UTC+12)
timezones["FKST"] = timezone("Atlantic/Stanley") # Falkland Islands Summer Time (UTC03)
timezones["FKT"] = timezone("Atlantic/Stanley") # Falkland Islands Time (UTC04)
timezones["FNT"] = timezone("Brazil/DeNoronha") # Fernando de Noronha Time (UTC02)
timezones["GALT"] = timezone("Pacific/Galapagos") # Galapagos Time (UTC06)
timezones["GAMT"] = timezone("Pacific/Gambier") # Gambier Islands (UTC09)
timezones["GET"] = timezone("Asia/Tbilisi") # Georgia Standard Time (UTC+04)
timezones["GFT"] = timezone("America/Cayenne") # French Guiana Time (UTC03)
timezones["GILT"] = timezone("Pacific/Tarawa") # Gilbert Island Time (UTC+12)
timezones["GIT"] = timezone("Pacific/Gambier") # Gambier Island Time (UTC09)
timezones["GMT"] = timezone("GMT") # Greenwich Mean Time (UTC±00)
timezones["GST"] = timezone("Asia/Muscat") # Gulf Standard Time (UTC+04)
timezones["GYT"] = timezone("America/Guyana") # Guyana Time (UTC04)
timezones["HADT"] = timezone("Pacific/Honolulu") # Hawaii-Aleutian Daylight Time (UTC09)
timezones["HAEC"] = timezone("Europe/Paris") # Heure Avancée d'Europe Centrale (CEST) (UTC+02)
timezones["HAST"] = timezone("Pacific/Honolulu") # Hawaii-Aleutian Standard Time (UTC10)
timezones["HKT"] = timezone("Asia/Hong_Kong") # Hong Kong Time (UTC+08)
timezones["HMT"] = timezone("Indian/Kerguelen") # Heard and McDonald Islands Time (UTC+05)
timezones["HOVST"] = timezone("Asia/Hovd") # Khovd Summer Time (UTC+08)
timezones["HOVT"] = timezone("Asia/Hovd") # Khovd Standard Time (UTC+07)
timezones["ICT"] = timezone("Asia/Ho_Chi_Minh") # Indochina Time (UTC+07)
timezones["IDT"] = timezone("Asia/Jerusalem") # Israel Daylight Time (UTC+03)
timezones["IOT"] = timezone("Etc/GMT+3") # Indian Ocean Time (UTC+03)
timezones["IRDT"] = timezone("Asia/Tehran") # Iran Daylight Time (UTC+04:30)
timezones["IRKT"] = timezone("Asia/Irkutsk") # Irkutsk Time (UTC+08)
timezones["IRST"] = timezone("Asia/Tehran") # Iran Standard Time (UTC+03:30)
timezones["IST"] = timezone("Asia/Kolkata") # Indian Standard Time (UTC+05:30)
timezones["JST"] = timezone("Asia/Tokyo") # Japan Standard Time (UTC+09)
timezones["KGT"] = timezone("Asia/Bishkek") # Kyrgyzstan time (UTC+06)
timezones["KOST"] = timezone("Pacific/Kosrae") # Kosrae Time (UTC+11)
timezones["KRAT"] = timezone("Asia/Krasnoyarsk") # Krasnoyarsk Time (UTC+07)
timezones["KST"] = timezone("Asia/Seoul") # Korea Standard Time (UTC+09)
timezones["LHST"] = timezone("Australia/Lord_Howe") # Lord Howe Standard Time (UTC+10:30)
timezones["LINT"] = timezone("Pacific/Kiritimati") # Line Islands Time (UTC+14)
timezones["MAGT"] = timezone("Asia/Magadan") # Magadan Time (UTC+12)
timezones["MART"] = timezone("Pacific/Marquesas") # Marquesas Islands Time (UTC09:30)
timezones["MAWT"] = timezone("Antarctica/Mawson") # Mawson Station Time (UTC+05)
timezones["MDT"] = timezone(
"America/Denver"
) # Mountain Daylight Time (North America) (UTC06)
timezones["MEST"] = timezone(
"Europe/Paris"
) # Middle European Summer Time Same zone as CEST (UTC+02)
timezones["MET"] = timezone("Europe/Berlin") # Middle European Time Same zone as CET (UTC+01)
timezones["MHT"] = timezone("Pacific/Kwajalein") # Marshall Islands (UTC+12)
timezones["MIST"] = timezone("Antarctica/Macquarie") # Macquarie Island Station Time (UTC+11)
timezones["MIT"] = timezone("Pacific/Marquesas") # Marquesas Islands Time (UTC09:30)
timezones["MMT"] = timezone("Asia/Rangoon") # Myanmar Standard Time (UTC+06:30)
timezones["MSK"] = timezone("Europe/Moscow") # Moscow Time (UTC+03)
timezones["MST"] = timezone(
"America/Denver"
) # Mountain Standard Time (North America) (UTC07)
timezones["MUT"] = timezone("Indian/Mauritius") # Mauritius Time (UTC+04)
timezones["MVT"] = timezone("Indian/Maldives") # Maldives Time (UTC+05)
timezones["MYT"] = timezone("Asia/Kuching") # Malaysia Time (UTC+08)
timezones["NCT"] = timezone("Pacific/Noumea") # New Caledonia Time (UTC+11)
timezones["NDT"] = timezone("Canada/Newfoundland") # Newfoundland Daylight Time (UTC02:30)
timezones["NFT"] = timezone("Pacific/Norfolk") # Norfolk Time (UTC+11)
timezones["NPT"] = timezone("Asia/Kathmandu") # Nepal Time (UTC+05:45)
timezones["NST"] = timezone("Canada/Newfoundland") # Newfoundland Standard Time (UTC03:30)
timezones["NT"] = timezone("Canada/Newfoundland") # Newfoundland Time (UTC03:30)
timezones["NUT"] = timezone("Pacific/Niue") # Niue Time (UTC11)
timezones["NZDT"] = timezone("Pacific/Auckland") # New Zealand Daylight Time (UTC+13)
timezones["NZST"] = timezone("Pacific/Auckland") # New Zealand Standard Time (UTC+12)
timezones["OMST"] = timezone("Asia/Omsk") # Omsk Time (UTC+06)
timezones["ORAT"] = timezone("Asia/Oral") # Oral Time (UTC+05)
timezones["PDT"] = timezone(
"America/Los_Angeles"
) # Pacific Daylight Time (North America) (UTC07)
timezones["PET"] = timezone("America/Lima") # Peru Time (UTC05)
timezones["PETT"] = timezone("Asia/Kamchatka") # Kamchatka Time (UTC+12)
timezones["PGT"] = timezone("Pacific/Port_Moresby") # Papua New Guinea Time (UTC+10)
timezones["PHOT"] = timezone("Pacific/Enderbury") # Phoenix Island Time (UTC+13)
timezones["PKT"] = timezone("Asia/Karachi") # Pakistan Standard Time (UTC+05)
timezones["PMDT"] = timezone(
"America/Miquelon"
) # Saint Pierre and Miquelon Daylight time (UTC02)
timezones["PMST"] = timezone(
"America/Miquelon"
) # Saint Pierre and Miquelon Standard Time (UTC03)
timezones["PONT"] = timezone("Pacific/Pohnpei") # Pohnpei Standard Time (UTC+11)
timezones["PST"] = timezone(
"America/Los_Angeles"
) # Pacific Standard Time (North America) (UTC08)
timezones["PYST"] = timezone(
"America/Asuncion"
) # Paraguay Summer Time (South America)[7] (UTC03)
timezones["PYT"] = timezone("America/Asuncion") # Paraguay Time (South America)[8] (UTC04)
timezones["RET"] = timezone("Indian/Reunion") # Réunion Time (UTC+04)
timezones["ROTT"] = timezone("Antarctica/Rothera") # Rothera Research Station Time (UTC03)
timezones["SAKT"] = timezone("Asia/Vladivostok") # Sakhalin Island time (UTC+11)
timezones["SAMT"] = timezone("Europe/Samara") # Samara Time (UTC+04)
timezones["SAST"] = timezone("Africa/Johannesburg") # South African Standard Time (UTC+02)
timezones["SBT"] = timezone("Pacific/Guadalcanal") # Solomon Islands Time (UTC+11)
timezones["SCT"] = timezone("Indian/Mahe") # Seychelles Time (UTC+04)
timezones["SGT"] = timezone("Asia/Singapore") # Singapore Time (UTC+08)
timezones["SLST"] = timezone("Asia/Colombo") # Sri Lanka Standard Time (UTC+05:30)
timezones["SRET"] = timezone("Asia/Srednekolymsk") # Srednekolymsk Time (UTC+11)
timezones["SRT"] = timezone("America/Paramaribo") # Suriname Time (UTC03)
timezones["SST"] = timezone("Asia/Singapore") # Singapore Standard Time (UTC+08)
timezones["SYOT"] = timezone("Antarctica/Syowa") # Showa Station Time (UTC+03)
timezones["TAHT"] = timezone("Pacific/Tahiti") # Tahiti Time (UTC10)
timezones["TFT"] = timezone("Indian/Kerguelen") # Indian/Kerguelen (UTC+05)
timezones["THA"] = timezone("Asia/Bangkok") # Thailand Standard Time (UTC+07)
timezones["TJT"] = timezone("Asia/Dushanbe") # Tajikistan Time (UTC+05)
timezones["TKT"] = timezone("Pacific/Fakaofo") # Tokelau Time (UTC+13)
timezones["TLT"] = timezone("Asia/Dili") # Timor Leste Time (UTC+09)
timezones["TMT"] = timezone("Asia/Ashgabat") # Turkmenistan Time (UTC+05)
timezones["TOT"] = timezone("Pacific/Tongatapu") # Tonga Time (UTC+13)
timezones["TVT"] = timezone("Pacific/Funafuti") # Tuvalu Time (UTC+12)
timezones["ULAST"] = timezone("Asia/Ulan_Bator") # Ulaanbaatar Summer Time (UTC+09)
timezones["ULAT"] = timezone("Asia/Ulan_Bator") # Ulaanbaatar Standard Time (UTC+08)
timezones["USZ1"] = timezone("Europe/Kaliningrad") # Kaliningrad Time (UTC+02)
timezones["UTC"] = timezone("UTC") # Coordinated Universal Time (UTC±00)
timezones["UYST"] = timezone("America/Montevideo") # Uruguay Summer Time (UTC02)
timezones["UYT"] = timezone("America/Montevideo") # Uruguay Standard Time (UTC03)
timezones["UZT"] = timezone("Asia/Tashkent") # Uzbekistan Time (UTC+05)
timezones["VET"] = timezone("America/Caracas") # Venezuelan Standard Time (UTC04)
timezones["VLAT"] = timezone("Asia/Vladivostok") # Vladivostok Time (UTC+10)
timezones["VOLT"] = timezone("Europe/Volgograd") # Volgograd Time (UTC+04)
timezones["VOST"] = timezone("Antarctica/Vostok") # Vostok Station Time (UTC+06)
timezones["VUT"] = timezone("Pacific/Efate") # Vanuatu Time (UTC+11)
timezones["WAKT"] = timezone("Pacific/Wake") # Wake Island Time (UTC+12)
timezones["WAST"] = timezone("Africa/Lagos") # West Africa Summer Time (UTC+02)
timezones["WAT"] = timezone("Africa/Lagos") # West Africa Time (UTC+01)
timezones["WEST"] = timezone("Europe/London") # Western European Summer Time (UTC+01)
timezones["WET"] = timezone("Europe/London") # Western European Time (UTC±00)
timezones["WIT"] = timezone("Asia/Jakarta") # Western Indonesian Time (UTC+07)
timezones["WST"] = timezone("Australia/Perth") # Western Standard Time (UTC+08)
timezones["YAKT"] = timezone("Asia/Yakutsk") # Yakutsk Time (UTC+09)
timezones["YEKT"] = timezone("Asia/Yekaterinburg") # Yekaterinburg Time (UTC+05)
return timezones
dt = datetime(2020, 1, 1)
timezones.update((x, y.localize(dt).tzinfo) for x, y in timezones.items())
return timezones

@ -11,6 +11,7 @@
"tags": [
"bobloy",
"utilities",
"tool"
"tool",
"tools"
]
}

@ -53,12 +53,9 @@ class Flag(Cog):
@commands.group()
async def flagset(self, ctx: commands.Context):
"""
My custom cog
Extra information goes here
Commands for managing Flag settings
"""
if ctx.invoked_subcommand is None:
pass
pass
@flagset.command(name="expire")
async def flagset_expire(self, ctx: commands.Context, days: int):

@ -30,8 +30,8 @@ class ForceMention(Cog):
@commands.command()
async def forcemention(self, ctx: commands.Context, role: str, *, message=""):
"""
Mentions that role, regardless if it's unmentionable
"""
Mentions that role, regardless if it's unmentionable
"""
role_obj = get(ctx.guild.roles, name=role)
if role_obj is None:
await ctx.maybe_send_embed("Couldn't find role named {}".format(role))

@ -6,4 +6,3 @@ def setup(bot):
n = Hangman(bot)
data_manager.bundled_data_path(n)
bot.add_cog(n)
bot.add_listener(n.on_react, "on_reaction_add")

@ -50,27 +50,27 @@ class Hangman(Cog):
theface = await self.config.guild(guild).theface()
self.hanglist[guild] = (
""">
\_________
\\_________
|/
|
|
|
|
|
|\___
|\\___
""",
""">
\_________
\\_________
|/ |
|
|
|
|
|
|\___
|\\___
H""",
""">
\_________
\\_________
|/ |
| """
+ theface
@ -79,10 +79,10 @@ class Hangman(Cog):
|
|
|
|\___
|\\___
HA""",
""">
\________
\\________
|/ |
| """
+ theface
@ -91,10 +91,10 @@ class Hangman(Cog):
| |
|
|
|\___
|\\___
HAN""",
""">
\_________
\\_________
|/ |
| """
+ theface
@ -103,43 +103,43 @@ class Hangman(Cog):
| |
|
|
|\___
|\\___
HANG""",
""">
\_________
\\_________
|/ |
| """
+ theface
+ """
| /|\
| /|\\
| |
|
|
|\___
|\\___
HANGM""",
""">
\________
\\________
|/ |
| """
+ theface
+ """
| /|\
| /|\\
| |
| /
|
|\___
|\\___
HANGMA""",
""">
\________
\\________
|/ |
| """
+ theface
+ """
| /|\
| /|\\
| |
| / \
| / \\
|
|\___
|\\___
HANGMAN""",
)
@ -147,8 +147,7 @@ class Hangman(Cog):
@checks.mod_or_permissions(administrator=True)
async def hangset(self, ctx):
"""Adjust hangman settings"""
if ctx.invoked_subcommand is None:
pass
pass
@hangset.command()
async def face(self, ctx: commands.Context, theface):
@ -250,21 +249,19 @@ class Hangman(Cog):
self.winbool[guild] = True
for i in self.the_data[guild]["answer"]:
if i == " " or i == "-":
if i in [" ", "-"]:
out_str += i * 2
elif i in self.the_data[guild]["guesses"] or i not in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
out_str += "__" + i + "__ "
else:
out_str += "**\_** "
out_str += "**\\_** "
self.winbool[guild] = False
return out_str
def _guesslist(self, guild):
"""Returns the current letter list"""
out_str = ""
for i in self.the_data[guild]["guesses"]:
out_str += str(i) + ","
out_str = "".join(str(i) + "," for i in self.the_data[guild]["guesses"])
out_str = out_str[:-1]
return out_str
@ -286,10 +283,10 @@ class Hangman(Cog):
await self._reprintgame(message)
@commands.Cog.listener()
@commands.Cog.listener("on_reaction_add")
async def on_react(self, reaction, user: Union[discord.User, discord.Member]):
""" Thanks to flapjack reactpoll for guidelines
https://github.com/flapjax/FlapJack-Cogs/blob/master/reactpoll/reactpoll.py"""
"""Thanks to flapjack reactpoll for guidelines
https://github.com/flapjax/FlapJack-Cogs/blob/master/reactpoll/reactpoll.py"""
guild: discord.Guild = getattr(user, "guild", None)
if guild is None:
return

@ -1,5 +1,7 @@
from .infochannel import InfoChannel
def setup(bot):
bot.add_cog(InfoChannel(bot))
async def setup(bot):
ic_cog = InfoChannel(bot)
bot.add_cog(ic_cog)
await ic_cog.initialize()

@ -1,25 +1,53 @@
import asyncio
from typing import Union
import logging
from collections import defaultdict
from typing import Dict, Optional, Union
import discord
from redbot.core import Config, checks, commands
from redbot.core.bot import Red
from redbot.core.commands import Cog
# Cog: Any = getattr(commands, "Cog", object)
# listener = getattr(commands.Cog, "listener", None) # Trusty + Sinbad
# if listener is None:
# def listener(name=None):
# return lambda x: x
RATE_LIMIT_DELAY = 60 * 10 # If you're willing to risk rate limiting, you can decrease the delay
# 10 minutes. Rate limit is 2 per 10, so 1 per 6 is safe.
RATE_LIMIT_DELAY = 60 * 6 # If you're willing to risk rate limiting, you can decrease the delay
log = logging.getLogger("red.fox_v3.infochannel")
async def get_channel_counts(category, guild):
# Gets count of bots
bot_num = len([m for m in guild.members if m.bot])
# Gets count of roles in the server
roles_num = len(guild.roles) - 1
# Gets count of channels in the server
# <number of total channels> - <number of channels in the stats category> - <categories>
channels_num = len(guild.channels) - len(category.voice_channels) - len(guild.categories)
# Gets all counts of members
members = guild.member_count
offline_num = len(list(filter(lambda m: m.status is discord.Status.offline, guild.members)))
online_num = members - offline_num
# Gets count of actual users
human_num = members - bot_num
# count amount of premium subs/nitro subs.
boosters = guild.premium_subscription_count
return {
"members": members,
"humans": human_num,
"boosters": boosters,
"bots": bot_num,
"roles": roles_num,
"channels": channels_num,
"online": online_num,
"offline": offline_num,
}
class InfoChannel(Cog):
"""
Create a channel with updating server info
Less important information about the cog
This relies on editing channels, which is a strictly rate-limited activity.
As such, updates will not be frequent. Currently capped at 1 per 5 minutes per server.
"""
def __init__(self, bot: Red):
@ -29,23 +57,56 @@ class InfoChannel(Cog):
self, identifier=731101021116710497110110101108, force_registration=True
)
# self. so I can get the keys from this later
self.default_channel_names = {
"members": "Members: {count}",
"humans": "Humans: {count}",
"boosters": "Boosters: {count}",
"bots": "Bots: {count}",
"roles": "Roles: {count}",
"channels": "Channels: {count}",
"online": "Online: {count}",
"offline": "Offline: {count}",
}
default_channel_ids = {k: None for k in self.default_channel_names}
# Only members is enabled by default
default_enabled_counts = {k: k == "members" for k in self.default_channel_names}
default_guild = {
"channel_id": None,
"botchannel_id": None,
"onlinechannel_id": None,
"member_count": True,
"bot_count": False,
"online_count": False,
"category_id": None,
"channel_ids": default_channel_ids,
"enabled_channels": default_enabled_counts,
"channel_names": self.default_channel_names,
}
self.config.register_guild(**default_guild)
self.default_role = {"enabled": False, "channel_id": None, "name": "{role}: {count}"}
self.config.register_role(**self.default_role)
self._critical_section_wooah_ = 0
self.channel_data = defaultdict(dict)
self.edit_queue = defaultdict(lambda: defaultdict(lambda: asyncio.Queue(maxsize=2)))
self._rate_limited_edits: Dict[int, Dict[str, Optional[asyncio.Task]]] = defaultdict(
lambda: defaultdict(lambda: None)
)
async def red_delete_data_for_user(self, **kwargs):
"""Nothing to delete"""
return
async def initialize(self):
for guild in self.bot.guilds:
await self.update_infochannel(guild)
def cog_unload(self):
self.stop_all_queues()
@commands.command()
@checks.admin()
async def infochannel(self, ctx: commands.Context):
@ -61,233 +122,466 @@ class InfoChannel(Cog):
)
guild: discord.Guild = ctx.guild
channel_id = await self.config.guild(guild).channel_id()
channel = None
if channel_id is not None:
channel: Union[discord.VoiceChannel, None] = guild.get_channel(channel_id)
category_id = await self.config.guild(guild).category_id()
category = None
if category_id is not None:
category: Union[discord.CategoryChannel, None] = guild.get_channel(category_id)
if channel_id is not None and channel is None:
await ctx.send("Info channel has been deleted, recreate it?")
elif channel_id is None:
await ctx.send("Enable info channel on this server?")
if category_id is not None and category is None:
await ctx.maybe_send_embed("Info category has been deleted, recreate it?")
elif category_id is None:
await ctx.maybe_send_embed("Enable info channels on this server?")
else:
await ctx.send("Do you wish to delete current info channels?")
await ctx.maybe_send_embed("Do you wish to delete current info channels?")
msg = await self.bot.wait_for("message", check=check)
if msg.content.upper() in ["N", "NO"]:
await ctx.send("Cancelled")
await ctx.maybe_send_embed("Cancelled")
return
if channel is None:
if category is None:
try:
await self.make_infochannel(guild)
except discord.Forbidden:
await ctx.send("Failure: Missing permission to create voice channel")
await ctx.maybe_send_embed(
"Failure: Missing permission to create necessary channels"
)
return
else:
await self.delete_all_infochannels(guild)
ctx.message = msg
if not await ctx.tick():
await ctx.send("Done!")
await ctx.maybe_send_embed("Done!")
@commands.group()
@commands.group(aliases=["icset"])
@checks.admin()
async def infochannelset(self, ctx: commands.Context):
"""
Toggle different types of infochannels
"""
if not ctx.invoked_subcommand:
pass
@infochannelset.command(name="botcount")
async def _infochannelset_botcount(self, ctx: commands.Context, enabled: bool = None):
"""
Toggle an infochannel that shows the amount of bots in the server
pass
@infochannelset.command(name="togglechannel")
async def _infochannelset_togglechannel(
self, ctx: commands.Context, channel_type: str, enabled: Optional[bool] = None
):
"""Toggles the infochannel for the specified channel type.
Valid Types are:
- `members`: Total members on the server
- `humans`: Total members that aren't bots
- `boosters`: Total amount of boosters
- `bots`: Total bots
- `roles`: Total number of roles
- `channels`: Total number of channels excluding infochannels,
- `online`: Total online members,
- `offline`: Total offline members,
"""
guild = ctx.guild
if channel_type not in self.default_channel_names.keys():
await ctx.maybe_send_embed("Invalid channel type provided.")
return
if enabled is None:
enabled = not await self.config.guild(guild).bot_count()
enabled = not await self.config.guild(guild).enabled_channels.get_raw(channel_type)
await self.config.guild(guild).bot_count.set(enabled)
await self.make_infochannel(ctx.guild)
await self.config.guild(guild).enabled_channels.set_raw(channel_type, value=enabled)
await self.make_infochannel(ctx.guild, channel_type=channel_type)
if enabled:
await ctx.send("InfoChannel for bot count has been enabled.")
await ctx.maybe_send_embed(f"InfoChannel `{channel_type}` has been enabled.")
else:
await ctx.send("InfoChannel for bot count has been disabled.")
await ctx.maybe_send_embed(f"InfoChannel `{channel_type}` has been disabled.")
@infochannelset.command(name="onlinecount")
async def _infochannelset_onlinecount(self, ctx: commands.Context, enabled: bool = None):
"""
Toggle an infochannel that shows the amount of online users in the server
"""
guild = ctx.guild
@infochannelset.command(name="togglerole")
async def _infochannelset_rolecount(
self, ctx: commands.Context, role: discord.Role, enabled: bool = None
):
"""Toggle an infochannel that shows the count of users with the specified role"""
if enabled is None:
enabled = not await self.config.guild(guild).online_count()
enabled = not await self.config.role(role).enabled()
await self.config.role(role).enabled.set(enabled)
await self.config.guild(guild).online_count.set(enabled)
await self.make_infochannel(ctx.guild)
await self.make_infochannel(ctx.guild, channel_role=role)
if enabled:
await ctx.send("InfoChannel for online user count has been enabled.")
await ctx.maybe_send_embed(f"InfoChannel for {role.name} count has been enabled.")
else:
await ctx.send("InfoChannel for online user count has been disabled.")
await ctx.maybe_send_embed(f"InfoChannel for {role.name} count has been disabled.")
async def make_infochannel(self, guild: discord.Guild):
botcount = await self.config.guild(guild).bot_count()
onlinecount = await self.config.guild(guild).online_count()
overwrites = {
guild.default_role: discord.PermissionOverwrite(connect=False),
guild.me: discord.PermissionOverwrite(manage_channels=True, connect=True),
}
@infochannelset.command(name="name")
async def _infochannelset_name(self, ctx: commands.Context, channel_type: str, *, text=None):
"""
Change the name of the infochannel for the specified channel type.
{count} must be used to display number of total members in the server.
Leave blank to set back to default.
Examples:
- `[p]infochannelset name members Cool Cats: {count}`
- `[p]infochannelset name bots {count} Robot Overlords`
Valid Types are:
- `members`: Total members on the server
- `humans`: Total members that aren't bots
- `boosters`: Total amount of boosters
- `bots`: Total bots
- `roles`: Total number of roles
- `channels`: Total number of channels excluding infochannels
- `online`: Total online members
- `offline`: Total offline members
Warning: This command counts against the channel update rate limit and may be queued.
"""
guild = ctx.guild
if channel_type not in self.default_channel_names.keys():
await ctx.maybe_send_embed("Invalid channel type provided.")
return
if text is None:
text = self.default_channel_names.get(channel_type)
elif "{count}" not in text:
await ctx.maybe_send_embed(
"Improperly formatted. Make sure to use `{count}` in your channel name"
)
return
elif len(text) > 93:
await ctx.maybe_send_embed("Name is too long, max length is 93.")
return
await self.config.guild(guild).channel_names.set_raw(channel_type, value=text)
await self.update_infochannel(guild, channel_type=channel_type)
if not await ctx.tick():
await ctx.maybe_send_embed("Done!")
@infochannelset.command(name="rolename")
async def _infochannelset_rolename(
self, ctx: commands.Context, role: discord.Role, *, text=None
):
"""
Change the name of the infochannel for specific roles.
{count} must be used to display number members with the given role.
{role} can be used for the roles name.
Leave blank to set back to default.
Default is set to: `{role}: {count}`
Examples:
- `[p]infochannelset rolename @Patrons {role}: {count}`
- `[p]infochannelset rolename Elite {count} members with {role} role`
- `[p]infochannelset rolename "Space Role" Total boosters: {count}`
# Remove the old info channel first
channel_id = await self.config.guild(guild).channel_id()
Warning: This command counts against the channel update rate limit and may be queued.
"""
guild = ctx.message.guild
if text is None:
text = self.default_role["name"]
elif "{count}" not in text:
await ctx.maybe_send_embed(
"Improperly formatted. Make sure to use `{count}` in your channel name"
)
return
await self.config.role(role).name.set(text)
await self.update_infochannel(guild, channel_role=role)
if not await ctx.tick():
await ctx.maybe_send_embed("Done!")
async def create_individual_channel(
self, guild, category: discord.CategoryChannel, overwrites, channel_type, count
):
# Delete the channel if it exists
channel_id = await self.config.guild(guild).channel_ids.get_raw(channel_type)
if channel_id is not None:
channel: discord.VoiceChannel = guild.get_channel(channel_id)
if channel:
self.stop_queue(guild.id, channel_type)
await channel.delete(reason="InfoChannel delete")
# Then create the new one
channel = await guild.create_voice_channel(
"Total Humans:", reason="InfoChannel make", overwrites=overwrites
)
await self.config.guild(guild).channel_id.set(channel.id)
# Only make the channel if it's enabled
if await self.config.guild(guild).enabled_channels.get_raw(channel_type):
name = await self.config.guild(guild).channel_names.get_raw(channel_type)
name = name.format(count=count)
channel = await category.create_voice_channel(
name, reason="InfoChannel make", overwrites=overwrites
)
await self.config.guild(guild).channel_ids.set_raw(channel_type, value=channel.id)
return channel
return None
async def create_role_channel(
self, guild, category: discord.CategoryChannel, overwrites, role: discord.Role
):
# Delete the channel if it exists
channel_id = await self.config.role(role).channel_id()
if channel_id is not None:
channel: discord.VoiceChannel = guild.get_channel(channel_id)
if channel:
self.stop_queue(guild.id, role.id)
await channel.delete(reason="InfoChannel delete")
if botcount:
# Remove the old bot channel first
botchannel_id = await self.config.guild(guild).botchannel_id()
if channel_id is not None:
botchannel: discord.VoiceChannel = guild.get_channel(botchannel_id)
if botchannel:
await botchannel.delete(reason="InfoChannel delete")
# Only make the channel if it's enabled
if await self.config.role(role).enabled():
count = len(role.members)
name = await self.config.role(role).name()
name = name.format(role=role.name, count=count)
channel = await category.create_voice_channel(
name, reason="InfoChannel make", overwrites=overwrites
)
await self.config.role(role).channel_id.set(channel.id)
return channel
return None
# Then create the new one
botchannel = await guild.create_voice_channel(
"Bots:", reason="InfoChannel botcount", overwrites=overwrites
async def make_infochannel(self, guild: discord.Guild, channel_type=None, channel_role=None):
overwrites = {
guild.default_role: discord.PermissionOverwrite(connect=False),
guild.me: discord.PermissionOverwrite(manage_channels=True, connect=True),
}
# Check for and create the Infochannel category
category_id = await self.config.guild(guild).category_id()
if category_id is not None:
category: discord.CategoryChannel = guild.get_channel(category_id)
if category is None: # Category id is invalid, probably deleted.
category_id = None
if category_id is None:
category: discord.CategoryChannel = await guild.create_category(
"Server Stats", reason="InfoChannel Category make"
)
await self.config.guild(guild).botchannel_id.set(botchannel.id)
if onlinecount:
# Remove the old online channel first
onlinechannel_id = await self.config.guild(guild).onlinechannel_id()
if channel_id is not None:
onlinechannel: discord.VoiceChannel = guild.get_channel(onlinechannel_id)
if onlinechannel:
await onlinechannel.delete(reason="InfoChannel delete")
await self.config.guild(guild).category_id.set(category.id)
await category.edit(position=0)
category_id = category.id
category: discord.CategoryChannel = guild.get_channel(category_id)
channel_data = await get_channel_counts(category, guild)
# Then create the new one
onlinechannel = await guild.create_voice_channel(
"Online:", reason="InfoChannel onlinecount", overwrites=overwrites
# Only update a single channel
if channel_type is not None:
await self.create_individual_channel(
guild, category, overwrites, channel_type, channel_data[channel_type]
)
return
if channel_role is not None:
await self.create_role_channel(guild, category, overwrites, channel_role)
return
# Update all channels
for channel_type in self.default_channel_names.keys():
await self.create_individual_channel(
guild, category, overwrites, channel_type, channel_data[channel_type]
)
await self.config.guild(guild).onlinechannel_id.set(onlinechannel.id)
await self.update_infochannel(guild)
for role in guild.roles:
await self.create_role_channel(guild, category, overwrites, role)
# await self.update_infochannel(guild)
async def delete_all_infochannels(self, guild: discord.Guild):
self.stop_guild_queues(guild.id) # Stop processing edits
# Delete regular channels
for channel_type in self.default_channel_names.keys():
channel_id = await self.config.guild(guild).channel_ids.get_raw(channel_type)
if channel_id is not None:
channel = guild.get_channel(channel_id)
if channel is not None:
await channel.delete(reason="InfoChannel delete")
await self.config.guild(guild).channel_ids.clear_raw(channel_type)
# Delete role channels
for role in guild.roles:
channel_id = await self.config.role(role).channel_id()
if channel_id is not None:
channel = guild.get_channel(channel_id)
if channel is not None:
await channel.delete(reason="InfoChannel delete")
await self.config.role(role).channel_id.clear()
# Delete the category last
category_id = await self.config.guild(guild).category_id()
if category_id is not None:
category = guild.get_channel(category_id)
if category is not None:
await category.delete(reason="InfoChannel delete")
async def add_to_queue(self, guild, channel, identifier, count, formatted_name):
self.channel_data[guild.id][identifier] = (count, formatted_name, channel.id)
if not self.edit_queue[guild.id][identifier].full():
try:
self.edit_queue[guild.id][identifier].put_nowait(identifier)
except asyncio.QueueFull:
pass # If queue is full, disregard
if self._rate_limited_edits[guild.id][identifier] is None:
await self.start_queue(guild.id, identifier)
async def update_individual_channel(self, guild, channel_type, count, guild_data):
name = guild_data["channel_names"][channel_type]
name = name.format(count=count)
channel = guild.get_channel(guild_data["channel_ids"][channel_type])
if channel is None:
return # abort
await self.add_to_queue(guild, channel, channel_type, count, name)
async def update_role_channel(self, guild, role: discord.Role, role_data):
if not role_data["enabled"]:
return # Not enabled
count = len(role.members)
name = role_data["name"]
name = name.format(role=role.name, count=count)
channel = guild.get_channel(role_data["channel_id"])
if channel is None:
return # abort
await self.add_to_queue(guild, channel, role.id, count, name)
async def update_infochannel(self, guild: discord.Guild, channel_type=None, channel_role=None):
if channel_type is None and channel_role is None:
return await self.trigger_updates_for(
guild,
members=True,
humans=True,
boosters=True,
bots=True,
roles=True,
channels=True,
online=True,
offline=True,
extra_roles=set(guild.roles),
)
if channel_type is not None:
return await self.trigger_updates_for(guild, **{channel_type: True})
return await self.trigger_updates_for(guild, extra_roles={channel_role})
async def start_queue(self, guild_id, identifier):
self._rate_limited_edits[guild_id][identifier] = asyncio.create_task(
self._process_queue(guild_id, identifier)
)
def stop_queue(self, guild_id, identifier):
if self._rate_limited_edits[guild_id][identifier] is not None:
self._rate_limited_edits[guild_id][identifier].cancel()
def stop_guild_queues(self, guild_id):
for identifier in self._rate_limited_edits[guild_id].keys():
self.stop_queue(guild_id, identifier)
def stop_all_queues(self):
for guild_id in self._rate_limited_edits.keys():
self.stop_guild_queues(guild_id)
async def _process_queue(self, guild_id, identifier):
while True:
identifier = await self.edit_queue[guild_id][identifier].get() # Waits forever
count, formatted_name, channel_id = self.channel_data[guild_id][identifier]
channel: discord.VoiceChannel = self.bot.get_channel(channel_id)
if channel.name == formatted_name:
continue # Nothing to process
log.debug(f"Processing guild_id: {guild_id} - identifier: {identifier}")
try:
await channel.edit(reason="InfoChannel update", name=formatted_name)
except (discord.Forbidden, discord.HTTPException):
pass # Don't bother figuring it out
except discord.InvalidArgument:
log.exception(f"Invalid formatted infochannel: {formatted_name}")
else:
await asyncio.sleep(RATE_LIMIT_DELAY) # Wait a reasonable amount of time
async def trigger_updates_for(self, guild, **kwargs):
extra_roles: Optional[set] = kwargs.pop("extra_roles", False)
guild_data = await self.config.guild(guild).all()
botchannel_id = guild_data["botchannel_id"]
onlinechannel_id = guild_data["onlinechannel_id"]
botchannel: discord.VoiceChannel = guild.get_channel(botchannel_id)
onlinechannel: discord.VoiceChannel = guild.get_channel(onlinechannel_id)
channel_id = guild_data["channel_id"]
channel: discord.VoiceChannel = guild.get_channel(channel_id)
await channel.delete(reason="InfoChannel delete")
if botchannel_id is not None:
await botchannel.delete(reason="InfoChannel delete")
if onlinechannel_id is not None:
await onlinechannel.delete(reason="InfoChannel delete")
await self.config.guild(guild).clear()
async def update_infochannel(self, guild: discord.Guild):
guild_data = await self.config.guild(guild).all()
botcount = guild_data["bot_count"]
onlinecount = guild_data["online_count"]
# Gets count of bots
# bots = lambda x: x.bot
# def bots(x): return x.bot
bot_num = len([m for m in guild.members if m.bot])
# bot_msg = f"Bots: {num}"
# Gets count of online users
members = guild.member_count
offline = len(list(filter(lambda m: m.status is discord.Status.offline, guild.members)))
online_num = members - offline
# online_msg = f"Online: {num}"
# Gets count of actual users
total = lambda x: not x.bot
human_num = len([m for m in guild.members if total(m)])
# human_msg = f"Total Humans: {num}"
channel_id = guild_data["channel_id"]
if channel_id is None:
return False
botchannel_id = guild_data["botchannel_id"]
onlinechannel_id = guild_data["onlinechannel_id"]
channel_id = guild_data["channel_id"]
channel: discord.VoiceChannel = guild.get_channel(channel_id)
botchannel: discord.VoiceChannel = guild.get_channel(botchannel_id)
onlinechannel: discord.VoiceChannel = guild.get_channel(onlinechannel_id)
if guild_data["member_count"]:
name = f"{channel.name.split(':')[0]}: {human_num}"
await channel.edit(reason="InfoChannel update", name=name)
if botcount:
name = f"{botchannel.name.split(':')[0]}: {bot_num}"
await botchannel.edit(reason="InfoChannel update", name=name)
if onlinecount:
name = f"{onlinechannel.name.split(':')[0]}: {online_num}"
await onlinechannel.edit(reason="InfoChannel update", name=name)
async def update_infochannel_with_cooldown(self, guild):
"""My attempt at preventing rate limits, lets see how it goes"""
if self._critical_section_wooah_:
if self._critical_section_wooah_ == 2:
# print("Already pending, skipping")
return # Another one is already pending, don't queue more than one
# print("Queuing another update")
self._critical_section_wooah_ = 2
while self._critical_section_wooah_:
await asyncio.sleep(
RATE_LIMIT_DELAY // 4
) # Max delay ends up as 1.25 * RATE_LIMIT_DELAY
# print("Issuing queued update")
return await self.update_infochannel_with_cooldown(guild)
# print("Entering critical")
self._critical_section_wooah_ = 1
await self.update_infochannel(guild)
await asyncio.sleep(RATE_LIMIT_DELAY)
self._critical_section_wooah_ = 0
# print("Exiting critical")
@Cog.listener()
async def on_member_join(self, member: discord.Member):
to_update = (
kwargs.keys() & [key for key, value in guild_data["enabled_channels"].items() if value]
) # Value in kwargs doesn't matter
if to_update or extra_roles:
log.debug(f"{to_update=}\n"
f"{extra_roles=}")
category = guild.get_channel(guild_data["category_id"])
if category is None:
log.debug('Channel category is missing, updating must be off')
return # Nothing to update, must be off
channel_data = await get_channel_counts(category, guild)
if to_update:
for channel_type in to_update:
await self.update_individual_channel(
guild, channel_type, channel_data[channel_type], guild_data
)
if extra_roles:
role_data = await self.config.all_roles()
for channel_role in extra_roles:
if channel_role.id in role_data:
await self.update_role_channel(
guild, channel_role, role_data[channel_role.id]
)
@Cog.listener(name="on_member_join")
@Cog.listener(name="on_member_remove")
async def on_member_join_remove(self, member: discord.Member):
if await self.bot.cog_disabled_in_guild(self, member.guild):
return
await self.update_infochannel_with_cooldown(member.guild)
@Cog.listener()
async def on_member_remove(self, member: discord.Member):
if await self.bot.cog_disabled_in_guild(self, member.guild):
return
await self.update_infochannel_with_cooldown(member.guild)
if member.bot:
await self.trigger_updates_for(
member.guild, members=True, bots=True, online=True, offline=True
)
else:
await self.trigger_updates_for(
member.guild, members=True, humans=True, online=True, offline=True
)
@Cog.listener()
async def on_member_update(self, before: discord.Member, after: discord.Member):
if await self.bot.cog_disabled_in_guild(self, after.guild):
return
onlinecount = await self.config.guild(after.guild).online_count()
if onlinecount:
if before.status != after.status:
await self.update_infochannel_with_cooldown(after.guild)
if before.status != after.status:
return await self.trigger_updates_for(after.guild, online=True, offline=True)
# XOR
c = set(after.roles) ^ set(before.roles)
if c:
await self.trigger_updates_for(after.guild, extra_roles=c)
@Cog.listener("on_guild_channel_create")
@Cog.listener("on_guild_channel_delete")
async def on_guild_channel_create_delete(self, channel: discord.TextChannel):
if await self.bot.cog_disabled_in_guild(self, channel.guild):
return
await self.trigger_updates_for(channel.guild, channels=True)
@Cog.listener()
async def on_guild_role_create(self, role):
if await self.bot.cog_disabled_in_guild(self, role.guild):
return
await self.trigger_updates_for(role.guild, roles=True)
@Cog.listener()
async def on_guild_role_delete(self, role):
if await self.bot.cog_disabled_in_guild(self, role.guild):
return
await self.trigger_updates_for(role.guild, roles=True)
role_channel_id = await self.config.role(role).channel_id()
if role_channel_id is not None:
rolechannel: discord.VoiceChannel = role.guild.get_channel(role_channel_id)
if rolechannel:
await rolechannel.delete(reason="InfoChannel delete")
await self.config.role(role).clear()

@ -11,6 +11,7 @@
"tags": [
"bobloy",
"utilities",
"tool"
"tool",
"tools"
]
}

@ -10,9 +10,9 @@ log = logging.getLogger("red.fox_v3.isitdown")
class IsItDown(commands.Cog):
"""
Cog Description
Cog for checking whether a website is down or not.
Less important information about the cog
Uses the `isitdown.site` API
"""
def __init__(self, bot: Red):
@ -36,23 +36,25 @@ class IsItDown(commands.Cog):
Alias: iid
"""
try:
resp = await self._check_if_down(url_to_check)
resp, url = await self._check_if_down(url_to_check)
except AssertionError:
await ctx.maybe_send_embed("Invalid URL provided. Make sure not to include `http://`")
return
# log.debug(resp)
if resp["isitdown"]:
await ctx.maybe_send_embed(f"{url_to_check} is DOWN!")
await ctx.maybe_send_embed(f"{url} is DOWN!")
else:
await ctx.maybe_send_embed(f"{url_to_check} is UP!")
await ctx.maybe_send_embed(f"{url} is UP!")
async def _check_if_down(self, url_to_check):
url = re.compile(r"https?://(www\.)?")
url.sub("", url_to_check).strip().strip("/")
re_compiled = re.compile(r"https?://(www\.)?")
url = re_compiled.sub("", url_to_check).strip().strip("/")
url = f"https://isitdown.site/api/v3/{url}"
# log.debug(url)
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
assert response.status == 200
resp = await response.json()
return resp
return resp, url

@ -8,7 +8,7 @@
"install_msg": "Thank you for installing LaunchLib. Get started with `[p]load launchlib`, then `[p]help LaunchLib`",
"short": "Access launch data for space flights",
"end_user_data_statement": "This cog does not store any End User Data",
"requirements": ["python-launch-library>=1.0.6"],
"requirements": ["python-launch-library>=2.0.3"],
"tags": [
"bobloy",
"utils",

@ -1,7 +1,7 @@
import asyncio
import functools
import logging
import re
import discord
import launchlibrary as ll
from redbot.core import Config, commands
@ -14,9 +14,7 @@ log = logging.getLogger("red.fox_v3.launchlib")
class LaunchLib(commands.Cog):
"""
Cog Description
Less important information about the cog
Cog using `thespacedevs` API to get details about rocket launches
"""
def __init__(self, bot: Red):
@ -37,27 +35,30 @@ class LaunchLib(commands.Cog):
return
async def _embed_launch_data(self, launch: ll.AsyncLaunch):
status: ll.AsyncLaunchStatus = await launch.get_status()
# status: ll.AsyncLaunchStatus = await launch.get_status()
status = launch.status
rocket: ll.AsyncRocket = launch.rocket
title = launch.name
description = status.description
description = status["name"]
urls = launch.vid_urls + launch.info_urls
if not urls and rocket:
urls = rocket.info_urls + [rocket.wiki_url]
if urls:
url = urls[0]
else:
url = None
if rocket:
urls += [rocket.info_url, rocket.wiki_url]
if launch.pad:
urls += [launch.pad.info_url, launch.pad.wiki_url]
color = discord.Color.green() if status.id in [1, 3] else discord.Color.red()
url = next((url for url in urls if urls is not None), None) if urls else None
color = discord.Color.green() if status["id"] in [1, 3] else discord.Color.red()
em = discord.Embed(title=title, description=description, url=url, color=color)
if rocket and rocket.image_url and rocket.image_url != "Array":
em.set_image(url=rocket.image_url)
elif launch.pad and launch.pad.map_image:
em.set_image(url=launch.pad.map_image)
agency = getattr(launch, "agency", None)
if agency is not None:
@ -89,6 +90,18 @@ class LaunchLib(commands.Cog):
data = mission.get(f[0], None)
if data is not None and data:
em.add_field(name=f[1], value=data)
if launch.pad:
location_url = getattr(launch.pad, "map_url", None)
pad_name = getattr(launch.pad, "name", None)
if pad_name is not None:
if location_url is not None:
location_url = re.sub(
"[^a-zA-Z0-9/:.'+\"°?=,-]", "", location_url
) # Fix bad URLS
em.add_field(name="Launch Pad Name", value=f"[{pad_name}]({location_url})")
else:
em.add_field(name="Launch Pad Name", value=pad_name)
if rocket and rocket.family:
em.add_field(name="Rocket Family", value=rocket.family)
@ -101,11 +114,16 @@ class LaunchLib(commands.Cog):
@commands.group()
async def launchlib(self, ctx: commands.Context):
if ctx.invoked_subcommand is None:
pass
"""Base command for getting launches"""
pass
@launchlib.command()
async def next(self, ctx: commands.Context, num_launches: int = 1):
"""
Show the next launches
Use `num_launches` to get more than one.
"""
# launches = await api.async_next_launches(num_launches)
# loop = asyncio.get_running_loop()
#
@ -115,6 +133,8 @@ class LaunchLib(commands.Cog):
#
launches = await self.api.async_fetch_launch(num=num_launches)
# log.debug(str(launches))
async with ctx.typing():
for x, launch in enumerate(launches):
if x >= num_launches:

@ -25,8 +25,7 @@ class Leaver(Cog):
@checks.mod_or_permissions(administrator=True)
async def leaverset(self, ctx):
"""Adjust leaver settings"""
if ctx.invoked_subcommand is None:
pass
pass
@leaverset.command()
async def channel(self, ctx: Context):
@ -57,5 +56,3 @@ class Leaver(Cog):
)
else:
await channel.send(out)
else:
pass

@ -33,23 +33,27 @@ class LoveCalculator(Cog):
x.replace(" ", "+"), y.replace(" ", "+")
)
async with aiohttp.ClientSession(headers={"Connection": "keep-alive"}) as session:
async with session.get(url) as response:
async with session.get(url, ssl=False) as response:
assert response.status == 200
resp = await response.text()
log.debug(f"{resp=}")
soup_object = BeautifulSoup(resp, "html.parser")
description = soup_object.find("div", class_="result__score").get_text()
description = soup_object.find("div", class_="result__score")
if description is None:
description = "Dr. Love is busy right now"
else:
description = description.strip()
description = description.get_text().strip()
result_image = soup_object.find("img", class_="result__image").get("src")
result_text = soup_object.find("div", class_="result-text").get_text()
result_text = soup_object.find("div", class_="result-text")
if result_text is None:
result_text = f"{x} and {y} aren't compatible 😔"
else:
result_text = result_text.get_text()
result_text = " ".join(result_text.split())
try:
@ -60,14 +64,11 @@ class LoveCalculator(Cog):
else:
emoji = "💔"
title = f"Dr. Love says that the love percentage for {x} and {y} is: {emoji} {description} {emoji}"
except:
except (TypeError, ValueError):
title = "Dr. Love has left a note for you."
em = discord.Embed(
title=title,
description=result_text,
color=discord.Color.red(),
url=f"https://www.lovecalculator.com/{result_image}",
title=title, description=result_text, color=discord.Color.red(), url=url
)
em.set_image(url=f"https://www.lovecalculator.com/{result_image}")
await ctx.send(embed=em)

@ -45,14 +45,12 @@ class LastSeen(Cog):
@staticmethod
def get_date_time(s):
d = dateutil.parser.parse(s)
return d
return dateutil.parser.parse(s)
@commands.group(aliases=["setlseen"], name="lseenset")
async def lset(self, ctx: commands.Context):
"""Change settings for lseen"""
if ctx.invoked_subcommand is None:
pass
pass
@lset.command(name="toggle")
async def lset_toggle(self, ctx: commands.Context):
@ -75,17 +73,17 @@ class LastSeen(Cog):
else:
last_seen = await self.config.member(member).seen()
if last_seen is None:
await ctx.maybe_send_embed(
embed=discord.Embed(description="I've never seen this user")
)
await ctx.maybe_send_embed("I've never seen this user")
return
last_seen = self.get_date_time(last_seen)
# embed = discord.Embed(
# description="{} was last seen at this date and time".format(member.display_name),
# timestamp=last_seen)
embed = discord.Embed(
description="{} was last seen at this date and time".format(member.display_name),
timestamp=last_seen,
color=await self.bot.get_embed_color(ctx),
)
embed = discord.Embed(timestamp=last_seen)
# embed = discord.Embed(timestamp=last_seen, color=await self.bot.get_embed_color(ctx))
await ctx.send(embed=embed)
@commands.Cog.listener()

@ -8,9 +8,7 @@ from redbot.core.data_manager import cog_data_path
class Nudity(commands.Cog):
"""
V3 Cog Template
"""
"""Monitor images for NSFW content and moves them to a nsfw channel if possible"""
def __init__(self, bot: Red):
super().__init__()
@ -85,7 +83,9 @@ class Nudity(commands.Cog):
if r["unsafe"] > 0.7:
await nsfw_channel.send(
"NSFW Image from {}".format(message.channel.mention),
file=discord.File(image,),
file=discord.File(
image,
),
)
@commands.Cog.listener()

@ -111,9 +111,8 @@ async def _withdraw_points(gardener: Gardener, amount):
if (gardener.points - amount) < 0:
return False
else:
gardener.points -= amount
return True
gardener.points -= amount
return True
class PlantTycoon(commands.Cog):
@ -245,11 +244,9 @@ class PlantTycoon(commands.Cog):
await self._load_plants_products()
modifiers = sum(
[
self.products[product]["modifier"]
for product in gardener.products
if gardener.products[product] > 0
]
self.products[product]["modifier"]
for product in gardener.products
if gardener.products[product] > 0
)
degradation = (
@ -290,38 +287,31 @@ class PlantTycoon(commands.Cog):
product = product.lower()
product_category = product_category.lower()
if product in self.products and self.products[product]["category"] == product_category:
if product in gardener.products:
if gardener.products[product] > 0:
gardener.current["health"] += self.products[product]["health"]
gardener.products[product] -= 1
if gardener.products[product] == 0:
del gardener.products[product.lower()]
if product_category == "water":
emoji = ":sweat_drops:"
elif product_category == "fertilizer":
emoji = ":poop:"
# elif product_category == "tool":
else:
emoji = ":scissors:"
message = "Your plant got some health back! {}".format(emoji)
if gardener.current["health"] > gardener.current["threshold"]:
gardener.current["health"] -= self.products[product]["damage"]
if product_category == "tool":
damage_msg = "You used {} too many times!".format(product)
else:
damage_msg = "You gave too much of {}.".format(product)
message = "{} Your plant lost some health. :wilted_rose:".format(
damage_msg
)
gardener.points += self.defaults["points"]["add_health"]
await gardener.save_gardener()
if product in gardener.products and gardener.products[product] > 0:
gardener.current["health"] += self.products[product]["health"]
gardener.products[product] -= 1
if gardener.products[product] == 0:
del gardener.products[product.lower()]
if product_category == "fertilizer":
emoji = ":poop:"
elif product_category == "water":
emoji = ":sweat_drops:"
else:
message = "You have no {}. Go buy some!".format(product)
emoji = ":scissors:"
message = "Your plant got some health back! {}".format(emoji)
if gardener.current["health"] > gardener.current["threshold"]:
gardener.current["health"] -= self.products[product]["damage"]
if product_category == "tool":
damage_msg = "You used {} too many times!".format(product)
else:
damage_msg = "You gave too much of {}.".format(product)
message = "{} Your plant lost some health. :wilted_rose:".format(damage_msg)
gardener.points += self.defaults["points"]["add_health"]
await gardener.save_gardener()
elif product in gardener.products or product_category != "tool":
message = "You have no {}. Go buy some!".format(product)
else:
if product_category == "tool":
message = "You don't have a {}. Go buy one!".format(product)
else:
message = "You have no {}. Go buy some!".format(product)
message = "You don't have a {}. Go buy one!".format(product)
else:
message = "Are you sure you are using {}?".format(product_category)
@ -360,7 +350,9 @@ class PlantTycoon(commands.Cog):
``{0}prune``: Prune your plant.\n"""
em = discord.Embed(
title=title, description=description.format(prefix), color=discord.Color.green(),
title=title,
description=description.format(prefix),
color=discord.Color.green(),
)
em.set_thumbnail(url="https://image.prntscr.com/image/AW7GuFIBSeyEgkR2W3SeiQ.png")
em.set_footer(
@ -410,24 +402,18 @@ class PlantTycoon(commands.Cog):
gardener.current = plant
await gardener.save_gardener()
em = discord.Embed(description=message, color=discord.Color.green())
else:
plant = gardener.current
message = "You're already growing {} **{}**, silly.".format(
plant["article"], plant["name"]
)
em = discord.Embed(description=message, color=discord.Color.green())
em = discord.Embed(description=message, color=discord.Color.green())
await ctx.send(embed=em)
@_gardening.command(name="profile")
async def _profile(self, ctx: commands.Context, *, member: discord.Member = None):
"""Check your gardening profile."""
if member is not None:
author = member
else:
author = ctx.author
author = member if member is not None else ctx.author
gardener = await self._gardener(author)
try:
await self._apply_degradation(gardener)
@ -438,9 +424,7 @@ class PlantTycoon(commands.Cog):
avatar = author.avatar_url if author.avatar else author.default_avatar_url
em.set_author(name="Gardening profile of {}".format(author.name), icon_url=avatar)
em.add_field(name="**Thneeds**", value=str(gardener.points))
if not gardener.current:
em.add_field(name="**Currently growing**", value="None")
else:
if gardener.current:
em.set_thumbnail(url=gardener.current["image"])
em.add_field(
name="**Currently growing**",
@ -448,16 +432,15 @@ class PlantTycoon(commands.Cog):
gardener.current["name"], gardener.current["health"]
),
)
else:
em.add_field(name="**Currently growing**", value="None")
if not gardener.badges:
em.add_field(name="**Badges**", value="None")
else:
badges = ""
for badge in gardener.badges:
badges += "{}\n".format(badge.capitalize())
badges = "".join("{}\n".format(badge.capitalize()) for badge in gardener.badges)
em.add_field(name="**Badges**", value=badges)
if not gardener.products:
em.add_field(name="**Products**", value="None")
else:
if gardener.products:
products = ""
for product_name, product_data in gardener.products.items():
if self.products[product_name] is None:
@ -468,6 +451,8 @@ class PlantTycoon(commands.Cog):
self.products[product_name]["modifier"],
)
em.add_field(name="**Products**", value=products)
else:
em.add_field(name="**Products**", value="None")
if gardener.current:
degradation = await self._degradation(gardener)
die_in = await _die_in(gardener, degradation)
@ -525,7 +510,8 @@ class PlantTycoon(commands.Cog):
if t:
em = discord.Embed(
title="Plant statistics of {}".format(plant["name"]), color=discord.Color.green(),
title="Plant statistics of {}".format(plant["name"]),
color=discord.Color.green(),
)
em.set_thumbnail(url=plant["image"])
em.add_field(name="**Name**", value=plant["name"])
@ -583,7 +569,8 @@ class PlantTycoon(commands.Cog):
author = ctx.author
if product is None:
em = discord.Embed(
title="All gardening supplies that you can buy:", color=discord.Color.green(),
title="All gardening supplies that you can buy:",
color=discord.Color.green(),
)
for pd in self.products:
em.add_field(
@ -596,7 +583,6 @@ class PlantTycoon(commands.Cog):
self.products[pd]["category"],
),
)
await ctx.send(embed=em)
else:
if amount <= 0:
message = "Invalid amount! Must be greater than 1"
@ -616,13 +602,17 @@ class PlantTycoon(commands.Cog):
await gardener.save_gardener()
message = "You bought {}.".format(product.lower())
else:
message = "You don't have enough Thneeds. You have {}, but need {}.".format(
gardener.points, self.products[product.lower()]["cost"] * amount,
message = (
"You don't have enough Thneeds. You have {}, but need {}.".format(
gardener.points,
self.products[product.lower()]["cost"] * amount,
)
)
else:
message = "I don't have this product."
em = discord.Embed(description=message, color=discord.Color.green())
await ctx.send(embed=em)
await ctx.send(embed=em)
@_gardening.command(name="convert")
async def _convert(self, ctx: commands.Context, amount: int):
@ -656,8 +646,7 @@ class PlantTycoon(commands.Cog):
else:
gardener.current = {}
message = "You successfully shovelled your plant out."
if gardener.points < 0:
gardener.points = 0
gardener.points = max(gardener.points, 0)
await gardener.save_gardener()
em = discord.Embed(description=message, color=discord.Color.dark_grey())
@ -674,12 +663,12 @@ class PlantTycoon(commands.Cog):
except discord.Forbidden:
# Couldn't DM the degradation
await ctx.send("ERROR\nYou blocked me, didn't you?")
product = "water"
product_category = "water"
if not gardener.current:
message = "You're currently not growing a plant."
await _send_message(channel, message)
else:
product = "water"
product_category = "water"
await self._add_health(channel, gardener, product, product_category)
@commands.command(name="fertilize")
@ -693,11 +682,11 @@ class PlantTycoon(commands.Cog):
await ctx.send("ERROR\nYou blocked me, didn't you?")
channel = ctx.channel
product = fertilizer
product_category = "fertilizer"
if not gardener.current:
message = "You're currently not growing a plant."
await _send_message(channel, message)
else:
product_category = "fertilizer"
await self._add_health(channel, gardener, product, product_category)
@commands.command(name="prune")
@ -710,12 +699,12 @@ class PlantTycoon(commands.Cog):
# Couldn't DM the degradation
await ctx.send("ERROR\nYou blocked me, didn't you?")
channel = ctx.channel
product = "pruner"
product_category = "tool"
if not gardener.current:
message = "You're currently not growing a plant."
await _send_message(channel, message)
else:
product = "pruner"
product_category = "tool"
await self._add_health(channel, gardener, product, product_category)
# async def check_degradation(self):
@ -786,7 +775,7 @@ class PlantTycoon(commands.Cog):
pass
await asyncio.sleep(self.defaults["timers"]["notification"] * 60)
def __unload(self):
def cog_unload(self):
self.completion_task.cancel()
# self.degradation_task.cancel()
self.notification_task.cancel()

@ -67,8 +67,10 @@ class QRInvite(Cog):
extension = pathlib.Path(image_url).parts[-1].replace(".", "?").split("?")[1]
save_as_name = f"{ctx.guild.id}-{ctx.author.id}"
path: pathlib.Path = cog_data_path(self)
image_path = path / (ctx.guild.icon + "." + extension)
image_path = path / f"{save_as_name}.{extension}"
async with aiohttp.ClientSession() as session:
async with session.get(image_url) as response:
image = await response.read()
@ -77,27 +79,29 @@ class QRInvite(Cog):
file.write(image)
if extension == "webp":
new_path = convert_webp_to_png(str(image_path))
new_image_path = convert_webp_to_png(str(image_path))
elif extension == "gif":
await ctx.maybe_send_embed("gif is not supported yet, stay tuned")
return
elif extension == "png":
new_path = str(image_path)
new_image_path = str(image_path)
elif extension == "jpg":
new_image_path = convert_jpg_to_png(str(image_path))
else:
await ctx.maybe_send_embed(f"{extension} is not supported yet, stay tuned")
return
myqr.run(
invite,
picture=new_path,
save_name=ctx.guild.icon + "_qrcode.png",
picture=new_image_path,
save_name=f"{save_as_name}_qrcode.png",
save_dir=str(cog_data_path(self)),
colorized=colorized,
)
png_path: pathlib.Path = path / (ctx.guild.icon + "_qrcode.png")
with png_path.open("rb") as png_fp:
await ctx.send(file=discord.File(png_fp.read(), "qrcode.png"))
png_path: pathlib.Path = path / f"{save_as_name}_qrcode.png"
# with png_path.open("rb") as png_fp:
await ctx.send(file=discord.File(png_path, "qrcode.png"))
def convert_webp_to_png(path):
@ -110,3 +114,10 @@ def convert_webp_to_png(path):
new_path = path.replace(".webp", ".png")
im.save(new_path, transparency=255)
return new_path
def convert_jpg_to_png(path):
im = Image.open(path)
new_path = path.replace(".jpg", ".png")
im.save(new_path)
return new_path

@ -1,3 +1,4 @@
import logging
from typing import List, Union
import discord
@ -5,6 +6,8 @@ from redbot.core import Config, commands
from redbot.core.bot import Red
from redbot.core.commands import Cog
log = logging.getLogger("red.fox_v3.reactrestrict")
class ReactRestrictCombo:
def __init__(self, message_id, role_id):
@ -94,9 +97,7 @@ class ReactRestrict(Cog):
"""
current_combos = await self.combo_list()
to_keep = [
c for c in current_combos if not (c.message_id == message_id and c.role_id == role.id)
]
to_keep = [c for c in current_combos if c.message_id != message_id or c.role_id != role.id]
if to_keep != current_combos:
await self.set_combo_list(to_keep)
@ -131,10 +132,12 @@ class ReactRestrict(Cog):
If no such channel or member can be found.
"""
channel = self.bot.get_channel(channel_id)
if channel is None:
raise LookupError("no channel found.")
try:
member = channel.guild.get_member(user_id)
except AttributeError as e:
raise LookupError("No channel found.") from e
raise LookupError("No member found.") from e
if member is None:
raise LookupError("No member found.")
@ -168,7 +171,7 @@ class ReactRestrict(Cog):
"""
channel = self.bot.get_channel(channel_id)
try:
return await channel.get_message(message_id)
return await channel.fetch_message(message_id)
except discord.NotFound:
pass
except AttributeError: # VoiceChannel object has no attribute 'get_message'
@ -186,9 +189,11 @@ class ReactRestrict(Cog):
:param message_id:
:return:
"""
for channel in ctx.guild.channels:
guild: discord.Guild = ctx.guild
for channel in guild.text_channels:
try:
return await channel.get_message(message_id)
return await channel.fetch_message(message_id)
except discord.NotFound:
pass
except AttributeError: # VoiceChannel object has no attribute 'get_message'
@ -203,8 +208,7 @@ class ReactRestrict(Cog):
"""
Base command for this cog. Check help for the commands list.
"""
if ctx.invoked_subcommand is None:
pass
pass
@reactrestrict.command()
async def add(self, ctx: commands.Context, message_id: int, *, role: discord.Role):
@ -232,7 +236,7 @@ class ReactRestrict(Cog):
# noinspection PyTypeChecker
await self.add_reactrestrict(message_id, role)
await ctx.maybe_send_embed("Message|Role combo added.")
await ctx.maybe_send_embed("Message|Role restriction added.")
@reactrestrict.command()
async def remove(self, ctx: commands.Context, message_id: int, role: discord.Role):
@ -248,37 +252,38 @@ class ReactRestrict(Cog):
# noinspection PyTypeChecker
await self.remove_react(message_id, role)
await ctx.send("Reaction removed.")
await ctx.send("React restriction removed.")
@commands.Cog.listener()
async def on_raw_reaction_add(
self, emoji: discord.PartialEmoji, message_id: int, channel_id: int, user_id: int
):
async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent):
"""
Event handler for long term reaction watching.
:param discord.PartialReactionEmoji emoji:
:param int message_id:
:param int channel_id:
:param int user_id:
:return:
"""
if emoji.is_custom_emoji():
emoji_id = emoji.id
else:
emoji_id = emoji.name
emoji = payload.emoji
message_id = payload.message_id
channel_id = payload.channel_id
user_id = payload.user_id
# if emoji.is_custom_emoji():
# emoji_id = emoji.id
# else:
# emoji_id = emoji.name
has_reactrestrict, combos = await self.has_reactrestrict_combo(message_id)
if not has_reactrestrict:
log.debug("Message not react restricted")
return
try:
member = self._get_member(channel_id, user_id)
except LookupError:
log.exception("Unable to get member from guild")
return
if member.bot:
log.debug("Won't remove reactions added by bots")
return
if await self.bot.cog_disabled_in_guild(self, member.guild):
@ -287,14 +292,19 @@ class ReactRestrict(Cog):
try:
roles = [self._get_role(member.guild, c.role_id) for c in combos]
except LookupError:
log.exception("Couldn't get approved roles from combos")
return
for apprrole in roles:
if apprrole in member.roles:
log.debug("Has approved role")
return
message = await self._get_message_from_channel(channel_id, message_id)
await message.remove_reaction(emoji, member)
try:
await message.remove_reaction(emoji, member)
except (discord.Forbidden, discord.NotFound, discord.HTTPException):
log.exception("Unable to remove reaction")
# try:
# await member.add_roles(*roles)

@ -32,6 +32,7 @@ class RecyclingPlant(Cog):
x = 0
reward = 0
timeoutcount = 0
await ctx.send(
"{0} has signed up for a shift at the Recycling Plant! Type ``exit`` to terminate it early.".format(
ctx.author.display_name
@ -53,14 +54,25 @@ class RecyclingPlant(Cog):
return m.author == ctx.author and m.channel == ctx.channel
try:
answer = await self.bot.wait_for("message", timeout=120, check=check)
answer = await self.bot.wait_for("message", timeout=20, check=check)
except asyncio.TimeoutError:
answer = None
if answer is None:
await ctx.send(
"``{}`` fell down the conveyor belt to be sorted again!".format(used["object"])
)
if timeoutcount == 2:
await ctx.send(
"{} slacked off at work, so they were sacked with no pay.".format(
ctx.author.display_name
)
)
break
else:
await ctx.send(
"{} is slacking, and if they carry on not working, they'll be fired.".format(
ctx.author.display_name
)
)
timeoutcount += 1
elif answer.content.lower().strip() == used["action"]:
await ctx.send(
"Congratulations! You put ``{}`` down the correct chute! (**+50**)".format(

@ -28,8 +28,8 @@ class RPSLS(Cog):
@commands.command()
async def rpsls(self, ctx: commands.Context, choice: str):
"""
Play Rock Paper Scissors Lizard Spock by Sam Kass in Discord!
Play Rock Paper Scissors Lizard Spock by Sam Kass in Discord!
Rules:
Scissors cuts Paper
Paper covers Rock
@ -69,13 +69,12 @@ class RPSLS(Cog):
def get_emote(self, choice):
if choice == "rock":
emote = ":moyai:"
return ":moyai:"
elif choice == "spock":
emote = ":vulcan:"
return ":vulcan:"
elif choice == "paper":
emote = ":page_facing_up:"
return ":page_facing_up:"
elif choice in ["scissors", "lizard"]:
emote = ":{}:".format(choice)
return ":{}:".format(choice)
else:
emote = None
return emote
return None

@ -177,7 +177,3 @@ class SCP(Cog):
msg = "http://www.scp-wiki.net/log-of-unexplained-locations"
await ctx.maybe_send_embed(msg)
def setup(bot):
bot.add_cog(SCP(bot))

@ -6,6 +6,7 @@ import discord
from redbot.core import Config, checks, commands
from redbot.core.bot import Red
from redbot.core.commands import Cog
from redbot.core.utils.chat_formatting import pagify
log = logging.getLogger("red.fox_v3.stealemoji")
# Replaced with discord.Asset.read()
@ -16,16 +17,16 @@ log = logging.getLogger("red.fox_v3.stealemoji")
async def check_guild(guild, emoji):
if len(guild.emojis) >= 100:
if len(guild.emojis) >= 2 * guild.emoji_limit:
return False
if len(guild.emojis) < 50:
if len(guild.emojis) < guild.emoji_limit:
return True
if emoji.animated:
return sum(e.animated for e in guild.emojis) < 50
return sum(e.animated for e in guild.emojis) < guild.emoji_limit
else:
return sum(not e.animated for e in guild.emojis) < 50
return sum(not e.animated for e in guild.emojis) < guild.emoji_limit
class StealEmoji(Cog):
@ -50,6 +51,7 @@ class StealEmoji(Cog):
default_global = {
"stolemoji": {},
"guildbanks": [],
"autobanked_guilds": [],
"on": False,
"notify": 0,
"autobank": False,
@ -68,8 +70,7 @@ class StealEmoji(Cog):
"""
Base command for this cog. Check help for the commands list.
"""
if ctx.invoked_subcommand is None:
pass
pass
@checks.is_owner()
@stealemoji.command(name="clearemojis")
@ -99,7 +100,8 @@ class StealEmoji(Cog):
await ctx.maybe_send_embed("No stolen emojis yet")
return
await ctx.maybe_send_embed(emoj)
for page in pagify(emoj, delims=[" "]):
await ctx.maybe_send_embed(page)
@checks.is_owner()
@stealemoji.command(name="notify")
@ -145,11 +147,54 @@ class StealEmoji(Cog):
await ctx.maybe_send_embed("AutoBanking is now " + str(not curr_setting))
@checks.is_owner()
@commands.guild_only()
@stealemoji.command(name="deleteserver", aliases=["deleteguild"])
async def se_deleteserver(self, ctx: commands.Context, guild_id=None):
"""Delete servers the bot is the owner of.
Useful for auto-generated guildbanks."""
if guild_id is None:
guild = ctx.guild
else:
guild = await self.bot.get_guild(guild_id)
if guild is None:
await ctx.maybe_send_embed("Failed to get guild, cancelling")
return
guild: discord.Guild
await ctx.maybe_send_embed(
f"Will attempt to delete {guild.name} ({guild.id})\n" f"Okay to continue? (yes/no)"
)
def check(m):
return m.author == ctx.author and m.channel == ctx.channel
try:
answer = await self.bot.wait_for("message", timeout=120, check=check)
except asyncio.TimeoutError:
await ctx.send("Timed out, canceling")
return
if answer.content.upper() not in ["Y", "YES"]:
await ctx.maybe_send_embed("Cancelling")
return
try:
await guild.delete()
except discord.Forbidden:
log.exception("No permission to delete. I'm probably not the guild owner")
await ctx.maybe_send_embed("No permission to delete. I'm probably not the guild owner")
except discord.HTTPException:
log.exception("Unexpected error when deleting guild")
await ctx.maybe_send_embed("Unexpected error when deleting guild")
else:
await self.bot.send_to_owners(f"Guild {guild.name} deleted")
@checks.is_owner()
@commands.guild_only()
@stealemoji.command(name="bank")
async def se_bank(self, ctx):
"""Add current server as emoji bank"""
"""Add or remove current server as emoji bank"""
def check(m):
return (
@ -224,34 +269,36 @@ class StealEmoji(Cog):
break
if guildbank is None:
if await self.config.autobank():
try:
guildbank: discord.Guild = await self.bot.create_guild(
"StealEmoji Guildbank", code="S93bqTqKQ9rM"
)
except discord.HTTPException:
await self.config.autobank.set(False)
log.exception("Unable to create guilds, disabling autobank")
return
async with self.config.guildbanks() as guildbanks:
guildbanks.append(guildbank.id)
await asyncio.sleep(2)
if guildbank.text_channels:
channel = guildbank.text_channels[0]
else:
# Always hits the else.
# Maybe create_guild doesn't return guild object with
# the template channel?
channel = await guildbank.create_text_channel("invite-channel")
invite = await channel.create_invite()
await self.bot.send_to_owners(invite)
log.info(f"Guild created id {guildbank.id}. Invite: {invite}")
else:
if not await self.config.autobank():
return
try:
guildbank: discord.Guild = await self.bot.create_guild(
"StealEmoji Guildbank", code="S93bqTqKQ9rM"
)
except discord.HTTPException:
await self.config.autobank.set(False)
log.exception("Unable to create guilds, disabling autobank")
return
async with self.config.guildbanks() as guildbanks:
guildbanks.append(guildbank.id)
# Track generated guilds for easier deletion
async with self.config.autobanked_guilds() as autobanked_guilds:
autobanked_guilds.append(guildbank.id)
await asyncio.sleep(2)
if guildbank.text_channels:
channel = guildbank.text_channels[0]
else:
# Always hits the else.
# Maybe create_guild doesn't return guild object with
# the template channel?
channel = await guildbank.create_text_channel("invite-channel")
invite = await channel.create_invite()
await self.bot.send_to_owners(invite)
log.info(f"Guild created id {guildbank.id}. Invite: {invite}")
# Next, have I saved this emoji before (because uploaded emoji != orignal emoji)
if str(emoji.id) in await self.config.stolemoji():

@ -1,6 +1,7 @@
import asyncio
import logging
from datetime import datetime, timedelta
from typing import Optional
import discord
from redbot.core import Config, checks, commands
@ -19,6 +20,15 @@ async def sleep_till_next_hour():
await asyncio.sleep((next_hour - datetime.utcnow()).seconds)
async def announce_to_channel(channel, results, title):
if channel is not None and results:
await channel.send(title)
for page in pagify(results, shorten_by=50):
await channel.send(page)
elif results: # Channel is None, log the results
log.info(results)
class Timerole(Cog):
"""Add roles to users based on time on server"""
@ -27,10 +37,15 @@ class Timerole(Cog):
self.bot = bot
self.config = Config.get_conf(self, identifier=9811198108111121, force_registration=True)
default_global = {}
default_guild = {"announce": None, "roles": {}}
default_guild = {"announce": None, "reapply": True, "roles": {}, "skipbots": True}
default_rolemember = {"had_role": False, "check_again_time": None}
self.config.register_global(**default_global)
self.config.register_guild(**default_guild)
self.config.init_custom("RoleMember", 2)
self.config.register_custom("RoleMember", **default_rolemember)
self.updating = asyncio.create_task(self.check_hour())
async def red_delete_data_for_user(self, **kwargs):
@ -49,18 +64,20 @@ class Timerole(Cog):
Useful for troubleshooting the initial setup
"""
async with ctx.typing():
pre_run = datetime.utcnow()
await self.timerole_update()
after_run = datetime.utcnow()
await ctx.tick()
await ctx.maybe_send_embed(f"Took {after_run-pre_run} seconds")
@commands.group()
@checks.mod_or_permissions(administrator=True)
@commands.guild_only()
async def timerole(self, ctx):
"""Adjust timerole settings"""
if ctx.invoked_subcommand is None:
pass
pass
@timerole.command()
async def addrole(
@ -75,6 +92,9 @@ class Timerole(Cog):
await ctx.maybe_send_embed("Error: Invalid time string.")
return
if parsed_time is None:
return await ctx.maybe_send_embed("Error: Invalid time string.")
days = parsed_time.days
hours = parsed_time.seconds // 60 // 60
@ -84,9 +104,7 @@ class Timerole(Cog):
await self.config.guild(guild).roles.set_raw(role.id, value=to_set)
await ctx.maybe_send_embed(
"Time Role for {0} set to {1} days and {2} hours until added".format(
role.name, days, hours
)
f"Time Role for {role.name} set to {days} days and {hours} hours until added"
)
@timerole.command()
@ -114,18 +132,35 @@ class Timerole(Cog):
await self.config.guild(guild).roles.set_raw(role.id, value=to_set)
await ctx.maybe_send_embed(
"Time Role for {0} set to {1} days and {2} hours until removed".format(
role.name, days, hours
)
f"Time Role for {role.name} set to {days} days and {hours} hours until removed"
)
@timerole.command()
async def channel(self, ctx: commands.Context, channel: discord.TextChannel):
async def channel(self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None):
"""Sets the announce channel for role adds"""
guild = ctx.guild
if channel is None:
await self.config.guild(guild).announce.clear()
await ctx.maybe_send_embed(f"Announce channel has been cleared")
else:
await self.config.guild(guild).announce.set(channel.id)
await ctx.send(f"Announce channel set to {channel.mention}")
await self.config.guild(guild).announce.set(channel.id)
await ctx.send("Announce channel set to {0}".format(channel.mention))
@timerole.command()
async def reapply(self, ctx: commands.Context):
"""Toggle reapplying roles if the member loses it somehow. Defaults to True"""
guild = ctx.guild
current_setting = await self.config.guild(guild).reapply()
await self.config.guild(guild).reapply.set(not current_setting)
await ctx.maybe_send_embed(f"Reapplying roles is now set to: {not current_setting}")
@timerole.command()
async def skipbots(self, ctx: commands.Context):
"""Toggle skipping bots when adding/removing roles. Defaults to True"""
guild = ctx.guild
current_setting = await self.config.guild(guild).skipbots()
await self.config.guild(guild).skipbots.set(not current_setting)
await ctx.maybe_send_embed(f"Skipping bots is now set to: {not current_setting}")
@timerole.command()
async def delrole(self, ctx: commands.Context, role: discord.Role):
@ -133,7 +168,8 @@ class Timerole(Cog):
guild = ctx.guild
await self.config.guild(guild).roles.set_raw(role.id, value=None)
await ctx.send("{0} will no longer be applied".format(role.name))
await self.config.custom("RoleMember", role.id).clear()
await ctx.maybe_send_embed(f"{role.name} will no longer be applied")
@timerole.command()
async def list(self, ctx: commands.Context):
@ -153,95 +189,211 @@ class Timerole(Cog):
str(discord.utils.get(guild.roles, id=int(new_id)))
for new_id in r_data["required"]
]
out += "{} | {} days | requires: {}\n".format(str(role), r_data["days"], r_roles)
out += f"{role} | {r_data['days']} days | requires: {r_roles}\n"
await ctx.maybe_send_embed(out)
async def timerole_update(self):
utcnow = datetime.utcnow()
all_guilds = await self.config.all_guilds()
# all_mrs = await self.config.custom("RoleMember").all()
# log.debug(f"Begin timerole update")
for guild in self.bot.guilds:
addlist = []
removelist = []
guild_id = guild.id
if guild_id not in all_guilds:
log.debug(f"Guild has no configured settings: {guild}")
continue
add_results = ""
remove_results = ""
reapply = all_guilds[guild_id]["reapply"]
role_dict = all_guilds[guild_id]["roles"]
skipbots = all_guilds[guild_id]["skipbots"]
role_dict = await self.config.guild(guild).roles()
if not any(role_data for role_data in role_dict.values()): # No roles
if not any(role_dict.values()): # No roles
log.debug(f"No roles are configured for guild: {guild}")
continue
async for member in AsyncIter(guild.members):
has_roles = [r.id for r in member.roles]
# all_mr = await self.config.all_custom("RoleMember")
# log.debug(f"{all_mr=}")
async for member in AsyncIter(guild.members, steps=10):
if member.bot and skipbots:
continue
addlist = []
removelist = []
for role_id, role_data in role_dict.items():
# Skip non-configured roles
if not role_data:
continue
mr_dict = await self.config.custom("RoleMember", role_id, member.id).all()
# Stop if they've had the role and reapplying is disabled
if not reapply and mr_dict["had_role"]:
log.debug(f"{member.display_name} - Not reapplying")
continue
# Stop if the check_again_time hasn't passed yet
if (
mr_dict["check_again_time"] is not None
and datetime.fromisoformat(mr_dict["check_again_time"]) >= utcnow
):
log.debug(f"{member.display_name} - Not time to check again yet")
continue
member: discord.Member
has_roles = {r.id for r in member.roles}
# Stop if they currently have or don't have the role, and mark had_role
if (int(role_id) in has_roles and not role_data["remove"]) or (
int(role_id) not in has_roles and role_data["remove"]
):
if not mr_dict["had_role"]:
await self.config.custom(
"RoleMember", role_id, member.id
).had_role.set(True)
log.debug(f"{member.display_name} - applying had_role")
continue
# Stop if they don't have all the required roles
if role_data is None or (
"required" in role_data and not set(role_data["required"]) & has_roles
):
continue
check_time = member.joined_at + timedelta(
days=role_data["days"],
hours=role_data.get("hours", 0),
)
# Check if enough time has passed to get the role and save the check_again_time
if check_time >= utcnow:
await self.config.custom(
"RoleMember", role_id, member.id
).check_again_time.set(check_time.isoformat())
log.debug(
f"{member.display_name} - Not enough time has passed to qualify for the role\n"
f"Waiting until {check_time}"
)
continue
if role_data["remove"]:
removelist.append(role_id)
else:
addlist.append(role_id)
# Done iterating through roles, now add or remove the roles
if not addlist and not removelist:
continue
# log.debug(f"{addlist=}\n{removelist=}")
add_roles = [
int(rID)
for rID, r_data in role_dict.items()
if r_data is not None and not r_data["remove"]
discord.utils.get(guild.roles, id=int(role_id)) for role_id in addlist
]
remove_roles = [
int(rID)
for rID, r_data in role_dict.items()
if r_data is not None and r_data["remove"]
discord.utils.get(guild.roles, id=int(role_id)) for role_id in removelist
]
check_add_roles = set(add_roles) - set(has_roles)
check_remove_roles = set(remove_roles) & set(has_roles)
await self.check_required_and_date(
addlist, check_add_roles, has_roles, member, role_dict
)
await self.check_required_and_date(
removelist, check_remove_roles, has_roles, member, role_dict
)
if None in add_roles or None in remove_roles:
log.info(
f"Timerole ran into an error with the roles in: {add_roles + remove_roles}"
)
if addlist:
try:
await member.add_roles(*add_roles, reason="Timerole", atomic=False)
except (discord.Forbidden, discord.NotFound) as e:
log.exception("Failed Adding Roles")
add_results += f"{member.display_name} : **(Failed Adding Roles)**\n"
else:
add_results += (
" \n".join(
f"{member.display_name} : {role.name}" for role in add_roles
)
+ "\n"
)
for role_id in addlist:
await self.config.custom(
"RoleMember", role_id, member.id
).had_role.set(True)
if removelist:
try:
await member.remove_roles(*remove_roles, reason="Timerole", atomic=False)
except (discord.Forbidden, discord.NotFound) as e:
log.exception("Failed Removing Roles")
remove_results += f"{member.display_name} : **(Failed Removing Roles)**\n"
else:
remove_results += (
" \n".join(
f"{member.display_name} : {role.name}" for role in remove_roles
)
+ "\n"
)
for role_id in removelist:
await self.config.custom(
"RoleMember", role_id, member.id
).had_role.set(True)
# Done iterating through members, now maybe announce to the guild
channel = await self.config.guild(guild).announce()
if channel is not None:
channel = guild.get_channel(channel)
title = "**These members have received the following roles**\n"
await self.announce_roles(title, addlist, channel, guild, to_add=True)
title = "**These members have lost the following roles**\n"
await self.announce_roles(title, removelist, channel, guild, to_add=False)
async def announce_roles(self, title, role_list, channel, guild, to_add: True):
results = ""
for member, role_id in role_list:
role = discord.utils.get(guild.roles, id=role_id)
try:
if to_add:
await member.add_roles(role, reason="Timerole")
else:
await member.remove_roles(role, reason="Timerole")
except (discord.Forbidden, discord.NotFound) as e:
results += "{} : {} **(Failed)**\n".format(member.display_name, role.name)
else:
results += "{} : {}\n".format(member.display_name, role.name)
if channel is not None and results:
await channel.send(title)
for page in pagify(results, shorten_by=50):
await channel.send(page)
elif results: # Channel is None, log the results
log.info(results)
async def check_required_and_date(self, role_list, check_roles, has_roles, member, role_dict):
for role_id in check_roles:
# Check for required role
if "required" in role_dict[str(role_id)]:
if not set(role_dict[str(role_id)]["required"]) & set(has_roles):
# Doesn't have required role
continue
if (
member.joined_at
+ timedelta(
days=role_dict[str(role_id)]["days"],
hours=role_dict[str(role_id)].get("hours", 0),
)
<= datetime.today()
):
# Qualifies
role_list.append((member, role_id))
if add_results:
title = "**These members have received the following roles**\n"
await announce_to_channel(channel, add_results, title)
if remove_results:
title = "**These members have lost the following roles**\n"
await announce_to_channel(channel, remove_results, title)
# End
# async def announce_roles(self, title, role_list, channel, guild, to_add: True):
# results = ""
# async for member, role_id in AsyncIter(role_list):
# role = discord.utils.get(guild.roles, id=role_id)
# try:
# if to_add:
# await member.add_roles(role, reason="Timerole")
# else:
# await member.remove_roles(role, reason="Timerole")
# except (discord.Forbidden, discord.NotFound) as e:
# results += f"{member.display_name} : {role.name} **(Failed)**\n"
# else:
# results += f"{member.display_name} : {role.name}\n"
# if channel is not None and results:
# await channel.send(title)
# for page in pagify(results, shorten_by=50):
# await channel.send(page)
# elif results: # Channel is None, log the results
# log.info(results)
# async def check_required_and_date(self, role_list, check_roles, has_roles, member, role_dict):
# async for role_id in AsyncIter(check_roles):
# # Check for required role
# if "required" in role_dict[str(role_id)]:
# if not set(role_dict[str(role_id)]["required"]) & set(has_roles):
# # Doesn't have required role
# continue
#
# if (
# member.joined_at
# + timedelta(
# days=role_dict[str(role_id)]["days"],
# hours=role_dict[str(role_id)].get("hours", 0),
# )
# <= datetime.utcnow()
# ):
# # Qualifies
# role_list.append((member, role_id))
async def check_hour(self):
await sleep_till_next_hour()
while self is self.bot.get_cog("Timerole"):
await self.timerole_update()
await sleep_till_next_hour()

@ -1,11 +1,35 @@
import io
import logging
from typing import Optional, TYPE_CHECKING
import discord
from discord.ext.commands import BadArgument, Converter
from gtts import gTTS
from gtts.lang import _fallback_deprecated_lang, tts_langs
from redbot.core import Config, commands
from redbot.core.bot import Red
from redbot.core.commands import Cog
log = logging.getLogger("red.fox_v3.tts")
if TYPE_CHECKING:
ISO639Converter = str
else:
class ISO639Converter(Converter):
async def convert(self, ctx, argument) -> str:
lang = _fallback_deprecated_lang(argument)
try:
langs = tts_langs()
if lang not in langs:
raise BadArgument("Language not supported: %s" % lang)
except RuntimeError as e:
log.debug(str(e), exc_info=True)
log.warning(str(e))
return lang
class TTS(Cog):
"""
@ -18,7 +42,7 @@ class TTS(Cog):
self.config = Config.get_conf(self, identifier=9811198108111121, force_registration=True)
default_global = {}
default_guild = {}
default_guild = {"language": "en"}
self.config.register_global(**default_global)
self.config.register_guild(**default_guild)
@ -27,13 +51,29 @@ class TTS(Cog):
"""Nothing to delete"""
return
@commands.mod()
@commands.command()
async def ttslang(self, ctx: commands.Context, lang: ISO639Converter):
"""
Sets the default language for TTS in this guild.
Default is `en` for English
"""
await self.config.guild(ctx.guild).language.set(lang)
await ctx.send(f"Default tts language set to {lang}")
@commands.command(aliases=["t2s", "text2"])
async def tts(self, ctx: commands.Context, *, text: str):
async def tts(
self, ctx: commands.Context, lang: Optional[ISO639Converter] = None, *, text: str
):
"""
Send Text to speech messages as an mp3
"""
Send Text to speech messages as an mp3
"""
if lang is None:
lang = await self.config.guild(ctx.guild).language()
mp3_fp = io.BytesIO()
tts = gTTS(text, lang="en")
tts = gTTS(text, lang=lang)
tts.write_to_fp(mp3_fp)
mp3_fp.seek(0)
await ctx.send(file=discord.File(mp3_fp, "text.mp3"))

@ -19,8 +19,7 @@ class Unicode(Cog):
@commands.group(name="unicode", pass_context=True)
async def unicode(self, ctx):
"""Encode/Decode a Unicode character."""
if ctx.invoked_subcommand is None:
pass
pass
@unicode.command()
async def decode(self, ctx: commands.Context, character):

@ -1,5 +1,7 @@
import bisect
import logging
from collections import defaultdict
from operator import attrgetter
from random import choice
import discord
@ -8,77 +10,55 @@ import discord
# Import all roles here
from redbot.core import commands
from .roles.seer import Seer
from .roles.vanillawerewolf import VanillaWerewolf
from .roles.villager import Villager
from redbot.core.utils.menus import menu, prev_page, next_page, close_menu
# from .roles.seer import Seer
# from .roles.vanillawerewolf import VanillaWerewolf
# from .roles.villager import Villager
# All roles in this list for iterating
from werewolf import roles
from redbot.core.utils.menus import menu, prev_page, next_page, close_menu
ROLE_LIST = sorted([Villager, Seer, VanillaWerewolf], key=lambda x: x.alignment)
from werewolf.constants import ROLE_CATEGORY_DESCRIPTIONS
from werewolf.role import Role
ALIGNMENT_COLORS = [0x008000, 0xff0000, 0xc0c0c0]
TOWN_ROLES = [(idx, role) for idx, role in enumerate(ROLE_LIST) if role.alignment == 1]
WW_ROLES = [(idx, role) for idx, role in enumerate(ROLE_LIST) if role.alignment == 2]
OTHER_ROLES = [(idx, role) for idx, role in enumerate(ROLE_LIST) if role.alignment not in [0, 1]]
log = logging.getLogger("red.fox_v3.werewolf.builder")
ROLE_PAGES = []
PAGE_GROUPS = [0]
# All roles in this list for iterating
ROLE_CATEGORIES = {
1: "Random", 2: "Investigative", 3: "Protective", 4: "Government",
5: "Killing", 6: "Power (Special night action)",
11: "Random", 12: "Deception", 15: "Killing", 16: "Support",
21: "Benign", 22: "Evil", 23: "Killing"}
ROLE_DICT = {name: cls for name, cls in roles.__dict__.items() if isinstance(cls, type)}
ROLE_LIST = sorted(
[cls for cls in ROLE_DICT.values()],
key=attrgetter("alignment"),
)
CATEGORY_COUNT = []
log.debug(f"{ROLE_DICT=}")
# Town, Werewolf, Neutral
ALIGNMENT_COLORS = [0x008000, 0xFF0000, 0xC0C0C0]
def role_embed(idx, role, color):
embed = discord.Embed(title="**{}** - {}".format(idx, str(role.__name__)), description=role.game_start_message,
color=color)
embed.add_field(name='Alignment', value=['Town', 'Werewolf', 'Neutral'][role.alignment - 1], inline=True)
embed.add_field(name='Multiples Allowed', value=str(not role.unique), inline=True)
embed.add_field(name='Role Type', value=", ".join(ROLE_CATEGORIES[x] for x in role.category), inline=True)
embed.add_field(name='Random Option', value=str(role.rand_choice), inline=True)
ROLE_PAGES = []
return embed
def role_embed(idx, role: Role, color):
embed = discord.Embed(
title=f"**{idx}** - {role.__name__}",
description=role.game_start_message,
color=color,
)
if role.icon_url is not None:
embed.set_thumbnail(url=role.icon_url)
embed.add_field(
name="Alignment", value=["Town", "Werewolf", "Neutral"][role.alignment - 1], inline=False
)
embed.add_field(name="Multiples Allowed", value=str(not role.unique), inline=False)
embed.add_field(
name="Role Types",
value=", ".join(ROLE_CATEGORY_DESCRIPTIONS[x] for x in role.category),
inline=False,
)
embed.add_field(name="Random Option", value=str(role.rand_choice), inline=False)
def setup():
# Roles
last_alignment = ROLE_LIST[0].alignment
for idx, role in enumerate(ROLE_LIST):
if role.alignment != last_alignment and len(ROLE_PAGES) - 1 not in PAGE_GROUPS:
PAGE_GROUPS.append(len(ROLE_PAGES) - 1)
last_alignment = role.alignment
ROLE_PAGES.append(role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]))
# Random Town Roles
if len(ROLE_PAGES) - 1 not in PAGE_GROUPS:
PAGE_GROUPS.append(len(ROLE_PAGES) - 1)
for k, v in ROLE_CATEGORIES.items():
if 0 < k <= 6:
ROLE_PAGES.append(discord.Embed(title="RANDOM:Town Role", description="Town {}".format(v), color=0x008000))
CATEGORY_COUNT.append(k)
# Random WW Roles
if len(ROLE_PAGES) - 1 not in PAGE_GROUPS:
PAGE_GROUPS.append(len(ROLE_PAGES) - 1)
for k, v in ROLE_CATEGORIES.items():
if 10 < k <= 16:
ROLE_PAGES.append(
discord.Embed(title="RANDOM:Werewolf Role", description="Werewolf {}".format(v), color=0xff0000))
CATEGORY_COUNT.append(k)
# Random Neutral Roles
if len(ROLE_PAGES) - 1 not in PAGE_GROUPS:
PAGE_GROUPS.append(len(ROLE_PAGES) - 1)
for k, v in ROLE_CATEGORIES.items():
if 20 < k <= 26:
ROLE_PAGES.append(
discord.Embed(title="RANDOM:Neutral Role", description="Neutral {}".format(v), color=0xc0c0c0))
CATEGORY_COUNT.append(k)
return embed
"""
@ -91,6 +71,7 @@ W1, W2, W5, W6 = Random Werewolf
N1 = Benign Neutral
0001-1112T11W112N2
which translates to
0,0,0,1,11,12,E1,R1,R1,R1,R2,P2
pre-letter = exact role position
@ -109,7 +90,7 @@ async def parse_code(code, game):
if len(built) < digits:
built += c
if built == "T" or built == "W" or built == "N":
if built in ["T", "W", "N"]:
# Random Towns
category = built
built = ""
@ -135,8 +116,6 @@ async def parse_code(code, game):
options = [role for role in ROLE_LIST if 10 + idx in role.category]
elif category == "N":
options = [role for role in ROLE_LIST if 20 + idx in role.category]
pass
if not options:
raise IndexError("No Match Found")
@ -147,15 +126,12 @@ async def parse_code(code, game):
return decode
async def encode(roles, rand_roles):
async def encode(role_list, rand_roles):
"""Convert role list to code"""
out_code = ""
digit_sort = sorted(role for role in roles if role < 10)
for role in digit_sort:
out_code += str(role)
digit_sort = sorted(role for role in role_list if role < 10)
out_code = "".join(str(role) for role in digit_sort)
digit_sort = sorted(role for role in roles if 10 <= role < 100)
digit_sort = sorted(role for role in role_list if 10 <= role < 100)
if digit_sort:
out_code += "-"
for role in digit_sort:
@ -187,49 +163,20 @@ async def encode(roles, rand_roles):
return out_code
async def next_group(ctx: commands.Context, pages: list,
controls: dict, message: discord.Message, page: int,
timeout: float, emoji: str):
perms = message.channel.permissions_for(ctx.guild.me)
if perms.manage_messages: # Can manage messages, so remove react
try:
await message.remove_reaction(emoji, ctx.author)
except discord.NotFound:
pass
page = bisect.bisect_right(PAGE_GROUPS, page)
if page == len(PAGE_GROUPS):
page = PAGE_GROUPS[0]
else:
page = PAGE_GROUPS[page]
return await menu(ctx, pages, controls, message=message,
page=page, timeout=timeout)
async def prev_group(ctx: commands.Context, pages: list,
controls: dict, message: discord.Message, page: int,
timeout: float, emoji: str):
perms = message.channel.permissions_for(ctx.guild.me)
if perms.manage_messages: # Can manage messages, so remove react
try:
await message.remove_reaction(emoji, ctx.author)
except discord.NotFound:
pass
page = PAGE_GROUPS[bisect.bisect_left(PAGE_GROUPS, page) - 1]
return await menu(ctx, pages, controls, message=message,
page=page, timeout=timeout)
def role_from_alignment(alignment):
return [role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1])
for idx, role in enumerate(ROLE_LIST) if alignment == role.alignment]
return [
role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1])
for idx, role in enumerate(ROLE_LIST)
if alignment == role.alignment
]
def role_from_category(category):
return [role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1])
for idx, role in enumerate(ROLE_LIST) if category in role.category]
return [
role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1])
for idx, role in enumerate(ROLE_LIST)
if category in role.category
]
def role_from_id(idx):
@ -242,8 +189,11 @@ def role_from_id(idx):
def role_from_name(name: str):
return [role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1])
for idx, role in enumerate(ROLE_LIST) if name in role.__name__]
return [
role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1])
for idx, role in enumerate(ROLE_LIST)
if name in role.__name__
]
def say_role_list(code_list, rand_roles):
@ -255,34 +205,87 @@ def say_role_list(code_list, rand_roles):
for role in rand_roles:
if 0 < role <= 6:
role_dict["Town {}".format(ROLE_CATEGORIES[role])] += 1
role_dict[f"Town {ROLE_CATEGORY_DESCRIPTIONS[role]}"] += 1
if 10 < role <= 16:
role_dict["Werewolf {}".format(ROLE_CATEGORIES[role])] += 1
role_dict[f"Werewolf {ROLE_CATEGORY_DESCRIPTIONS[role]}"] += 1
if 20 < role <= 26:
role_dict["Neutral {}".format(ROLE_CATEGORIES[role])] += 1
role_dict[f"Neutral {ROLE_CATEGORY_DESCRIPTIONS[role]}"] += 1
for k, v in role_dict.items():
embed.add_field(name=k, value="Count: {}".format(v), inline=True)
embed.add_field(name=k, value=f"Count: {v}", inline=True)
return embed
class GameBuilder:
def __init__(self):
self.code = []
self.rand_roles = []
setup()
self.page_groups = [0]
self.category_count = []
self.setup()
def setup(self):
# Roles
last_alignment = ROLE_LIST[0].alignment
for idx, role in enumerate(ROLE_LIST):
if role.alignment != last_alignment and len(ROLE_PAGES) - 1 not in self.page_groups:
self.page_groups.append(len(ROLE_PAGES) - 1)
last_alignment = role.alignment
ROLE_PAGES.append(role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]))
# Random Town Roles
if len(ROLE_PAGES) - 1 not in self.page_groups:
self.page_groups.append(len(ROLE_PAGES) - 1)
for k, v in ROLE_CATEGORY_DESCRIPTIONS.items():
if 0 < k <= 9:
ROLE_PAGES.append(
discord.Embed(
title="RANDOM:Town Role",
description=f"Town {v}",
color=ALIGNMENT_COLORS[0],
)
)
self.category_count.append(k)
# Random WW Roles
if len(ROLE_PAGES) - 1 not in self.page_groups:
self.page_groups.append(len(ROLE_PAGES) - 1)
for k, v in ROLE_CATEGORY_DESCRIPTIONS.items():
if 10 < k <= 19:
ROLE_PAGES.append(
discord.Embed(
title="RANDOM:Werewolf Role",
description=f"Werewolf {v}",
color=ALIGNMENT_COLORS[1],
)
)
self.category_count.append(k)
# Random Neutral Roles
if len(ROLE_PAGES) - 1 not in self.page_groups:
self.page_groups.append(len(ROLE_PAGES) - 1)
for k, v in ROLE_CATEGORY_DESCRIPTIONS.items():
if 20 < k <= 29:
ROLE_PAGES.append(
discord.Embed(
title=f"RANDOM:Neutral Role",
description=f"Neutral {v}",
color=ALIGNMENT_COLORS[2],
)
)
self.category_count.append(k)
async def build_game(self, ctx: commands.Context):
new_controls = {
'': prev_group,
"": self.prev_group,
"": prev_page,
'': self.select_page,
"": self.select_page,
"": next_page,
'': next_group,
'📇': self.list_roles,
"": close_menu
"": self.next_group,
"📇": self.list_roles,
"": close_menu,
}
await ctx.send("Browse through roles and add the ones you want using the check mark")
@ -292,10 +295,17 @@ class GameBuilder:
out = await encode(self.code, self.rand_roles)
return out
async def list_roles(self, ctx: commands.Context, pages: list,
controls: dict, message: discord.Message, page: int,
timeout: float, emoji: str):
perms = message.channel.permissions_for(ctx.guild.me)
async def list_roles(
self,
ctx: commands.Context,
pages: list,
controls: dict,
message: discord.Message,
page: int,
timeout: float,
emoji: str,
):
perms = message.channel.permissions_for(ctx.me)
if perms.manage_messages: # Can manage messages, so remove react
try:
await message.remove_reaction(emoji, ctx.author)
@ -304,13 +314,19 @@ class GameBuilder:
await ctx.send(embed=say_role_list(self.code, self.rand_roles))
return await menu(ctx, pages, controls, message=message,
page=page, timeout=timeout)
async def select_page(self, ctx: commands.Context, pages: list,
controls: dict, message: discord.Message, page: int,
timeout: float, emoji: str):
perms = message.channel.permissions_for(ctx.guild.me)
return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout)
async def select_page(
self,
ctx: commands.Context,
pages: list,
controls: dict,
message: discord.Message,
page: int,
timeout: float,
emoji: str,
):
perms = message.channel.permissions_for(ctx.me)
if perms.manage_messages: # Can manage messages, so remove react
try:
await message.remove_reaction(emoji, ctx.author)
@ -318,9 +334,53 @@ class GameBuilder:
pass
if page >= len(ROLE_LIST):
self.rand_roles.append(CATEGORY_COUNT[page - len(ROLE_LIST)])
self.rand_roles.append(self.category_count[page - len(ROLE_LIST)])
else:
self.code.append(page)
return await menu(ctx, pages, controls, message=message,
page=page, timeout=timeout)
return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout)
async def next_group(
self,
ctx: commands.Context,
pages: list,
controls: dict,
message: discord.Message,
page: int,
timeout: float,
emoji: str,
):
perms = message.channel.permissions_for(ctx.me)
if perms.manage_messages: # Can manage messages, so remove react
try:
await message.remove_reaction(emoji, ctx.author)
except discord.NotFound:
pass
page = bisect.bisect_right(self.page_groups, page)
if page == len(self.page_groups):
page = self.page_groups[0]
else:
page = self.page_groups[page]
return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout)
async def prev_group(
self,
ctx: commands.Context,
pages: list,
controls: dict,
message: discord.Message,
page: int,
timeout: float,
emoji: str,
):
perms = message.channel.permissions_for(ctx.me)
if perms.manage_messages: # Can manage messages, so remove react
try:
await message.remove_reaction(emoji, ctx.author)
except discord.NotFound:
pass
page = self.page_groups[bisect.bisect_left(self.page_groups, page) - 1]
return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout)

@ -0,0 +1,91 @@
"""
Role Constants
Role Alignment guide as follows:
Town: 1
Werewolf: 2
Neutral: 3
Additional alignments may be added when warring factions are added
(Rival werewolves, cultists, vampires)
Role Category enrollment guide as follows (See Role.category):
Town:
1: Random, 2: Investigative, 3: Protective, 4: Government,
5: Killing, 6: Power (Special night action)
Werewolf:
11: Random, 12: Deception, 15: Killing, 16: Support
Neutral:
21: Benign, 22: Evil, 23: Killing
Example category:
category = [1, 5, 6] Could be Veteran
category = [1, 5] Could be Bodyguard
category = [11, 16] Could be Werewolf Silencer
category = [22] Could be Blob (non-killing)
category = [22, 23] Could be Serial-Killer
"""
ALIGNMENT_TOWN = 1
ALIGNMENT_WEREWOLF = 2
ALIGNMENT_NEUTRAL = 3
ALIGNMENT_MAP = {"Town": 1, "Werewolf": 2, "Neutral": 3}
# 0-9: Town Role Categories
# 10-19: Werewolf Role Categories
# 20-29: Neutral Role Categories
CATEGORY_TOWN_RANDOM = 1
CATEGORY_TOWN_INVESTIGATIVE = 2
CATEGORY_TOWN_PROTECTIVE = 3
CATEGORY_TOWN_GOVERNMENT = 4
CATEGORY_TOWN_KILLING = 5
CATEGORY_TOWN_POWER = 6
CATEGORY_WW_RANDOM = 11
CATEGORY_WW_DECEPTION = 12
CATEGORY_WW_KILLING = 15
CATEGORY_WW_SUPPORT = 16
CATEGORY_NEUTRAL_BENIGN = 21
CATEGORY_NEUTRAL_EVIL = 22
CATEGORY_NEUTRAL_KILLING = 23
ROLE_CATEGORY_DESCRIPTIONS = {
CATEGORY_TOWN_RANDOM: "Random",
CATEGORY_TOWN_INVESTIGATIVE: "Investigative",
CATEGORY_TOWN_PROTECTIVE: "Protective",
CATEGORY_TOWN_GOVERNMENT: "Government",
CATEGORY_TOWN_KILLING: "Killing",
CATEGORY_TOWN_POWER: "Power (Special night action)",
CATEGORY_WW_RANDOM: "Random",
CATEGORY_WW_DECEPTION: "Deception",
CATEGORY_WW_KILLING: "Killing",
CATEGORY_WW_SUPPORT: "Support",
CATEGORY_NEUTRAL_BENIGN: "Benign",
CATEGORY_NEUTRAL_EVIL: "Evil",
CATEGORY_NEUTRAL_KILLING: "Killing",
}
"""
Listener Actions Priority Guide
Action priority guide as follows (see listeners.py for wolflistener):
_at_night_start
0. No Action
1. Detain actions (Jailer/Kidnapper)
2. Group discussions and choose targets
_at_night_end
0. No Action
1. Self actions (Veteran)
2. Target switching and role blocks (bus driver, witch, escort)
3. Protection / Preempt actions (bodyguard/framer)
4. Non-disruptive actions (seer/silencer)
5. Disruptive actions (Killing)
6. Role altering actions (Cult / Mason / Shifter)
"""

@ -0,0 +1,28 @@
from typing import TYPE_CHECKING, Union
import discord
from discord.ext.commands import BadArgument, Converter
from redbot.core import commands
from werewolf.player import Player
if TYPE_CHECKING:
PlayerConverter = Union[int, discord.Member]
CronConverter = str
else:
class PlayerConverter(Converter):
async def convert(self, ctx, argument) -> Player:
try:
target = await commands.MemberConverter().convert(ctx, argument)
except BadArgument:
try:
target = int(argument)
assert target >= 0
except (ValueError, AssertionError):
raise BadArgument
# TODO: Get the game for context without making a new one
# TODO: Get player from game based on either ID or member object
return target

File diff suppressed because it is too large Load Diff

@ -4,10 +4,10 @@
],
"min_bot_version": "3.3.0",
"description": "Customizable Werewolf Game",
"hidden": true,
"hidden": false,
"install_msg": "Thank you for installing Werewolf! Get started with `[p]load werewolf`\n Use `[p]wwset` to run inital setup",
"requirements": [],
"short": "Werewolf Game",
"short": "[ALPHA] Play Werewolf (Mafia) Game in discord",
"end_user_data_statement": "This stores user IDs in memory while they're actively using the cog, and stores no persistent End User Data.",
"tags": [
"mafia",

@ -0,0 +1,106 @@
import inspect
def wolflistener(name=None, priority=0):
"""A decorator that marks a function as a listener.
This is the werewolf.Game equivalent of :meth:`.Cog.listener`.
Parameters
------------
name: :class:`str`
The name of the event being listened to. If not provided, it
defaults to the function's name.
priority: :class:`int`
The priority of the listener.
Priority guide as follows:
_at_night_start
0. No Action
1. Detain actions (Jailer/Kidnapper)
2. Group discussions and choose targets
_at_night_end
0. No Action
1. Self actions (Veteran)
2. Target switching and role blocks (bus driver, witch, escort)
3. Protection / Preempt actions (bodyguard/framer)
4. Non-disruptive actions (seer/silencer)
5. Disruptive actions (Killing)
6. Role altering actions (Cult / Mason / Shifter)
Raises
--------
TypeError
The function is not a coroutine function or a string was not passed as
the name.
"""
if name is not None and not isinstance(name, str):
raise TypeError(
"Game.listener expected str but received {0.__class__.__name__!r} instead.".format(
name
)
)
def decorator(func):
actual = func
if isinstance(actual, staticmethod):
actual = actual.__func__
if not inspect.iscoroutinefunction(actual):
raise TypeError("Listener function must be a coroutine function.")
actual.__wolf_listener__ = priority
to_assign = name or actual.__name__
try:
actual.__wolf_listener_names__.append((priority, to_assign))
except AttributeError:
actual.__wolf_listener_names__ = [(priority, to_assign)]
# we have to return `func` instead of `actual` because
# we need the type to be `staticmethod` for the metaclass
# to pick it up but the metaclass unfurls the function and
# thus the assignments need to be on the actual function
return func
return decorator
class WolfListenerMeta(type):
def __new__(mcs, *args, **kwargs):
name, bases, attrs = args
listeners = {}
need_at_msg = "Listeners must start with at_ (in method {0.__name__}.{1})"
new_cls = super().__new__(mcs, name, bases, attrs, **kwargs)
for base in reversed(new_cls.__mro__):
for elem, value in base.__dict__.items():
if elem in listeners:
del listeners[elem]
is_static_method = isinstance(value, staticmethod)
if is_static_method:
value = value.__func__
if inspect.iscoroutinefunction(value):
try:
is_listener = getattr(value, "__wolf_listener__")
except AttributeError:
continue
else:
# if not elem.startswith("at_"):
# raise TypeError(need_at_msg.format(base, elem))
listeners[elem] = value
listeners_as_list = []
for listener in listeners.values():
for priority, listener_name in listener.__wolf_listener_names__:
# I use __name__ instead of just storing the value so I can inject
# the self attribute when the time comes to add them to the bot
listeners_as_list.append((priority, listener_name, listener.__name__))
new_cls.__wolf_listeners__ = listeners_as_list
return new_cls
class WolfListener(metaclass=WolfListenerMeta):
def __init__(self, game):
for priority, name, method_name in self.__wolf_listeners__:
game.add_ww_listener(getattr(self, method_name), priority, name)

@ -1,4 +1,8 @@
from .role import Role
import logging
from werewolf.role import Role
log = logging.getLogger("red.fox_v3.werewolf.night_powers")
def night_immune(role: Role):

@ -1,5 +1,9 @@
import logging
import discord
log = logging.getLogger("red.fox_v3.werewolf.player")
class Player:
"""
@ -16,6 +20,9 @@ class Player:
self.muted = False
self.protected = False
def __repr__(self):
return f"{self.__class__.__name__}({self.member})"
async def assign_role(self, role):
"""
Give this player a role
@ -28,6 +35,15 @@ class Player:
async def send_dm(self, message):
try:
await self.member.send(message) # Lets do embeds later
await self.member.send(message) # Lets ToDo embeds later
except discord.Forbidden:
await self.role.game.village_channel.send("Couldn't DM {}, uh oh".format(self.mention))
log.info(f"Unable to mention {self.member.__repr__()}")
await self.role.game.village_channel.send(
f"Couldn't DM {self.mention}, uh oh",
allowed_mentions=discord.AllowedMentions(users=[self.member]),
)
except AttributeError:
log.exception("Someone messed up and added a bot to the game (I think)")
await self.role.game.village_channel.send(
"Someone messed up and added a bot to the game :eyes:"
)

@ -1,31 +1,41 @@
class Role:
import inspect
import logging
from werewolf.listener import WolfListener, wolflistener
log = logging.getLogger("red.fox_v3.werewolf.role")
class Role(WolfListener):
"""
Base Role class for werewolf game
Category enrollment guide as follows (category property):
Town:
1: Random, 2: Investigative, 3: Protective, 4: Government,
5: Killing, 6: Power (Special night action)
Werewolf:
11: Random, 12: Deception, 15: Killing, 16: Support
Neutral:
21: Benign, 22: Evil, 23: Killing
Example category:
category = [1, 5, 6] Could be Veteran
category = [1, 5] Could be Bodyguard
category = [11, 16] Could be Werewolf Silencer
Action guide as follows (on_event function):
category = [22] Could be Blob (non-killing)
category = [22, 23] Could be Serial-Killer
Action priority guide as follows (on_event function):
_at_night_start
0. No Action
1. Detain actions (Jailer/Kidnapper)
2. Group discussions and choose targets
_at_night_end
0. No Action
1. Self actions (Veteran)
@ -33,13 +43,15 @@ class Role:
3. Protection / Preempt actions (bodyguard/framer)
4. Non-disruptive actions (seer/silencer)
5. Disruptive actions (Killing)
6. Role altering actions (Cult / Mason)
6. Role altering actions (Cult / Mason / Shifter)
"""
rand_choice = False # Determines if it can be picked as a random role (False for unusually disruptive roles)
# Determines if it can be picked as a random role (False for unusually disruptive roles)
rand_choice = False # TODO: Rework random with categories
town_balance = 0 # Guess at power level and it's balance on the town
category = [0] # List of enrolled categories (listed above)
alignment = 0 # 1: Town, 2: Werewolf, 3: Neutral
channel_id = "" # Empty for no private channel
channel_name = "" # Empty for no private channel
unique = False # Only one of this role per game
game_start_message = (
"Your role is **Default**\n"
@ -54,32 +66,17 @@ class Role:
icon_url = None # Adding a URL here will enable a thumbnail of the role
def __init__(self, game):
super().__init__(game)
self.game = game
self.player = None
self.blocked = False
self.properties = {} # Extra data for other roles (i.e. arsonist)
self.action_list = [
(self._at_game_start, 1), # (Action, Priority)
(self._at_day_start, 0),
(self._at_voted, 0),
(self._at_kill, 0),
(self._at_hang, 0),
(self._at_day_end, 0),
(self._at_night_start, 0),
(self._at_night_end, 0),
(self._at_visit, 0)
]
def __str__(self):
return self.__repr__()
def __repr__(self):
return self.__class__.__name__
async def on_event(self, event, data):
"""
See Game class for event guide
"""
await self.action_list[event][0](data)
return f"{self.__class__.__name__}({self.player.__repr__()})"
async def assign_player(self, player):
"""
@ -90,7 +87,9 @@ class Role:
player.role = self
self.player = player
async def get_alignment(self, source=None):
log.debug(f"Assigned {self} to {player}")
async def get_alignment(self, source=None): # TODO: Rework to be "strength" tiers
"""
Interaction for powerful access of alignment
(Village, Werewolf, Other)
@ -101,7 +100,7 @@ class Role:
async def see_alignment(self, source=None):
"""
Interaction for investigative roles attempting
to see alignment (Village, Werewolf Other)
to see alignment (Village, Werewolf, Other)
"""
return "Other"
@ -119,35 +118,16 @@ class Role:
"""
return "Default"
async def _at_game_start(self, data=None):
if self.channel_id:
await self.game.register_channel(self.channel_id, self)
@wolflistener("at_game_start", priority=2)
async def _at_game_start(self):
if self.channel_name:
await self.game.register_channel(self.channel_name, self)
await self.player.send_dm(self.game_start_message) # Maybe embeds eventually
async def _at_day_start(self, data=None):
pass
async def _at_voted(self, data=None):
pass
async def _at_kill(self, data=None):
pass
async def _at_hang(self, data=None):
pass
async def _at_day_end(self, data=None):
pass
async def _at_night_start(self, data=None):
pass
async def _at_night_end(self, data=None):
pass
async def _at_visit(self, data=None):
pass
try:
await self.player.send_dm(self.game_start_message) # Maybe embeds eventually
except AttributeError as e:
log.exception(self.__repr__())
raise e
async def kill(self, source):
"""

@ -0,0 +1,11 @@
from .villager import Villager
from .seer import Seer
from .vanillawerewolf import VanillaWerewolf
from .shifter import Shifter
# Don't sort these imports. They are unstably in order
# TODO: Replace with unique IDs for roles in the future
__all__ = ["Seer", "Shifter", "VanillaWerewolf", "Villager"]

@ -0,0 +1,101 @@
import logging
import random
from werewolf.constants import ALIGNMENT_NEUTRAL, CATEGORY_NEUTRAL_EVIL
from werewolf.listener import wolflistener
from werewolf.player import Player
from werewolf.role import Role
log = logging.getLogger("red.fox_v3.werewolf.role.blob")
class TheBlob(Role):
rand_choice = True
category = [CATEGORY_NEUTRAL_EVIL] # List of enrolled categories
alignment = ALIGNMENT_NEUTRAL # 1: Town, 2: Werewolf, 3: Neutral
channel_id = "" # Empty for no private channel
unique = True # Only one of this role per game
game_start_message = (
"Your role is **The Blob**\n"
"You win by absorbing everyone town\n"
"Lynch players during the day with `[p]ww vote <ID>`\n"
"Each night you will absorb an adjacent player"
)
description = (
"A mysterious green blob of jelly, slowly growing in size.\n"
"The Blob fears no evil, must be dealt with in town"
)
def __init__(self, game):
super().__init__(game)
self.blob_target = None
async def see_alignment(self, source=None):
"""
Interaction for investigative roles attempting
to see team (Village, Werewolf, Other)
"""
return ALIGNMENT_NEUTRAL
async def get_role(self, source=None):
"""
Interaction for powerful access of role
Unlikely to be able to deceive this
"""
return "The Blob"
async def see_role(self, source=None):
"""
Interaction for investigative roles.
More common to be able to deceive these roles
"""
return "The Blob"
async def kill(self, source):
"""
Called when someone is trying to kill you!
Can you do anything about it?
self.player.alive is now set to False, set to True to stay alive
"""
# Blob cannot simply be killed
self.player.alive = True
@wolflistener("at_night_start", priority=2)
async def _at_night_start(self):
if not self.player.alive:
return
self.blob_target = None
idx = self.player.id
left_or_right = random.choice((-1, 1))
while self.blob_target is None:
idx += left_or_right
if idx >= len(self.game.players):
idx = 0
player = self.game.players[idx]
# you went full circle, everyone is a blob or something else is wrong
if player == self.player:
break
if player.role.properties.get("been_blobbed", False):
self.blob_target = player
if self.blob_target is not None:
await self.player.send_dm(f"**You will attempt to absorb {self.blob_target} tonight**")
else:
await self.player.send_dm(f"**No player will be absorbed tonight**")
@wolflistener("at_night_end", priority=4)
async def _at_night_end(self):
if self.blob_target is None or not self.player.alive:
return
target: "Player" = await self.game.visit(self.blob_target, self.player)
if target is not None:
target.role.properties["been_blobbed"] = True
self.game.night_results.append("The Blob grows...")

@ -1,11 +1,26 @@
from ..night_powers import pick_target
from ..role import Role
import logging
from werewolf.constants import (
ALIGNMENT_TOWN,
ALIGNMENT_WEREWOLF,
CATEGORY_TOWN_INVESTIGATIVE,
CATEGORY_TOWN_RANDOM,
)
from werewolf.listener import wolflistener
from werewolf.night_powers import pick_target
from werewolf.role import Role
log = logging.getLogger("red.fox_v3.werewolf.role.seer")
class Seer(Role):
rand_choice = True # Determines if it can be picked as a random role (False for unusually disruptive roles)
category = [1, 2] # List of enrolled categories (listed above)
alignment = 1 # 1: Town, 2: Werewolf, 3: Neutral
rand_choice = True
town_balance = 4
category = [
CATEGORY_TOWN_RANDOM,
CATEGORY_TOWN_INVESTIGATIVE,
] # List of enrolled categories (listed above)
alignment = ALIGNMENT_TOWN # 1: Town, 2: Werewolf, 3: Neutral
channel_id = "" # Empty for no private channel
unique = False # Only one of this role per game
game_start_message = (
@ -14,8 +29,10 @@ class Seer(Role):
"Lynch players during the day with `[p]ww vote <ID>`\n"
"Check for werewolves at night with `[p]ww choose <ID>`"
)
description = "A mystic in search of answers in a chaotic town.\n" \
"Calls upon the cosmos to discern those of Lycan blood"
description = (
"A mystic in search of answers in a chaotic town.\n"
"Calls upon the cosmos to discern those of Lycan blood"
)
def __init__(self, game):
super().__init__(game)
@ -24,47 +41,49 @@ class Seer(Role):
# self.blocked = False
# self.properties = {} # Extra data for other roles (i.e. arsonist)
self.see_target = None
self.action_list = [
(self._at_game_start, 1), # (Action, Priority)
(self._at_day_start, 0),
(self._at_voted, 0),
(self._at_kill, 0),
(self._at_hang, 0),
(self._at_day_end, 0),
(self._at_night_start, 2),
(self._at_night_end, 4),
(self._at_visit, 0)
]
# self.action_list = [
# (self._at_game_start, 1), # (Action, Priority)
# (self._at_day_start, 0),
# (self._at_voted, 0),
# (self._at_kill, 0),
# (self._at_hang, 0),
# (self._at_day_end, 0),
# (self._at_night_start, 2),
# (self._at_night_end, 4),
# (self._at_visit, 0),
# ]
async def see_alignment(self, source=None):
"""
Interaction for investigative roles attempting
to see team (Village, Werewolf Other)
to see team (Village, Werewolf, Other)
"""
return "Village"
return ALIGNMENT_TOWN
async def get_role(self, source=None):
"""
Interaction for powerful access of role
Unlikely to be able to deceive this
"""
return "Villager"
return "Seer"
async def see_role(self, source=None):
"""
Interaction for investigative roles.
More common to be able to deceive these roles
"""
return "Villager"
return "Seer"
async def _at_night_start(self, data=None):
@wolflistener("at_night_start", priority=2)
async def _at_night_start(self):
if not self.player.alive:
return
self.see_target = None
await self.game.generate_targets(self.player.member)
await self.player.send_dm("**Pick a target to see tonight**")
async def _at_night_end(self, data=None):
@wolflistener("at_night_end", priority=4)
async def _at_night_end(self):
if self.see_target is None:
if self.player.alive:
await self.player.send_dm("You will not use your powers tonight...")
@ -75,9 +94,9 @@ class Seer(Role):
if target:
alignment = await target.role.see_alignment(self.player)
if alignment == "Werewolf":
if alignment == ALIGNMENT_WEREWOLF:
out = "Your insight reveals this player to be a **Werewolf!**"
else:
else: # Don't reveal neutrals
out = "You fail to find anything suspicious about this player..."
await self.player.send_dm(out)
@ -87,4 +106,6 @@ class Seer(Role):
await super().choose(ctx, data)
self.see_target, target = await pick_target(self, ctx, data)
await ctx.send("**You will attempt to see the role of {} tonight...**".format(target.member.display_name))
await ctx.send(
f"**You will attempt to see the role of {target.member.display_name} tonight...**"
)

@ -1,35 +1,41 @@
from ..night_powers import pick_target
from ..role import Role
import logging
from werewolf.constants import ALIGNMENT_NEUTRAL, CATEGORY_NEUTRAL_BENIGN
from werewolf.listener import wolflistener
from werewolf.night_powers import pick_target
from werewolf.role import Role
log = logging.getLogger("red.fox_v3.werewolf.role.shifter")
class Shifter(Role):
"""
Base Role class for werewolf game
Category enrollment guide as follows (category property):
Town:
1: Random, 2: Investigative, 3: Protective, 4: Government,
5: Killing, 6: Power (Special night action)
Werewolf:
11: Random, 12: Deception, 15: Killing, 16: Support
Neutral:
21: Benign, 22: Evil, 23: Killing
Example category:
category = [1, 5, 6] Could be Veteran
category = [1, 5] Could be Bodyguard
category = [11, 16] Could be Werewolf Silencer
Action guide as follows (on_event function):
_at_night_start
0. No Action
1. Detain actions (Jailer/Kidnapper)
2. Group discussions and choose targets
_at_night_end
0. No Action
1. Self actions (Veteran)
@ -37,12 +43,13 @@ class Shifter(Role):
3. Protection / Preempt actions (bodyguard/framer)
4. Non-disruptive actions (seer/silencer)
5. Disruptive actions (Killing)
6. Role altering actions (Cult / Mason)
6. Role altering actions (Cult / Mason / Shifter)
"""
rand_choice = False # Determines if it can be picked as a random role (False for unusually disruptive roles)
category = [22] # List of enrolled categories (listed above)
alignment = 3 # 1: Town, 2: Werewolf, 3: Neutral
town_balance = -3
category = [CATEGORY_NEUTRAL_BENIGN] # List of enrolled categories (listed above)
alignment = ALIGNMENT_NEUTRAL # 1: Town, 2: Werewolf, 3: Neutral
channel_id = "" # Empty for no private channel
unique = False # Only one of this role per game
game_start_message = (
@ -61,22 +68,22 @@ class Shifter(Role):
super().__init__(game)
self.shift_target = None
self.action_list = [
(self._at_game_start, 1), # (Action, Priority)
(self._at_day_start, 0),
(self._at_voted, 0),
(self._at_kill, 0),
(self._at_hang, 0),
(self._at_day_end, 0),
(self._at_night_start, 2), # Chooses targets
(self._at_night_end, 6), # Role Swap
(self._at_visit, 0)
]
# self.action_list = [
# (self._at_game_start, 1), # (Action, Priority)
# (self._at_day_start, 0),
# (self._at_voted, 0),
# (self._at_kill, 0),
# (self._at_hang, 0),
# (self._at_day_end, 0),
# (self._at_night_start, 2), # Chooses targets
# (self._at_night_end, 6), # Role Swap
# (self._at_visit, 0),
# ]
async def see_alignment(self, source=None):
"""
Interaction for investigative roles attempting
to see alignment (Village, Werewolf, Other)
to see alignment (Village, Werewolf,, Other)
"""
return "Other"
@ -94,14 +101,14 @@ class Shifter(Role):
"""
return "Shifter"
async def _at_night_start(self, data=None):
await super()._at_night_start(data)
@wolflistener("at_night_start", priority=2)
async def _at_night_start(self):
self.shift_target = None
await self.game.generate_targets(self.player.member)
await self.player.send_dm("**Pick a target to shift into**")
async def _at_night_end(self, data=None):
await super()._at_night_end(data)
@wolflistener("at_night_end", priority=6)
async def _at_night_end(self):
if self.shift_target is None:
if self.player.alive:
await self.player.send_dm("You will not use your powers tonight...")
@ -114,16 +121,20 @@ class Shifter(Role):
# Roles have now been swapped
await self.player.send_dm("Your role has been stolen...\n"
"You are now a **Shifter**.")
await self.player.send_dm(
"Your role has been stolen...\n" "You are now a **Shifter**."
)
await self.player.send_dm(self.game_start_message)
await target.send_dm(target.role.game_start_message)
else:
await self.player.send_dm("**Your shift failed...**")
async def choose(self, ctx, data):
"""Handle night actions"""
await super().choose(ctx, data)
self.shift_target, target = await pick_target(self, ctx, data)
await ctx.send("**You will attempt to see the role of {} tonight...**".format(target.member.display_name))
await ctx.send(
f"**You will attempt to see the role of {target.member.display_name} tonight...**"
)

@ -1,13 +1,19 @@
from ..role import Role
import logging
from ..votegroups.wolfvote import WolfVote
from werewolf.constants import ALIGNMENT_WEREWOLF, CATEGORY_WW_KILLING, CATEGORY_WW_RANDOM
from werewolf.listener import wolflistener
from werewolf.role import Role
from werewolf.votegroups.wolfvote import WolfVote
log = logging.getLogger("red.fox_v3.werewolf.role.vanillawerewolf")
class VanillaWerewolf(Role):
rand_choice = True
category = [11, 15]
alignment = 2 # 1: Town, 2: Werewolf, 3: Neutral
channel_id = "werewolves"
town_balance = -6
category = [CATEGORY_WW_RANDOM, CATEGORY_WW_KILLING]
alignment = ALIGNMENT_WEREWOLF # 1: Town, 2: Werewolf, 3: Neutral
channel_name = "werewolves"
unique = False
game_start_message = (
"Your role is **Werewolf**\n"
@ -16,34 +22,19 @@ class VanillaWerewolf(Role):
"Vote to kill players at night with `[p]ww vote <ID>`"
)
def __init__(self, game):
super().__init__(game)
self.action_list = [
(self._at_game_start, 1), # (Action, Priority)
(self._at_day_start, 0),
(self._at_voted, 0),
(self._at_kill, 0),
(self._at_hang, 0),
(self._at_day_end, 0),
(self._at_night_start, 0),
(self._at_night_end, 0),
(self._at_visit, 0)
]
async def see_alignment(self, source=None):
"""
Interaction for investigative roles attempting
to see team (Village, Werewolf Other)
"""
return "Werewolf"
return ALIGNMENT_WEREWOLF
async def get_role(self, source=None):
"""
Interaction for powerful access of role
Unlikely to be able to deceive this
"""
return "Werewolf"
return "VanillaWerewolf"
async def see_role(self, source=None):
"""
@ -52,10 +43,13 @@ class VanillaWerewolf(Role):
"""
return "Werewolf"
async def _at_game_start(self, data=None):
if self.channel_id:
print("Wolf has channel_id: " + self.channel_id)
await self.game.register_channel(self.channel_id, self, WolfVote) # Add VoteGroup WolfVote
@wolflistener("at_game_start", priority=2)
async def _at_game_start(self):
if self.channel_name:
log.debug("Wolf has channel_name: " + self.channel_name)
await self.game.register_channel(
self.channel_name, self, WolfVote
) # Add VoteGroup WolfVote
await self.player.send_dm(self.game_start_message)

@ -1,10 +1,17 @@
from ..role import Role
import logging
from werewolf.constants import ALIGNMENT_TOWN, CATEGORY_TOWN_RANDOM
from werewolf.role import Role
log = logging.getLogger("red.fox_v3.werewolf.role.villager")
class Villager(Role):
rand_choice = True # Determines if it can be picked as a random role (False for unusually disruptive roles)
category = [1] # List of enrolled categories (listed above)
alignment = 1 # 1: Town, 2: Werewolf, 3: Neutral
# Determines if it can be picked as a random role (False for unusually disruptive roles)
rand_choice = True
town_balance = 1
category = [CATEGORY_TOWN_RANDOM] # List of enrolled categories (listed above)
alignment = ALIGNMENT_TOWN # 1: Town, 2: Werewolf, 3: Neutral
channel_id = "" # Empty for no private channel
unique = False # Only one of this role per game
game_start_message = (
@ -13,15 +20,12 @@ class Villager(Role):
"Lynch players during the day with `[p]ww vote <ID>`"
)
def __init__(self, game):
super().__init__(game)
async def see_alignment(self, source=None):
"""
Interaction for investigative roles attempting
to see team (Village, Werewolf Other)
to see team (Village, Werewolf, Other)
"""
return "Village"
return ALIGNMENT_TOWN
async def get_role(self, source=None):
"""

@ -1,4 +1,11 @@
class VoteGroup:
import logging
from werewolf.listener import WolfListener, wolflistener
log = logging.getLogger("red.fox_v3.werewolf.votegroup")
class VoteGroup(WolfListener):
"""
Base VoteGroup class for werewolf game
Handles secret channels and group decisions
@ -8,58 +15,41 @@ class VoteGroup:
channel_id = ""
def __init__(self, game, channel):
super().__init__(game)
self.game = game
self.channel = channel
self.players = []
self.vote_results = {}
self.properties = {} # Extra data for other options
self.action_list = [
(self._at_game_start, 1), # (Action, Priority)
(self._at_day_start, 0),
(self._at_voted, 0),
(self._at_kill, 1),
(self._at_hang, 1),
(self._at_day_end, 0),
(self._at_night_start, 2),
(self._at_night_end, 0),
(self._at_visit, 0)
]
async def on_event(self, event, data):
"""
See Game class for event guide
"""
def __repr__(self):
return f"{self.__class__.__name__}({self.channel},{self.players})"
await self.action_list[event][0](data)
async def _at_game_start(self, data=None):
@wolflistener("at_game_start", priority=1)
async def _at_game_start(self):
await self.channel.send(" ".join(player.mention for player in self.players))
async def _at_day_start(self, data=None):
pass
async def _at_voted(self, data=None):
pass
async def _at_kill(self, data=None):
if data["player"] in self.players:
self.players.remove(data["player"])
async def _at_hang(self, data=None):
if data["player"] in self.players:
self.players.remove(data["player"])
@wolflistener("at_kill", priority=1)
async def _at_kill(self, player):
if player in self.players:
self.players.remove(player)
async def _at_day_end(self, data=None):
pass
@wolflistener("at_hang", priority=1)
async def _at_hang(self, player):
if player in self.players:
self.players.remove(player)
async def _at_night_start(self, data=None):
@wolflistener("at_night_start", priority=2)
async def _at_night_start(self):
if self.channel is None:
return
self.vote_results = {}
await self.game.generate_targets(self.channel)
async def _at_night_end(self, data=None):
@wolflistener("at_night_end", priority=5)
async def _at_night_end(self):
if self.channel is None:
return
@ -70,11 +60,8 @@ class VoteGroup:
target = max(set(vote_list), key=vote_list.count)
if target:
# Do what you voted on
pass
async def _at_visit(self, data=None):
pass
# Do what the votegroup votes on
raise NotImplementedError
async def register_players(self, *players):
"""
@ -90,7 +77,7 @@ class VoteGroup:
self.players.remove(player)
if not self.players:
# ToDo: Trigger deletion of votegroup
# TODO: Confirm deletion
pass
async def vote(self, target, author, target_id):

@ -0,0 +1 @@
from .wolfvote import WolfVote

@ -1,6 +1,12 @@
import logging
import random
from ..votegroup import VoteGroup
import discord
from werewolf.listener import wolflistener
from werewolf.votegroup import VoteGroup
log = logging.getLogger("red.fox_v3.werewolf.votegroup.wolfvote")
class WolfVote(VoteGroup):
@ -13,71 +19,29 @@ class WolfVote(VoteGroup):
kill_messages = [
"**{ID}** - {target} was mauled by wolves",
"**{ID}** - {target} was found torn to shreds"]
"**{ID}** - {target} was found torn to shreds",
]
def __init__(self, game, channel):
super().__init__(game, channel)
# self.game = game
# self.channel = channel
# self.players = []
# self.vote_results = {}
# self.properties = {} # Extra data for other options
self.killer = None # Added killer
self.action_list = [
(self._at_game_start, 1), # (Action, Priority)
(self._at_day_start, 0),
(self._at_voted, 0),
(self._at_kill, 1),
(self._at_hang, 1),
(self._at_day_end, 0),
(self._at_night_start, 2),
(self._at_night_end, 5), # Kill priority
(self._at_visit, 0)
]
# async def on_event(self, event, data):
# """
# See Game class for event guide
# """
#
# await action_list[event][0](data)
#
# async def _at_game_start(self, data=None):
# await self.channel.send(" ".join(player.mention for player in self.players))
#
# async def _at_day_start(self, data=None):
# pass
#
# async def _at_voted(self, data=None):
# pass
#
# async def _at_kill(self, data=None):
# if data["player"] in self.players:
# self.players.pop(data["player"])
#
# async def _at_hang(self, data=None):
# if data["player"] in self.players:
# self.players.pop(data["player"])
#
# async def _at_day_end(self, data=None):
# pass
async def _at_night_start(self, data=None):
if self.channel is None:
return
@wolflistener("at_night_start", priority=2)
async def _at_night_start(self):
await super()._at_night_start()
await self.game.generate_targets(self.channel)
mention_list = " ".join(player.mention for player in self.players)
if mention_list != "":
await self.channel.send(mention_list)
self.killer = random.choice(self.players)
await self.channel.send("{} has been selected as tonight's killer".format(self.killer.member.display_name))
await self.channel.send(
f"{self.killer.member.display_name} has been selected as tonight's killer"
)
async def _at_night_end(self, data=None):
@wolflistener("at_night_end", priority=5)
async def _at_night_end(self):
if self.channel is None:
return
@ -87,34 +51,23 @@ class WolfVote(VoteGroup):
if vote_list:
target_id = max(set(vote_list), key=vote_list.count)
print("Target id: {}\nKiller: {}".format(target_id, self.killer.member.display_name))
log.debug(f"Target id: {target_id}\nKiller: {self.killer.member.display_name}")
if target_id is not None and self.killer:
await self.game.kill(target_id, self.killer, random.choice(self.kill_messages))
await self.channel.send("**{} has left to complete the kill...**".format(self.killer.member.display_name))
await self.channel.send(
"*{} has left to complete the kill...*".format(self.killer.member.display_name)
)
else:
await self.channel.send("**No kill will be attempted tonight...**")
# async def _at_visit(self, data=None):
# pass
#
# async def register_players(self, *players):
# """
# Extend players by passed list
# """
# self.players.extend(players)
#
# async def remove_player(self, player):
# """
# Remove a player from player list
# """
# if player.id in self.players:
# self.players.remove(player)
await self.channel.send("*No kill will be attempted tonight...*")
async def vote(self, target, author, target_id):
"""
Receive vote from game
"""
self.vote_results[author.id] = target_id
await super().vote(target, author, target_id)
await self.channel.send("{} has voted to kill {}".format(author.mention, target.member.display_name))
await self.channel.send(
"{} has voted to kill {}".format(author.mention, target.member.display_name),
allowed_mentions=discord.AllowedMentions(everyone=False, users=[author]),
)

@ -1,17 +1,22 @@
import logging
from typing import Optional
import discord
from redbot.core import Config, checks, commands
from redbot.core.bot import Red
from redbot.core.commands import Cog
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu
from .builder import (
from werewolf.builder import (
GameBuilder,
role_from_alignment,
role_from_category,
role_from_id,
role_from_name,
)
from .game import Game
from werewolf.game import Game, anyone_has_role
log = logging.getLogger("red.fox_v3.werewolf")
class Werewolf(Cog):
@ -42,25 +47,27 @@ class Werewolf(Cog):
"""Nothing to delete"""
return
def __unload(self):
print("Unload called")
for game in self.games.values():
del game
def cog_unload(self):
log.debug("Unload called")
for key in self.games.keys():
del self.games[key]
@commands.command()
async def buildgame(self, ctx: commands.Context):
"""
Create game codes to run custom games.
Pick the roles or randomized roles you want to include in a game
Pick the roles or randomized roles you want to include in a game.
Note: The same role can be picked more than once.
"""
gb = GameBuilder()
code = await gb.build_game(ctx)
if code != "":
await ctx.send("Your game code is **{}**".format(code))
await ctx.maybe_send_embed(f"Your game code is **{code}**")
else:
await ctx.send("No code generated")
await ctx.maybe_send_embed("No code generated")
@checks.guildowner()
@commands.group()
@ -68,8 +75,7 @@ class Werewolf(Cog):
"""
Base command to adjust settings. Check help for command list.
"""
if ctx.invoked_subcommand is None:
pass
pass
@commands.guild_only()
@wwset.command(name="list")
@ -77,31 +83,33 @@ class Werewolf(Cog):
"""
Lists current guild settings
"""
success, role, category, channel, log_channel = await self._get_settings(ctx)
if not success:
await ctx.send("Failed to get settings")
return None
valid, role, category, channel, log_channel = await self._get_settings(ctx)
embed = discord.Embed(title="Current Guild Settings")
embed = discord.Embed(
title="Current Guild Settings",
description=f"Valid: {valid}",
color=0x008000 if valid else 0xFF0000,
)
embed.add_field(name="Role", value=str(role))
embed.add_field(name="Category", value=str(category))
embed.add_field(name="Channel", value=str(channel))
embed.add_field(name="Log Channel", value=str(log_channel))
await ctx.send(embed=embed)
@commands.guild_only()
@wwset.command(name="role")
async def wwset_role(self, ctx: commands.Context, role: discord.Role = None):
"""
Assign the game role
Set the game role
This role should not be manually assigned
"""
if role is None:
await self.config.guild(ctx.guild).role_id.set(None)
await ctx.send("Cleared Game Role")
await ctx.maybe_send_embed("Cleared Game Role")
else:
await self.config.guild(ctx.guild).role_id.set(role.id)
await ctx.send("Game Role has been set to **{}**".format(role.name))
await ctx.maybe_send_embed("Game Role has been set to **{}**".format(role.name))
@commands.guild_only()
@wwset.command(name="category")
@ -111,14 +119,16 @@ class Werewolf(Cog):
"""
if category_id is None:
await self.config.guild(ctx.guild).category_id.set(None)
await ctx.send("Cleared Game Channel Category")
await ctx.maybe_send_embed("Cleared Game Channel Category")
else:
category = discord.utils.get(ctx.guild.categories, id=int(category_id))
if category is None:
await ctx.send("Category not found")
await ctx.maybe_send_embed("Category not found")
return
await self.config.guild(ctx.guild).category_id.set(category.id)
await ctx.send("Game Channel Category has been set to **{}**".format(category.name))
await ctx.maybe_send_embed(
"Game Channel Category has been set to **{}**".format(category.name)
)
@commands.guild_only()
@wwset.command(name="channel")
@ -128,10 +138,12 @@ class Werewolf(Cog):
"""
if channel is None:
await self.config.guild(ctx.guild).channel_id.set(None)
await ctx.send("Cleared Game Channel")
await ctx.maybe_send_embed("Cleared Game Channel")
else:
await self.config.guild(ctx.guild).channel_id.set(channel.id)
await ctx.send("Game Channel has been set to **{}**".format(channel.mention))
await ctx.maybe_send_embed(
"Game Channel has been set to **{}**".format(channel.mention)
)
@commands.guild_only()
@wwset.command(name="logchannel")
@ -141,18 +153,19 @@ class Werewolf(Cog):
"""
if channel is None:
await self.config.guild(ctx.guild).log_channel_id.set(None)
await ctx.send("Cleared Game Log Channel")
await ctx.maybe_send_embed("Cleared Game Log Channel")
else:
await self.config.guild(ctx.guild).log_channel_id.set(channel.id)
await ctx.send("Game Log Channel has been set to **{}**".format(channel.mention))
await ctx.maybe_send_embed(
"Game Log Channel has been set to **{}**".format(channel.mention)
)
@commands.group()
async def ww(self, ctx: commands.Context):
"""
Base command for this cog. Check help for the commands list.
"""
if ctx.invoked_subcommand is None:
pass
pass
@commands.guild_only()
@ww.command(name="new")
@ -162,9 +175,9 @@ class Werewolf(Cog):
"""
game = await self._get_game(ctx, game_code)
if not game:
await ctx.send("Failed to start a new game")
await ctx.maybe_send_embed("Failed to start a new game")
else:
await ctx.send("Game is ready to join! Use `[p]ww join`")
await ctx.maybe_send_embed("Game is ready to join! Use `[p]ww join`")
@commands.guild_only()
@ww.command(name="join")
@ -173,28 +186,49 @@ class Werewolf(Cog):
Joins a game of Werewolf
"""
game = await self._get_game(ctx)
game: Game = await self._get_game(ctx)
if not game:
await ctx.maybe_send_embed("Failed to join a game!")
return
await game.join(ctx, ctx.author)
await ctx.tick()
@commands.guild_only()
@commands.admin()
@ww.command(name="forcejoin")
async def ww_forcejoin(self, ctx: commands.Context, target: discord.Member):
"""
Force someone to join a game of Werewolf
"""
game: Game = await self._get_game(ctx)
if not game:
await ctx.send("No game to join!\nCreate a new one with `[p]ww new`")
await ctx.maybe_send_embed("Failed to join a game!")
return
await game.join(ctx.author, ctx.channel)
await game.join(ctx, target)
await ctx.tick()
@commands.guild_only()
@ww.command(name="code")
async def ww_code(self, ctx: commands.Context, code):
"""
Adjust game code
Adjusts the game code.
See `[p]buildgame` to generate a new code
"""
game = await self._get_game(ctx)
if not game:
await ctx.send("No game to join!\nCreate a new one with `[p]ww new`")
await ctx.maybe_send_embed("No game to join!\nCreate a new one with `[p]ww new`")
return
await game.set_code(ctx, code)
await ctx.tick()
@commands.guild_only()
@ww.command(name="quit")
@ -206,6 +240,7 @@ class Werewolf(Cog):
game = await self._get_game(ctx)
await game.quit(ctx.author, ctx.channel)
await ctx.tick()
@commands.guild_only()
@ww.command(name="start")
@ -215,10 +250,13 @@ class Werewolf(Cog):
"""
game = await self._get_game(ctx)
if not game:
await ctx.send("No game running, cannot start")
await ctx.maybe_send_embed("No game running, cannot start")
return
if not await game.setup(ctx):
pass # Do something?
pass # ToDo something?
await ctx.tick()
@commands.guild_only()
@ww.command(name="stop")
@ -226,17 +264,19 @@ class Werewolf(Cog):
"""
Stops the current game
"""
if ctx.guild is None:
# Private message, can't get guild
await ctx.send("Cannot start game from PM!")
return
# if ctx.guild is None:
# # Private message, can't get guild
# await ctx.send("Cannot stop game from PM!")
# return
if ctx.guild.id not in self.games or self.games[ctx.guild.id].game_over:
await ctx.send("No game to stop")
await ctx.maybe_send_embed("No game to stop")
return
game = await self._get_game(ctx)
game.game_over = True
await ctx.send("Game has been stopped")
if game.current_action:
game.current_action.cancel()
await ctx.maybe_send_embed("Game has been stopped")
@commands.guild_only()
@ww.command(name="vote")
@ -250,7 +290,7 @@ class Werewolf(Cog):
target_id = None
if target_id is None:
await ctx.send("`id` must be an integer")
await ctx.maybe_send_embed("`id` must be an integer")
return
# if ctx.guild is None:
@ -267,7 +307,7 @@ class Werewolf(Cog):
game = await self._get_game(ctx)
if game is None:
await ctx.send("No game running, cannot vote")
await ctx.maybe_send_embed("No game running, cannot vote")
return
# Game handles response now
@ -277,7 +317,7 @@ class Werewolf(Cog):
elif channel in (c["channel"] for c in game.p_channels.values()):
await game.vote(ctx.author, target_id, channel)
else:
await ctx.send("Nothing to vote for in this channel")
await ctx.maybe_send_embed("Nothing to vote for in this channel")
@ww.command(name="choose")
async def ww_choose(self, ctx: commands.Context, data):
@ -288,7 +328,7 @@ class Werewolf(Cog):
"""
if ctx.guild is not None:
await ctx.send("This action is only available in DM's")
await ctx.maybe_send_embed("This action is only available in DM's")
return
# DM nonsense, find their game
# If multiple games, panic
@ -296,7 +336,7 @@ class Werewolf(Cog):
if await game.get_player_by_member(ctx.author):
break # game = game
else:
await ctx.send("You're not part of any werewolf game")
await ctx.maybe_send_embed("You're not part of any werewolf game")
return
await game.choose(ctx, data)
@ -306,8 +346,7 @@ class Werewolf(Cog):
"""
Find custom roles by name, alignment, category, or ID
"""
if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self.ww_search:
pass
pass
@ww_search.command(name="name")
async def ww_search_name(self, ctx: commands.Context, *, name):
@ -317,7 +356,7 @@ class Werewolf(Cog):
if from_name:
await menu(ctx, from_name, DEFAULT_CONTROLS)
else:
await ctx.send("No roles containing that name were found")
await ctx.maybe_send_embed("No roles containing that name were found")
@ww_search.command(name="alignment")
async def ww_search_alignment(self, ctx: commands.Context, alignment: int):
@ -327,7 +366,7 @@ class Werewolf(Cog):
if from_alignment:
await menu(ctx, from_alignment, DEFAULT_CONTROLS)
else:
await ctx.send("No roles with that alignment were found")
await ctx.maybe_send_embed("No roles with that alignment were found")
@ww_search.command(name="category")
async def ww_search_category(self, ctx: commands.Context, category: int):
@ -337,7 +376,7 @@ class Werewolf(Cog):
if pages:
await menu(ctx, pages, DEFAULT_CONTROLS)
else:
await ctx.send("No roles in that category were found")
await ctx.maybe_send_embed("No roles in that category were found")
@ww_search.command(name="index")
async def ww_search_index(self, ctx: commands.Context, idx: int):
@ -347,28 +386,36 @@ class Werewolf(Cog):
if idx_embed is not None:
await ctx.send(embed=idx_embed)
else:
await ctx.send("Role ID not found")
await ctx.maybe_send_embed("Role ID not found")
async def _get_game(self, ctx: commands.Context, game_code=None):
guild: discord.Guild = ctx.guild
async def _get_game(self, ctx: commands.Context, game_code=None) -> Optional[Game]:
guild: discord.Guild = getattr(ctx, "guild", None)
if guild is None:
# Private message, can't get guild
await ctx.send("Cannot start game from PM!")
await ctx.maybe_send_embed("Cannot start game from DM!")
return None
if guild.id not in self.games or self.games[guild.id].game_over:
await ctx.send("Starting a new game...")
success, role, category, channel, log_channel = await self._get_settings(ctx)
await ctx.maybe_send_embed("Starting a new game...")
valid, role, category, channel, log_channel = await self._get_settings(ctx)
if not success:
await ctx.send("Cannot start a new game")
if not valid:
await ctx.maybe_send_embed("Cannot start a new game")
return None
self.games[guild.id] = Game(guild, role, category, channel, log_channel, game_code)
who_has_the_role = await anyone_has_role(guild.members, role)
if who_has_the_role:
await ctx.maybe_send_embed(
f"Cannot continue, {who_has_the_role.display_name} already has the game role."
)
return None
self.games[guild.id] = Game(
self.bot, guild, role, category, channel, log_channel, game_code
)
return self.games[guild.id]
async def _game_start(self, game):
async def _game_start(self, game: Game):
await game.start()
async def _get_settings(self, ctx):
@ -385,23 +432,30 @@ class Werewolf(Cog):
if role_id is not None:
role = discord.utils.get(guild.roles, id=role_id)
if role is None:
await ctx.send("Game Role is invalid")
return False, None, None, None, None
# if role is None:
# # await ctx.send("Game Role is invalid")
# return False, None, None, None, None
if category_id is not None:
category = discord.utils.get(guild.categories, id=category_id)
if category is None:
await ctx.send("Game Category is invalid")
return False, None, None, None, None
# if category is None:
# # await ctx.send("Game Category is invalid")
# return False, role, None, None, None
if channel_id is not None:
channel = discord.utils.get(guild.text_channels, id=channel_id)
if channel is None:
await ctx.send("Village Channel is invalid")
return False, None, None, None, None
# if channel is None:
# # await ctx.send("Village Channel is invalid")
# return False, role, category, None, None
if log_channel_id is not None:
log_channel = discord.utils.get(guild.text_channels, id=log_channel_id)
if log_channel is None:
await ctx.send("Log Channel is invalid")
return False, None, None, None, None
return True, role, category, channel, log_channel
# if log_channel is None:
# # await ctx.send("Log Channel is invalid")
# return False, None, None, None, None
return (
role is not None and category is not None and channel is not None,
role,
category,
channel,
log_channel,
)

Loading…
Cancel
Save