Compare commits

...
262 Commits
Author SHA1 Message Date
imcompiling 3a280696ca Need to install specific poetry version, export gets removed in v2.
build_latest / build (push) Successful in 1m41s
build_release / build (push) Successful in 19s
2025-01-19 18:35:38 -05:00
imcompiling 8825afdb46 Missed removing a parameter from buiild_votes_str().
build_latest / build (push) Failing after 29s
build_release / build (push) Failing after 29s
2025-01-19 18:19:51 -05:00
imcompiling 6fc91e0708 dakiny-bot#83 fix the view points bug when someone isn't in the guild anymore.
build_latest / build (push) Successful in 1m36s
build_release / build (push) Successful in 18s
build_develop / build (push) Successful in 23s
2024-12-22 22:21:01 -05:00
imcompiling 95cfcca600 Issue #79, add a 2 hour timeout to event submission.
build_latest / build (push) Successful in 1m27s
build_release / build (push) Successful in 22s
2024-12-22 17:11:54 -05:00
imcompiling 0419c9dfd6 Add auto complete for view clan points command.
build_latest / build (push) Successful in 1m30s
2024-12-22 16:51:10 -05:00
imcompiling 6ec2862aed Merge branch '76-clan-points'
build_latest / build (push) Successful in 17s
build_release / build (push) Successful in 17s
2024-12-22 15:49:24 -05:00
imcompiling c8f5c5bffb Add view clan points command. Break out function for creating the string so that code isn't duplicated.
build_develop / build (push) Successful in 2m11s
2024-12-22 15:48:10 -05:00
imcompiling a56b47a5fa Revamped utils to handle clan and event points in one. Updated record as needed. 2024-12-22 15:34:14 -05:00
imcompiling 906b63d57f Update view event points for new table name. 2024-12-22 15:13:35 -05:00
imcompiling 1bae320446 Fix in typos. 2024-12-22 15:10:47 -05:00
imcompiling a081718f8d Combine event and clan points into one table. 2024-12-22 14:41:50 -05:00
imcompiling 0dd1a90edd Merge branch 'main' into 76-clan-points 2024-12-22 14:04:03 -05:00
imcompiling 1305e15edf Change cutoff months for rollover.
build_latest / build (push) Successful in 1m49s
build_develop / build (push) Successful in 24s
build_release / build (push) Successful in 20s
2024-12-14 18:36:47 -05:00
imcompiling 86e9e3e32b Write a new test case for quarter name. 2024-12-14 17:57:01 -05:00
imcompiling 8498f42ba0 update ruff version
build_develop / build (push) Successful in 2m41s
2024-10-05 14:16:01 -04:00
imcompiling be46edd4ea Updated docustrings.
build_develop / build (push) Successful in 1m24s
2024-09-20 21:52:00 -04:00
imcompiling e34d847364 Add all settings commands for clan points.
build_develop / build (push) Successful in 1m25s
2024-09-20 21:49:13 -04:00
imcompiling 50d10f5168 Rework point settings to combine clan and event points into same class for simplicity. 2024-09-20 21:43:08 -04:00
imcompiling a01503c2c3 Add new sql tables to sql data classes 2024-09-20 21:21:07 -04:00
imcompiling 4c40a4c0e5 Reordered to alphabetical like rest of classes.
build_develop / build (push) Successful in 1m25s
2024-09-20 21:19:23 -04:00
imcompiling c3f25b6b54 Add settings to DakinySettings.
build_develop / build (push) Has been cancelled
2024-09-20 21:18:14 -04:00
imcompiling e36b2425aa Add new tables for clan points.
build_develop / build (push) Successful in 1m24s
2024-09-20 21:15:09 -04:00
imcompiling d7a0480c2b Merge branch '78-config-for-skipping-election-cycle'
build_latest / build (push) Successful in 17s
build_release / build (push) Successful in 17s
build_develop / build (push) Has been cancelled
2024-09-07 15:49:21 -04:00
imcompiling 8457c09b5b Remove bad import.
build_develop / build (push) Successful in 1m24s
2024-09-07 15:46:52 -04:00
imcompiling 3c31d0ad33 Add active parameter to elections. Only return an election for use if the election is active. 2024-09-07 15:46:30 -04:00
imcompiling 70676a98dd Merge branch '77-verification-for-splits'
build_latest / build (push) Successful in 16s
build_release / build (push) Successful in 16s
2024-09-07 15:15:26 -04:00
imcompiling 512802def7 Add ability to configure event verification.
build_develop / build (push) Successful in 1m22s
2024-09-07 15:14:50 -04:00
imcompiling 87b21782ce Remove unneeded import. 2024-09-07 15:08:53 -04:00
imcompiling 38ef2f4cb8 process_event isn't part of the class anymore. 2024-09-07 15:07:59 -04:00
imcompiling 79706fbe34 Fix bad array index. Send event to event channel when approved.
build_develop / build (push) Successful in 1m22s
2024-09-07 15:05:34 -04:00
imcompiling 83d39d0002 Add an approval process for event submissions.
build_develop / build (push) Successful in 1m25s
2024-09-07 15:00:16 -04:00
imcompiling d81ff295dd Don't need to save right after loading.
build_latest / build (push) Successful in 1m26s
2024-09-05 15:40:35 -04:00
imcompiling 9bfbafe15b Merge branch '21-edit-configs-from-discord'
build_latest / build (push) Successful in 17s
build_release / build (push) Successful in 17s
2024-09-05 15:30:36 -04:00
imcompiling b42f04aada Implemented rest of config settings commands.
build_develop / build (push) Successful in 1m27s
2024-09-05 15:30:08 -04:00
imcompiling 2ba56ead82 Alphabetically sorted DakinySettings. 2024-09-05 15:21:23 -04:00
imcompiling be824de52e Add rest of setting channel id commands. 2024-09-05 15:02:40 -04:00
imcompiling d3084c4ddb Create new settings command. Create first channel id settings command.
build_develop / build (push) Successful in 1m25s
2024-09-05 14:49:30 -04:00
imcompiling da7e529502 Remove unneeded typechecking import.
build_develop / build (push) Successful in 1m20s
2024-09-03 19:47:49 -04:00
imcompiling 4044c84811 Change file copied by Dockerfile. Remove last references to old config in docstrings. 2024-09-03 19:46:02 -04:00
imcompiling 44586183fe Replace rest of bot.config references. Clear record.py of old config usage. 2024-09-03 19:43:37 -04:00
imcompiling 3a47f58382 Remove config param from edit_clan_council_info(). 2024-09-03 19:33:13 -04:00
imcompiling 06d81a4604 Remove config param from record_event_points(). 2024-09-03 19:31:40 -04:00
imcompiling b1f8e9d742 Remove config param from build_highcouncil_votes_str(). 2024-09-03 19:28:05 -04:00
imcompiling 609fa38f20 Remove config param from build_votes_str(). 2024-09-03 16:13:35 -04:00
imcompiling f81e95381e Remove config param from get_quarter_cycle(). 2024-09-03 16:08:27 -04:00
imcompiling 62f9902298 Remove config param from get_current_election_cycle(). Update test.py to use new settings. 2024-09-03 16:07:23 -04:00
imcompiling 1dd5a33da5 Remove config param from get_quarter_name(). 2024-09-03 16:04:33 -04:00
imcompiling 8dc2668f44 Remove config param from get_election_name(). 2024-09-03 16:03:36 -04:00
imcompiling 3b289e74c8 Updated files in classes directory to use new configs. Ensured usages are updated. 2024-09-03 15:57:50 -04:00
imcompiling 2244765d87 Organized settings better within the dataclass. 2024-09-03 15:13:47 -04:00
imcompiling 62f53bde55 Renamed to DakinySettings. Converted dataclass to YAMLWizard.
build_develop / build (push) Failing after 17s
2024-09-03 14:37:06 -04:00
imcompiling b15e99ca68 Remove hydra from main. Install PyYAML. 2024-09-03 14:14:22 -04:00
imcompiling 628152c22c Change DakinyConfig.py file name. 2024-09-03 13:53:11 -04:00
imcompiling f00b6502f6 Merge branch '65-database-changes'
build_release / build (push) Successful in 16s
build_latest / build (push) Successful in 16s
2024-09-03 11:55:04 -04:00
imcompiling a55a90b922 Fixed missed column name change.
build_develop / build (push) Successful in 1m24s
2024-09-03 11:52:40 -04:00
imcompiling 7e0ff1da18 Rename EventPoints.Cycle to EventPoints.QuarterName.
build_develop / build (push) Successful in 1m23s
2024-09-03 11:42:20 -04:00
imcompiling eb5102578d Change ElectionCycle class to Election. Change ElectionCycle column in database to ElectionName. Update some variables for clarification. 2024-09-03 11:24:28 -04:00
imcompiling 306e0d687a Update ElectionCycle to ElectionName in database creation script. 2024-09-03 11:10:10 -04:00
imcompiling 8fb59ba394 Remove unused noqa.
build_develop / build (push) Failing after 42s
2024-09-03 11:00:48 -04:00
imcompiling 7b521893d3 Rename ParticipationPoints to EventPoints. 2024-09-03 10:59:39 -04:00
imcompiling 19e654aed0 Update ruff version 2024-09-03 10:59:28 -04:00
imcompiling 0d38bc82c4 Merge commit 'b64e2471fa7fdca5bb925c54eaa9adee78a19e5d'
build_latest / build (push) Successful in 16s
build_release / build (push) Successful in 16s
build_develop / build (push) Successful in 2m10s
2024-08-02 16:23:45 -04:00
imcompiling b64e2471fa Prevent double period.
build_develop / build (push) Successful in 1m28s
2024-08-02 16:23:35 -04:00
imcompiling 0b5330f7ca Merge commit '4e64eac17669e5e9b4e4626b3e2f9501b55c7aac'
build_latest / build (push) Successful in 16s
build_release / build (push) Successful in 16s
2024-08-01 22:07:55 -04:00
imcompiling 4e64eac176 Handle case when there are no non-discord members to view points for.
build_develop / build (push) Successful in 1m25s
2024-08-01 22:07:28 -04:00
imcompiling c90ddf42c3 Merge branch 'main' of https://gitea.cheezers.org/imcompiling/dakiny-bot
build_latest / build (push) Successful in 1m25s
build_develop / build (push) Successful in 16s
2024-08-01 21:50:02 -04:00
imcompiling 8eb2457ae9 Merge commit '2bdb7cb17b44a80286d5bbf689b1f2e5a88dcda0'
build_release / build (push) Successful in 16s
2024-08-01 21:49:21 -04:00
imcompiling 2bdb7cb17b Add the record point command.
build_develop / build (push) Successful in 1m28s
2024-08-01 21:48:33 -04:00
imcompiling 07906aaea1 Spell check and ruff fixes. 2024-08-01 21:46:16 -04:00
imcompiling 9109ac53b3 Use KANDY const instead of int.
build_latest / build (push) Successful in 1m44s
2024-07-13 12:54:55 -04:00
imcompiling 47a33128be Merge commit 'fdb04149aabea373e1f9b9570da0e5e66f6ac9da'
build_latest / build (push) Successful in 22s
build_release / build (push) Successful in 16s
build_develop / build (push) Successful in 1m57s
2024-05-31 15:21:06 -04:00
imcompiling fdb04149aa Define the /time at command.
build_develop / build (push) Successful in 1m32s
2024-05-31 15:20:59 -04:00
imcompiling 4805f6cd5d Add a constant for guild ids. By setting guild ids in commands, commands are added immediately.
Added the /time gametime command.
2024-05-31 14:28:22 -04:00
imcompiling ff85c8d485 Bugfix in record command. Missed moving a positional argument.
build_latest / build (push) Successful in 1m36s
build_release / build (push) Successful in 17s
build_develop / build (push) Successful in 24s
2024-05-26 20:06:43 -04:00
imcompiling ca0d27a929 Update dockerfile image. Need python3.12
build_latest / build (push) Successful in 1m47s
build_release / build (push) Successful in 23s
2024-05-24 14:19:17 -04:00
imcompiling 8ebb689ee7 Merge branch '63-bad-sorting'
build_latest / build (push) Failing after 31s
build_release / build (push) Failing after 29s
2024-05-24 14:11:44 -04:00
imcompiling ecc81c5f35 Sort the council member info.
build_develop / build (push) Failing after 31s
2024-05-24 14:03:23 -04:00
imcompiling 96c1fcb17c Sort all members by lowercase display name. 2024-05-24 13:58:25 -04:00
imcompiling 840e17e03a Adjust the presence because I can. Easier to understand.
build_develop / build (push) Failing after 29s
2024-05-24 13:56:39 -04:00
imcompiling f744d0d387 Merge branch '52-update-packages'
build_latest / build (push) Failing after 30s
build_release / build (push) Failing after 28s
2024-05-24 13:39:56 -04:00
imcompiling 9c71d24fe3 Unneeded file.
build_develop / build (push) Failing after 36s
2024-05-24 13:36:56 -04:00
imcompiling cdc335936c update dependencies
build_develop / build (push) Failing after 30s
2024-05-22 23:35:40 -04:00
imcompiling 303f0aa804 Update python, sqlalchemy, pandas, and nextcord. 2024-05-22 23:16:22 -04:00
imcompiling bc743cbb1a Merge branch 'fix-ruff-lint-smells'
build_latest / build (push) Successful in 18s
build_release / build (push) Successful in 19s
build_develop / build (push) Successful in 16s
2024-05-22 23:01:08 -04:00
imcompiling ed0895056d remove quotes from type annotation.
build_develop / build (push) Successful in 1m23s
2024-05-22 22:48:54 -04:00
imcompiling 985e253954 edits to the pyproject.
build_develop / build (push) Successful in 1m23s
2024-05-22 21:59:12 -04:00
imcompiling 9c175f3e52 Fix issues found while live testing.
build_develop / build (push) Successful in 1m35s
2024-05-22 21:15:37 -04:00
imcompiling b9621a67c9 Finish up smells in the commands directory.
Add a discord max message length constant.
Move boolean argument to not be positional.
2024-05-22 20:48:44 -04:00
imcompiling 9331e7ba4f aiofiles is async. Need to use it properly. 2024-05-22 20:06:50 -04:00
imcompiling 6500f68d86 Start working on files in cogs.
build_develop / build (push) Successful in 1m24s
2024-05-22 17:35:43 -04:00
imcompiling d9914529ca Fix Path().is_dir() usage.
build_latest / build (push) Successful in 1m24s
build_release / build (push) Successful in 17s
2024-05-22 17:30:01 -04:00
imcompiling 6cd82dfcc4 Fix ruff smells in classes dir and config.py. 2024-05-22 16:58:27 -04:00
imcompiling e485c119f6 Fix database_setup and main lint smells. 2024-05-22 16:33:09 -04:00
imcompiling 1fd1be5576 Use datetime.now().date() instead of today().
build_latest / build (push) Successful in 1m53s
build_release / build (push) Successful in 17s
2024-05-22 16:23:53 -04:00
imcompiling ed06748984 Fix all smells in utils. 2024-05-22 16:12:20 -04:00
imcompiling 02c313aee5 Fix lint errors in voting_views. 2024-05-22 15:40:50 -04:00
imcompiling af37ce6518 Minor ruff config changes. 2024-05-22 15:28:54 -04:00
imcompiling ccbe86623a Run ruff linter with autofixing. 2024-05-22 15:18:44 -04:00
imcompiling 796b052bfc Turned on all ruff rules. Formatted all files. 2024-05-22 15:06:39 -04:00
imcompiling 3ac164ab65 Add .vscode dir
build_latest / build (push) Successful in 1m26s
2024-05-22 13:28:19 -04:00
imcompiling 4fc569f4a3 Add ruff configuration. 2024-05-22 12:15:06 -04:00
imcompiling 2c5821e434 Merge branch '64-Limit-weekly-event-points'
build_release / build (push) Successful in 18s
build_latest / build (push) Successful in 17s
2024-05-19 22:32:52 -04:00
imcompiling cbfd6e20bc Move limit object management into separate function. Need to also get allowed points when creating a new quarter's event point tracker.
build_develop / build (push) Successful in 1m27s
2024-05-19 22:27:47 -04:00
imcompiling 34da664af0 Add a limit to the amount of event points one can earn weekly. 2024-05-19 22:09:10 -04:00
imcompiling c6c44afb76 Add table for limiting event points. 2024-05-19 12:05:43 -04:00
imcompiling c2d8239911 Revert "Rename ParticipationPoints table to EventPoints"
This reverts commit bec6494697.
2024-05-19 11:59:56 -04:00
imcompiling bec6494697 Rename ParticipationPoints table to EventPoints
build_develop / build (push) Successful in 1m21s
2024-05-19 11:52:25 -04:00
imcompiling 29a6b000ac Merge branch '49-view-votes-cant-see-HC-elections'
build_latest / build (push) Successful in 17s
build_release / build (push) Successful in 17s
2024-05-19 11:29:41 -04:00
imcompiling dbd2863aab Pass VoteType str instead of manual one in VoteSelect.
build_develop / build (push) Successful in 1m24s
2024-05-19 11:29:04 -04:00
imcompiling 426f009ca7 Use VoteType in utils. Remove old functions of getting clan leader and discord manager election names. 2024-05-19 10:50:23 -04:00
imcompiling ed07b2e231 Use VoteType in view command. 2024-05-19 10:46:05 -04:00
imcompiling 0caa41cc90 use VoteType in voting views. 2024-05-19 10:40:41 -04:00
imcompiling e93f37dcf0 Use VoteType in voting highcouncil views. 2024-05-19 10:38:10 -04:00
imcompiling 44c3961c46 Use VoteType in election_cycle 2024-05-19 10:31:16 -04:00
imcompiling 0dcdd038b8 Make the VoteType StrEnum and edit column in database to enforce only 3 values.
build_develop / build (push) Successful in 1m21s
2024-05-18 23:08:01 -04:00
imcompiling eb5d465ac9 Merge branch 'main' into 49--view-votes-can't-see-HC-elections
build_develop / build (push) Failing after 16s
2024-05-18 22:41:11 -04:00
imcompiling eb1c87fbf4 install sqlite3 onto container. Using it a lot for debugging/fixes.
build_latest / build (push) Successful in 1m29s
build_release / build (push) Successful in 22s
2024-05-18 22:38:57 -04:00
imcompiling cbd73a104e Merge branch '61-Create-virtual-environment-and-update-python-version' 2024-05-18 22:34:35 -04:00
imcompiling 95ad47368c Remove requirements.txt
build_develop / build (push) Successful in 22s
2024-05-18 22:34:13 -04:00
imcompiling dea3f7cabf Update dockerfile to use poetry for dependencies.
build_develop / build (push) Successful in 1m35s
Decided to just use poetry to just export requirements instead of building package.
2024-05-18 22:29:18 -04:00
imcompiling 6a71e9ab1d Add poetry for dependency management.
Update python version.
Using virtual environment in vscode.
2024-05-18 21:56:11 -04:00
imcompiling bc56830396 Merge branch '49--view-votes-can't-see-HC-elections' of https://gitea.cheezers.org/imcompiling/dakiny-bot into 49--view-votes-can't-see-HC-elections
build_develop / build (push) Failing after 17s
2024-05-18 16:36:03 -04:00
imcompiling 1d14f8171d Add vote type to one statement. 2024-05-18 16:34:33 -04:00
imcompiling 2efd380a7e Add VoteType Enum. 2024-05-18 16:26:11 -04:00
imcompiling 475170025a Edit database to add a type field to Votes. 2024-05-18 16:25:19 -04:00
imcompiling f4f3bfe103 Edit database to add a type field to Votes.
build_develop / build (push) Failing after 18s
2024-05-18 15:39:13 -04:00
imcompiling 6827658b4a Change sqlalchemy req.
build_latest / build (push) Successful in 1m2s
build_develop / build (push) Successful in 25s
2024-05-17 23:41:57 -04:00
imcompiling 85a3ea0418 Merge branch '54-Only-edit-changed-channels'
build_latest / build (push) Successful in 17s
build_release / build (push) Successful in 16s
2024-05-16 22:44:08 -04:00
imcompiling 0b27c60bea Clarify some method names.
build_develop / build (push) Successful in 57s
current election cycle shoud return quarter cycle if it can't find a cycle.
2024-05-16 22:41:27 -04:00
imcompiling 1a6f373e03 Merge branch 'main' into 54-Only-edit-changed-channels
build_develop / build (push) Successful in 57s
2024-05-16 22:02:56 -04:00
imcompiling 0ff13453be Fix docker file.
build_latest / build (push) Successful in 59s
build_release / build (push) Successful in 25s
2024-05-16 22:02:31 -04:00
imcompiling 6b29e26140 Revert "Add a fail notification to builds."
This reverts commit bbcb4e4dff.
2024-05-16 22:02:22 -04:00
imcompiling bbcb4e4dff Add a fail notification to builds.
build_latest / build (push) Failing after 25s
2024-05-16 21:59:34 -04:00
imcompiling 00c8227b8e Update refresh messages command to be able to choose a channel.
build_latest / build (push) Failing after 17s
build_develop / build (push) Failing after 17s
2024-05-16 21:45:36 -04:00
imcompiling 04c44bc470 only trigger updating clan messages in updated directories.
Update clan info message if the template was updated.
2024-05-16 21:37:10 -04:00
imcompiling 3761d3bb77 Add BaseModel class for the gitea webhook packet. 2024-05-16 18:01:52 -04:00
imcompiling a76bc02d4a Merge branch '55-Use-dakiny-messages-repo-for-message-templates'
build_release / build (push) Failing after 20s
build_latest / build (push) Failing after 16s
2024-05-16 16:47:19 -04:00
imcompiling 3890c30d1f Found niche bug when testing. Need to get election cycle here because election cycle might be different than current cycle.
build_develop / build (push) Failing after 19s
2024-05-16 16:41:36 -04:00
imcompiling 1e1407c918 Update templates to be used from dakiny messages repo. 2024-05-16 16:25:50 -04:00
imcompiling be408307aa Remove template messages from repo. 2024-05-16 16:22:18 -04:00
imcompiling 069dafe9ae Merge branch '56-announce-clan-council'
build_latest / build (push) Successful in 21s
build_release / build (push) Successful in 16s
build_develop / build (push) Successful in 17s
2024-05-16 16:10:30 -04:00
imcompiling 7d54a0c30b Create refresh council info command. Change method name.
build_develop / build (push) Successful in 59s
2024-05-16 16:10:16 -04:00
imcompiling 39fd55146e move edit clan council message into utils. 2024-05-16 16:04:08 -04:00
imcompiling a8323c9a88 Update the message to look more like current. 2024-05-16 15:55:09 -04:00
imcompiling 51d6713940 format returns the string. 2024-05-16 15:45:33 -04:00
imcompiling 65ed5e7b59 Made function to gather and send the council members message in the clan info channel. 2024-05-16 15:37:34 -04:00
imcompiling 2059814394 Broke edit clan message to own function to allow for usage in other places. 2024-05-16 15:36:50 -04:00
imcompiling 063baa07ee Merge branch '53-Not-recording-High-Council-results-in-CouncilMembers-database'
build_latest / build (push) Successful in 55s
build_develop / build (push) Successful in 16s
2024-05-16 14:53:55 -04:00
imcompiling 24ffb02869 Update CouncilMembers database with high council winners.
build_develop / build (push) Successful in 59s
2024-05-16 14:41:03 -04:00
imcompiling 370bd71154 The defer needs to be ephemeral too.
build_latest / build (push) Successful in 57s
build_release / build (push) Successful in 22s
2024-05-16 09:55:33 -04:00
imcompiling cb3eb4b09b Merge branch 'main' of https://gitea.cheezers.org/imcompiling/dakiny-bot
build_latest / build (push) Successful in 22s
build_release / build (push) Successful in 17s
build_develop / build (push) Successful in 22s
2024-05-16 09:19:11 -04:00
imcompiling c39e2c137b Merge branch '6-add-clan-rules' 2024-05-16 09:18:35 -04:00
imcompiling eedaf1c71f Don't need to refresh messages on bot start.
build_develop / build (push) Successful in 1m11s
2024-05-16 09:13:34 -04:00
imcompiling 70e4a84db3 Set uvicorn to listen on all addresses.
build_develop / build (push) Successful in 57s
2024-05-15 23:16:12 -04:00
imcompiling 443b92dfd3 Revert "Add expose to see if that fixes issue."
build_develop / build (push) Successful in 21s
This reverts commit e1be3218b3.
2024-05-15 20:43:19 -04:00
imcompiling e1be3218b3 Add expose to see if that fixes issue.
build_develop / build (push) Successful in 1m10s
2024-05-15 20:40:31 -04:00
imcompiling dfd4ef9347 Need to defer followup on refresh because it takes a while. 2024-05-15 17:27:35 -04:00
imcompiling 094a22f5e7 Update README.md
build_latest / build (push) Successful in 23s
2024-05-15 17:13:45 -04:00
imcompiling 7333100b1d Need git on the docker image now.
build_develop / build (push) Successful in 1m32s
2024-05-15 14:52:02 -04:00
imcompiling e5d885e2a8 Merge branch 'main' into 6-add-clan-rules
build_develop / build (push) Successful in 1m4s
2024-05-15 14:36:50 -04:00
imcompiling 592819e028 Update dockerfile to use main.
build_latest / build (push) Successful in 1m2s
2024-05-15 14:31:26 -04:00
imcompiling f822ed2e2d Correct sub command name. Add response.
build_develop / build (push) Failing after 19s
2024-05-15 14:17:27 -04:00
imcompiling 5726875c59 Store updated channel id. 2024-05-15 14:15:11 -04:00
imcompiling 52ee85f17c Add a refresh command. 2024-05-15 14:07:27 -04:00
imcompiling cf5b5f8068 Move update clan messages into utils, a slash command will need the functionality. 2024-05-15 13:57:51 -04:00
imcompiling ee6b301791 Fill out some docustrings 2024-05-15 13:46:13 -04:00
imcompiling b9362f7eca Handle a channel being deleted after entered into database. 2024-05-15 13:33:07 -04:00
imcompiling 36c738bfaa Finish the update messages function. 2024-05-15 13:27:18 -04:00
imcompiling a1f955abbe Fill out more of the update message method. 2024-05-14 18:15:58 -04:00
imcompiling 9ab4d9e339 Add the messages table to database. 2024-05-14 18:06:16 -04:00
imcompiling f51f66697f Start working on update_messages function. 2024-05-14 17:54:58 -04:00
imcompiling a3e195925a Get on the right branch 2024-05-14 17:17:51 -04:00
imcompiling 9d78070c45 Add gitpython requirement. Clone the repo. 2024-05-14 16:34:31 -04:00
imcompiling 4ae31159dc Move the webhook listener into a cog so that I have access to the bot for the action method. 2024-05-14 16:04:52 -04:00
imcompiling 3fec5fab41 Webhook is using external file.
build_develop / build (push) Failing after 16s
2024-05-14 12:19:11 -04:00
imcompiling b12e2427a3 Merge branch 'main' into 6-add-clan-rules
build_develop / build (push) Failing after 19s
2024-05-14 11:19:19 -04:00
imcompiling 967d41b8ef Rename dakiny_bot.py to main.py.
build_latest / build (push) Failing after 16s
2024-05-14 11:18:55 -04:00
imcompiling 4776b1857b Start using fastapi. 2024-05-14 11:16:10 -04:00
imcompiling 01687ae8e8 Reduce duplicate code when getting cycle name.
build_develop / build (push) Successful in 48s
2024-05-13 21:32:19 -04:00
imcompiling 3089da7282 Sort members by display name then non members.
build_develop / build (push) Successful in 1m16s
build_latest / build (push) Successful in 29s
build_release / build (push) Successful in 21s
change how view message looks.
2024-05-11 23:51:44 -04:00
imcompiling 24ae0fe21f Allow multiple quarters to be selected from search string, as long as they match the start with.
build_develop / build (push) Successful in 49s
2024-05-11 13:34:30 -04:00
imcompiling ad3da6f2c6 Hosts don't get participation points. 2024-05-11 13:31:39 -04:00
imcompiling 5057e9a844 Change pandas version.
build_develop / build (push) Successful in 50s
2024-05-10 08:49:11 -04:00
imcompiling fea590bf9a Add handling for bad quarter name.
build_develop / build (push) Successful in 48s
2024-05-09 23:33:23 -04:00
imcompiling 058bdfef50 Add auto complete for event points 2024-05-09 23:29:28 -04:00
imcompiling 257a7eccd7 Add view points command. 2024-05-09 23:27:06 -04:00
imcompiling 443609f5ba Fix small bugs with getting cycle name and recording events. 2024-05-09 20:57:36 -04:00
imcompiling fde17edf09 Have to make sure things exist before making list of them.
Don't delete the embed before done using it.
2024-05-09 20:53:48 -04:00
imcompiling 084c9df65c Record points in the command. 2024-05-09 20:34:16 -04:00
imcompiling 4f9524a4c1 Slight database change, add util functions for adding points to users. 2024-05-09 20:25:01 -04:00
imcompiling 1495fb7ee6 Updated all docustrings in record.py. 2024-05-09 17:13:59 -04:00
imcompiling fa312eec87 Change database to handle non-discord participants. 2024-05-09 17:00:20 -04:00
imcompiling eb3cf2b77f Clear submittion view after submitted.
build_develop / build (push) Successful in 36s
2024-05-09 16:52:44 -04:00
imcompiling c08cd84bed Cleaned up embed and event record message. 2024-05-09 16:51:07 -04:00
imcompiling 905314cfb5 Create custom user select to edit the embed with selected people. 2024-05-09 16:37:45 -04:00
imcompiling bd3debf025 Change up recording process. use a modal to edit text values. 2024-05-09 16:26:20 -04:00
imcompiling 3a3c4b7995 Remove hard coded guild id. 2024-05-08 21:43:37 -04:00
imcompiling f465117435 Added basic event recording. No points being tracked yet.
build_develop / build (push) Successful in 34s
2024-05-08 21:34:18 -04:00
imcompiling 0777b71578 More filling out of the record command. The UserSelect seems to work for what I want.
build_develop / build (push) Successful in 39s
2024-05-08 17:13:32 -04:00
imcompiling a2afdc0295 Create record command class. Update configs for recording events. 2024-05-08 16:40:16 -04:00
imcompiling 3b6bef3ff0 fix incorrect description. 2024-05-08 16:39:49 -04:00
imcompiling a963171b16 change get election cycle name, add new function to get the current cycle for quarter name usage. 2024-05-08 16:20:46 -04:00
imcompiling e1787f68d5 Add new table to database 2024-05-08 16:20:19 -04:00
imcompiling a8697b0187 Bandaid fix so HC votes can be viewed.
build_latest / build (push) Successful in 1m39s
build_release / build (push) Successful in 48s
build_develop / build (push) Successful in 52s
2024-01-09 20:04:49 -05:00
imcompiling 8c8dc2c638 Merge branch '44-sort-candidates'
build_latest / build (push) Successful in 47s
build_release / build (push) Successful in 51s
2024-01-03 21:47:59 -05:00
imcompiling 54933b427e Sort nominees in voting views by display name.
build_develop / build (push) Successful in 1m27s
2024-01-03 21:46:54 -05:00
imcompiling 5d05006a23 Prevent unneeded exceptions.
build_latest / build (push) Successful in 1m23s
build_develop / build (push) Successful in 1m7s
2024-01-01 14:11:07 -05:00
imcompiling 2596b9fa7b Merge branch '47-incorrect-election-cycle'
build_latest / build (push) Successful in 44s
build_release / build (push) Successful in 45s
2024-01-01 13:32:41 -05:00
imcompiling 8c654612a9 Ensure that rolback months are properly year-ed.
build_develop / build (push) Successful in 1m27s
2024-01-01 13:32:17 -05:00
imcompiling e836ead399 Force crash on no election cycle. -1 was causing last item in config array to be used.
build_develop / build (push) Successful in 1m38s
2024-01-01 13:23:29 -05:00
imcompiling fa691ced5d Space on the wrong side of the quote.
build_latest / build (push) Successful in 1m33s
build_release / build (push) Successful in 45s
2023-12-27 09:50:01 -05:00
imcompiling 81f4c7741b Merge branch '40-remove-presence'
build_latest / build (push) Successful in 1m29s
build_release / build (push) Successful in 45s
2023-12-23 14:48:23 -05:00
imcompiling 2cde9f7109 Fix bug preventing remove from working.
build_develop / build (push) Successful in 1m40s
Remove guild ids.
2023-12-23 14:48:12 -05:00
imcompiling 2632bb883f Filled out remove presence some more. Not currently working.
build_develop / build (push) Successful in 1m25s
2023-12-22 22:05:07 -05:00
imcompiling 2f75f0cdcb Finish up presence auto complete function. 2023-12-22 21:56:29 -05:00
imcompiling deb405f02b Might not have worked without list cast. 2023-12-22 21:35:52 -05:00
imcompiling ee7cbaf2f6 Merge branch '39-view-presences'
build_latest / build (push) Successful in 1m26s
2023-12-22 21:33:45 -05:00
imcompiling 7ba7d3563c Rework logic to use less loops.
build_develop / build (push) Successful in 1m28s
2023-12-22 21:33:34 -05:00
imcompiling 39746123f1 Add beginning of remove presence. 2023-12-22 21:23:29 -05:00
imcompiling c69adb8374 Limit items sent back on autocomplete (small bug) 2023-12-22 21:23:18 -05:00
imcompiling f722a6a87b Do not want guild ids 2023-12-22 16:36:31 -05:00
imcompiling b3550bbf55 Merge commit '0a18d819afecade9a9812df4c9056ba19bb4d211'
build_develop / build (push) Successful in 1m25s
2023-12-22 16:32:42 -05:00
imcompiling 0a18d819af Add command to view presences. 2023-12-22 16:32:32 -05:00
imcompiling 2bd4fcb136 Format change for presence request.
build_latest / build (push) Successful in 1m37s
build_release / build (push) Successful in 45s
build_develop / build (push) Successful in 52s
2023-12-21 19:23:13 -05:00
imcompiling 2a91274d7f Need to use the and keyword. 2023-12-21 19:22:48 -05:00
imcompiling 4fb4927fb8 Merge branch '36-bot-presence'
build_latest / build (push) Successful in 43s
build_develop / build (push) Successful in 45s
2023-12-20 21:47:07 -05:00
imcompiling fbd8419ccf Add the add presence request.
build_develop / build (push) Successful in 1m33s
2023-12-20 21:45:20 -05:00
imcompiling 09ff620e51 Add loop for changing presence. 2023-12-20 21:38:52 -05:00
imcompiling 9c64e574fe Add new table to record bot presences. 2023-12-20 21:02:04 -05:00
imcompiling 011b6551b0 highcouncil election message update.
build_latest / build (push) Successful in 1m31s
build_release / build (push) Successful in 45s
build_develop / build (push) Successful in 56s
2023-12-20 09:27:18 -05:00
imcompiling 45b304308b Merge branch '35-remove-nomination-views'
build_latest / build (push) Successful in 45s
build_release / build (push) Successful in 45s
2023-12-17 15:36:50 -05:00
imcompiling 43ef19fd82 Cover highcouncil edge case.
build_develop / build (push) Successful in 1m29s
2023-12-17 15:36:04 -05:00
imcompiling 757d2e5c19 Stored nomination message in wrong place. 2023-12-17 15:27:54 -05:00
imcompiling 8394cdd6fd Remove nomination views once they are done. 2023-12-17 15:06:19 -05:00
imcompiling a9c3719c29 Add two new fields to election information. 2023-12-17 14:44:32 -05:00
imcompiling 250de5baf8 Merge branch '18-highcouncil-election'
build_latest / build (push) Successful in 1m39s
build_develop / build (push) Successful in 48s
2023-12-17 14:36:57 -05:00
imcompiling d449dc6d1a Clear voting buttons for highcouncil on end.
build_develop / build (push) Successful in 46s
2023-12-17 14:29:03 -05:00
imcompiling b8e35b5e90 Fix bugs with the voting results string builder. 2023-12-17 14:17:35 -05:00
imcompiling 644eb1dc31 Fix none query issue. 2023-12-17 14:11:04 -05:00
imcompiling 09ad45c897 Correct election name mixup. Use the highcouncil build string. 2023-12-17 13:52:57 -05:00
imcompiling 55259a2653 Add comment about ties in highcouncil elect. 2023-12-17 13:01:12 -05:00
imcompiling d75c64ba28 Update message decorator for each type of highcouncil elect. 2023-12-17 12:49:44 -05:00
imcompiling 08cf614a60 Add new test command. 2023-12-17 12:45:41 -05:00
imcompiling 18d433ce8c Add highcouncil winners message. 2023-12-17 12:33:05 -05:00
imcompiling a1896416fc Make highcouncil vote string builder.
build_develop / build (push) Successful in 1m36s
Break out repeated code into helper function.
2023-12-16 23:14:23 -05:00
imcompiling a7eafdd52a Create highcouncil voting views. 2023-12-16 23:01:51 -05:00
imcompiling 8e27e03709 Need a message to track highcouncil votes. 2023-12-16 22:32:34 -05:00
imcompiling be507f3970 Add highcouncil nominate view to message. 2023-12-16 22:21:35 -05:00
imcompiling 88d8e6178c Need to have one view. 2023-12-16 22:20:36 -05:00
imcompiling ee13019a0f Create clan leader nomination views.
build_develop / build (push) Successful in 1m41s
2023-12-16 22:11:52 -05:00
imcompiling 6e8f999611 Create discord manager nominate views. 2023-12-16 22:06:39 -05:00
imcompiling 505f27110d Don't need booleans, use ids as booleans. 2023-12-16 21:59:21 -05:00
imcompiling 1c33139f00 Modify database for highcouncil elections. 2023-12-16 21:53:11 -05:00
imcompiling b292f956e8 Add functions for different election names within same cycle. 2023-12-16 21:52:56 -05:00
imcompiling 654c74e622 Move a function to utils. 2023-12-16 21:52:14 -05:00
imcompiling 969880fdb6 Merge commit 'f9da9fe7ef1b4d134884bba202af6a8ae5b06203'
build_latest / build (push) Successful in 1m10s
build_release / build (push) Successful in 49s
2023-12-13 17:36:01 -05:00
imcompiling f9da9fe7ef Merge branch 'main' into 33-fixes-from-mock-election
build_develop / build (push) Successful in 1m24s
2023-11-08 22:53:40 -05:00
imcompiling b5ea709c3a Add council role management to term start.
build_develop / build (push) Successful in 1m29s
2023-11-08 22:50:42 -05:00
imcompiling 9980c49764 Need to use display name in case someone doesn't have a nickname.
build_develop / build (push) Successful in 1m27s
2023-11-08 22:22:32 -05:00
imcompiling 8d02e28271 clean up test file a little.
build_latest / build (push) Successful in 1m26s
2023-11-08 16:31:38 -05:00
imcompiling 3a5add3c60 Remove unneeded config items.
build_latest / build (push) Successful in 1m29s
2023-11-08 16:28:07 -05:00
imcompiling dd1c304522 Add role check to nominate button.
build_develop / build (push) Successful in 1m24s
2023-11-08 16:17:16 -05:00
46 changed files with 3653 additions and 980 deletions
+4 -5
View File
@@ -105,6 +105,7 @@ venv/
ENV/ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
poetry.lock
# Spyder project settings # Spyder project settings
.spyderproject .spyderproject
@@ -124,13 +125,10 @@ dmypy.json
# Pyre type checker # Pyre type checker
.pyre/ .pyre/
# IDE
.vscode/
# logging # logging
nextcord.log nextcord.log
# data file for bot # bot files
highscores_data.json highscores_data.json
highscores_config.json highscores_config.json
deleted_backups/ deleted_backups/
@@ -139,4 +137,5 @@ TODO.txt
sql_tester.py sql_tester.py
conf conf
outputs outputs
database database
dakiny-messages/
+9
View File
@@ -0,0 +1,9 @@
{
"recommendations": [
"ms-python.vscode-pylance",
"ms-python.python",
"ms-python.debugpy",
"charliermarsh.ruff",
"alexcvzz.vscode-sqlite"
]
}
+15
View File
@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Module",
"type": "debugpy",
"request": "launch",
"module": "main",
"justMyCode": true
}
]
}
+11
View File
@@ -0,0 +1,11 @@
{
"cSpell.words": [
"aiofiles",
"autofilled",
"blurple",
"CLANLEADER",
"Dakiny",
"DISCORDMANAGER",
"highcouncil"
]
}
+86
View File
@@ -0,0 +1,86 @@
"""File contains classes defining Dakiny settings structure."""
from dataclasses import dataclass
from dataclass_wizard import YAMLWizard
CONFIG_LOCATION = "conf/config.yaml"
@dataclass
class ChannelIds:
"""Class holds all channel ids needed by bot."""
admin: int # might want to rename this...
announcement: int
clan_info: int
event_post: int
general: int
voting: int
@dataclass
class Election:
"""Class defines relevant election dates."""
active: bool
election_start: str
highcouncil_election_announce: str
highcouncil_election_end: str
highcouncil_election_start: str
name: str
term_end: str
term_start: str
voting_end: str
voting_start: str
@dataclass
class Params:
"""Class defines start parameters that shouldn't be changed."""
discord_token: str
git_branch: str
guild_id: int
@dataclass
class PointSettings:
"""Class defines point settings editable by Clan Council."""
clan_points: int
clan_points_weekly_limit: int
events_weekly_limit: int
host_points: int
participate_points: int
verify_clan_points: bool
verify_events: bool
@dataclass
class RoleIds:
"""Class holds all role ids needed by bot."""
clan_council: int
clan_council_elect: int
clan_member: int
event_host: int
high_council: int
@dataclass
class DakinySettings(YAMLWizard):
"""Class defines all configs required for Dakiny Bot."""
channel_ids: ChannelIds
election_cycles: list[Election]
params: Params
point_settings: PointSettings
role_ids: RoleIds
def save(self) -> None:
"""Saves settings to the yaml file."""
self.to_yaml_file(CONFIG_LOCATION)
settings = DakinySettings.from_yaml_file(CONFIG_LOCATION)
+8 -15
View File
@@ -1,26 +1,19 @@
FROM python:3.10.6-slim FROM python:3.12.3-slim
# install dependencies for mysqlclient # install dependencies
# RUN apt-get update && apt-get -y upgrade && apt-get -y install python3-dev default-libmysqlclient-dev build-essential RUN apt-get update && apt-get -y upgrade && apt-get -y install git sqlite3
RUN pip install --upgrade pip
WORKDIR /home WORKDIR /home
COPY requirements.txt requirements.txt COPY DakinySettings.py DakinySettings.py
COPY main.py main.py
COPY config.py config.py
COPY dakiny_bot.py dakiny_bot.py
COPY utils.py utils.py COPY utils.py utils.py
COPY pyproject.toml pyproject.toml
COPY bot_messages bot_messages/
COPY classes classes/ COPY classes classes/
COPY cogs cogs/ COPY cogs cogs/
COPY database_setup database_setup/ COPY database_setup database_setup/
RUN pip install -r requirements.txt RUN pip install --upgrade pip && pip install poetry==1.8.3 && poetry export -f requirements.txt --output requirements.txt && pip install -r requirements.txt
# remove build deps that aren't needed to run CMD ["python3", "-u", "./main.py"]
# RUN apt-get -y remove python3-dev build-essential && apt-get -y autoremove
CMD ["python3", "-u", "./dakiny_bot.py"]
+7 -2
View File
@@ -1,5 +1,10 @@
# Dakiny Bot # Dakiny Bot
Bot assumes that it always did the step before it. If the step before an action didn't happen, behavior is undefined. Dakiny clan's own discord bot!
### Assumptions
- For Elections: Bot assumes that it always did the step before it. If the step before an action didn't happen, behavior is undefined.
Object creation in the middle of a cycle is only there for restarting the bot. All relevant information should already Object creation in the middle of a cycle is only there for restarting the bot. All relevant information should already
exist in the database. exist in the database.
- Bot is running on machine that is usint UTC time. All today() calls specify UTC time zone.
-7
View File
@@ -1,7 +0,0 @@
Here's the {quarter} Clan Council:
Dakiny Council:
{council}
Dakiny High Council:
{high_council}
-54
View File
@@ -1,54 +0,0 @@
```fix
Dakiny's Clan Council election
```
Dakiny is an organization built by and for its members. We are a community of people who come together to support each other in the creation of this magical and unique experience. To sustain what we've created here, and to grow and involve into the future, we vote in our leadership every 3 months to provide an opportunity for new clan members to represent their faction of Dakiny in the Clan Council.
This clan is about human interaction, and the Clan Council positions serve to prop up certain voices within our community, in an effort to shape it as a force for good. Our mission here is to spread love. The more loving Council Members (CMs) we have on the team, the greater the community will grow for all of us to enjoy. There are no term limits, but if you go inactive on RS or become uninterested in pouring your effort into this community, request to step down so that others may contribute where you are no longer able to.
We want all CMs to grow and succeed. Please, reach out for assistance and advice if you find yourself going even a few days without taking real, actionable steps towards the prosperity of Dakiny. Recruiting, hosting events, participating in the Clan Council Discord, and demonstrating the Core Elements of the position are required from each and every CM to grow a happy and healthy clan.
__So here's how it works:__
---message_break---
Each quarter, a channel is created here on Discord for clan members to cast their vote to determine the Clan Council for that 3-month period.
**Only clan members can vote and all clan members are granted exactly one equally weighted vote.** If you vote twice for different people, your vote will be annulled.
All clan members are permitted to run for a seat on the Clan Council as long as they:
**i)** have been in the clan for at least 13 weeks (enough time to have participated in at least one election);
**ii)** have no warnings under their name in the last 6 months and
**iii)** have been active in the clan for the past 13 weeks prior to the first day of elections. To be considered active in that timeframe, one must fulfill the following criteria: hosted/participated in at least three clan events, each one in a different week;
**iv)** are not running under aliases that hide their true identity.
Candidates are permitted to vote for themselves. Once a candidate reaches 3 votes from clan members, they have earned their seat, meaning no more votes can be cast for them.
Then, the new Clan Council Elect will come together to select the 2 High Council positions:
RuneScape 3 Clan Leader
Discord Server Manager
The Clan Council will be denoted by the Discord ranks of <@&{clan_council_id}> and <@&{high_council_id}> and in-game with the following ranks.
<:Rank_Admin:479008185240518656> Admin
<:Rank_Organiser:479008215024009216> Organizer
<:Rank_Coordinator:479008238654849045> Coordinator
<:Rank_Overseer:479008269382451201> Overseer
<:Rank_Deputy:479008295319896064> Deputy Owner
<:Rank_Owner:479008325137072155> Clan Leader
---message_break---
__Here's the timeline:__
Election begins: {election_start}
Term: {term_start} - {term_end}
**{election_start}**: Post here to announce candidacy
**{voting_start}**: Voting begins
**{voting_end}**: Votes are tallied and winners are announced
**{highcouncil_election}**: The new Clan Council votes who among them shall receive the two High Council positions of: Clan Leader and Discord Server Manager.
**{term_start}**: The new Clan Council is officially inaugurated and a drop party (or a giveaway event) begins to commemorate the new leadership.
This process will begin again next quarter on {next_election_start} with the inauguration scheduled for {term_end}.
---message_break---
**You may now announce your candidacy for a seat on the Dakiny Clan Council!
**
You may include a speech with your announcement, as well as specific pertinent facts such as total recruits, time in the clan, past Clan Council experience, and donations made. This data is available upon request via <@&{clan_council_id}>.
-13
View File
@@ -1,13 +0,0 @@
`Leadership Elections - candidacy time`
<@&{clan_council_elect_role_id}>, welcome. We are starting the second part of our elections. On {highcouncil_election_announce} this channel will be open to write for all of you. Everyone else will be able to read this channel, but not write in it. Only members of the Clan Council Elected (CCE) can vote now. Any votes from others will be annulled.
From {highcouncil_election_announce} to {highcouncil_election_start}, you will be able to announce your candidacy for High Council. The positions are:
Clan Leader
Discord Server Manager
Each member of the CCE can cast one for each category by using the emotes: <:Rank_Owner:479008325137072155> for Clan Leader and <:Rank_Deputy:479008295319896064> for Discord Server Manager.
If needed, the icon 🤷 can be used to abstain to vote in any or both categories and it should be applied to all candidates of that category.
Votes can be cast until {highcouncil_election_end}, so we can announce and have everyone set up to start the term by {term_start}.
-1
View File
@@ -1 +0,0 @@
<@&{clan_council_elect_role_id}> it is now time to vote for the high council! You have until {term_start} to complete the vote.
-14
View File
@@ -1,14 +0,0 @@
====================
<@&{clan_member_id}>, it's time to vote for clan council.
Press the Vote button to vote!
Voting ends next {voting_end_day}, {voting_end}.
Results will then be announced.
After a candidate receives 3 votes, their spot is secured in the new council, but you can still vote for them to show your support.
**Just a reminder:** only the people listed below are eligible to vote, and each clan member may **only vote for one person**. Votes from guests, diplomats, or allies will be annulled.
Votes for multiple candidates from the same person will not be counted.
RS3 Clan Members
OSRS Clan Members
+389
View File
@@ -0,0 +1,389 @@
"""Contains the election cycle class."""
import collections
from datetime import UTC, datetime, timedelta
from pathlib import Path
import nextcord
from classes.nominate_highcouncil_views import NominateHighCouncilView
from classes.nominate_views import NominateView
from classes.voting_highcouncil_views import VoteHighcouncilView
from classes.voting_views import VoteView
from DakinySettings import settings
from database_setup.sql_data_classes import (
CouncilMembers,
ElectionInformation,
Votes,
engine,
)
from sqlalchemy import select, true, update
from sqlalchemy.orm import Session
from utils import (
TEMPLATES_DIR,
VoteType,
build_highcouncil_votes_str,
build_votes_str,
edit_clan_council_info,
get_datetime,
get_election_name,
)
REQUIRED_VOTES_TO_BE_ELECTED = 3
class Election:
"""
Election class.
This class represents all the dates required for an election cycle along with all the functions needed to operate
the cycle.
"""
def __init__(self, cycle: int, guild: nextcord.Guild) -> None:
"""
Generates all dates relevant to the election cycle determined by the config and cycle.
Args:
cycle (int): Which election cycle this cycle is.
guild (nextcord.Guild): The guild that this election is running in.
"""
self.cycle = cycle
self.election_cycle = settings.election_cycles[self.cycle]
self.active = self.election_cycle.active
self.election_start: datetime = get_datetime(self.election_cycle.election_start)
self.term_start: datetime = get_datetime(self.election_cycle.term_start)
self.term_end: datetime = get_datetime(self.election_cycle.term_end)
self.voting_start: datetime = get_datetime(self.election_cycle.voting_start)
self.voting_end: datetime = get_datetime(self.election_cycle.voting_end)
self.highcouncil_election_announce: datetime = get_datetime(self.election_cycle.highcouncil_election_announce)
self.highcouncil_election_start: datetime = get_datetime(self.election_cycle.highcouncil_election_start)
self.highcouncil_election_end: datetime = get_datetime(self.election_cycle.highcouncil_election_end)
self.guild: nextcord.Guild = guild
self.election_over: bool = False
def build_election_announcement(self) -> list[str]:
"""
Builds the messages to announce the election is starting.
Returns:
list[str]: A list of message contents in the correct order. Must be a string because the message is too
long.
"""
with Path(f"{TEMPLATES_DIR}/election_announcement.txt", encoding="utf8").open() as file:
message = file.read()
next_election = get_datetime(
settings.election_cycles[(self.cycle + 1) % len(settings.election_cycles)].election_start
)
message = message.format(
clan_council_id=settings.role_ids.clan_council,
high_council_id=settings.role_ids.high_council,
election_start=self.election_start.strftime("%B %d"),
term_start=self.term_start.strftime("%B %d"),
term_end=self.term_end.strftime("%B %d"),
voting_start=self.voting_start.strftime("%B %d"),
voting_end=self.voting_end.strftime("%B %d"),
highcouncil_election=self.highcouncil_election_start.strftime("%B %d"),
next_election_start=next_election.strftime("%B %d"),
)
return message.split("---message_break---")
def build_highcouncil_election_announce(self) -> str:
"""
Builds message to announce highcouncil election start.
Returns:
str: Message to send.
"""
with Path(f"{TEMPLATES_DIR}/highcouncil_election.txt", encoding="utf8").open() as file:
message = file.read()
return message.format(
clan_council_elect_role_id=settings.role_ids.clan_council_elect,
highcouncil_election_announce=self.highcouncil_election_announce.strftime("%B %d"),
highcouncil_election_start=self.highcouncil_election_start.strftime("%B %d"),
highcouncil_election_end=self.highcouncil_election_end.strftime("%B %d"),
term_start=self.term_start.strftime("%B %d"),
)
def build_highcouncil_vote_start(self) -> str:
"""
Builds the message to announce that highcouncil voting has started.
Returns:
str: Message to send.
"""
with Path(f"{TEMPLATES_DIR}/highcouncil_vote.txt", encoding="utf8").open() as file:
message = file.read()
return message.format(
clan_council_elect_role_id=settings.role_ids.clan_council_elect,
term_start=self.term_start.strftime("%B %d"),
)
def build_vote_start(self) -> str:
"""
Builds the voting start message out of template and configurations.
Returns:
str: Message to send
"""
with Path(f"{TEMPLATES_DIR}/vote_start.txt", encoding="utf8").open() as file:
message = file.read()
vote_end: datetime = get_datetime(settings.election_cycles[self.cycle].voting_end)
return message.format(
clan_member_id=settings.role_ids.clan_member,
voting_end=vote_end.strftime("%B %d"),
voting_end_day=vote_end.strftime("%A"),
)
async def check_date(self) -> None:
"""Function checks current date to see if something needs to be run."""
today = datetime.now(tz=UTC)
# check all dates if action is needed
if today.date() == self.election_start.date():
await self.election_start_action()
elif today.date() == self.voting_start.date():
await self.voting_start_action()
elif (self.voting_end - timedelta(3)).date() <= today.date() < self.voting_end.date():
await self.election_reminder()
elif today.date() == self.voting_end.date():
await self.voting_end_action()
elif today.date() == self.highcouncil_election_start.date():
await self.highcouncil_election_start_action()
elif today.date() == self.term_start.date():
await self.term_start_action()
async def election_start_action(self) -> None:
"""Performs required actions for when the election cycle starts."""
# create new channel for election
parent: nextcord.TextChannel = self.guild.get_channel(settings.channel_ids.voting)
quarter_name: str = get_election_name()
thread_msg: nextcord.Message = await parent.send(content=quarter_name)
thread: nextcord.Thread = await parent.create_thread(name=quarter_name, message=thread_msg)
# post message announcing election
# election start message is returned as an array of string because it is too long. send each string
# individually.
messages = self.build_election_announcement()
for index, message in enumerate(messages):
# if last item, add the nominate button
if index + 1 == len(messages):
nominate_msg: nextcord.Message = await thread.send(message, view=NominateView(self.guild))
else:
await thread.send(message)
# create new entry into database.
with Session(engine) as session:
new_cycle = ElectionInformation(
ElectionName=quarter_name,
ThreadId=thread.id,
CouncilNominationMsgId=nominate_msg.id,
)
session.add(new_cycle)
session.commit()
async def voting_start_action(self) -> None:
"""
Actions performed when starting a vote.
Sends message informing members that voting has started.
Removes write permission from the general clan member role.
"""
message = self.build_vote_start()
election_thread: nextcord.Thread = self.guild.get_thread(self.get_election_info().ThreadId)
await election_thread.send(message)
vote_msg = await election_thread.send(
build_votes_str(self.guild),
view=VoteView(self.guild),
)
with Session(engine) as session:
stmt = select(ElectionInformation).where(ElectionInformation.ElectionName == get_election_name())
info = session.scalar(stmt)
info.CouncilVoteMsgId = vote_msg.id
session.commit()
thread: nextcord.Thread = self.guild.get_thread(info.ThreadId)
nominate_msg: nextcord.Message = await thread.fetch_message(info.CouncilNominationMsgId)
await nominate_msg.edit(view=None)
async def voting_end_action(self) -> None:
"""Performs required actions for when voting time has ended."""
message = self.build_highcouncil_election_announce()
council_elect_role: nextcord.Role = self.guild.get_role(settings.role_ids.clan_council_elect)
election_thread: nextcord.Thread = self.guild.get_thread(self.get_election_info().ThreadId)
nominate_msg: nextcord.Message = await election_thread.send(message, view=NominateHighCouncilView(self.guild))
election_name = get_election_name()
with Session(engine) as session:
# remove vote button from vote message
stmt = select(ElectionInformation).where(ElectionInformation.ElectionName == election_name)
info = session.scalar(stmt)
vote_msg: nextcord.Message = await election_thread.fetch_message(info.CouncilVoteMsgId)
await vote_msg.edit(view=None)
# add the highcouncil nomination message to election information
info.HighCouncilNominationMsgId = nominate_msg.id
session.commit()
# add the highcouncil role to everyone with 3 or more votes. set elected to true in database
stmt = (
select(Votes.VotedForId)
.where(Votes.ElectionName == election_name)
.where(Votes.Type == VoteType.COUNCIL)
)
vote_count = collections.Counter(session.scalars(stmt))
for user_id, votes in vote_count.items():
if votes >= REQUIRED_VOTES_TO_BE_ELECTED:
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionName == election_name)
.where(CouncilMembers.MemberId == user_id)
)
council_member = session.scalar(stmt)
council_member.Elected = True
session.commit()
member: nextcord.Member = self.guild.get_member(user_id)
await member.add_roles(
council_elect_role,
reason=f"{member.display_name} has won a nomination and become a Council Elect.",
)
async def highcouncil_election_start_action(self) -> None:
"""Performs required actions for when the highcouncil is elected."""
message = self.build_highcouncil_vote_start()
election_thread: nextcord.Thread = self.guild.get_thread(self.get_election_info().ThreadId)
await election_thread.send(message)
vote_msg = await election_thread.send(
build_highcouncil_votes_str(self.guild),
view=VoteHighcouncilView(self.guild),
)
with Session(engine) as session:
stmt = select(ElectionInformation).where(ElectionInformation.ElectionName == get_election_name())
info = session.scalar(stmt)
info.HighCouncilVoteMsgId = vote_msg.id
session.commit()
# remove highcouncil nomination view
nomination_msg: nextcord.Message = await election_thread.fetch_message(info.HighCouncilNominationMsgId)
await nomination_msg.edit(view=None)
async def term_start_action(self) -> None:
"""Marks the election as over."""
self.election_over = True
elect_role: nextcord.Role = self.guild.get_role(settings.role_ids.clan_council_elect)
council_role: nextcord.Role = self.guild.get_role(settings.role_ids.clan_council)
election_name = get_election_name()
with Session(engine) as session:
# role management
stmt = (
select(CouncilMembers.MemberId)
.where(CouncilMembers.ElectionName == election_name)
.where(CouncilMembers.Elected == true())
)
for member_id in session.scalars(stmt):
member = self.guild.get_member(member_id)
await member.remove_roles(
elect_role,
reason=f"{member.display_name} no longer needs the elect role.",
)
await member.add_roles(
council_role,
reason=f"{member.display_name} has been elected to council.",
)
# announce high council members
# ties are undefined behavior.
stmt = (
select(Votes.VotedForId)
.where(Votes.ElectionName == election_name)
.where(Votes.Type == VoteType.DISCORDMANAGER)
)
dm_votes = collections.Counter(session.scalars(stmt))
stmt = (
select(Votes.VotedForId)
.where(Votes.ElectionName == election_name)
.where(Votes.Type == VoteType.CLANLEADER)
)
cl_votes = collections.Counter(session.scalars(stmt))
try:
dm_winner = max(dm_votes, key=dm_votes.get)
stmt = (
update(CouncilMembers)
.where(CouncilMembers.ElectionName == election_name)
.where(CouncilMembers.MemberId == dm_winner)
.values(DiscordManager=True)
)
session.execute(stmt)
except ValueError:
dm_winner = "No winner"
try:
cl_winner = max(cl_votes, key=cl_votes.get)
stmt = (
update(CouncilMembers)
.where(CouncilMembers.ElectionName == election_name)
.where(CouncilMembers.MemberId == cl_winner)
.values(ClanLeader=True)
)
session.execute(stmt)
except ValueError:
cl_winner = "No winner"
session.commit()
election_thread: nextcord.Thread = self.guild.get_thread(self.get_election_info().ThreadId)
# clear view from vote message
stmt = select(ElectionInformation.HighCouncilVoteMsgId).where(
ElectionInformation.ElectionName == get_election_name()
)
vote_msg_id = session.scalar(stmt)
vote_msg: nextcord.Message = await election_thread.fetch_message(vote_msg_id)
await vote_msg.edit(view=None)
highcouncil_msg: str = f"Congratulations for our high council members!\nClan Leader: <@{cl_winner}>\n"
highcouncil_msg += f"Discord Manager: <@{dm_winner}>"
await election_thread.send(highcouncil_msg)
await election_thread.send("This election cycle is now over. Thank you everyone for participating!")
await election_thread.edit(archived=True, locked=True)
await edit_clan_council_info(self.guild)
def get_election_info(self) -> ElectionInformation:
"""
Gets information for current election.
Returns:
ElectionInformation: The information from the database about the current election.
"""
with Session(engine) as session:
stmt = select(ElectionInformation).where(ElectionInformation.ElectionName == get_election_name())
return session.scalar(stmt)
async def election_reminder(self) -> None:
"""Sends a message to general chat reminder clan about election."""
general: nextcord.TextChannel = self.guild.get_channel(settings.channel_ids.general)
message = f"Reminder to <@&{settings.role_ids.clan_member}>! Clan elections are happening! Head over "
message += f"to <#{self.get_election_info().ThreadId}> to cast your vote!"
await general.send(content=message)
-340
View File
@@ -1,340 +0,0 @@
"""
Contains the election cycle class
"""
import collections
from datetime import datetime, timedelta
import nextcord
from sqlalchemy import select
from sqlalchemy.orm import Session
from classes.nominate_views import NominateView
from classes.voting_views import VoteView
from config import DakinyConfig
from database_setup.sql_data_classes import (
CouncilMembers,
ElectionInformation,
Votes,
engine,
)
from utils import build_votes_str, get_datetime, get_election_cycle_name
class ElectionCycle:
"""
This class represents all the dates required for an election cycle along with all the functions needed to operate
the cycle.
"""
def __init__(self, cycle: int, config: DakinyConfig, guild: nextcord.Guild) -> None:
"""
Generates all dates relevant to the election cycle determined by the config and cycle.
Args:
cycle (int): Which election cycle this cycle is.
config (DakinyConfig): The entire config object.
guild (nextcord.Guild): The guild that this election is running in.
"""
self.config: DakinyConfig = config
self.cycle = cycle
self.election_cycle = self.config.election_cycles[self.cycle]
self.election_start: datetime = get_datetime(self.election_cycle.election_start)
self.term_start: datetime = get_datetime(self.election_cycle.term_start)
self.term_end: datetime = get_datetime(self.election_cycle.term_end)
self.voting_start: datetime = get_datetime(self.election_cycle.voting_start)
self.voting_end: datetime = get_datetime(self.election_cycle.voting_end)
self.highcouncil_election_announce: datetime = get_datetime(
self.election_cycle.highcouncil_election_announce
)
self.highcouncil_election_start: datetime = get_datetime(
self.election_cycle.highcouncil_election_start
)
self.highcouncil_election_end: datetime = get_datetime(
self.election_cycle.highcouncil_election_end
)
self.guild: nextcord.Guild = guild
self.election_over: bool = False
def build_election_announcement(self) -> list[str]:
"""
Builds the messages to announce the election is starting.
Returns:
list[str]: A list of message contents in the correct order. Must be a string because the message is too
long.
"""
with open(
"bot_messages/election_announcement.txt", mode="r", encoding="utf8"
) as file:
message = file.read()
next_election = get_datetime(
self.config.election_cycles[
(self.cycle + 1) % len(self.config.election_cycles)
].election_start
)
message = message.format(
clan_council_id=self.config.params.clan_council_role_id,
high_council_id=self.config.params.high_council_role_id,
election_start=self.election_start.strftime("%B %d"),
term_start=self.term_start.strftime("%B %d"),
term_end=self.term_end.strftime("%B %d"),
voting_start=self.voting_start.strftime("%B %d"),
voting_end=self.voting_end.strftime("%B %d"),
highcouncil_election=self.highcouncil_election_start.strftime("%B %d"),
next_election_start=next_election.strftime("%B %d"),
)
return message.split("---message_break---")
def build_highcouncil_election_announce(self) -> str:
"""
Builds message to announce highcouncil election start.
Returns:
str: Message to send.
"""
with open(
"bot_messages/highcouncil_election.txt", mode="r", encoding="utf8"
) as file:
message = file.read()
return message.format(
clan_council_elect_role_id=self.config.params.clan_council_elect_role_id,
highcouncil_election_announce=self.highcouncil_election_announce.strftime(
"%B %d"
),
highcouncil_election_start=self.highcouncil_election_start.strftime(
"%B %d"
),
highcouncil_election_end=self.highcouncil_election_end.strftime(
"%B %d"
),
term_start=self.term_start.strftime("%B %d"),
)
def build_highcouncil_vote_start(self) -> str:
"""
Builds the message to announce that highcouncil voting has started.
Returns:
str: Message to send.
"""
with open(
"bot_messages/highcouncil_vote.txt", mode="r", encoding="utf8"
) as file:
message = file.read()
return message.format(
clan_council_elect_role_id=self.config.params.clan_council_elect_role_id,
term_start=self.term_start.strftime("%B %d"),
)
def build_vote_start(self) -> str:
"""
Builds the voting start message out of template and configurations.
Returns:
str: Message to send
"""
with open("bot_messages/vote_start.txt", mode="r", encoding="utf8") as file:
message = file.read()
vote_end: datetime = get_datetime(
self.config.election_cycles[self.cycle].voting_end
)
return message.format(
clan_member_id=self.config.params.clan_member_role_id,
voting_end=vote_end.strftime("%B %d"),
voting_end_day=vote_end.strftime("%A"),
)
async def check_date(self) -> None:
"""
Function checks current date to see if something needs to be run.
"""
today = datetime.today()
# check all dates if action is needed
if today.date() == self.election_start.date():
await self.election_start_action()
elif today.date() == self.voting_start.date():
await self.voting_start_action()
elif (
(self.voting_end - timedelta(3)).date()
<= today.date()
< self.voting_end.date()
):
await self.election_reminder()
elif today.date() == self.voting_end.date():
await self.voting_end_action()
elif today.date() == self.highcouncil_election_start.date():
await self.highcouncil_election_start_action()
elif today.date() == self.term_start.date():
await self.term_start_action()
async def election_start_action(self) -> None:
"""
Performs required actions for when the election cycle starts.
"""
# create new channel for election
parent: nextcord.TextChannel = self.guild.get_channel(
self.config.params.voting_parent_id
)
quarter_name: str = get_election_cycle_name(self.config)
thread_msg: nextcord.Message = await parent.send(content=quarter_name)
# create new entry into database.
thread: nextcord.Thread = await parent.create_thread(
name=quarter_name, message=thread_msg
)
with Session(engine) as session:
new_cycle = ElectionInformation(
ElectionCycle=quarter_name, ThreadId=thread.id
)
session.add(new_cycle)
session.commit()
# post message announcing election
# election start message is returned as an array of string because it is too long. send each string
# individually.
messages = self.build_election_announcement()
for index, message in enumerate(messages):
# if last item, add the nominate button
if index + 1 == len(messages):
await thread.send(message, view=NominateView(self.config))
else:
await thread.send(message)
async def voting_start_action(self) -> None:
"""
Sends message informing members that voting has started.
Removes write permission from the general clan member role.
"""
message = self.build_vote_start()
election_thread: nextcord.Thread = self.guild.get_thread(
self.get_election_info().ThreadId
)
await election_thread.send(message)
vote_msg = await election_thread.send(
build_votes_str(self.config), view=VoteView(self.config, self.guild)
)
with Session(engine) as session:
stmt = select(ElectionInformation).where(
ElectionInformation.ElectionCycle
== get_election_cycle_name(self.config)
)
info = session.scalar(stmt)
info.CouncilVoteMsgId = vote_msg.id
session.commit()
async def voting_end_action(self) -> None:
"""
Performs required actions for when voting time has ended.
"""
message = self.build_highcouncil_election_announce()
council_elect_role: nextcord.Role = self.guild.get_role(
self.config.params.clan_council_elect_role_id
)
election_thread: nextcord.Thread = self.guild.get_thread(
self.get_election_info().ThreadId
)
# remove vote button from vote message
election_name = get_election_cycle_name(self.config)
with Session(engine) as session:
stmt = select(ElectionInformation.CouncilVoteMsgId).where(
ElectionInformation.ElectionCycle == election_name
)
vote_msg_id = session.scalar(stmt)
vote_msg: nextcord.Message = await election_thread.fetch_message(vote_msg_id)
await vote_msg.edit(view=None)
# add the highcouncil role to everyone with 3 or more votes. set elected to true in database
with Session(engine) as session:
stmt = select(Votes.VotedForId).where(Votes.ElectionCycle == election_name)
vote_count = collections.Counter(session.scalars(stmt))
for user_id, votes in vote_count.items():
if votes >= 3:
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionCycle == election_name)
.where(CouncilMembers.MemberId == user_id)
)
council_member = session.scalar(stmt)
council_member.Elected = True
session.commit()
user: nextcord.Member = self.guild.get_member(user_id)
await user.add_roles(council_elect_role)
await election_thread.send(message)
async def highcouncil_election_start_action(self) -> None:
"""
Performs required actions for when the highcouncil is elected.
"""
message = self.build_highcouncil_vote_start()
election_thread: nextcord.Thread = self.guild.get_thread(
self.get_election_info().ThreadId
)
await election_thread.send(message)
async def term_start_action(self) -> None:
"""
Marks the election as over.
"""
self.election_over = True
election_thread: nextcord.Thread = self.guild.get_thread(
self.get_election_info().ThreadId
)
await election_thread.send(
"This election cycle is now over. Thank you everyone for participating!"
)
await election_thread.edit(archived=True, locked=True)
def get_election_info(self) -> ElectionInformation:
"""
Gets information for current election.
Returns:
ElectionInformation: The information from the database about the current election.
"""
with Session(engine) as session:
stmt = select(ElectionInformation).where(
ElectionInformation.ElectionCycle
== get_election_cycle_name(self.config)
)
return session.scalar(stmt)
async def election_reminder(self) -> None:
"""
Sends a message to general chat reminder clan about election.
"""
general: nextcord.TextChannel = self.guild.get_channel(
self.config.params.general_channel_id
)
message = f"Reminder to <@&{self.config.params.clan_member_role_id}>! Clan elections are happening! Head over "
message += f"to <#{self.get_election_info().ThreadId}> to cast your vote!"
await general.send(content=message)
+193
View File
@@ -0,0 +1,193 @@
# generated by datamodel-codegen:
# filename: gitea_webhook.json
# timestamp: 2024-05-16T21:15:27+00:00
from __future__ import annotations
from pydantic import BaseModel
class Author(BaseModel):
name: str
email: str
username: str
class Committer(BaseModel):
name: str
email: str
username: str
class Commit(BaseModel):
id: str
message: str
url: str
author: Author
committer: Committer
verification: None
timestamp: str
added: list[str]
removed: list[str]
modified: list[str]
class HeadCommit(BaseModel):
id: str
message: str
url: str
author: Author
committer: Committer
verification: None
timestamp: str
added: list[str]
removed: list[str]
modified: list[str]
class Owner(BaseModel):
id: int
login: str
login_name: str
full_name: str
email: str
avatar_url: str
language: str
is_admin: bool
last_login: str
created: str
restricted: bool
active: bool
prohibit_login: bool
location: str
website: str
description: str
visibility: str
followers_count: int
following_count: int
starred_repos_count: int
username: str
class Permissions(BaseModel):
admin: bool
push: bool
pull: bool
class Repository(BaseModel):
id: int
owner: Owner
name: str
full_name: str
description: str
empty: bool
private: bool
fork: bool
template: bool
parent: None
mirror: bool
size: int
language: str
languages_url: str
html_url: str
url: str
link: str
ssh_url: str
clone_url: str
original_url: str
website: str
stars_count: int
forks_count: int
watchers_count: int
open_issues_count: int
open_pr_counter: int
release_counter: int
default_branch: str
archived: bool
created_at: str
updated_at: str
archived_at: str
permissions: Permissions
has_issues: bool
has_wiki: bool
has_pull_requests: bool
has_projects: bool
has_releases: bool
has_packages: bool
has_actions: bool
ignore_whitespace_conflicts: bool
allow_merge_commits: bool
allow_rebase: bool
allow_rebase_explicit: bool
allow_squash_merge: bool
allow_rebase_update: bool
default_delete_branch_after_merge: bool
default_merge_style: str
default_allow_maintainer_edit: bool
avatar_url: str
internal: bool
mirror_interval: str
mirror_updated: str
repo_transfer: None
class Pusher(BaseModel):
id: int
login: str
login_name: str
full_name: str
email: str
avatar_url: str
language: str
is_admin: bool
last_login: str
created: str
restricted: bool
active: bool
prohibit_login: bool
location: str
website: str
description: str
visibility: str
followers_count: int
following_count: int
starred_repos_count: int
username: str
class Sender(BaseModel):
id: int
login: str
login_name: str
full_name: str
email: str
avatar_url: str
language: str
is_admin: bool
last_login: str
created: str
restricted: bool
active: bool
prohibit_login: bool
location: str
website: str
description: str
visibility: str
followers_count: int
following_count: int
starred_repos_count: int
username: str
class GiteaWebhook(BaseModel):
ref: str
before: str
after: str
compare_url: str
commits: list[Commit]
total_commits: int
head_commit: HeadCommit
repository: Repository
pusher: Pusher
sender: Sender
+228
View File
@@ -0,0 +1,228 @@
"""Contains various views to be used in the high council nomination process."""
import nextcord
from DakinySettings import settings
from database_setup.sql_data_classes import CouncilMembers, engine
from nextcord.interactions import Interaction
from sqlalchemy import select
from sqlalchemy.orm import Session
from utils import (
NOMINEE_SPLITTER,
decorate_cl_nomination,
decorate_dm_nomination,
get_election_name,
)
class NominateDMModal(nextcord.ui.Modal):
"""
Modal for nominating oneself.
User writes a speech and then the callback sends the speech into the correct channel.
"""
def __init__(
self,
usr_id: int,
usr_nominated: bool, # noqa: FBT001
speech_content: str,
):
super().__init__(
"Discord Manager Nomination",
timeout=5 * 60, # 5 minutes
)
self.usr_id: int = usr_id
self.usr_nominated = usr_nominated
self.speech = nextcord.ui.TextInput(
label="Nomination Speech",
style=nextcord.TextInputStyle.paragraph,
placeholder="Please write your speech here.",
required=True,
max_length=1500,
default_value=speech_content,
)
self.add_item(self.speech)
async def callback(self, interaction: Interaction) -> None:
"""
Sends the nomination speech into the voting channel and enters the user into the voting database.
Args:
interaction (Interaction): Interaction with user.
"""
speech = decorate_dm_nomination(interaction.user.id, self.speech.value)
with Session(engine) as session:
# user already in table
election_name = get_election_name()
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionName == election_name)
.where(CouncilMembers.MemberId == self.usr_id)
)
council_elect = session.scalar(stmt)
if not self.usr_nominated:
msg = await interaction.channel.send(speech)
council_elect.DiscordManagerSpeechId = msg.id
session.commit()
else:
msg: nextcord.Message = await interaction.channel.fetch_message(council_elect.DiscordManagerSpeechId)
await msg.edit(content=decorate_dm_nomination(interaction.user.id, self.speech.value))
class NominateCLModal(nextcord.ui.Modal):
"""
Modal for nominating oneself.
User writes a speech and then the callback sends the speech into the correct channel.
"""
def __init__(
self,
usr_id: int,
usr_nominated: bool, # noqa: FBT001
speech_content: str,
):
super().__init__(
"Clan Leader Nomination",
timeout=5 * 60, # 5 minutes
)
self.usr_id: int = usr_id
self.usr_nominated = usr_nominated
self.speech = nextcord.ui.TextInput(
label="Nomination Speech",
style=nextcord.TextInputStyle.paragraph,
placeholder="Please write your speech here.",
required=True,
max_length=1500,
default_value=speech_content,
)
self.add_item(self.speech)
async def callback(self, interaction: Interaction) -> None:
"""
Sends the nomination speech into the voting channel and enters the user into the voting database.
Args:
interaction (Interaction): Interaction with user.
"""
speech = decorate_cl_nomination(interaction.user.id, self.speech.value)
with Session(engine) as session:
# user already in table
election_name = get_election_name()
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionName == election_name)
.where(CouncilMembers.MemberId == self.usr_id)
)
council_elect = session.scalar(stmt)
if not self.usr_nominated:
msg = await interaction.channel.send(speech)
council_elect.ClanLeaderSpeechId = msg.id
session.commit()
else:
msg: nextcord.Message = await interaction.channel.fetch_message(council_elect.ClanLeaderSpeechId)
await msg.edit(content=decorate_cl_nomination(interaction.user.id, self.speech.value))
class NominateHighCouncilView(nextcord.ui.View):
"""The buttons to press to start your High Council nomination."""
def __init__(self, guild: nextcord.Guild):
super().__init__(timeout=None)
self.guild: nextcord.Guild = guild
@nextcord.ui.button(
label="Nominate for Discord Manager",
style=nextcord.ButtonStyle.primary,
custom_id="nominate-dm",
)
async def nominate_dm(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
"""
Button for nominating oneself for Discord Manager.
Gathers previous information about the user if they have already nominated themselves, then launches
the nomination modal to get their speech.
Args:
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
interaction (nextcord.Interaction): Interaction of user who pressed the button
"""
# enforce council elect only
if self.guild.get_role(settings.role_ids.clan_council_elect) not in interaction.user.roles:
await interaction.response.send_message(
f"You must have the <@&{settings.role_ids.clan_council_elect}> role to vote.",
ephemeral=True,
)
return
election_name = get_election_name()
usr_nominated = False
speech_content = ""
with Session(engine) as session:
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionName == election_name)
.where(CouncilMembers.MemberId == interaction.user.id)
)
obj = session.scalar(stmt)
# user needs to exist to get here anyways...
if obj.DiscordManagerSpeechId is not None:
usr_nominated = True
msg = await interaction.channel.fetch_message(obj.DiscordManagerSpeechId)
speech_content = msg.content.split(NOMINEE_SPLITTER)[1]
await interaction.response.send_modal(NominateDMModal(interaction.user.id, usr_nominated, speech_content))
@nextcord.ui.button(
label="Nominate for Clan Leader",
style=nextcord.ButtonStyle.primary,
custom_id="nominate-cl",
)
async def nominate_cl(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
"""
Button for nominating oneself for Clan Leader.
Gathers previous information about the user if they have already nominated themselves, then launches
the nomination modal to get their speech.
Args:
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
interaction (nextcord.Interaction): Interaction of user who pressed the button
"""
# enforce council elect only
if self.guild.get_role(settings.role_ids.clan_council_elect) not in interaction.user.roles:
await interaction.response.send_message(
f"You must have the <@&{settings.role_ids.clan_council_elect}> role to vote.",
ephemeral=True,
)
return
election_name = get_election_name()
usr_nominated = False
speech_content = ""
with Session(engine) as session:
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionName == election_name)
.where(CouncilMembers.MemberId == interaction.user.id)
)
obj = session.scalar(stmt)
# user needs to exist to get here anyways...
if obj.ClanLeaderSpeechId is not None:
usr_nominated = True
msg = await interaction.channel.fetch_message(obj.ClanLeaderSpeechId)
speech_content = msg.content.split(NOMINEE_SPLITTER)[1]
await interaction.response.send_modal(NominateCLModal(interaction.user.id, usr_nominated, speech_content))
+37 -46
View File
@@ -1,31 +1,32 @@
""" """Contains various views to be used in the nomination process."""
Contains various views to be used in the nomination process.
"""
import nextcord import nextcord
from DakinySettings import settings
from database_setup.sql_data_classes import CouncilMembers, engine
from nextcord.interactions import Interaction from nextcord.interactions import Interaction
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from utils import NOMINEE_SPLITTER, decorate_nomination, get_election_name
from config import DakinyConfig
from database_setup.sql_data_classes import CouncilMembers, engine
from utils import get_election_cycle_name
NOMINEE_SPLITTER = "========================================\n\n"
class NominateModal(nextcord.ui.Modal): class NominateModal(nextcord.ui.Modal):
""" """
Modal for nominating oneself. User writes a speech and then the callback sends the speech into the correct channel. Modal for nominating oneself.
User writes a speech and then the callback sends the speech into the correct channel.
""" """
def __init__(self, config: DakinyConfig, usr_id: int, usr_nominated: bool, speech_content: str): def __init__(
self,
usr_id: int,
usr_nominated: bool, # noqa: FBT001
speech_content: str,
):
super().__init__( super().__init__(
"Nomination", "Nomination",
timeout=5 * 60, # 5 minutes timeout=5 * 60, # 5 minutes
) )
self.usr_id: int = usr_id self.usr_id: int = usr_id
self.config: DakinyConfig = config
self.usr_nominated = usr_nominated self.usr_nominated = usr_nominated
self.speech = nextcord.ui.TextInput( self.speech = nextcord.ui.TextInput(
@@ -46,15 +47,15 @@ class NominateModal(nextcord.ui.Modal):
interaction (Interaction): Interaction with user. interaction (Interaction): Interaction with user.
""" """
speech = decorate_nomination(interaction.user.id, self.speech.value) speech = decorate_nomination(interaction.user.id, self.speech.value)
election_name = get_election_name()
with Session(engine) as session: with Session(engine) as session:
if not self.usr_nominated: if not self.usr_nominated:
msg = await interaction.channel.send(speech) msg = await interaction.channel.send(speech)
council_member = CouncilMembers( council_member = CouncilMembers(
ElectionCycle=get_election_cycle_name(self.config), ElectionName=election_name,
MemberId=interaction.user.id, MemberId=interaction.user.id,
SpeechMsgId=msg.id, SpeechMsgId=msg.id,
) )
@@ -62,10 +63,9 @@ class NominateModal(nextcord.ui.Modal):
session.commit() session.commit()
else: else:
# user already nominated, just need to update the election speech # user already nominated, just need to update the election speech
cycle = get_election_cycle_name(self.config)
stmt = ( stmt = (
select(CouncilMembers) select(CouncilMembers)
.where(CouncilMembers.ElectionCycle == cycle) .where(CouncilMembers.ElectionName == election_name)
.where(CouncilMembers.MemberId == self.usr_id) .where(CouncilMembers.MemberId == self.usr_id)
) )
obj = session.scalar(stmt) obj = session.scalar(stmt)
@@ -75,17 +75,21 @@ class NominateModal(nextcord.ui.Modal):
class NominateView(nextcord.ui.View): class NominateView(nextcord.ui.View):
""" """The button to press to start your nomination."""
The button to press to start your nomination.
"""
def __init__(self, config): def __init__(self, guild: nextcord.Guild):
super().__init__(timeout=None) super().__init__(timeout=None)
self.config: DakinyConfig = config self.guild: nextcord.Guild = guild
@nextcord.ui.button(label="Nominate Yourself", style=nextcord.ButtonStyle.primary, custom_id="nominate") @nextcord.ui.button(
async def nominate(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: label="Nominate Yourself",
style=nextcord.ButtonStyle.primary,
custom_id="nominate",
)
async def nominate(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
""" """
Button to nominate oneself for Clan Council.
Gathers previous information about the user if they have already nominated themselves, then launches Gathers previous information about the user if they have already nominated themselves, then launches
the nomination modal to get their speech. the nomination modal to get their speech.
@@ -93,14 +97,21 @@ class NominateView(nextcord.ui.View):
button (nextcord.ui.Button): Unused. idk why its needed in the decorator button (nextcord.ui.Button): Unused. idk why its needed in the decorator
interaction (nextcord.Interaction): Interaction of user who pressed the button interaction (nextcord.Interaction): Interaction of user who pressed the button
""" """
# enforce members only
if self.guild.get_role(settings.role_ids.clan_member) not in interaction.user.roles:
await interaction.response.send_message(
f"You must have the <@&{settings.role_ids.clan_member}> role to vote.",
ephemeral=True,
)
return
cycle = get_election_cycle_name(self.config) election_name = get_election_name()
usr_nominated = False usr_nominated = False
speech_content = "" speech_content = ""
with Session(engine) as session: with Session(engine) as session:
stmt = ( stmt = (
select(CouncilMembers) select(CouncilMembers)
.where(CouncilMembers.ElectionCycle == cycle) .where(CouncilMembers.ElectionName == election_name)
.where(CouncilMembers.MemberId == interaction.user.id) .where(CouncilMembers.MemberId == interaction.user.id)
) )
obj = session.scalar(stmt) obj = session.scalar(stmt)
@@ -109,24 +120,4 @@ class NominateView(nextcord.ui.View):
msg = await interaction.channel.fetch_message(obj.SpeechMsgId) msg = await interaction.channel.fetch_message(obj.SpeechMsgId)
speech_content = msg.content.split(NOMINEE_SPLITTER)[1] speech_content = msg.content.split(NOMINEE_SPLITTER)[1]
await interaction.response.send_modal( await interaction.response.send_modal(NominateModal(interaction.user.id, usr_nominated, speech_content))
NominateModal(self.config, interaction.user.id, usr_nominated, speech_content)
)
def decorate_nomination(user_id: int, speech: str) -> str:
"""
Decorates a nomination speech so that it sits within multiple speeches better.
Args:
user_id (int): ID of user sending the speech
speech (str): Speech from the user
Returns:
str: Decorated speech
"""
msg = f"**Clan Council Nominee: <@{user_id}>**\n"
msg += NOMINEE_SPLITTER
msg += speech
return msg
+10
View File
@@ -0,0 +1,10 @@
[tool.ruff]
# Extend the `pyproject.toml` file in the parent directory...
extend = "../pyproject.toml"
# Exclude a variety of commonly ignored directories.
exclude = ["gitea_webhook.py"]
[tool.ruff.lint]
# ignore implicit-namespace-package in this directory.
ignore = ["INP001"]
+236
View File
@@ -0,0 +1,236 @@
"""Contains various views to be used while voting for high council. Controls the logic when performing the vote."""
import nextcord
from DakinySettings import settings
from database_setup.sql_data_classes import (
CouncilMembers,
ElectionInformation,
Votes,
engine,
)
from nextcord import SelectOption
from nextcord.interactions import Interaction
from sqlalchemy import exc, select, update
from sqlalchemy.orm import Session
from utils import VoteType, build_highcouncil_votes_str, get_election_name
class VoteSelect(nextcord.ui.StringSelect):
"""Vote view string select."""
def __init__(self, options: list["SelectOption"], election_type: str) -> None:
super().__init__(
placeholder="Choose 1",
min_values=1,
max_values=1,
options=options,
)
self.election_type = election_type
async def callback(self, interaction: Interaction) -> None:
"""
Callback override for the StringSelect.
Submits the High Council vote to database if a user is selected. Handles adding vs editing a vote.
Returns message informing user that vote has been recorded.
Args:
interaction (Interaction): Interaction with this callback.
"""
election_name: str = get_election_name()
with Session(engine) as session:
stmt = select(ElectionInformation.HighCouncilVoteMsgId).where(
ElectionInformation.ElectionName == election_name
)
vote_msg_id = session.scalar(stmt)
voted_for = None if self.values[0] == "Abstain" else int(self.values[0])
new_vote = Votes(
ElectionName=election_name,
VoterId=interaction.user.id,
VotedForId=voted_for,
Type=self.election_type,
)
try:
session.add(new_vote)
session.commit()
except exc.IntegrityError:
# user already has a vote in the election and election type
session.rollback()
stmt = (
update(Votes)
.where(Votes.ElectionName == election_name)
.where(Votes.VoterId == interaction.user.id)
.where(Votes.Type == self.election_type)
.values(VotedForId=voted_for)
)
session.execute(stmt)
session.commit()
vote_message_str = build_highcouncil_votes_str(interaction.guild)
vote_msg: nextcord.Message = await interaction.channel.fetch_message(vote_msg_id)
await vote_msg.edit(content=vote_message_str)
msg = f"You have voted for <@{voted_for}>" if voted_for is not None else "You have abstained from voting."
await interaction.edit(content=msg, view=None)
class VoteHighcouncilView(nextcord.ui.View):
"""The button to press to start voting for HighCouncil."""
def __init__(self, guild: nextcord.Guild):
super().__init__(timeout=None)
self.guild = guild
@nextcord.ui.button(
label="Vote for Clan Leader",
style=nextcord.ButtonStyle.primary,
custom_id="vote-cl",
)
async def vote_cl(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
"""
Button to vote for Clan Leader.
Enforces only council elect vote. Gets election cycle, creates voting
options for clan leader (checking if user has voted already),
then sends the voting view.
Args:
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
interaction (nextcord.Interaction): Interaction of user who pressed the button
"""
# enforce council elect only
if self.guild.get_role(settings.role_ids.clan_council_elect) not in interaction.user.roles:
await interaction.response.send_message(
f"You must have the <@&{settings.role_ids.clan_council_elect}> role to vote.",
ephemeral=True,
)
return
election_name: str = get_election_name()
with Session(engine) as session:
stmt = (
select(Votes.VotedForId)
.where(Votes.ElectionName == election_name)
.where(Votes.VoterId == interaction.user.id)
.where(Votes.Type == VoteType.CLANLEADER)
)
voted_for = session.scalar(stmt)
stmt = (
select(CouncilMembers.MemberId)
.where(CouncilMembers.ElectionName == election_name)
.filter(CouncilMembers.ClanLeaderSpeechId.is_not(None))
)
options = [
SelectOption(
label=(
f"{self.guild.get_member(nominee).display_name} ☑️"
if voted_for == nominee
else f"{self.guild.get_member(nominee).display_name}"
),
value=str(nominee),
)
for nominee in sorted(
session.scalars(stmt),
key=lambda nominee: self.guild.get_member(nominee).display_name.lower(),
)
]
options.append(
SelectOption(
label="Abstain ☑️" if voted_for is None else "Abstain",
value="Abstain",
)
)
view = nextcord.ui.View()
view.add_item(VoteSelect(options=options, election_type=VoteType.CLANLEADER))
await interaction.response.send_message(
content="Please vote for a Clan Leader!",
view=view,
ephemeral=True,
)
@nextcord.ui.button(
label="Vote for Discord Manager",
style=nextcord.ButtonStyle.primary,
custom_id="vote-dm",
)
async def vote_dm(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
"""
Button to vote for Discord Manager.
Enforces only council elect vote. Gets election cycle, creates voting
options for discord manager (checking if user has voted already),
then sends the voting view.
Args:
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
interaction (nextcord.Interaction): Interaction of user who pressed the button
"""
# enforce council elect only
if self.guild.get_role(settings.role_ids.clan_council_elect) not in interaction.user.roles:
await interaction.response.send_message(
f"You must have the <@&{settings.role_ids.clan_council_elect}> role to vote.",
ephemeral=True,
)
return
election_name: str = get_election_name()
with Session(engine) as session:
stmt = (
select(Votes.VotedForId)
.where(Votes.ElectionName == election_name)
.where(Votes.VoterId == interaction.user.id)
.where(Votes.Type == VoteType.DISCORDMANAGER)
)
voted_for = session.scalar(stmt)
stmt = (
select(CouncilMembers.MemberId)
.where(CouncilMembers.ElectionName == election_name)
.filter(CouncilMembers.DiscordManagerSpeechId.is_not(None))
)
options = [
SelectOption(
label=(
f"{self.guild.get_member(nominee).display_name} ☑️"
if voted_for == nominee
else f"{self.guild.get_member(nominee).display_name}"
),
value=str(nominee),
)
for nominee in sorted(
session.scalars(stmt),
key=lambda nominee: self.guild.get_member(nominee).display_name.lower(),
)
]
options.append(
SelectOption(
label="Abstain ☑️" if voted_for is None else "Abstain",
value="Abstain",
)
)
view = nextcord.ui.View()
view.add_item(
VoteSelect(
options=options,
election_type=VoteType.DISCORDMANAGER,
)
)
await interaction.response.send_message(
content="Please vote for a Discord Manager!",
view=view,
ephemeral=True,
)
+53 -61
View File
@@ -1,52 +1,53 @@
""" """Contains various views to be used while voting. Controls the logic when performing the vote."""
Contains various views to be used while voting. Controls the logic when performing the vote.
"""
from typing import List
import nextcord import nextcord
from nextcord import SelectOption from DakinySettings import settings
from nextcord.interactions import Interaction
from sqlalchemy import exc, select, update
from sqlalchemy.orm import Session
from config import DakinyConfig
from database_setup.sql_data_classes import ( from database_setup.sql_data_classes import (
CouncilMembers, CouncilMembers,
ElectionInformation, ElectionInformation,
Votes, Votes,
engine, engine,
) )
from utils import build_votes_str, get_election_cycle_name from nextcord import SelectOption
from nextcord.interactions import Interaction
from sqlalchemy import exc, select, update
from sqlalchemy.orm import Session
from utils import VoteType, build_votes_str, get_election_name
class VoteSelect(nextcord.ui.StringSelect): class VoteSelect(nextcord.ui.StringSelect):
def __init__(self, options: List["SelectOption"], config: DakinyConfig) -> None: """Vote view string select."""
def __init__(self, options: list["SelectOption"]) -> None:
super().__init__( super().__init__(
placeholder="Choose 1", placeholder="Choose 1",
min_values=1, min_values=1,
max_values=1, max_values=1,
options=options, options=options,
) )
self.config = config
async def callback(self, interaction: Interaction): async def callback(self, interaction: Interaction) -> None:
election_name = get_election_cycle_name(self.config) """
Callback override for the StringSelect.
Submits the vote to database if a user is selected. Handles adding vs editing a vote. Returns message
informing user that vote has been recorded.
Args:
interaction (Interaction): Interaction with this callback.
"""
election_name = get_election_name()
with Session(engine) as session: with Session(engine) as session:
stmt = select(ElectionInformation.CouncilVoteMsgId).where( stmt = select(ElectionInformation.CouncilVoteMsgId).where(ElectionInformation.ElectionName == election_name)
ElectionInformation.ElectionCycle == election_name
)
vote_msg_id = session.scalar(stmt) vote_msg_id = session.scalar(stmt)
if self.values[0] == "Abstain": voted_for = None if self.values[0] == "Abstain" else int(self.values[0])
voted_for = None
else:
voted_for = int(self.values[0])
new_vote = Votes( new_vote = Votes(
ElectionCycle=election_name, ElectionName=election_name,
VoterId=interaction.user.id, VoterId=interaction.user.id,
VotedForId=voted_for, VotedForId=voted_for,
Type=VoteType.COUNCIL,
) )
try: try:
@@ -57,85 +58,76 @@ class VoteSelect(nextcord.ui.StringSelect):
session.rollback() session.rollback()
stmt = ( stmt = (
update(Votes) update(Votes)
.where(Votes.ElectionCycle == election_name) .where(Votes.ElectionName == election_name)
.where(Votes.VoterId == interaction.user.id) .where(Votes.VoterId == interaction.user.id)
.where(Votes.Type == VoteType.COUNCIL)
.values(VotedForId=voted_for) .values(VotedForId=voted_for)
) )
session.execute(stmt) session.execute(stmt)
session.commit() session.commit()
vote_message_str = build_votes_str(self.config) vote_message_str = build_votes_str(interaction.guild)
vote_msg: nextcord.Message = await interaction.channel.fetch_message( vote_msg: nextcord.Message = await interaction.channel.fetch_message(vote_msg_id)
vote_msg_id
)
await vote_msg.edit(content=vote_message_str) await vote_msg.edit(content=vote_message_str)
if voted_for is not None: msg = f"You have voted for <@{voted_for}>" if voted_for is not None else "You have abstained from voting."
msg = f"You have voted for <@{voted_for}>"
else:
msg = "You have abstained from voting."
await interaction.edit(content=msg, view=None) await interaction.edit(content=msg, view=None)
class VoteView(nextcord.ui.View): class VoteView(nextcord.ui.View):
""" """The button to press to start your nomination."""
The button to press to start your nomination.
"""
def __init__(self, config: DakinyConfig, guild: nextcord.Guild): def __init__(self, guild: nextcord.Guild):
super().__init__(timeout=None) super().__init__(timeout=None)
self.config = config
self.guild = guild self.guild = guild
@nextcord.ui.button( @nextcord.ui.button(label="Vote", style=nextcord.ButtonStyle.primary, custom_id="vote")
label="Vote", style=nextcord.ButtonStyle.primary, custom_id="vote" async def vote(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
)
async def vote(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
) -> None:
""" """
Enforces only members vote. Gets election cycle, creates voting options (checking if user has voted already), Enforces only members vote.
Gets election cycle, creates voting options (checking if user has voted already),
then sends the voting view. then sends the voting view.
Args: Args:
button (nextcord.ui.Button): Unused. idk why its needed in the decorator button (nextcord.ui.Button): Unused. idk why its needed in the decorator
interaction (nextcord.Interaction): Interaction of user who pressed the button interaction (nextcord.Interaction): Interaction of user who pressed the button
""" """
# enforce members only # enforce members only
if ( if self.guild.get_role(settings.role_ids.clan_member) not in interaction.user.roles:
self.guild.get_role(self.config.params.clan_member_role_id)
not in interaction.user.roles
):
await interaction.response.send_message( await interaction.response.send_message(
f"You must have the <@&{self.config.params.clan_member_role_id}> role to vote.", f"You must have the <@&{settings.role_ids.clan_member}> role to vote.",
ephemeral=True, ephemeral=True,
) )
return return
election: str = get_election_cycle_name(self.config) election_name: str = get_election_name()
with Session(engine) as session: with Session(engine) as session:
stmt = ( stmt = (
select(Votes.VotedForId) select(Votes.VotedForId)
.where(Votes.ElectionCycle == election) .where(Votes.ElectionName == election_name)
.where(Votes.VoterId == interaction.user.id) .where(Votes.VoterId == interaction.user.id)
.where(Votes.Type == VoteType.COUNCIL)
) )
voted_for = session.scalar(stmt) voted_for = session.scalar(stmt)
stmt = select(CouncilMembers.MemberId).where( stmt = select(CouncilMembers.MemberId).where(CouncilMembers.ElectionName == election_name)
CouncilMembers.ElectionCycle == election
)
options = [ options = [
SelectOption( SelectOption(
label=f"{self.guild.get_member(nominee).nick} ☑️" label=(
if voted_for == nominee f"{self.guild.get_member(nominee).display_name} ☑️"
else f"{self.guild.get_member(nominee).nick}", if voted_for == nominee
else f"{self.guild.get_member(nominee).display_name}"
),
value=str(nominee), value=str(nominee),
) )
for nominee in session.scalars(stmt) for nominee in sorted(
session.scalars(stmt),
key=lambda nominee: self.guild.get_member(nominee).display_name.lower(),
)
] ]
options.append( options.append(
SelectOption( SelectOption(
@@ -145,7 +137,7 @@ class VoteView(nextcord.ui.View):
) )
view = nextcord.ui.View() view = nextcord.ui.View()
view.add_item(VoteSelect(options=options, config=self.config)) view.add_item(VoteSelect(options))
await interaction.response.send_message( await interaction.response.send_message(
content="Please vote for a clan member!", content="Please vote for a clan member!",
+91
View File
@@ -0,0 +1,91 @@
"""File contains logic for creating a webhook listener, pulling from git, and updating all messages."""
from pathlib import Path
from typing import TYPE_CHECKING
import uvicorn
from fastapi import FastAPI
from git import Repo
from nextcord.ext import commands
from classes.gitea_webhook import GiteaWebhook
from DakinySettings import settings
from utils import REPO_DIR, edit_clan_council_info, update_clan_messages
if TYPE_CHECKING:
import nextcord
app = FastAPI()
REPO_URL = "https://gitea.cheezers.org/imcompiling/dakiny-messages.git"
class ClanMessages(commands.Cog, name="Clan Messages"):
"""
Cog to be loaded into the bot.
Used to be able to have a fastapi post listener and access to the bot at
the same time.
"""
def __init__(self, bot: commands.Bot):
self.bot = bot
self.repo_setup()
@commands.Cog.listener()
async def on_ready(self) -> None:
"""Triggers when the bot is in ready state. Adds the webhook listener to FastAPI and serves."""
app.add_api_route("/messages/", self.update_messages, methods=["POST"])
# Bind to all interfaces, don't know the docker interface ahead of time.
config = uvicorn.Config(app=app, host="0.0.0.0", port=5000, log_level="info") # noqa: S104
server = uvicorn.Server(config)
await server.serve()
async def update_messages(self, hook_info: GiteaWebhook) -> dict:
"""
Triggered by webhook.
Pulls from git and then calls method to update all the discord messages as described by the
files in the git repo.
"""
# Get all modified/added files
modified_files: list[str] = [
file for files in [commit.modified + commit.added for commit in hook_info.commits] for file in files
]
template_updates = {file.split("/")[1] for file in modified_files if "message_templates" in file}
channel_updates = {file.split("/")[1] for file in modified_files if file.split("/")[1] not in template_updates}
self.origin.pull()
guild: nextcord.Guild = self.bot.get_guild(settings.params.guild_id)
await update_clan_messages(guild, channel_updates)
# if the council members template has changed, update the message
if "council_members.txt" in template_updates:
await edit_clan_council_info(self.bot.get_guild(settings.params.guild_id))
return {"message": "Successfully updated all discord messages."}
def repo_setup(self) -> None:
"""Checks if the repo exists and creates repo object, clones if it does not."""
# check if repo has been cloned. open repo object
if Path(REPO_DIR).is_dir():
self.repo = Repo(REPO_DIR)
else:
self.repo = Repo.clone_from(REPO_URL, REPO_DIR)
self.origin = self.repo.remote("origin")
if settings.params.git_branch == "test":
self.repo.create_head("test", self.origin.refs.test).set_tracking_branch(self.origin.refs.test).checkout()
else:
self.repo.create_head("main", self.origin.refs.main).set_tracking_branch(self.origin.refs.main).checkout()
self.origin.pull()
# This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot.
def setup(bot: commands.Bot) -> None:
"""Adds function to bot."""
bot.add_cog(ClanMessages(bot))
+15 -10
View File
@@ -1,23 +1,28 @@
""" """Cog loader. Adds all command cogs to the bot."""
Cog loader. Adds all command cogs to the bot.
"""
from nextcord.ext import commands from nextcord.ext import commands
from cogs.commands.record import RecordCog
from cogs.commands.refresh import RefreshCog
from cogs.commands.remove import RemoveCog
from cogs.commands.request import RequestCog from cogs.commands.request import RequestCog
from cogs.commands.set import setCog from cogs.commands.set import SetCog
from cogs.commands.settings import SettingsCog
from cogs.commands.test import TestCog from cogs.commands.test import TestCog
from cogs.commands.time import TimeCog
from cogs.commands.view import ViewCog from cogs.commands.view import ViewCog
# This function will be called when this extension is loaded. # This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot. # It is necessary to add these functions to the bot.
def setup(bot: commands.Bot): def setup(bot: commands.Bot) -> None:
""" """Adds all command classes to bot."""
Adds all command classes to bot. bot.add_cog(RecordCog(bot))
""" bot.add_cog(RefreshCog(bot))
bot.add_cog(RemoveCog())
bot.add_cog(RequestCog(bot)) bot.add_cog(RequestCog(bot))
bot.add_cog(SetCog())
bot.add_cog(SettingsCog())
bot.add_cog(TestCog(bot)) bot.add_cog(TestCog(bot))
bot.add_cog(setCog()) bot.add_cog(TimeCog())
bot.add_cog(ViewCog()) bot.add_cog(ViewCog())
+358
View File
@@ -0,0 +1,358 @@
"""Contained commands and views for record commands."""
import nextcord
from discord import TextInputStyle
from nextcord.ext import commands
from DakinySettings import settings
from utils import GUILDS, MAX_DISCORD_MESSAGE_LENGTH, PointType, record_points
class RecordCog(commands.Cog, name="Record Commands"):
"""Class holds record commands."""
def __init__(self, bot: commands.Bot) -> None:
"""Default constructor for RecordCog class. Does nothing."""
super().__init__()
self.bot: commands.Bot = bot
@nextcord.slash_command(guild_ids=GUILDS)
async def record(self, interaction: nextcord.Interaction) -> None:
"""Parent slash command for record. Never called."""
@record.subcommand(description="Record an event and its participants.")
async def event(
self,
interaction: nextcord.Interaction,
name: str = nextcord.SlashOption(required=True, description="Name of the event that was hosted."),
host: nextcord.Member = nextcord.SlashOption(
required=False,
description="An alternative host if you are recording event for someone else.",
),
) -> None:
"""
Create and send recording event.
This command creates the base event recording embed and sends a message with it and the EventView
for gathering more information on the event.
Args:
interaction (nextcord.Interaction): interaction with this command.
name (str): Name of the event.
host (nextcord.Member, optional): Alternative host to user using command.
"""
# check role
if interaction.guild.get_role(settings.role_ids.event_host) not in interaction.user.roles:
await interaction.response.send_message(
f"You must have the <@&{settings.role_ids.event_host}> role to record an event.",
ephemeral=True,
)
return
if host is None:
host = interaction.user
embed: nextcord.Embed = nextcord.Embed(title=name, description="")
embed.add_field(name="Participants", value="")
embed.add_field(name="Non-Discord Participants", value="")
embed.add_field(name="Hosted by", value=f"<@{host.id}>", inline=False)
await interaction.response.send_message(
"Fill out event details and select all members who participated!",
view=EventView(name, host.id),
ephemeral=True,
embed=embed,
)
@record.subcommand(description="Record a miscellaneous point for a clan member.")
async def point(
self,
interaction: nextcord.Interaction,
activity: str = nextcord.SlashOption(
description="The type of activity to receive points for.",
required=True,
choices=[
"Clan chat activity",
"Teaching or sharing in-game tips",
"Recruiting efforts",
"Successful recruitment",
],
),
justification: str = nextcord.SlashOption(
required=True,
description="Reason to give the recipient a point.",
max_length=int(MAX_DISCORD_MESSAGE_LENGTH / 2),
),
recipient: nextcord.Member = nextcord.SlashOption(
required=False,
description="Discord user to give point to.",
),
non_discord_recipient: str = nextcord.SlashOption(
required=False, description="Non Discord user to give point to.", max_length=32
),
) -> None:
"""
Gives a point to the provided recipient. Sends recording message to event participation channel.
Only one recipient should be given point/provided to the command. Fails if not exactly one recipient
is given.
Args:
interaction (nextcord.Interaction): interaction with this command.
activity (str): Choice list of activities to give point for.
justification (str): Reason to give point.
recipient (nextcord.Member): Discord member to give point to.
non_discord_recipient (str): Non Discord member to give point to.
"""
# role check
if interaction.guild.get_role(settings.role_ids.clan_member) not in interaction.user.roles:
await interaction.response.send_message(
f"You must have the <@&{settings.role_ids.clan_member}> role to record points.",
ephemeral=True,
)
return
# pylint or whatever seems to think that there are lines of unreachable code. That is false.
if recipient and non_discord_recipient:
await interaction.response.send_message("Please only provide 1 recipient.", ephemeral=True)
return
elif recipient is not None: # noqa: RET505
record_points(
[str(recipient.id)],
settings.point_settings.clan_points,
PointType.CLAN,
is_member=True,
)
recipient_str = f"<@{recipient.id}>"
elif non_discord_recipient is not None:
record_points(
[non_discord_recipient],
settings.point_settings.clan_points,
PointType.CLAN,
is_member=False,
)
recipient_str = f'"{non_discord_recipient}"'
else:
await interaction.response.send_message("You must provide a recipient.", ephemeral=True)
return
event_channel: nextcord.TextChannel = interaction.guild.get_channel(settings.channel_ids.event_post)
msg = f"<@{interaction.user.id}> has given {recipient_str} points for the {activity} activity. Justification: {justification}" # noqa: E501
if not msg.endswith("."):
msg += "."
await event_channel.send(msg)
await interaction.response.send_message("Point has been added.", ephemeral=True)
class EventView(nextcord.ui.View):
"""
Class handles gathering information on an event.
Has buttons to edit the information, cancel, and submit.
Also contains a UserSelect object to pick discord members.
"""
def __init__(self, name: str, host_id: int):
super().__init__(timeout=60 * 60 * 2) # timeout 2 hours
self.name: str = name
self.host_id: int = host_id
self.event_participants = ParticipantSelect()
self.add_item(self.event_participants)
@nextcord.ui.button(label="Cancel", style=nextcord.ButtonStyle.red, row=1)
async def cancel(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
"""Cancels event submission. Removes embed and view."""
await interaction.response.edit_message("Canceled", ephemeral=True, embed=None, view=None)
self.stop()
@nextcord.ui.button(label="Edit", style=nextcord.ButtonStyle.blurple, row=1)
async def edit(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
"""Calls modal to update the text input information of the event."""
await interaction.response.send_modal(ParticipantModal(interaction.message.embeds[0]))
@nextcord.ui.button(label="Confirm", style=nextcord.ButtonStyle.green, row=1)
async def confirm(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
"""
Submits the event embed to the event channel.
Removes view and embed from recording message.
Sends point information to database.
"""
if settings.point_settings.verify_events:
verification_channel: nextcord.TextChannel = interaction.guild.get_channel(settings.channel_ids.admin)
verification_view = EventVerification(
self.event_participants.values.members, interaction.message.embeds[0].fields[1].value, self.host_id
)
await verification_channel.send(
content="Event record request.", embed=interaction.message.embeds[0], view=verification_view
)
submission_response = "Event has been submitted to admins for approval."
else:
process_event(
self.event_participants.values.members,
interaction.message.embeds[0].fields[1].value,
self.host_id,
)
event_channel: nextcord.TextChannel = interaction.guild.get_channel(settings.channel_ids.event_post)
await event_channel.send(embed=interaction.message.embeds[0])
submission_response = "Event has been submitted."
await interaction.response.edit_message(content=submission_response, view=None, embed=None)
self.stop()
class EventVerification(nextcord.ui.View):
"""Class contains code to approve/deny event submissions."""
def __init__(
self,
discord_users: list[nextcord.Member],
non_discord_users: str,
host_id: int,
):
super().__init__(timeout=None, auto_defer=True)
self.discord_users: list[nextcord.Member] = discord_users
self.non_discord_users: str = non_discord_users
self.host_id: int = host_id
@nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green)
async def approve(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
"""
Approve button for an event submission.
Calls the process event function. Records who approved the event.
Removes the view from the interaction message.
Args:
button (nextcord.ui.Button): Unused.
interaction (nextcord.Interaction): The interaction of the user clicking the button.
"""
process_event(self.discord_users, self.non_discord_users, self.host_id)
event_channel: nextcord.TextChannel = interaction.guild.get_channel(settings.channel_ids.event_post)
await event_channel.send(embed=interaction.message.embeds[0])
content = f"\nApproved by <@{interaction.user.id}>."
await interaction.response.edit_message(content=content, view=None)
@nextcord.ui.button(label="Deny", style=nextcord.ButtonStyle.red)
async def deny(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
"""
Deny button for an event submission. Just removes the view and logs who denied the request.
Args:
button (nextcord.ui.Button): Unused.
interaction (nextcord.Interaction): The interaction of the user clicking the button.
"""
content = f"\nDenied by <@{interaction.user.id}>."
await interaction.response.edit_message(content=content, view=None)
class ParticipantModal(nextcord.ui.Modal):
"""
This modal contains all the text edits for an event.
You can change the name, description, and add the usernames of participants who are not in the discord.
"""
# consider turning this embed into a dict
def __init__(self, embed: nextcord.Embed):
super().__init__(
"Edit the event.",
timeout=5 * 60,
)
self.embed: nextcord.Embed = embed
self.name: nextcord.ui.TextInput = nextcord.ui.TextInput(
required=True,
label="Event Name",
default_value=self.embed.title,
)
self.description: nextcord.ui.TextInput = nextcord.ui.TextInput(
required=False,
label="Event Description",
default_value=self.embed.description,
style=TextInputStyle.paragraph,
placeholder="Add a description if desired.",
max_length=1000,
)
self.participants: nextcord.ui.TextInput = nextcord.ui.TextInput(
required=False,
label="Non-Discord Usernames",
style=TextInputStyle.paragraph,
placeholder="Please separate usernames with a new line.",
default_value=self.embed.fields[1].value,
)
self.add_item(self.name)
self.add_item(self.description)
self.add_item(self.participants)
async def callback(self, interaction: nextcord.Interaction) -> None:
"""
Edits the interaction message to contain the Non-Discord participants.
Args:
interaction (nextcord.Interaction): Interaction of the callback.
"""
self.embed.title = self.name.value
self.embed.description = self.description.value
self.embed.set_field_at(index=1, name="Non-Discord Participants", value=self.participants.value)
await interaction.response.edit_message(embed=self.embed)
class ParticipantSelect(nextcord.ui.UserSelect):
"""A selection drop down for discord members. Updates the embed with all users selected."""
def __init__(self) -> None:
super().__init__(
min_values=1,
max_values=25,
placeholder="Select all participants",
row=0,
)
async def callback(self, interaction: nextcord.Interaction) -> None:
"""
Edits the interaction message to contain the Discord participants.
Args:
interaction (nextcord.Interaction): Interaction of the callback.
"""
embed: nextcord.Embed = interaction.message.embeds[0]
participants: str = "\n".join([f"<@{participant.id}>" for participant in self.values.members])
embed.set_field_at(0, name="Participants", value=participants)
await interaction.response.edit_message(embed=embed)
def process_event(discord_users: list[nextcord.Member], non_discord_users: str, host_id: int) -> None:
"""
Processes all points to give to participants and host for an event.
Args:
discord_users (list[nextcord.Member]): List of discord members that participated in event.
non_discord_users (str): Newline separated list of non discord members that participated in event.
host_id (int): Discord ID of host.
"""
if discord_users:
discord_users_list: list[str] = [str(user.id) for user in discord_users if user.id is not host_id]
record_points(
discord_users_list,
settings.point_settings.participate_points,
PointType.EVENT,
is_member=True,
)
if non_discord_users:
non_discord_users_list: list[str] = non_discord_users.split("\n")
record_points(
non_discord_users_list,
settings.point_settings.participate_points,
PointType.EVENT,
is_member=False,
)
record_points([host_id], settings.point_settings.host_points, PointType.EVENT, is_member=True)
+65
View File
@@ -0,0 +1,65 @@
"""Contained commands and views for refresh commands."""
import os
from pathlib import Path
import nextcord
from nextcord.ext import commands
from utils import MESSAGES_DIR, edit_clan_council_info, update_clan_messages
class RefreshCog(commands.Cog, name="Refresh Commands"):
"""Class holds refresh commands."""
def __init__(self, bot: commands.Bot) -> None:
"""Default constructor for RefreshCog class. Does nothing."""
super().__init__()
self.bot: commands.Bot = bot
@nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True))
async def refresh(self, interaction: nextcord.Interaction) -> None:
"""Parent slash command for refresh. Never called."""
@refresh.subcommand(description="Force refresh all of the bot managed messages.")
async def messages(
self,
interaction: nextcord.Interaction,
channel: nextcord.TextChannel = nextcord.SlashOption(
description="Channel to refresh messages in.",
required=False,
),
) -> None:
"""
Updates all messages.
Calls update_clan_messages() to update all messages.
Args:
interaction (nextcord.Interaction): Interaction with the command.
channel (nextcord.TextChannel): channel to refresh messages in.
"""
await interaction.response.defer(ephemeral=True)
if channel is None:
channel_updates = [
dir_name for dir_name in os.listdir(MESSAGES_DIR) if Path(f"{MESSAGES_DIR}/{dir_name}").is_dir()
]
else:
channel_updates = [channel.name]
await update_clan_messages(interaction.guild, channel_updates)
await interaction.followup.send("Messages have been refreshed.", ephemeral=True)
@refresh.subcommand(description="Updates the current council info message.")
async def council_info(self, interaction: nextcord.Interaction) -> None:
"""
Edits the clan council info message.
Calls edit_clan_council_info().
Args:
interaction (nextcord.Interaction): Interaction with the command.
"""
await edit_clan_council_info(interaction.guild)
await interaction.response.send_message("Clan Council Info message updated.", ephemeral=True)
+67
View File
@@ -0,0 +1,67 @@
"""Commands for removing information in the database."""
import nextcord
from nextcord.ext import commands
from sqlalchemy import delete, select
from sqlalchemy.orm import Session
from database_setup.sql_data_classes import Presences, engine
class RemoveCog(commands.Cog, name="Remove Commands"):
"""Holds the view commands."""
def __init__(self) -> None:
"""Default constructor for RemoveCog class. Does nothing."""
@nextcord.slash_command(
default_member_permissions=nextcord.Permissions(administrator=True),
)
async def remove(self, interaction: nextcord.Interaction) -> None:
"""Parent slash command for set. Never called."""
@remove.subcommand(description="Remove a presence from the database.")
async def presence(
self,
interaction: nextcord.Interaction,
presence_str: str = nextcord.SlashOption(description="The presence to remove", required=True),
) -> None:
"""
Deletes the given presence id from the database.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
presence_str (str): Is SlashOption. The presence to remove. Is the ID of the presence.
"""
presence_id = int(presence_str)
with Session(engine) as session:
stmt = delete(Presences).where(Presences.Id == presence_id)
session.execute(stmt)
session.commit()
await interaction.response.send_message("Presence deleted.", ephemeral=True)
@presence.on_autocomplete("presence_str")
async def presence_autocomplete(self, interaction: nextcord.Interaction, presence_str: str) -> None:
"""
Collects all presences that start with the presence string.
Sends autocomplete dict {Presence Name, Presence Id}
Args:
interaction (nextcord.Interaction): Interaction with command.
presence_str (str): String to run autocomplete on.
"""
with Session(engine) as session:
stmt = select(Presences)
presences_dict: dict[str,str] = {pres.Presence: str(pres.Id) for pres in session.scalars(stmt)}
# if empty compare string
if not presence_str:
await interaction.response.send_autocomplete(dict(list(presences_dict.items())[:25]))
return
autofilled_presences: dict[str,str] = {
pres: pres_id for pres, pres_id in presences_dict.items() if presence_str.lower() in pres.lower()
}
await interaction.response.send_autocomplete(autofilled_presences[:25])
+93 -28
View File
@@ -1,67 +1,89 @@
""" """Contains commands and views for request commands."""
Contains commands and views for request commands.
"""
import nextcord import nextcord
from nextcord.ext import commands from nextcord.ext import commands
from sqlalchemy.orm import Session
from config import DakinyConfig from DakinySettings import settings
from database_setup.sql_data_classes import Presences, engine
class RequestCog(commands.Cog, name="Request Commands"): class RequestCog(commands.Cog, name="Request Commands"):
""" """Class holds request commands."""
Class holds request commands.
"""
def __init__(self, bot) -> None: def __init__(self, bot: commands.Bot) -> None:
""" """Default constructor for RequestCog class. Does nothing."""
Default constructor for RequestCog class. Does nothing.
"""
self.bot: commands.Bot = bot self.bot: commands.Bot = bot
@nextcord.slash_command() @nextcord.slash_command()
async def request(self, interaction: nextcord.Interaction): async def request(self, interaction: nextcord.Interaction) -> None:
"""Parent slash command for request. Never called."""
@request.subcommand(description="Request to add a presence to the bot's presence list.")
async def add_presence(
self,
interaction: nextcord.Interaction,
presence: str = nextcord.SlashOption(
required=True,
min_length=1,
max_length=100,
description="The presence you want to add.",
),
) -> None:
""" """
Parent slash command for help. Never called. Sends a message to admins to request to add a presence.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
presence (str): The presence to add.
""" """
content = f'<@{interaction.user.id}> would like to add presence "{presence}."'
view = PresenceChange(presence)
await self.bot.get_channel(settings.channel_ids.admin).send(content=content, view=view)
await interaction.response.send_message("Request sent to admins.", ephemeral=True)
@request.subcommand(description="Request to change your Dakiny nickname") @request.subcommand(description="Request to change your Dakiny nickname")
async def change_nickname( async def change_nickname(
self, self,
interaction: nextcord.Interaction, interaction: nextcord.Interaction,
new_nickname: str = nextcord.SlashOption( new_nickname: str = nextcord.SlashOption(
required=True, min_length=2, max_length=32, description="Your requested new nickname." required=True,
min_length=2,
max_length=32,
description="Your requested new nickname.",
), ),
): ) -> None:
""" """
Sends a message to admins to request a nickname change. Sends a message to admins to request a nickname change.
Args: Args:
interaction (nextcord.Interaction): Interaction that invoked the command. interaction (nextcord.Interaction): Interaction that invoked the command.
new_username(str): The username to change to. new_nickname(str): The username to change to.
""" """
config: DakinyConfig = self.bot.config
content = f'<@{interaction.user.id}> would like to change their username to "{new_nickname}."' content = f'<@{interaction.user.id}> would like to change their username to "{new_nickname}."'
view = UsernameRequest(interaction.user.id, new_nickname) view = UsernameRequest(interaction.user.id, new_nickname)
await self.bot.get_channel(config.params.admin_channel_id).send(content=content, view=view) await self.bot.get_channel(settings.channel_ids.admin).send(content=content, view=view)
await interaction.response.send_message("Request sent to admins.", ephemeral=True) await interaction.response.send_message("Request sent to admins.", ephemeral=True)
class UsernameRequest(nextcord.ui.View): class UsernameRequest(nextcord.ui.View):
""" """View provides approve and deny buttons and action for a username change request."""
View provides approve and deny buttons and action for a username change request.
"""
def __init__(self, user_id: int, new_nickname: str, *, timeout: float | None = 180, auto_defer: bool = True): def __init__(
super().__init__(timeout=timeout, auto_defer=auto_defer) self,
user_id: int,
new_nickname: str,
):
super().__init__(timeout=None, auto_defer=True)
self.user_id = user_id self.user_id = user_id
self.new_nickname = new_nickname self.new_nickname = new_nickname
@nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green) @nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green)
async def approve(self, button: nextcord.ui.Button, interaction: nextcord.Interaction): async def approve(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
""" """
Approve button for a nickname change request. Changes the target's nickname to self.new_nickname. Approve button for a nickname change request.
Changes the target's nickname to self.new_nickname.
Catches an error if the target as a higher role than the bot. Catches an error if the target as a higher role than the bot.
Removes the view from the interaction message. Removes the view from the interaction message.
@@ -87,11 +109,12 @@ class UsernameRequest(nextcord.ui.View):
await interaction.followup.edit_message(interaction.message.id, content=content, view=None) await interaction.followup.edit_message(interaction.message.id, content=content, view=None)
else: else:
await interaction.response.edit_message( await interaction.response.edit_message(
content="An error occured while changing the user's nickname.", view=None content="An error occurred while changing the user's nickname.",
view=None,
) )
@nextcord.ui.button(label="Deny", style=nextcord.ButtonStyle.red) @nextcord.ui.button(label="Deny", style=nextcord.ButtonStyle.red)
async def deny(self, button: nextcord.ui.Button, interaction: nextcord.Interaction): async def deny(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
""" """
Deny button for a nickname request. Just removes the view and logs who denied the request. Deny button for a nickname request. Just removes the view and logs who denied the request.
@@ -101,3 +124,45 @@ class UsernameRequest(nextcord.ui.View):
""" """
content = interaction.message.content + f"\nDenied by <@{interaction.user.id}>." content = interaction.message.content + f"\nDenied by <@{interaction.user.id}>."
await interaction.response.edit_message(content=content, view=None) await interaction.response.edit_message(content=content, view=None)
class PresenceChange(nextcord.ui.View):
"""View provides approve and deny buttons and action for a username change request."""
def __init__(
self,
presence: str,
):
super().__init__(timeout=None, auto_defer=True)
self.presence = presence
@nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green)
async def approve(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
"""
Approve button for a presence add request.
Adds the presence to the database.
Removes the view from the interaction message.
Args:
button (nextcord.ui.Button): Unused.
interaction (nextcord.Interaction): The interaction of the user clicking the button.
"""
with Session(engine) as session:
session.add(Presences(Presence=self.presence))
session.commit()
content = interaction.message.content + f"\nApproved by <@{interaction.user.id}>."
await interaction.response.edit_message(content=content, view=None)
@nextcord.ui.button(label="Deny", style=nextcord.ButtonStyle.red)
async def deny(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: # noqa: ARG002
"""
Deny button for a presence add request. Just removes the view and logs who denied the request.
Args:
button (nextcord.ui.Button): Unused.
interaction (nextcord.Interaction): The interaction of the user clicking the button.
"""
content = interaction.message.content + f"\nDenied by <@{interaction.user.id}>."
await interaction.response.edit_message(content=content, view=None)
+11 -20
View File
@@ -1,8 +1,6 @@
""" """Contains commands and views for set commands."""
Contains commands and views for set commands.
"""
from datetime import datetime from datetime import UTC, datetime
import nextcord import nextcord
from nextcord.ext import commands from nextcord.ext import commands
@@ -10,25 +8,19 @@ from sqlalchemy import delete, select
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from dakiny_bot import birthday_list
from database_setup.sql_data_classes import UserBirthday, engine from database_setup.sql_data_classes import UserBirthday, engine
from main import birthday_list
class setCog(commands.Cog, name="Set Commands"): class SetCog(commands.Cog, name="Set Commands"):
""" """Class holds set commands."""
Class holds set commands.
"""
def __init__(self) -> None: def __init__(self) -> None:
""" """Default constructor for SetCog class. Does nothing."""
Default constructor for SetCog class. Does nothing.
"""
@nextcord.slash_command() @nextcord.slash_command()
async def set(self, interaction: nextcord.Interaction): async def set(self, interaction: nextcord.Interaction) -> None:
""" """Parent slash command for set. Never called."""
Parent slash command for set. Never called.
"""
@set.subcommand(description="Sets your birth month and day for the Birthday Fun. Use 0/0 to remove yourself.") @set.subcommand(description="Sets your birth month and day for the Birthday Fun. Use 0/0 to remove yourself.")
async def birthday( async def birthday(
@@ -36,7 +28,7 @@ class setCog(commands.Cog, name="Set Commands"):
interaction: nextcord.Interaction, interaction: nextcord.Interaction,
month: int = nextcord.SlashOption(required=True, min_value=0, max_value=12, description="Your birth month."), month: int = nextcord.SlashOption(required=True, min_value=0, max_value=12, description="Your birth month."),
day: int = nextcord.SlashOption(required=True, min_value=0, max_value=31, description="Your birth day."), day: int = nextcord.SlashOption(required=True, min_value=0, max_value=31, description="Your birth day."),
): ) -> None:
""" """
Sets a user's birthday in the database. Removes on input 0/0. If birthday is today, adds to the birthday_list. Sets a user's birthday in the database. Removes on input 0/0. If birthday is today, adds to the birthday_list.
@@ -45,7 +37,6 @@ class setCog(commands.Cog, name="Set Commands"):
month (int, optional): The month a user was born. Is SlashOption. month (int, optional): The month a user was born. Is SlashOption.
day (int, optional): The day a user was born. Is SlashOption. day (int, optional): The day a user was born. Is SlashOption.
""" """
# Must be deferred, apparently datetime takes a little bit on invalid dates. # Must be deferred, apparently datetime takes a little bit on invalid dates.
await interaction.response.defer(ephemeral=True) await interaction.response.defer(ephemeral=True)
@@ -58,9 +49,9 @@ class setCog(commands.Cog, name="Set Commands"):
return return
# check if valid day # check if valid day
today = datetime.today() today = datetime.now(tz=UTC)
try: try:
birthday = datetime(today.year, month, day) birthday = datetime(today.year, month, day, tzinfo=UTC)
except ValueError: except ValueError:
await interaction.followup.send(f"Invalid date: {month}/{day}.", ephemeral=True) await interaction.followup.send(f"Invalid date: {month}/{day}.", ephemeral=True)
return return
+379
View File
@@ -0,0 +1,379 @@
"""Contains commands and views for settings commands."""
import nextcord
from nextcord.ext import commands
from DakinySettings import settings
from utils import GUILDS
class SettingsCog(commands.Cog, name="Settings Commands"):
"""Class holds settings commands."""
def __init__(self) -> None:
"""Default constructor for SettingsCog class. Does nothing."""
@nextcord.slash_command(guild_ids=GUILDS, default_member_permissions=nextcord.Permissions(administrator=True))
async def settings(self, interaction: nextcord.Interaction) -> None:
"""Parent slash command for settings. Never called."""
@settings.subcommand()
async def channel_ids(self, interaction: nextcord.Interaction) -> None:
"""Subcommand parent. Never called."""
@channel_ids.subcommand(description="Sets channel that bot sends verification requests to admins.")
async def admin(
self,
interaction: nextcord.Interaction,
channel: nextcord.TextChannel = nextcord.SlashOption(
required=False, description="Channel to set to. If left blank, uses current channel."
),
) -> None:
"""
Sets DakinySettings channel_ids.admin value to given channel id or current channel id.
Args:
interaction (nextcord.Interaction): Discord interaction of command.
channel (nextcord.TextChannel, optional): Channel to set in settings.
"""
channel_id = interaction.channel_id if channel is None else channel.id
settings.channel_ids.admin = channel_id
settings.save()
await interaction.response.send_message(f"Admin verification channel set to <#{channel_id}>.", ephemeral=True)
@channel_ids.subcommand(description="Sets announcement channel.")
async def announcement(
self,
interaction: nextcord.Interaction,
channel: nextcord.TextChannel = nextcord.SlashOption(
required=False, description="Channel to set to. If left blank, uses current channel."
),
) -> None:
"""
Sets DakinySettings channel_ids.announcement value to given channel id or current channel id.
Args:
interaction (nextcord.Interaction): Discord interaction of command.
channel (nextcord.TextChannel, optional): Channel to set in settings.
"""
channel_id = interaction.channel_id if channel is None else channel.id
settings.channel_ids.announcement = channel_id
settings.save()
await interaction.response.send_message(f"Announcement channel set to <#{channel_id}>.", ephemeral=True)
@channel_ids.subcommand(description="Sets clan info channel. Channel used for tracking current council.")
async def clan_info(
self,
interaction: nextcord.Interaction,
channel: nextcord.TextChannel = nextcord.SlashOption(
required=False, description="Channel to set to. If left blank, uses current channel."
),
) -> None:
"""
Sets DakinySettings channel_ids.clan_info value to given channel id or current channel id.
Args:
interaction (nextcord.Interaction): Discord interaction of command.
channel (nextcord.TextChannel, optional): Channel to set in settings.
"""
channel_id = interaction.channel_id if channel is None else channel.id
settings.channel_ids.clan_info = channel_id
settings.save()
await interaction.response.send_message(f"Clan information channel set to <#{channel_id}>.", ephemeral=True)
@channel_ids.subcommand(
description="Sets event post channel. Channel is where event tracking information is posted."
)
async def event_post(
self,
interaction: nextcord.Interaction,
channel: nextcord.TextChannel = nextcord.SlashOption(
required=False, description="Channel to set to. If left blank, uses current channel."
),
) -> None:
"""
Sets DakinySettings channel_ids.event_post value to given channel id or current channel id.
Args:
interaction (nextcord.Interaction): Discord interaction of command.
channel (nextcord.TextChannel, optional): Channel to set in settings.
"""
channel_id = interaction.channel_id if channel is None else channel.id
settings.channel_ids.event_post = channel_id
settings.save()
await interaction.response.send_message(f"Event posting channel set to <#{channel_id}>.", ephemeral=True)
@channel_ids.subcommand(description="Sets general channel.")
async def general(
self,
interaction: nextcord.Interaction,
channel: nextcord.TextChannel = nextcord.SlashOption(
required=False, description="Channel to set to. If left blank, uses current channel."
),
) -> None:
"""
Sets DakinySettings channel_ids.general value to given channel id or current channel id.
Args:
interaction (nextcord.Interaction): Discord interaction of command.
channel (nextcord.TextChannel, optional): Channel to set in settings.
"""
channel_id = interaction.channel_id if channel is None else channel.id
settings.channel_ids.general = channel_id
settings.save()
await interaction.response.send_message(f"General channel set to <#{channel_id}>.", ephemeral=True)
@channel_ids.subcommand(description="Sets voting channel. Channel used as parent for voting threads.")
async def voting(
self,
interaction: nextcord.Interaction,
channel: nextcord.TextChannel = nextcord.SlashOption(
required=False, description="Channel to set to. If left blank, uses current channel."
),
) -> None:
"""
Sets DakinySettings channel_ids.voting value to given channel id or current channel id.
Args:
interaction (nextcord.Interaction): Discord interaction of command.
channel (nextcord.TextChannel, optional): Channel to set in settings.
"""
channel_id = interaction.channel_id if channel is None else channel.id
settings.channel_ids.voting = channel_id
settings.save()
await interaction.response.send_message(f"Voting channel set to <#{channel_id}>.", ephemeral=True)
@settings.subcommand()
async def point_settings(self, interaction: nextcord.Interaction) -> None:
"""Subcommand parent. Never called."""
@point_settings.subcommand(description="Sets Points value.")
async def clan_points(
self,
interaction: nextcord.Interaction,
points: int = nextcord.SlashOption(
required=True, description="Number of Clan Points the recipient earns.", min_value=0
),
) -> None:
"""
Sets DakinySettings point_settings.clan_points to the given number of points.
Args:
interaction (nextcord.Interaction): Discord interaction of command.
points (int): Number of points to assign to PointSettings.points.
"""
settings.point_settings.clan_points = points
settings.save()
await interaction.response.send_message(f"Points set to {points}.", ephemeral=True)
@point_settings.subcommand(description="Sets Event Weekly Limit value for Clan Points.")
async def clan_points_weekly_limit(
self,
interaction: nextcord.Interaction,
points: int = nextcord.SlashOption(
required=True, description="Number of Clan Points a clan member can earn in a week.", min_value=0
),
) -> None:
"""
Sets DakinySettings point_settings.clan_points_weekly_limit to the given number of points.
Args:
interaction (nextcord.Interaction): Discord interaction of command.
points (int): Number of points to set.
"""
settings.point_settings.clan_points_weekly_limit = points
settings.save()
await interaction.response.send_message(f"Clan Points Weekly Limit set to {points}.", ephemeral=True)
@point_settings.subcommand(description="Sets Event Weekly Limit value.")
async def events_weekly_limit(
self,
interaction: nextcord.Interaction,
points: int = nextcord.SlashOption(
required=True, description="Number of Event Points a clan member can earn in a week.", min_value=0
),
) -> None:
"""
Sets DakinySettings point_settings.events_weekly_limit to the given number of points.
Args:
interaction (nextcord.Interaction): Discord interaction of command.
points (int): Number of points to set.
"""
settings.point_settings.events_weekly_limit = points
settings.save()
await interaction.response.send_message(f"Event Weekly Limit set to {points}.", ephemeral=True)
@point_settings.subcommand(description="Sets Event Host Points value.")
async def host_points(
self,
interaction: nextcord.Interaction,
points: int = nextcord.SlashOption(
required=True, description="Number of Event Points a host earns when hosting event.", min_value=0
),
) -> None:
"""
Sets DakinySettings point_settings.host_points to the given number of points.
Args:
interaction (nextcord.Interaction): Discord interaction of command.
points (int): Number of points to set.
"""
settings.point_settings.host_points = points
settings.save()
await interaction.response.send_message(f"Event Host Points set to {points}.", ephemeral=True)
@point_settings.subcommand(description="Sets Event Participation Points value.")
async def participate_points(
self,
interaction: nextcord.Interaction,
points: int = nextcord.SlashOption(
required=True,
description="Number of Event Points a participant earns when attending an event.",
min_value=0,
),
) -> None:
"""
Sets DakinySettings point_settings.participate_points to the given number of points.
Args:
interaction (nextcord.Interaction): Discord interaction of command.
points (int): Number of points to set.
"""
settings.point_settings.participate_points = points
settings.save()
await interaction.response.send_message(f"Event Participation Points set to {points}.", ephemeral=True)
@point_settings.subcommand(description="Sets whether to verify a clan point entry.")
async def verify_clan_points(
self,
interaction: nextcord.Interaction,
do_verification: bool = nextcord.SlashOption( # noqa: FBT001
required=True,
description="Whether an event should be verified or not.",
),
) -> None:
"""
Sets DakinySettings point_settings.verify_clan_points to the given number of points.
Args:
interaction (nextcord.Interaction): Discord interaction of command.
do_verification (bool): If the bot should verify events.
"""
settings.point_settings.verify_clan_points = do_verification
settings.save()
await interaction.response.send_message(f"Event verification set to {do_verification}.", ephemeral=True)
@point_settings.subcommand(description="Sets whether an event.")
async def verify_events(
self,
interaction: nextcord.Interaction,
do_verification: bool = nextcord.SlashOption( # noqa: FBT001
required=True,
description="Whether an event should be verified or not.",
),
) -> None:
"""
Sets DakinySettings point_settings.verify_events to the given number of points.
Args:
interaction (nextcord.Interaction): Discord interaction of command.
do_verification (bool): If the bot should verify events.
"""
settings.point_settings.verify_events = do_verification
settings.save()
await interaction.response.send_message(f"Event verification set to {do_verification}.", ephemeral=True)
@settings.subcommand()
async def role_ids(self, interaction: nextcord.Interaction) -> None:
"""Subcommand parent. Never called."""
@role_ids.subcommand(description="Sets Clan Council role id.")
async def clan_council(
self,
interaction: nextcord.Interaction,
role: nextcord.Role = nextcord.SlashOption(required=True, description="Role to set."),
) -> None:
"""
Sets DakinySettings role_ids.clan_council to the given role id.
Args:
interaction (nextcord.Interaction): Discord interaction of command.
role (nextcord.TextChannel): Role to set in settings.
"""
role_id = role.id
settings.role_ids.clan_council = role_id
settings.save()
await interaction.response.send_message(f"Clan Council role set to <@&{role_id}>.", ephemeral=True)
@role_ids.subcommand(description="Sets Clan Council Elect role id.")
async def clan_council_elect(
self,
interaction: nextcord.Interaction,
role: nextcord.Role = nextcord.SlashOption(required=True, description="Role to set."),
) -> None:
"""
Sets DakinySettings role_ids.clan_council_elect to the given role id.
Args:
interaction (nextcord.Interaction): Discord interaction of command.
role (nextcord.TextChannel): Role to set in settings.
"""
role_id = role.id
settings.role_ids.clan_council_elect = role_id
settings.save()
await interaction.response.send_message(f"Clan Council Elect role set to <@&{role_id}>.", ephemeral=True)
@role_ids.subcommand(description="Sets Clan Member role id.")
async def clan_member(
self,
interaction: nextcord.Interaction,
role: nextcord.Role = nextcord.SlashOption(required=True, description="Role to set."),
) -> None:
"""
Sets DakinySettings role_ids.clan_member to the given role id.
Args:
interaction (nextcord.Interaction): Discord interaction of command.
role (nextcord.TextChannel): Role to set in settings.
"""
role_id = role.id
settings.role_ids.clan_member = role_id
settings.save()
await interaction.response.send_message(f"Clan Member role set to <@&{role_id}>.", ephemeral=True)
@role_ids.subcommand(description="Sets Event Host role id.")
async def event_host(
self,
interaction: nextcord.Interaction,
role: nextcord.Role = nextcord.SlashOption(required=True, description="Role to set."),
) -> None:
"""
Sets DakinySettings role_ids.event_host to the given role id.
Args:
interaction (nextcord.Interaction): Discord interaction of command.
role (nextcord.TextChannel): Role to set in settings.
"""
role_id = role.id
settings.role_ids.event_host = role_id
settings.save()
await interaction.response.send_message(f"Event Host role set to <@&{role_id}>.", ephemeral=True)
@role_ids.subcommand(description="Sets High Council role id.")
async def high_council(
self,
interaction: nextcord.Interaction,
role: nextcord.Role = nextcord.SlashOption(required=True, description="Role to set."),
) -> None:
"""
Sets DakinySettings role_ids.high_council to the given role id.
Args:
interaction (nextcord.Interaction): Discord interaction of command.
role (nextcord.TextChannel): Role to set in settings.
"""
role_id = role.id
settings.role_ids.high_council = role_id
settings.save()
await interaction.response.send_message(f"High Council role set to <@&{role_id}>.", ephemeral=True)
+104 -87
View File
@@ -1,138 +1,155 @@
""" """Commands for testing."""
Commands for testing.
"""
import nextcord import nextcord
from nextcord.ext import application_checks, commands from nextcord.ext import application_checks, commands
from classes.election_cycle import ElectionCycle from classes.election import Election
from config import DakinyConfig from DakinySettings import settings
from utils import get_current_cycle from utils import GUILDS, get_active_election_cycle, get_quarter_name
ME = 206602713511886849
def is_me(): # noqa: ANN201
"""Predicate that makes sure only I can run the test commands."""
def predicate(interaction: nextcord.Interaction) -> bool:
return interaction.user.id == ME
return application_checks.check(predicate)
class TestCog(commands.Cog, name="Test commands."): class TestCog(commands.Cog, name="Test commands."):
""" """Class holds commands for testing."""
Class holds commands for testing.
"""
def __init__(self, bot) -> None: def __init__(self, bot: commands.Bot) -> None:
""" """Default constructor for HelpCog class. Does nothing."""
Default constructor for HelpCog class. Does nothing. self.bot: commands.Bot = bot
"""
self.bot = bot
def is_me(): @nextcord.slash_command(guild_ids=GUILDS)
def predicate(interaction: nextcord.Interaction): async def test(self, interaction: nextcord.Interaction) -> None:
return interaction.user.id == 206602713511886849 """Parent slash command for help. Never called."""
return application_checks.check(predicate)
@nextcord.slash_command()
@is_me()
async def test(self, interaction: nextcord.Interaction):
"""
Parent slash command for help. Never called.
"""
@test.subcommand() @test.subcommand()
@is_me() @is_me()
async def msg_builder(self, interaction: nextcord.Interaction): async def election_start(self, interaction: nextcord.Interaction) -> None:
""" """
Tests the message builder. Force starts election.
Args: Args:
interaction (nextcord.Interaction): Interaction that invoked the command. interaction (nextcord.Interaction): Interaction with the message.
""" """
cycle = Election(
cycle = ElectionCycle(0, self.bot) get_active_election_cycle(),
self.bot.get_guild(settings.params.guild_id),
message = cycle.build_election_announcement()
# doesn't tell discord that the message was responded to. Is a test command so I don't really care atm.
for m in message:
await interaction.channel.send(content=m)
@test.subcommand()
@is_me()
async def election_start(self, interaction: nextcord.Interaction):
"""
idc
Args:
interaction (nextcord.Interaction): _description_
"""
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(
get_current_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
) )
await cycle.election_start_action() await cycle.election_start_action()
await interaction.send("Test command sent.", ephemeral=True)
@test.subcommand() @test.subcommand()
@is_me() @is_me()
async def voting_start(self, interaction: nextcord.Interaction): async def voting_start(self, interaction: nextcord.Interaction) -> None:
""" """
tests the voting start action Force starts voting.
Args: Args:
interaction (nextcord.Interaction): _description_ interaction (nextcord.Interaction): Interaction with the message.
""" """
config: DakinyConfig = self.bot.config cycle = Election(
cycle = ElectionCycle( get_active_election_cycle(),
get_current_cycle(config), self.bot.get_guild(settings.params.guild_id),
config,
self.bot.get_guild(config.params.guild_id),
) )
await cycle.voting_start_action() await cycle.voting_start_action()
await interaction.send("Test command sent.", ephemeral=True)
@test.subcommand() @test.subcommand()
@is_me() @is_me()
async def voting_end(self, interaction: nextcord.Interaction): async def voting_end(self, interaction: nextcord.Interaction) -> None:
""" """
tests the voting start action Force ends voting.
Args: Args:
interaction (nextcord.Interaction): _description_ interaction (nextcord.Interaction): Interaction with the message.
""" """
config: DakinyConfig = self.bot.config cycle = Election(
cycle = ElectionCycle( get_active_election_cycle(),
get_current_cycle(config), self.bot.get_guild(settings.params.guild_id),
config,
self.bot.get_guild(config.params.guild_id),
) )
await cycle.voting_end_action() await cycle.voting_end_action()
await interaction.send("Test command sent.", ephemeral=True)
@test.subcommand() @test.subcommand()
@is_me() @is_me()
async def election_reminder(self, interaction: nextcord.Interaction): async def highcouncil_vote(self, interaction: nextcord.Interaction) -> None:
config: DakinyConfig = self.bot.config """
cycle = ElectionCycle( Force starts highcouncil voting.
get_current_cycle(config),
config, Args:
self.bot.get_guild(config.params.guild_id), interaction (nextcord.Interaction): Interaction with the message.
"""
cycle = Election(
get_active_election_cycle(),
self.bot.get_guild(settings.params.guild_id),
)
await cycle.highcouncil_election_start_action()
await interaction.send("Test command sent.", ephemeral=True)
@test.subcommand()
@is_me()
async def election_reminder(self, interaction: nextcord.Interaction) -> None:
"""
Forces election reminder.
Args:
interaction (nextcord.Interaction): Interaction with the message.
"""
cycle = Election(
get_active_election_cycle(),
self.bot.get_guild(settings.params.guild_id),
) )
await cycle.election_reminder() await cycle.election_reminder()
await interaction.send("Test command sent.", ephemeral=True)
@test.subcommand() @test.subcommand()
@is_me() @is_me()
async def daily(self, interaction: nextcord.Interaction): async def daily(self, interaction: nextcord.Interaction) -> None:
config: DakinyConfig = self.bot.config """
cycle = ElectionCycle( Forces daily function.
get_current_cycle(config),
config, Args:
self.bot.get_guild(config.params.guild_id), interaction (nextcord.Interaction): Interaction with the message.
"""
cycle = Election(
get_active_election_cycle(),
self.bot.get_guild(settings.params.guild_id),
) )
await cycle.check_date() await cycle.check_date()
await interaction.send("Test command sent.", ephemeral=True)
@test.subcommand() @test.subcommand()
@is_me() @is_me()
async def term_start(self, interaction: nextcord.Interaction): async def term_start(self, interaction: nextcord.Interaction) -> None:
config: DakinyConfig = self.bot.config """
cycle = ElectionCycle( Forces term start.
get_current_cycle(config),
config, Args:
self.bot.get_guild(config.params.guild_id), interaction (nextcord.Interaction): Interaction with the message.
"""
cycle = Election(
get_active_election_cycle(),
self.bot.get_guild(settings.params.guild_id),
) )
await cycle.term_start_action() await cycle.term_start_action()
await interaction.send("Test command sent.", ephemeral=True)
@test.subcommand()
@is_me()
async def get_quarter_name(self, interaction: nextcord.Interaction) -> None:
"""
Tests getting the quarter name.
Args:
interaction (nextcord.Interaction): Interaction with the message.
"""
name = get_quarter_name()
await interaction.send(name)
+109
View File
@@ -0,0 +1,109 @@
"""Contains commands for time commands."""
from datetime import MAXYEAR, MINYEAR, UTC, datetime
import nextcord
from nextcord.ext import commands
from utils import GUILDS
class TimeCog(commands.Cog, name="Time Commands"):
"""Class holds time commands."""
@nextcord.slash_command(guild_ids=GUILDS)
async def time(self, interaction: nextcord.Interaction) -> None:
"""Parent slash command. Never called."""
@time.subcommand(
description="Get a discord timestamp of the given game time. If date information is left blank, uses current day." # noqa: E501
)
async def at( # noqa: PLR0913
self,
interaction: nextcord.Interaction,
hour: int = nextcord.SlashOption(
description="The hour you want a timestamp of. Given in gametime/UTC.",
min_value=0,
max_value=23,
required=True,
),
minute: int = nextcord.SlashOption(
description="The minute you want a timestamp of. Given in gametime/UTC.",
min_value=0,
max_value=59,
required=True,
),
year: int = nextcord.SlashOption(
description="Year to make timestamp of.", required=False, min_value=MINYEAR, max_value=MAXYEAR
),
month: int = nextcord.SlashOption(
description="Month to make timestamp of.", required=False, min_value=1, max_value=12
),
day: int = nextcord.SlashOption(
description="Day to make timestamp of.", required=False, min_value=1, max_value=31
),
ephemeral: bool = nextcord.SlashOption( # noqa: FBT001
description="Whether the replying message will be public or not.", required=False, default=True
),
) -> None:
"""
Replies with a discord timestamp of given UTC time.
Args:
interaction (nextcord.Interaction): Interaction with the command.
hour (int): The hour in UTC to make the timestamp of..
minute (int): The minute in UTC to make the timestamp of..
year (int): The year to make the timestamp of.
month (int): The month to make the timestamp of.
day (int): The day to make the timestamp of.
ephemeral (bool): Whether the response is ephemeral or not.
"""
gametime_today: datetime = datetime.now(tz=UTC).replace(hour=hour, minute=minute)
if year is not None:
gametime_today = gametime_today.replace(year=year)
if month is not None and day is not None:
try:
gametime_today = gametime_today.replace(month=month, day=day)
except ValueError:
await interaction.response.send_message(f"Day {day} is out of range for month {month}.", ephemeral=True)
return
else:
if month is not None:
try:
gametime_today = gametime_today.replace(month=month)
except ValueError:
await interaction.response.send_message(
f"Today ({gametime_today.day}) is out of range for month {month}.", ephemeral=True
)
return
if day is not None:
try:
gametime_today = gametime_today.replace(day=day)
except ValueError:
await interaction.response.send_message(
f"Day {day} is out of range for this month {gametime_today.month}.", ephemeral=True
)
return
await interaction.response.send_message(content=f"<t:{int(gametime_today.timestamp())}:t>", ephemeral=ephemeral)
@time.subcommand(description="Get the current time in game.")
async def gametime(
self,
interaction: nextcord.Interaction,
ephemeral: bool = nextcord.SlashOption( # noqa: FBT001
description="Whether the replying message will be public or not.", required=False, default=True
),
) -> None:
"""
Replies with the current time in game.
Args:
interaction (nextcord.Interaction): Interaction with the command.
ephemeral: (bool): Whether the response is ephemeral or not.
"""
gametime = datetime.now(tz=UTC)
await interaction.response.send_message(content=f"{gametime.hour}:{gametime.minute}", ephemeral=ephemeral)
+235 -34
View File
@@ -1,42 +1,168 @@
""" """Commands for viewing information in the database."""
Commands for viewing information in the database.
"""
import nextcord import nextcord
import pandas as pd
from nextcord.ext import commands from nextcord.ext import commands
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from database_setup.sql_data_classes import ElectionInformation, Votes, engine from database_setup.sql_data_classes import (
Points,
Presences,
Votes,
engine,
)
from utils import GUILDS, MAX_DISCORD_MESSAGE_LENGTH, PointType, VoteType
class ViewCog(commands.Cog, name="View Commands"): class ViewCog(commands.Cog, name="View Commands"):
""" """Holds the view commands."""
Holds the view commands.
"""
def __init__(self) -> None: def __init__(self) -> None:
""" """Default constructor for ViewCog class. Does nothing."""
Default constructor for ViewCog class. Does nothing.
"""
@nextcord.slash_command() @nextcord.slash_command(guild_ids=GUILDS)
async def view(self, interaction: nextcord.Interaction): async def view(self, interaction: nextcord.Interaction) -> None:
""" """Parent slash command for set. Never called."""
Parent slash command for set. Never called.
"""
@view.subcommand( @view.subcommand(description="Show event points.")
description="Shows all voters and who they voted for in a given election." async def event_points(
) self,
interaction: nextcord.Interaction,
quarter_name: str = nextcord.SlashOption(
description="The quarter to view",
required=False,
),
) -> None:
"""
View the event points.
Builds a message with the event points for the quarter that matches with quarter_name.
Args:
interaction (nextcord.Interaction): Interaction with the message.
quarter_name (str, optional): Same of quarter to autofill with. Is SlashOption.
"""
# just in case
await interaction.response.defer(ephemeral=True)
with Session(engine) as session:
if quarter_name:
ppdf = pd.read_sql_query(
sql=session.query(Points).where(Points.QuarterName.startswith(quarter_name)).statement,
con=engine,
)
else:
ppdf = pd.read_sql_query(
sql=session.query(Points).statement,
con=engine,
)
if ppdf.empty:
await interaction.followup.send(f'Quarter "{quarter_name}" is invalid.', ephemeral=True)
return
quarters = ppdf.groupby("QuarterName")
content = create_points_string(interaction, quarters, PointType.EVENT)
if len(content) > MAX_DISCORD_MESSAGE_LENGTH:
await interaction.followup.send("Message length is too long, please select search to limit results.")
return
await interaction.followup.send(content=content, ephemeral=True)
@view.subcommand(description="Show event points.")
async def clan_points(
self,
interaction: nextcord.Interaction,
quarter_name: str = nextcord.SlashOption(
description="The quarter to view",
required=False,
),
) -> None:
"""
View the clan points.
Builds a message with the clan points for the quarter that matches with quarter_name.
Args:
interaction (nextcord.Interaction): Interaction with the message.
quarter_name (str, optional): Same of quarter to autofill with. Is SlashOption.
"""
# just in case
await interaction.response.defer(ephemeral=True)
with Session(engine) as session:
if quarter_name:
ppdf = pd.read_sql_query(
sql=session.query(Points).where(Points.QuarterName.startswith(quarter_name)).statement,
con=engine,
)
else:
ppdf = pd.read_sql_query(
sql=session.query(Points).statement,
con=engine,
)
if ppdf.empty:
await interaction.followup.send(f'Quarter "{quarter_name}" is invalid.', ephemeral=True)
return
quarters = ppdf.groupby("QuarterName")
content = create_points_string(interaction, quarters, PointType.CLAN)
if len(content) > MAX_DISCORD_MESSAGE_LENGTH:
await interaction.followup.send("Message length is too long, please select search to limit results.")
return
await interaction.followup.send(content=content, ephemeral=True)
@view.subcommand(description="Show all possible presence choices.")
async def presences(
self,
interaction: nextcord.Interaction,
search: str = nextcord.SlashOption(
description="A word or string to search for in the presences.",
required=False,
),
) -> None:
"""
Gathers all presences from the database and responds with a message showing them.
Args:
interaction (nextcord.Integration): User's interaction with the command.
search (str): string to search for in presences. Is SlashOption.
"""
# might take a while depending on number of presences
await interaction.response.defer(ephemeral=True)
with Session(engine) as session:
stmt = select(Presences.Presence)
presences = session.scalars(stmt)
if search:
message = "\n".join([presence for presence in presences if search.lower() in presence.lower()])
else:
message = "\n".join(list(presences))
await interaction.followup.send(content=message, ephemeral=True)
@view.subcommand(description="Shows all voters and who they voted for in a given election.")
async def votes( async def votes(
self, self,
interaction: nextcord.Interaction, interaction: nextcord.Interaction,
election_name: str = nextcord.SlashOption( election_name: str = nextcord.SlashOption(
required=True, description="The election you want to view votes for." required=True, description="The election you want to view votes for."
), ),
): election_type: str = nextcord.SlashOption(
required=True,
description="The election type. Council, Discord Manager, or Clan Leader.",
choices=VoteType,
),
) -> None:
""" """
View all voters and who they voted for.
Takes in an election name and sends a response message to the user with a list of people who voted in that Takes in an election name and sends a response message to the user with a list of people who voted in that
election and who they voted for. Notes those tho abstained. Tells a user if the election is invalid. Uses election and who they voted for. Notes those tho abstained. Tells a user if the election is invalid. Uses
autocompletion to assist with getting elections. autocompletion to assist with getting elections.
@@ -44,16 +170,14 @@ class ViewCog(commands.Cog, name="View Commands"):
Args: Args:
interaction (nextcord.Interaction): User's interaction with the command. interaction (nextcord.Interaction): User's interaction with the command.
election_name (str): Is SlashOption. The name of the election to view votes for. election_name (str): Is SlashOption. The name of the election to view votes for.
election_type(str): Is SlashOption. The type of election to search for. Choice from StrEnum VoteType.
""" """
stmt = select(Votes).where(Votes.ElectionName == election_name).where(Votes.Type == election_type)
stmt = select(Votes).where(Votes.ElectionCycle == election_name)
with Session(engine) as session: with Session(engine) as session:
# Check if election exists. # Check if election exists.
if session.scalar(stmt) is None: if session.scalar(stmt) is None:
await interaction.response.send_message( await interaction.response.send_message(f"Invalid election: {election_name}.", ephemeral=True)
f"Invalid election: {election_name}.", ephemeral=True
)
return return
votes = session.scalars(stmt) votes = session.scalars(stmt)
@@ -68,9 +192,7 @@ class ViewCog(commands.Cog, name="View Commands"):
await interaction.response.send_message(content=content, ephemeral=True) await interaction.response.send_message(content=content, ephemeral=True)
@votes.on_autocomplete("election_name") @votes.on_autocomplete("election_name")
async def election_autocomplete( async def election_autocomplete(self, interaction: nextcord.Interaction, election_name: str) -> None:
self, interaction: nextcord.Interaction, election_name: str
):
""" """
Collects all elections that start with the election string. Collects all elections that start with the election string.
@@ -78,19 +200,98 @@ class ViewCog(commands.Cog, name="View Commands"):
interaction (nextcord.Interaction): Interaction with command. interaction (nextcord.Interaction): Interaction with command.
election_name (str): String to run autocomplete on. election_name (str): String to run autocomplete on.
""" """
with Session(engine) as session: with Session(engine) as session:
stmt = select(ElectionInformation.ElectionCycle) stmt = select(Votes.ElectionName).distinct()
elections = session.scalars(stmt) elections = session.scalars(stmt)
# if empty compare string # if empty compare string
if not election_name: if not election_name:
await interaction.response.send_autocomplete(elections) await interaction.response.send_autocomplete(list(elections)[:25])
return return
autofilled_elections: "list[str]" = [ autofilled_elections: list[str] = [
election election for election in elections if election.lower().startswith(election_name.lower())
for election in elections
if election.lower().startswith(election_name.lower())
] ]
await interaction.response.send_autocomplete(autofilled_elections[:25]) await interaction.response.send_autocomplete(autofilled_elections[:25])
@event_points.on_autocomplete("quarter_name")
@clan_points.on_autocomplete("quarter_name")
async def event_points_autocomplete(self, interaction: nextcord.Interaction, quarter_name: str) -> None:
"""
Collects all elections that start with the election string.
Args:
interaction (nextcord.Interaction): Interaction with command.
quarter_name (str): String to run autocomplete on.
"""
with Session(engine) as session:
stmt = select(Points.QuarterName).distinct()
quarters = session.scalars(stmt)
# if empty compare string
if not quarter_name:
await interaction.response.send_autocomplete(list(quarters)[:25])
return
autofilled_quarters: list[str] = [
quarter for quarter in quarters if quarter.lower().startswith(quarter_name.lower())
]
await interaction.response.send_autocomplete(autofilled_quarters[:25])
def create_points_string(interaction: nextcord.Interaction, quarters: any, point_type: PointType) -> str:
"""
Creates the string to show how many clan/event points users have.
Args:
interaction (nextcord.Interaction): Discord interaction of the command.
quarters (any): Pandas objects grouped by quarters.
point_type (PointType): The type of points to create the message for.
Returns:
str: The string with the message content.
"""
content: str = ""
for quarter in quarters.groups:
content += f"**{quarter}**\n"
unsorted_all = quarters.get_group(quarter).groupby("DiscordMember")
members = unsorted_all.get_group(name=True)
# there can be no non-discord members sometimes.
try:
sorted_non_members = unsorted_all.get_group(name=False).sort_values(by="UserId")
except KeyError:
sorted_non_members = None
sorted_members = members.sort_values(
by="UserId",
key=lambda user_ids: ([member_lambda_string(interaction.guild, user_id) for user_id in user_ids]),
)
sorted_all = pd.concat([sorted_members, sorted_non_members], ignore_index=True, sort=False)
for row in sorted_all.itertuples():
points = row.EventPoints if point_type == PointType.EVENT else row.ClanPoints
discord_member = row.DiscordMember
if points > 0:
content += f"<@{row.UserId}>: {points}\n" if discord_member else f"{row.UserId}: {points}\n"
content += "\n"
return content
def member_lambda_string(guild: nextcord.Guild, user_id: int) -> str:
"""
Finds a user in a guild and returns the display name.
Returns the user id in string form if user is no longer in guild.
I don't know if I like this name very much.
Args:
guild (nextcord.Guild): Guild object to find the user in.
user_id (int): User id to get display name of.
Returns:
str: String to use for sorting.
"""
try:
return guild.get_member(int(user_id)).display_name.lower()
except AttributeError:
# The user is no longer a part of the guild
return str(user_id)
+6 -9
View File
@@ -1,15 +1,12 @@
""" """Initializing code for all loops."""
Initializing code for all loops.
"""
from nextcord.ext import commands from nextcord.ext import commands
from .daily_loop import DailyLoop from cogs.loops.bot_presence import BotPresence
from cogs.loops.daily_loop import DailyLoop
def setup(bot: commands.Bot): def setup(bot: commands.Bot) -> None:
""" """Adds functions to bot."""
Adds functions to bot. bot.add_cog(BotPresence(bot))
"""
bot.add_cog(DailyLoop(bot)) bot.add_cog(DailyLoop(bot))
+38
View File
@@ -0,0 +1,38 @@
"""Functions for controlling the bot's presence. Also contains the loop."""
import nextcord
from nextcord.ext import commands, tasks
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from database_setup.sql_data_classes import Presences, engine
class BotPresence(commands.Cog, name="Bot Presence"):
"""Class for bot presence functions."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.Cog.listener()
async def on_guild_join(self, guild: nextcord.Guild) -> None: # noqa: ARG002
"""Listener for Guild Join. Updated presence on guild join."""
await self.change_presence()
@commands.Cog.listener()
async def on_ready(self) -> None:
"""Starts the loop when bot is ready."""
await self.bot.wait_until_ready()
self.periodic_change_presence.start()
@tasks.loop(minutes=5)
async def periodic_change_presence(self) -> None:
"""Loops every 5 minutes and changes presence."""
await self.change_presence()
async def change_presence(self) -> None:
"""Randomly chooses a presence and updates bot's presence."""
with Session(engine) as session:
presence = session.scalar(select(Presences.Presence).order_by(func.random()))
await self.bot.change_presence(activity=nextcord.CustomActivity(presence))
+46 -44
View File
@@ -1,27 +1,28 @@
""" """Defines the daily loop that runs the Dakiny bot functions."""
Defines the daily loop that runs the Dakiny bot functions.
"""
import random import random
from datetime import datetime, time from datetime import UTC, datetime, time
from typing import TYPE_CHECKING
import nextcord
from nextcord.ext import commands, tasks from nextcord.ext import commands, tasks
from pytz import timezone from pytz import timezone
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from classes.election_cycle import ElectionCycle from classes.election import Election
from config import DakinyConfig from DakinySettings import settings
from dakiny_bot import birthday_list
from database_setup.sql_data_classes import UserBirthday, engine from database_setup.sql_data_classes import UserBirthday, engine
from utils import get_current_cycle from main import birthday_list
from utils import get_active_election_cycle
if TYPE_CHECKING:
import nextcord
KANDY = 239547993106284544
class DailyLoop(commands.Cog, name="Daily Loop"): class DailyLoop(commands.Cog, name="Daily Loop"):
""" """Class for performing daily functions."""
Class for performing daily functions.
"""
def __init__(self, bot: commands.Bot) -> None: def __init__(self, bot: commands.Bot) -> None:
self.bot = bot self.bot = bot
@@ -31,16 +32,17 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
# pylint doesn't recognize that daily_loop is subclassed from tasks.loop. # pylint doesn't recognize that daily_loop is subclassed from tasks.loop.
self.daily_loop.start() # pylint: disable=no-member self.daily_loop.start() # pylint: disable=no-member
@tasks.loop(time=time(hour=0, tzinfo=timezone("GMT"))) @tasks.loop(time=time(hour=0, tzinfo=timezone("UTC")))
async def daily_loop(self): async def daily_loop(self) -> None:
""" """
Runs daily. Checks if the current election is None. If so, checks if there is an active election. If there is Runs daily.
Checks if the current election is None. If so, checks if there is an active election. If there is
an active election, runs the check_date() function for the election to perform actions. Finally checks if the an active election, runs the check_date() function for the election to perform actions. Finally checks if the
election is over to delete the current election object. election is over to delete the current election object.
""" """
if self.current_election is None: if self.current_election is None:
self.current_election = self.get_election_cycle() self.current_election = self.get_election()
if self.current_election is not None: if self.current_election is not None:
await self.current_election.check_date() await self.current_election.check_date()
@@ -49,49 +51,49 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
self.update_birthdays() self.update_birthdays()
if 239547993106284544 in birthday_list: if KANDY in birthday_list:
# inform kandy that it is her "birthday" # inform kandy that it is her "birthday"
kandy: nextcord.User = await self.bot.fetch_user(239547993106284544) kandy: nextcord.User = await self.bot.fetch_user(KANDY)
await kandy.send('Happy "Birthday"!') await kandy.send('Happy "Birthday"!')
await self.cakedays() await self.cakedays()
@daily_loop.before_loop @daily_loop.before_loop
async def before_loop(self): async def before_loop(self) -> None:
""" """Actions run before the loop starts. Want to wait for bot to be ready and get the current cycle."""
Actions run before the loop starts. Want to wait for bot to be ready and get the current cycle.
"""
await self.bot.wait_until_ready() await self.bot.wait_until_ready()
# Bot needs to know guilds to get the current cycle. # Bot needs to know guilds to get the current cycle.
self.current_election = self.get_election_cycle() self.current_election = self.get_election()
def get_election_cycle(self) -> ElectionCycle: def get_election(self) -> Election:
""" """
Creates an object for the current election cycle if one is active. An election is considered active Creates an object for the current election cycle if one is active.
between the election start and the term start.
An election is considered active between the election start and the term start.
Returns: Returns:
ElectionCycle: Current election cycle, None if there is no election happening. Election: Current election cycle, None if there is no election happening.
""" """
cycle = get_active_election_cycle()
config: DakinyConfig = self.bot.config # prevent exception from using cycle as an Int in an array
cycle = get_current_cycle(config) if cycle is None:
return None
return ElectionCycle(cycle, config, self.bot.get_guild(config.params.guild_id)) election = Election(cycle, self.bot.get_guild(settings.params.guild_id))
return election if election.active else None
def update_birthdays(self) -> None: def update_birthdays(self) -> None:
""" """Goes through database to collect all birthdays that are today. Adds user's id to birthday list."""
Goes through database to collect all birthdays that are today. Adds user's id to birthday list.
"""
birthday_list.clear() birthday_list.clear()
# kandy's 1% chance # kandy's 1% chance
if random.randint(0, 99) == 0: if random.randint(0, 99) == 0: # noqa: S311 isn't cryptography related.
birthday_list.append(239547993106284544) birthday_list.append(KANDY)
today = datetime.today() today = datetime.now(tz=UTC).date()
birthday = f"{today.month}/{today.day}" birthday = f"{today.month}/{today.day}"
stmt = select(UserBirthday.UserId).where(UserBirthday.Birthday == birthday) stmt = select(UserBirthday.UserId).where(UserBirthday.Birthday == birthday)
@@ -101,13 +103,13 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
async def cakedays(self) -> None: async def cakedays(self) -> None:
""" """
Loops through the guild's humans checking for joined_at dates that match today's date. Sends a message Loops through the guild's humans checking for joined_at dates that match today's date.
saying happy cakeday.
"""
today = datetime.today() Sends a message saying happy cakeday.
guild: nextcord.Guild = self.bot.get_guild(self.bot.config.params.guild_id) """
announce_channel: nextcord.TextChannel = guild.get_channel(self.bot.config.params.general_channel_id) today = datetime.now(tz=UTC).date()
guild: nextcord.Guild = self.bot.get_guild(settings.params.guild_id)
announce_channel: nextcord.TextChannel = guild.get_channel(settings.channel_ids.general)
cakedays: list[nextcord.Member] = [ cakedays: list[nextcord.Member] = [
human for human in guild.humans if human.joined_at.month == today.month and human.joined_at.day == today.day human for human in guild.humans if human.joined_at.month == today.month and human.joined_at.day == today.day
+5 -8
View File
@@ -1,11 +1,9 @@
""" """Defines cog for on message listener."""
Defines cog for on message listener.
"""
import nextcord import nextcord
from nextcord.ext import commands from nextcord.ext import commands
from dakiny_bot import birthday_list from main import birthday_list
class SubmissionListener(commands.Cog, name="Message Listener"): class SubmissionListener(commands.Cog, name="Message Listener"):
@@ -15,7 +13,7 @@ class SubmissionListener(commands.Cog, name="Message Listener"):
self.bot = bot self.bot = bot
@commands.Cog.listener() @commands.Cog.listener()
async def on_message(self, message: nextcord.Message): async def on_message(self, message: nextcord.Message) -> None:
"""Listener for message creation. Adds score and attachments to submission.""" """Listener for message creation. Adds score and attachments to submission."""
# don't want to process any messages from bot. # don't want to process any messages from bot.
if message.author.id == self.bot.user.id: if message.author.id == self.bot.user.id:
@@ -26,8 +24,8 @@ class SubmissionListener(commands.Cog, name="Message Listener"):
# This function will be called when this extension is loaded. # This function will be called when this extension is loaded.
# It is necessary to add these functions to the bot. # It is necessary to add these functions to the bot.
def setup(bot: commands.Bot): def setup(bot: commands.Bot) -> None:
"""Adds function to bot""" """Adds function to bot."""
bot.add_cog(SubmissionListener(bot)) bot.add_cog(SubmissionListener(bot))
@@ -38,6 +36,5 @@ async def birthday_action(message: nextcord.Message) -> None:
Args: Args:
message (nextcord.Message): The message to check for a birthday person. message (nextcord.Message): The message to check for a birthday person.
""" """
if message.author.id in birthday_list or bool(set(message.raw_mentions) & set(birthday_list)): if message.author.id in birthday_list or bool(set(message.raw_mentions) & set(birthday_list)):
await message.add_reaction("🎂") await message.add_reaction("🎂")
-52
View File
@@ -1,52 +0,0 @@
"""
File contains classes defining the Hydra configuraton structure.
"""
from dataclasses import dataclass
@dataclass
class Params:
"""
Class defines the various configuration parameters.
"""
active_category_id: int
admin_channel_id: int # might want to rename this...
announcement_category_id: int
archive_channel_group_id: int
clan_council_elect_role_id: int
clan_council_role_id: int
clan_info_channel_id: int
clan_member_role_id: int
discord_token: str
general_channel_id: int
guild_id: int
high_council_role_id: int
voting_parent_id: int
@dataclass
class ElectionCycle:
"""
Class defines relevant election dates.
"""
election_start: str
highcouncil_election_announce: str
highcouncil_election_end: str
highcouncil_election_start: str
name: str
term_end: str
term_start: str
voting_end: str
voting_start: str
@dataclass
class DakinyConfig:
"""
Class defines all configs required for Dakiny Bot.
"""
election_cycles: list[ElectionCycle]
params: Params
-66
View File
@@ -1,66 +0,0 @@
import logging
import os
import sys
import hydra
import nextcord
from hydra.core.config_store import ConfigStore
from nextcord.ext import commands
import database_setup.create_database as create_database
from classes.nominate_views import NominateView
from classes.voting_views import VoteView
from config import DakinyConfig
cs = ConfigStore.instance()
cs.store(name="dakiny_config", node=DakinyConfig)
birthday_list: list[int] = []
logger = logging.getLogger("nextcord")
logger.setLevel(logging.DEBUG)
handler = logging.FileHandler(filename="nextcord.log", encoding="utf-8", mode="w")
handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s"))
logger.addHandler(handler)
@hydra.main(config_path="conf", config_name="config")
def main(cfg: DakinyConfig):
# Allows privledged intents for monitoring members joining, roles editing, and role assignments
# These need to be enabled in the developer portal as well
intents = nextcord.Intents.default()
# Required in order to read messages (eg. prefix commands)
intents.message_content = True
# To enable the guilds priveleged intent:
# intents.guilds = True
# To enable the members priveleged intent:
intents.members = True
bot = commands.Bot(intents=intents)
bot.config = cfg
# Get the modules of all cogs whose directory structure is ./cogs/<module_name>
for folder in os.listdir("cogs"):
bot.load_extension(f"cogs.{folder}")
@bot.listen()
async def on_ready():
"""When Discord is connected"""
assert bot.user is not None
create_database # pylint: disable=pointless-statement
bot.add_view(NominateView(cfg))
bot.add_view(VoteView(cfg, bot.get_guild(cfg.params.guild_id)))
print(f"{bot.user.name} has connected to Discord!")
# Run Discord bot
bot.run(bot.config.params.discord_token)
if __name__ == "__main__":
sys.argv.append("hydra.job.chdir=False")
sys.argv.append("hydra/job_logging=none")
sys.argv.append("hydra/hydra_logging=none")
main() # pylint: disable=no-value-for-parameter
+4 -6
View File
@@ -1,15 +1,13 @@
""" """Small script to create the database if it doesn't already exist."""
Small script to create the database if it doesn't already exist.
"""
import sqlite3 import sqlite3
from os.path import exists from pathlib import Path
if not exists("database/dakiny.db"): if not Path("database/dakiny.db").exists():
with sqlite3.connect("database/dakiny.db") as con: with sqlite3.connect("database/dakiny.db") as con:
cur = con.cursor() cur = con.cursor()
with open("database_setup/database.sql", mode="r", encoding="utf-8") as file: with Path("database_setup/database.sql", encoding="utf-8").open() as file:
database_script = file.read() database_script = file.read()
cur.executescript(database_script) cur.executescript(database_script)
con.commit() con.commit()
+42 -10
View File
@@ -1,32 +1,64 @@
CREATE TABLE ElectionInformation( CREATE TABLE ElectionInformation(
ElectionCycle varchar(30) not null unique, ElectionName varchar(30) not null unique,
ThreadId bigint unsigned not null, ThreadId bigint unsigned not null,
CouncilVoteMsgId bigint unsigned, CouncilVoteMsgId bigint unsigned,
PRIMARY KEY (ElectionCycle) HighCouncilVoteMsgId bigint unsigned,
CouncilNominationMsgId bigint unsigned,
HighCouncilNominationMsgId bigint unsigned,
PRIMARY KEY (ElectionName)
); );
CREATE TABLE CouncilMembers( CREATE TABLE CouncilMembers(
ElectionCycle varchar(30) not null, ElectionName varchar(30) not null,
MemberId bigint unsigned not null, MemberId bigint unsigned not null,
SpeechMsgId bigint unsigned not null, SpeechMsgId bigint unsigned not null,
Elected boolean DEFAULT false, Elected boolean DEFAULT false,
HighCouncilElect boolean DEFAULT false,
DiscordManager boolean DEFAULT false, DiscordManager boolean DEFAULT false,
ClanLeader boolean DEFAULT false, ClanLeader boolean DEFAULT false,
FOREIGN KEY (ElectionCycle) REFERENCES ElectionInformation(ElectionCycle), DiscordManagerSpeechId bigint unsigned,
PRIMARY KEY (ElectionCycle, MemberId) ClanLeaderSpeechId bigint unsigned,
FOREIGN KEY (ElectionName) REFERENCES ElectionInformation(ElectionName),
PRIMARY KEY (ElectionName, MemberId)
); );
CREATE TABLE Votes( CREATE TABLE Votes(
ElectionCycle varchar(30) not null, ElectionName varchar(30) not null,
VoterId bigint unsigned not null, VoterId bigint unsigned not null,
VotedForId bigint unsigned, VotedForId bigint unsigned,
FOREIGN KEY (ElectionCycle) REFERENCES ElectionInformation(ElectionCycle), Type varchar(14) not null DEFAULT 'council' check("Type" in ('council', 'discordmanager', 'clanleader')),
PRIMARY KEY (ElectionCycle, VoterId) FOREIGN KEY (ElectionName) REFERENCES ElectionInformation(ElectionName),
PRIMARY KEY (ElectionName, VoterId, Type)
); );
CREATE TABLE UserBirthday ( CREATE TABLE UserBirthday (
UserId bigint unsigned not null, UserId bigint unsigned not null,
Birthday varchar(5) not null, Birthday varchar(5) not null,
PRIMARY KEY (UserId) PRIMARY KEY (UserId)
);
CREATE TABLE Presences (
Id integer PRIMARY KEY,
Presence varchar(100) not null
);
CREATE TABLE Points (
QuarterName varchar(30) not null,
UserId varchar(32) not null,
DiscordMember boolean not null,
EventPoints int unsigned DEFAULT 0,
ClanPoints int unsigned DEFAULT 0,
PRIMARY KEY (QuarterName, UserId)
);
CREATE TABLE Messages (
Path varchar(100) PRIMARY KEY,
Id bigint unsigned not null
);
CREATE TABLE PointsLimit (
UserId varchar(32) not null,
EventPoints int unsigned DEFAULT 0,
ClanPoints int unsigned DEFAULT 0,
Week varchar(7) not null,
PRIMARY KEY (UserId, Week)
); );
+7
View File
@@ -0,0 +1,7 @@
[tool.ruff]
# Extend the `pyproject.toml` file in the parent directory...
extend = "../pyproject.toml"
[tool.ruff.lint]
# ignore implicit-namespace-package in this directory.
ignore = ["INP001"]
+8 -5
View File
@@ -1,6 +1,5 @@
""" """Simple file to set up the sqlalchemy connection to database."""
Simple file to set up the sqlalchemy connection to database.
"""
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.ext.automap import automap_base from sqlalchemy.ext.automap import automap_base
@@ -11,7 +10,11 @@ Base = automap_base()
Base.prepare(autoload_with=engine) Base.prepare(autoload_with=engine)
# define the classes # define the classes
ElectionInformation = Base.classes.ElectionInformation Points = Base.classes.Points
PointsLimit = Base.classes.PointsLimit
CouncilMembers = Base.classes.CouncilMembers CouncilMembers = Base.classes.CouncilMembers
Votes = Base.classes.Votes ElectionInformation = Base.classes.ElectionInformation
Messages = Base.classes.Messages
Presences = Base.classes.Presences
UserBirthday = Base.classes.UserBirthday UserBirthday = Base.classes.UserBirthday
Votes = Base.classes.Votes
+59
View File
@@ -0,0 +1,59 @@
"""Main file for bot."""
import logging
import os
import nextcord
from nextcord.ext import commands
from classes.nominate_highcouncil_views import NominateHighCouncilView
from classes.nominate_views import NominateView
from classes.voting_highcouncil_views import VoteHighcouncilView
from classes.voting_views import VoteView
from DakinySettings import settings
from database_setup import create_database
birthday_list: list[int] = []
logger = logging.getLogger("nextcord")
logger.setLevel(logging.DEBUG)
handler = logging.FileHandler(filename="nextcord.log", encoding="utf-8", mode="w")
handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s"))
logger.addHandler(handler)
def main() -> None:
"""Builds and runs bot."""
# Allows privileged intents for monitoring members joining, roles editing, and role assignments
# These need to be enabled in the developer portal as well
intents = nextcord.Intents.default()
# Required in order to read messages (eg. prefix commands)
intents.message_content = True
# To enable the members privileged intent:
intents.members = True
bot = commands.Bot(intents=intents)
# Get the modules of all cogs whose directory structure is ./cogs/<module_name>
for folder in os.listdir("cogs"):
bot.load_extension(f"cogs.{folder}")
@bot.listen()
async def on_ready() -> None:
"""When Discord is connected."""
assert bot.user is not None # noqa: S101 documentation does this
create_database # noqa: B018
bot.add_view(NominateView(bot.get_guild(settings.params.guild_id)))
bot.add_view(NominateHighCouncilView(bot.get_guild(settings.params.guild_id)))
bot.add_view(VoteView(bot.get_guild(settings.params.guild_id)))
bot.add_view(VoteHighcouncilView(bot.get_guild(settings.params.guild_id)))
print(f"{bot.user.name} has connected to Discord!") # noqa: T201 TODO make logging to print my stuff to console
# Run Discord bot
bot.run(settings.params.discord_token)
if __name__ == "__main__":
main()
+134
View File
@@ -0,0 +1,134 @@
[tool.poetry]
name = "dakiny-bot"
version = "0.1.0"
description = "Dakiny's Discord Bot."
authors = ["imcompiling <[email protected]>"]
readme = "README.md"
package-mode = false
[tool.poetry.dependencies]
python = "^3.12"
gitpython = "^3.1.43"
fastapi = "^0.111.0"
nextcord = "^2.6.0"
pandas = "^2.2.2"
pytz = "^2024.1"
sqlalchemy = "^2.0.30"
aiofiles = "^23.2.1"
setuptools = "^70.0.0"
dataclass-wizard = "^0.22.3"
[tool.poetry.group.dev.dependencies]
ruff = "^0.6.9"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.ruff]
# Exclude a variety of commonly ignored directories.
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".ipynb_checkpoints",
".mypy_cache",
".nox",
".pants.d",
".pyenv",
".pytest_cache",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
".vscode",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"site-packages",
"venv",
"dakiny-messages", # Don't need to lint this git repo.
]
# Same as Black.
line-length = 120
indent-width = 4
# Assume Python 3.8
target-version = "py312"
[tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
select = ["ALL"]
ignore = [
"ANN101",
"B008", # I build SlashOptions as arguments.
"COM812",
"COM819",
"D107", # don't want docstrings on init functions
"D203",
"D206",
"D212",
"D300",
"E111",
"E114",
"E117",
"ISC001",
"ISC002",
"PD011", # doesn't like me using '.values' on embeds....
"Q000",
"Q001",
"Q002",
"Q003",
"W191",
]
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.lint.flake8-annotations]
mypy-init-return = true
[tool.ruff.format]
# Like Black, use double quotes for strings.
quote-style = "double"
# Like Black, indent with spaces, rather than tabs.
indent-style = "space"
# Like Black, respect magic trailing commas.
skip-magic-trailing-comma = false
# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"
# Enable auto-formatting of code examples in docstrings. Markdown,
# reStructuredText code/literal blocks and doctests are all supported.
#
# This is currently disabled by default, but it is planned for this
# to be opt-out in the future.
docstring-code-format = true
# Set the line length limit used when formatting code snippets in
# docstrings.
#
# This only has an effect when the `docstring-code-format` setting is
# enabled.
docstring-code-line-length = "dynamic"
-5
View File
@@ -1,5 +0,0 @@
nextcord==2.4.2
python-dotenv==1.0.0
pytz==2022.7.1
hydra-core==1.3.2
sqlalchemy<2.0
-1
View File
@@ -1 +0,0 @@
python-3.10.4
+491 -37
View File
@@ -1,14 +1,36 @@
""" """File contains util functions."""
File contains util functions.
"""
from datetime import datetime import calendar
import os
from collections.abc import Sequence
from datetime import UTC, datetime
from enum import StrEnum
from pathlib import Path
from sqlalchemy import select import aiofiles
import nextcord
from sqlalchemy import select, true, update
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from config import DakinyConfig from DakinySettings import settings
from database_setup.sql_data_classes import CouncilMembers, Votes, engine from database_setup.sql_data_classes import (
CouncilMembers,
Messages,
Points,
PointsLimit,
Votes,
engine,
)
NOMINEE_SPLITTER = "========================================\n\n"
REPO_DIR = "./dakiny-messages"
MESSAGES_DIR = f"{REPO_DIR}/messages"
TEMPLATES_DIR = f"{REPO_DIR}/message_templates"
MAX_DISCORD_MESSAGE_LENGTH = 2000
GUILDS = [1000247361639030804, 239758371358638080]
VoteType = StrEnum("VoteType", ["COUNCIL", "DISCORDMANAGER", "CLANLEADER"])
PointType = StrEnum("PointType", ["CLAN", "EVENT"])
def get_datetime(day: str) -> datetime: def get_datetime(day: str) -> datetime:
@@ -21,83 +43,515 @@ def get_datetime(day: str) -> datetime:
Returns: Returns:
datetime: Datetime representation of Month/Day. datetime: Datetime representation of Month/Day.
""" """
year = datetime.today().year today = datetime.now(tz=UTC).date()
year = today.year
month = int(day.split("/")[0]) month = int(day.split("/")[0])
day = int(day.split("/")[1]) day = int(day.split("/")[1])
# ensure that year rolls over if current month is December and the next month is less than 12. # ensure that year rolls over if current month is December and the next month is less than 12.
if datetime.today().month == 12 & month < 12: if today.month == calendar.DECEMBER and month < calendar.OCTOBER:
year += 1 year += 1
return datetime(year, month, day) if today.month == calendar.JANUARY and month > calendar.MARCH:
year -= 1
return datetime(year, month, day, tzinfo=UTC)
def get_election_cycle_name(config: DakinyConfig) -> str: def get_election_name() -> str:
""" """
Builds the unique name of the current election cycle Builds the unique name of the current election cycle.
Returns: Returns:
str: the name of the election cycle str: the name of the election cycle
""" """
cycle = get_current_cycle(config) cycle = get_active_election_cycle()
current_cycle = config.election_cycles[cycle] current_cycle = settings.election_cycles[cycle]
return f"vote-{get_datetime(current_cycle.term_start).year}-{current_cycle.name}"
return f"vote-{get_datetime(current_cycle.election_start).year}-{current_cycle.name}"
def get_current_cycle(config: DakinyConfig) -> int: def get_quarter_name() -> str:
"""
Builds the name of the current cycle/quarter.
Returns:
str: the name of the cycle.
"""
cycle = get_quarter_cycle()
current_cycle = settings.election_cycles[cycle]
return f"{get_datetime(current_cycle.term_start).year}-{current_cycle.name}"
def get_active_election_cycle() -> int:
""" """
Returns index for active election. An election is considered active between the election start and the term start. Returns index for active election. An election is considered active between the election start and the term start.
Args:
config (DakinyConfig): Config object to use
Returns: Returns:
int: cycle index for active election. -1 for no active cycle int: cycle index for active election. None for no active cycle
""" """
today = datetime.now(tz=UTC)
today = datetime.today() for cycle, election in enumerate(settings.election_cycles):
for cycle, election in enumerate(config.election_cycles):
if ( if (
today.date() >= get_datetime(election.election_start).date() today.date() >= get_datetime(election.election_start).date()
and today.date() <= get_datetime(election.term_start).date() and today.date() <= get_datetime(election.term_start).date()
): ):
return cycle return cycle
return -1 # It isn't a current election, get the quarter
return get_quarter_cycle()
def build_votes_str(config: DakinyConfig) -> str: def get_quarter_cycle() -> int:
"""
Returns the index for the current cycle/quarter. Defined as between term start and end.
Can return None if entire year isn't covered by cycles.
Returns:
int: cycle index for current cycle.
"""
today = datetime.now(tz=UTC)
for cycle, election in enumerate(settings.election_cycles):
if (
today.date() >= get_datetime(election.term_start).date()
and today.date() <= get_datetime(election.term_end).date()
):
return cycle
return None
def build_votes_str(guild: nextcord.Guild) -> str:
""" """
Creates a string that contains all nominees and their votes to be sent as a discord message. Creates a string that contains all nominees and their votes to be sent as a discord message.
Args: Args:
config (DakinyConfig): Daking config object guild (nextcord.Guild): The guild object. Used to get display names for sorting.
Returns: Returns:
str: String that contains all nominees and their votes. str: String that contains all nominees and their votes.
""" """
votes: dict[int, int] = {} votes: dict[int, int] = {}
with Session(engine) as session: election_name = get_election_name()
stmt = select(CouncilMembers).where(CouncilMembers.ElectionCycle == get_election_cycle_name(config))
for nominee in session.scalars(stmt): with Session(engine) as session:
stmt = select(CouncilMembers).where(CouncilMembers.ElectionName == election_name)
for nominee in sorted(
session.scalars(stmt),
key=lambda nominee: guild.get_member(nominee.MemberId).display_name.lower(),
):
votes[nominee.MemberId] = 0 votes[nominee.MemberId] = 0
stmt = select(Votes).where(Votes.ElectionCycle == get_election_cycle_name(config)) stmt = select(Votes).where(Votes.ElectionName == election_name).where(Votes.Type == VoteType.COUNCIL)
for vote in session.scalars(stmt): for vote in session.scalars(stmt):
if vote.VotedForId is not None: if vote.VotedForId is not None:
votes[vote.VotedForId] += 1 votes[vote.VotedForId] += 1
votes_str: str = "====================" votes_str: str = "===================="
for user_id, num_votes in votes.items(): votes_str += user_vote_str(votes)
votes_str += f"\n<@{user_id}>: "
for num in range(num_votes):
votes_str += "☑️"
return votes_str return votes_str
def build_highcouncil_votes_str(guild: nextcord.Guild) -> str:
"""
Creates a string that contains all high council nominees and their votes to be sent as a discord message.
Args:
guild (nextcord.Guild): The guild object. Used to get display names for sorting.
Returns:
str: String that contains all nominees and their votes.
"""
cl_votes: dict[int, int] = {}
dm_votes: dict[int, int] = {}
election_name = get_election_name()
with Session(engine) as session:
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionName == election_name)
.filter(CouncilMembers.ClanLeaderSpeechId.is_not(None))
)
for nominee in sorted(
session.scalars(stmt),
key=lambda nominee: guild.get_member(nominee.MemberId).display_name.lower(),
):
cl_votes[nominee.MemberId] = 0
stmt = select(Votes).where(Votes.ElectionName == election_name).where(Votes.Type == VoteType.CLANLEADER)
for vote in session.scalars(stmt):
if vote.VotedForId is not None:
cl_votes[vote.VotedForId] += 1
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionName == election_name)
.filter(CouncilMembers.DiscordManagerSpeechId.is_not(None))
)
for nominee in sorted(
session.scalars(stmt),
key=lambda nominee: guild.get_member(nominee.MemberId).display_name.lower(),
):
dm_votes[nominee.MemberId] = 0
stmt = select(Votes).where(Votes.ElectionName == election_name).where(Votes.Type == VoteType.DISCORDMANAGER)
for vote in session.scalars(stmt):
if vote.VotedForId is not None:
dm_votes[vote.VotedForId] += 1
# build the votes string
votes_str: str = "Clan Leader Votes\n===================="
votes_str += user_vote_str(cl_votes)
votes_str += "\n\nDiscord Manager Votes\n===================="
votes_str += user_vote_str(dm_votes)
return votes_str
def user_vote_str(votes: dict[int, int]) -> str:
"""
Creates the string representing how many votes each user has.
Args:
votes (dict[int, int]): Dict of all users and how many votes they have.
Returns:
str: The string showing names and number of votes.
"""
ret_str: str = ""
for user_id, num_votes in votes.items():
ret_str += f"\n<@{user_id}>: {'☑️' * num_votes}"
return ret_str
def decorate_nomination(user_id: int, speech: str) -> str:
"""
Decorates a nomination speech so that it sits within multiple speeches better.
Args:
user_id (int): ID of user sending the speech
speech (str): Speech from the user
Returns:
str: Decorated speech
"""
msg = f"**Clan Council Nominee: <@{user_id}>**\n"
msg += NOMINEE_SPLITTER
msg += speech
return msg
def decorate_cl_nomination(user_id: int, speech: str) -> str:
"""
Decorates a nomination speech so that it sits within multiple speeches better.
Args:
user_id (int): ID of user sending the speech
speech (str): Speech from the user
Returns:
str: Decorated speech
"""
msg = f"**Clan Leader Nominee: <@{user_id}>**\n"
msg += NOMINEE_SPLITTER
msg += speech
return msg
def decorate_dm_nomination(user_id: int, speech: str) -> str:
"""
Decorates a nomination speech so that it sits within multiple speeches better.
Args:
user_id (int): ID of user sending the speech
speech (str): Speech from the user
Returns:
str: Decorated speech
"""
msg = f"**Discord Manager Nominee: <@{user_id}>**\n"
msg += NOMINEE_SPLITTER
msg += speech
return msg
def record_points(users: Sequence[str], add_points: int, point_type: PointType, *, is_member: bool) -> None:
"""
Adds points to all users listed.
Checks if they don't exist in order to make a new entry, otherwise
adds points to the existing value. Can be used for hosting or participating in events or for the clan
points.
Args:
users (Sequence[int]): users that participated and need points.
add_points (int): number of points to add
point_type (PointType): which point type to add to
is_member (bool): whether the users are a discord member or not
"""
quarter_name = get_quarter_name()
with Session(engine) as session:
for user in users:
stmt = select(Points).where(Points.UserId == user).where(Points.QuarterName == quarter_name)
points_obj = session.scalar(stmt)
add_points = get_allowed_points(session, point_type, user, add_points)
add_clan_points = add_points if point_type == PointType.CLAN else 0
add_event_points = add_points if point_type == PointType.EVENT else 0
if points_obj is None:
new_points = Points(
QuarterName=quarter_name,
UserId=user,
DiscordMember=is_member,
EventPoints=add_event_points,
ClanPoints=add_clan_points,
)
session.add(new_points)
else:
points_obj.EventPoints += add_event_points
points_obj.ClanPoints += add_clan_points
session.commit()
def get_allowed_points(session: Session, point_type: PointType, user: str, points: int) -> int:
"""
Gets the maximum number of points a user can be given.
Searches how many points they have been given
this week and subtracts that from the weekly limit. If under, returns up to "points" number of points (will not
go over points_limit) otherwise returns 0. Updates the limit object or creates a new one if one isn't found.
Args:
session (Session): Open connection to database.
point_type (PointType): Which type of points to check the limit for.
user (str): user to be given points. Used to check a user's limit.
points (int): maximum number of points to give to user.
Returns:
int: how many points are allowed to be given to the user.
"""
# check for limit
today = datetime.now(tz=UTC).date()
this_week = f"{today.isocalendar().year}-{today.isocalendar().week}"
points_limit = (
settings.point_settings.events_weekly_limit
if point_type == PointType.EVENT
else settings.point_settings.clan_points_weekly_limit
)
stmt = select(PointsLimit).where(PointsLimit.UserId == user).where(PointsLimit.Week == this_week)
# if under limit, can add up to however many points would make the current limit equal to the
# allowed limit.
limit_obj = session.scalar(stmt)
if limit_obj is None:
clan_points = points if point_type == PointType.CLAN else 0
event_points = points if point_type == PointType.EVENT else 0
new_limit = PointsLimit(UserId=user, Week=this_week, EventPoints=event_points, ClanPoints=clan_points)
add_points = points
session.add(new_limit)
else:
curr_points = limit_obj.EventPoints if point_type == PointType.EVENT else limit_obj.ClanPoints
add_points = points if curr_points + points <= points_limit else (max(0, points_limit - curr_points))
if point_type == PointType.EVENT:
limit_obj.EventPoints += add_points
else:
limit_obj.ClanPoints += add_points
session.commit()
return add_points
async def update_clan_messages(guild: nextcord.Guild, channel_updates: Sequence[str]) -> None:
"""
Updates all clan messages to contents in MESSAGES_DIR.
Does not to any git work. Is expected to be called after git is pulled.
For every directory, sort the files by number then edits/sends the message.
Channel fetching/management is handled by another function.
Message editing and sending is handled by another function.
Args:
guild (nextcord.Guild): Guild object that all channels and messages belong to.
channel_updates: (Sequence[str]): a sequence of the channels to update.
"""
dirs = [dir_name for dir_name in channel_updates if Path(f"{MESSAGES_DIR}/{dir_name}").is_dir()]
with Session(engine) as session:
for dir_name in dirs:
dir_path = f"{MESSAGES_DIR}/{dir_name}"
channel = await get_clan_message_channel(session, dir_name, guild)
sorted_files = sorted(
os.listdir(dir_path),
key=lambda filename: int(filename.split(".")[0]),
)
for file_name in sorted_files:
message_name = f"{dir_name}/{file_name}"
file_path = f"{dir_path}/{file_name}"
async with aiofiles.open(file_path, encoding="utf8") as reader:
await edit_clan_message(
session, channel, message_name, await reader.read(MAX_DISCORD_MESSAGE_LENGTH)
)
async def get_clan_message_channel(session: Session, channel_name: str, guild: nextcord.Guild) -> nextcord.TextChannel:
"""
Used to obtain a channel from the guild object.
Searches database for an id to get the channel. If that is not
found, then searches through all channels in the guild to find one with a matching name. If that also doesn't
turn up a channel, then creates it. Adds an entry into database for last two paths.
Assumes that there is an open session to the database.
Args:
session (Session): Open connection to database.
channel_name (str): Name of channel to obtain.
guild (nextcord.Guild): Guild object to get channel from.
Returns:
nextcord.TextChannel: Channel object with name channel_name.
"""
stmt = select(Messages.Id).where(Messages.Path == channel_name)
channel_id = session.scalar(stmt)
if channel_id is None:
ret_channel = None
# channel is not tracked. see if it exists
channels = await guild.fetch_channels()
for channel in channels:
if channel.name == channel_name and channel.__class__ is nextcord.TextChannel:
ret_channel = channel
if ret_channel is None:
# channel doesn't exist, we must create it
ret_channel = await guild.create_text_channel(
channel_name,
reason="Creating channel as required for by design of Dakiny-Messages repo.",
)
# record channel information
new_item = Messages(Path=channel_name, Id=ret_channel.id)
session.add(new_item)
session.commit()
return ret_channel
try:
return await guild.fetch_channel(channel_id)
except nextcord.NotFound:
# the channel was deleted for some reason
ret_channel = await guild.create_text_channel(
channel_name,
reason="Creating channel as required for by design of Dakiny-Messages repo.",
)
stmt = update(Messages).where(Messages.Path == channel_name).values(Id=ret_channel.id)
session.execute(stmt)
session.commit()
return ret_channel
async def edit_clan_message(
session: Session,
channel: nextcord.TextChannel,
message_name: str,
message_content: str,
) -> None:
"""
Obtains a message id from database.
If no id, then need to send a new message and record the id.
If there is a message, then edit the message. If that message is not found in discord, then need to
send a new message and record the id in the database.
Args:
session (Session): Open connection to database.
channel (nextcord.TextChannel): Channel object that message belongs to.
message_name (str): Name of the message. Corresponds to the file that contains message content.
message_content (str): Content of the message to edit/send.
"""
stmt = select(Messages.Id).where(Messages.Path == message_name)
msg_id = session.scalar(stmt)
if msg_id is None:
# don't have a message for this file
msg = await channel.send(message_content)
new_message = Messages(Path=message_name, Id=msg.id)
session.add(new_message)
session.commit()
else:
try:
msg: nextcord.Message = await channel.fetch_message(msg_id)
await msg.edit(message_content)
except nextcord.NotFound:
msg = await channel.send(message_content)
stmt = update(Messages).where(Messages.Path == message_name).values(Id=msg.id)
session.execute(stmt)
session.commit()
async def edit_clan_council_info(guild: nextcord.Guild) -> None:
"""
Gets the winners of the elections and updates the permanent message that states the current council.
Args:
guild (nextcord.Guild): Guild object that to use to get channel.
"""
clan_info_channel: nextcord.TextChannel = await guild.fetch_channel(settings.channel_ids.clan_info)
election_name = get_election_name()
with Session(engine) as session:
stmt = (
select(CouncilMembers.MemberId)
.where(CouncilMembers.ElectionName == election_name)
.where(CouncilMembers.DiscordManager == true())
)
discord_manager = session.scalar(stmt)
stmt = (
select(CouncilMembers.MemberId)
.where(CouncilMembers.ElectionName == election_name)
.where(CouncilMembers.ClanLeader == true())
)
clan_leader = session.scalar(stmt)
stmt = (
select(CouncilMembers.MemberId)
.where(CouncilMembers.ElectionName == election_name)
.where(CouncilMembers.Elected == true())
)
council_member_ids = sorted(
session.scalars(stmt), key=lambda user_id: guild.get_member(user_id).display_name.lower()
)
council_members_strs = [
f"<@{user_id}>" for user_id in council_member_ids if user_id not in (discord_manager, clan_leader)
]
quarter = get_quarter_name()
async with aiofiles.open(f"{TEMPLATES_DIR}/council_members.txt", encoding="utf8") as file:
message = await file.read()
message = message.format(
quarter=quarter,
council="\n".join(council_members_strs),
clan_leader=clan_leader,
discord_manager=discord_manager,
)
await edit_clan_message(session, clan_info_channel, "council_members.txt", message)