Compare commits

..
Author SHA1 Message Date
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
imcompiling 619ae92d03 Merge branch '30-threads'
build_develop / build (push) Successful in 46s
build_release / build (push) Successful in 44s
build_latest / build (push) Successful in 45s
2023-11-08 16:12:07 -05:00
imcompiling 431e9c0b35 Filled out term start action.
build_develop / build (push) Successful in 1m32s
2023-11-08 16:11:47 -05:00
imcompiling 27f90bbaef Add test for term start. 2023-11-08 15:51:06 -05:00
imcompiling c98bb5888c Give a message for thread to attach to. Lets it be public. 2023-11-08 15:44:19 -05:00
imcompiling c38632ca21 Remove another permission setting.
name a parameter.
2023-11-08 15:43:58 -05:00
imcompiling 75fc59ed5e Removed references to ElectionChannel.
Removed permission managing as threads don't have permissions.
2023-11-08 15:30:05 -05:00
imcompiling 8d2f2f7dba Thread create ini election start.
build_develop / build (push) Successful in 1m35s
Modified config and database to use threads.
2023-11-07 15:36:33 -05:00
imcompiling ab88652cdb Automatic reformat. Guessing from vscode update. 2023-11-07 15:35:43 -05:00
imcompiling b0e3162123 Change bot to use update query.
build_latest / build (push) Successful in 48s
build_develop / build (push) Successful in 50s
2023-11-07 14:59:07 -05:00
imcompiling 8c1eb8d11f Filled out a docstring.
For some reason, file wasn't formatted.
2023-11-07 14:52:22 -05:00
imcompiling 40d220e2bb Merge branch '32-view-votes-command'
build_develop / build (push) Successful in 50s
2023-11-07 14:44:33 -05:00
imcompiling 4e8032823d Fix check if election exists.
build_develop / build (push) Successful in 1m35s
Scalars doesn't return None if the thing doesn't exists while scalar does.
2023-11-07 14:44:07 -05:00
imcompiling 678fcd37e1 Add command to init.
Correctly set autocomplete.
Put newlines where I forgot to.
2023-11-07 14:12:42 -05:00
imcompiling 7530c59a94 Update function header.
build_develop / build (push) Successful in 1m25s
2023-11-06 20:27:36 -05:00
imcompiling 6abab5c4e5 Remove missedd code snippet.
build_develop / build (push) Successful in 1m26s
2023-11-06 20:26:24 -05:00
imcompiling 088de263cc Created initial view votes command.
build_develop / build (push) Successful in 1m26s
2023-11-06 20:20:58 -05:00
imcompiling ffc78a8814 Merge branch '33-fixes-from-mock-election'
build_latest / build (push) Successful in 50s
build_release / build (push) Successful in 47s
build_develop / build (push) Successful in 45s
2023-11-06 19:47:22 -05:00
imcompiling 3e88d62577 Edit election announcement first line.
build_develop / build (push) Successful in 1m24s
2023-11-06 19:46:54 -05:00
imcompiling 392044c658 Reduce length of header splitter.
build_develop / build (push) Successful in 1m34s
2023-11-06 19:45:49 -05:00
imcompiling 9442a294f3 Merge branch '28-add-election-reminder'
build_latest / build (push) Successful in 44s
build_develop / build (push) Successful in 53s
2023-11-03 17:32:00 -04:00
imcompiling d149caafea Add election reminder.
build_develop / build (push) Successful in 1m26s
Happens daily starting 3 days before election end.
2023-11-03 17:31:52 -04:00
imcompiling 000cb9b257 Merge branch '11-council-member-vote-automation'
build_latest / build (push) Successful in 1m23s
build_release / build (push) Successful in 45s
build_develop / build (push) Successful in 48s
2023-11-03 15:10:34 -04:00
imcompiling 3fb65a7b0a Process voting end.
build_develop / build (push) Successful in 1m41s
Give the elect role and set elected in database.
2023-11-03 15:06:52 -04:00
imcompiling 66e7159022 Enforce having clan member role for voting.
build_develop / build (push) Successful in 1m31s
2023-11-03 14:27:21 -04:00
imcompiling 5803c336c0 Display nickname while voting. 2023-11-03 13:39:42 -04:00
imcompiling 80c4f053bc Add abstaining from voting. 2023-11-03 13:38:23 -04:00
imcompiling 289b752678 Voting interacts with database. 2023-11-03 13:33:12 -04:00
imcompiling 181b1da879 Don't need voting emote anymore. 2023-11-03 13:24:34 -04:00
imcompiling dcea911564 Fix bug getting ElectionInformation object correctly.
build_develop / build (push) Successful in 1m31s
2023-11-03 12:06:27 -04:00
imcompiling 81e2885e8d Change newlines in votes string.
build_develop / build (push) Successful in 1m26s
2023-11-03 11:56:47 -04:00
imcompiling 14f2e13772 General rework to the voting views.
build_develop / build (push) Successful in 1m31s
2023-11-03 11:55:57 -04:00
imcompiling fa3284b9e3 Pull the vote count message out of vote start.
Make its own builder function in utils.
2023-11-03 11:55:21 -04:00
imcompiling a5ef579767 ElectionName is not unique in the other tables. 2023-11-03 11:53:47 -04:00
imcompiling 9c588a18af Need to track the message where votes are presented. 2023-11-03 11:00:55 -04:00
imcompiling ff68e7df1c Allow for abstain votes.
build_develop / build (push) Successful in 1m24s
2023-11-02 19:57:54 -04:00
imcompiling 5b1b4cfecb Change modal to a view. Still need to fill out functionality.
Funtionality needed: put into database, check on view creation of the user has voted already.
If so, then change the content to show they already voted and who they voted for.
Alert that they can change their vote.
2023-11-02 19:52:20 -04:00
imcompiling 626cca8b81 Start work on vote selection. Modal does not work.
build_develop / build (push) Successful in 1m25s
2023-11-02 17:26:05 -04:00
imcompiling e7bd608017 Add nominees and voting total to voting message. 2023-11-02 17:01:19 -04:00
imcompiling a5f0fb71e0 Update to the election rules.
build_latest / build (push) Successful in 1m26s
2023-11-02 16:36:16 -04:00
imcompiling a0f160c801 Rework to allow users to edit their nomination message.
build_develop / build (push) Successful in 1m27s
2023-11-02 16:22:02 -04:00
imcompiling 19baf84c80 Add speech msg id to database, add the id when submitting a nomination. 2023-11-02 15:12:28 -04:00
imcompiling 133ff79b94 Pass config along so that nominate can add to database with election cycle name. 2023-11-02 15:05:48 -04:00
imcompiling 647af0353d move functions to get election cycle index and name into utils.
build_develop / build (push) Successful in 1m52s
2023-11-02 14:44:26 -04:00
imcompiling 63e3b3edb2 Middle of adding user to council member table. Need to move function for getting election name to utils.
build_develop / build (push) Successful in 1m33s
2023-10-25 15:20:03 -04:00
imcompiling ad58f3a9cb Correct typo. Remove invalid docstring sentence. 2023-10-25 15:11:36 -04:00
imcompiling c013c85f80 Cleaned up nomination message. 2023-10-25 14:51:37 -04:00
imcompiling f86225849c add admin perms to testing command...
build_develop / build (push) Successful in 1m21s
2023-10-24 16:50:49 -04:00
imcompiling a8def8fd69 First start on nomination modal.
build_develop / build (push) Successful in 1m30s
2023-10-24 16:49:54 -04:00
imcompiling e5adfdb16f Changed name of channels to match what was already being used.
build_latest / build (push) Successful in 46s
build_develop / build (push) Successful in 47s
2023-10-24 15:37:27 -04:00
imcompiling ee19f32be9 Merge branch '27-supporting-first-live-action'
build_latest / build (push) Successful in 1m58s
build_develop / build (push) Successful in 47s
2023-10-24 10:35:21 -04:00
imcompiling 8ff53d9a13 Grammar corrections.
build_develop / build (push) Has been cancelled
2023-10-05 14:26:15 -04:00
imcompiling 9609837b1d Control user reactions in election channel. 2023-10-05 14:24:33 -04:00
imcompiling 8cf66351dd Allow clan members to send messages to new channel.
build_develop / build (push) Has been cancelled
2023-09-20 20:14:32 -04:00
imcompiling a4c02b2f77 Change develop build to make a branch specific image.
build_latest / build (push) Successful in 1m27s
build_release / build (push) Successful in 45s
build_develop / build (push) Successful in 45s
2023-09-15 09:53:45 -04:00
imcompiling 8d8af9ab35 Change cakeday to be put in general channel. 2023-09-15 09:42:58 -04:00
imcompiling f70a7989ae Updated election_cycle init docstring.
build_develop / build (push) Successful in 1m24s
build_latest / build (push) Successful in 43s
2023-09-14 11:55:08 -04:00
imcompiling fb909c009a Quick bugfix for finding next election. Wouldn't happen in live, but better than hard coding. 2023-09-14 11:52:33 -04:00
imcompiling bdefa035ff Merge branch '25-cakeday'
build_latest / build (push) Successful in 44s
build_develop / build (push) Successful in 45s
build_release / build (push) Successful in 43s
2023-09-14 11:34:14 -04:00
imcompiling 85eb52d2e1 add word to message string.
build_develop / build (push) Successful in 1m31s
2023-09-14 11:33:27 -04:00
imcompiling 7d1b2f7cac Create cakedays function. 2023-09-14 11:24:25 -04:00
imcompiling c5c79015a9 Correct spelling. 2023-09-14 11:22:53 -04:00
imcompiling 7b335efbe6 Merge branch '22-daily-loop-not-starting-election'
build_latest / build (push) Successful in 44s
build_release / build (push) Successful in 43s
build_develop / build (push) Successful in 45s
2023-09-13 10:52:08 -04:00
imcompiling e686cf88e5 Be inclusive when checkign for an election cycle.
build_develop / build (push) Has been cancelled
2023-09-13 10:51:16 -04:00
imcompiling 117dea48c4 Standardize variable name of dates. 2023-09-13 10:42:01 -04:00
imcompiling 62e4417ce8 Use correct action on voting start. 2023-09-13 10:32:59 -04:00
imcompiling 672df41f19 Make the configs and timeline more clear. 2023-09-13 10:16:50 -04:00
imcompiling 328f3fea78 Merge branch 'main' into 22-daily-loop-not-starting-election 2023-09-13 09:31:53 -04:00
imcompiling 18751bc85c Inform Kandy when it is her "birthday".
build_release / build (push) Successful in 48s
build_latest / build (push) Successful in 43s
build_develop / build (push) Successful in 43s
2023-09-12 13:17:25 -04:00
imcompiling e243b40be7 Merge branch '19-birthday-tracking'
build_latest / build (push) Successful in 44s
build_develop / build (push) Successful in 42s
build_release / build (push) Successful in 42s
2023-09-09 13:48:36 -04:00
imcompiling 7a91cd37ec Add birthdays from database.
build_develop / build (push) Has been cancelled
2023-09-09 13:48:22 -04:00
imcompiling efadcb30c7 Finish up set birthday command. 2023-09-09 13:27:22 -04:00
imcompiling 5668e4be7e Add release workflow.
build_release / build (push) Successful in 44s
build_develop / build (push) Successful in 43s
build_latest / build (push) Successful in 44s
2023-09-09 11:49:56 -04:00
imcompiling 1a8c5616ef Merge branch 'main' into 19-birthday-tracking
build_develop / build (push) Successful in 1m24s
2023-09-09 11:48:47 -04:00
imcompiling 43c1a5c6cd Merge branch '22-daily-loop-not-starting-election'
build_develop / build (push) Successful in 43s
build_latest / build (push) Successful in 42s
2023-09-09 11:39:31 -04:00
imcompiling c278bae498 I forgot to give the docker image the message files.
build_develop / build (push) Successful in 1m24s
2023-09-09 11:38:22 -04:00
imcompiling 8b831fb6aa start loop in init, use before loop, waiting on bot ready, to set up needed data. 2023-09-09 11:38:03 -04:00
imcompiling e7cfe8bd4c Fix logging issue.
build_develop / build (push) Successful in 1m24s
2023-09-08 20:07:08 -04:00
imcompiling 2640ebdad1 Move current_election setting. The bot needs to be ready first.
build_develop / build (push) Successful in 1m22s
2023-09-08 11:42:34 -04:00
imcompiling c5c242de21 Correct comparison in get_current_cycle().
build_develop / build (push) Successful in 1m30s
build_latest / build (push) Successful in 47s
2023-09-06 14:14:33 -04:00
imcompiling 0710db0654 Merge branch 'main' into 19-birthday-tracking
build_develop / build (push) Successful in 1m27s
2023-09-03 21:13:14 -04:00
imcompiling 369ea0dd55 Add logging 2023-09-03 21:12:42 -04:00
imcompiling 077119acdd Add initial set birthday command. 2023-08-30 22:49:28 -04:00
imcompiling ac8d1bc681 Start update_birthdays function.
build_develop / build (push) Successful in 1m24s
2023-08-26 23:04:31 -04:00
imcompiling df8cb3dd49 Add message listener portion of birthday fun. 2023-08-26 22:52:02 -04:00
imcompiling 25833364d7 Whoops, period on wrong side of bracket.
build_latest / build (push) Successful in 42s
build_develop / build (push) Successful in 44s
2023-08-26 22:15:12 -04:00
imcompiling d73b671257 Copy utils.py into docker image. sort copying.
build_develop / build (push) Successful in 1m25s
build_latest / build (push) Successful in 44s
2023-08-26 21:39:07 -04:00
imcompiling f6e827fba5 Rename a directory, need to store the db file in a directory. 2023-08-26 21:36:18 -04:00
imcompiling 2991f81e04 Update python version.
build_develop / build (push) Successful in 2m4s
build_latest / build (push) Successful in 42s
2023-08-26 21:02:22 -04:00
imcompiling b1c0b7013d Update dockerfile to include database.
build_develop / build (push) Successful in 1m45s
build_latest / build (push) Successful in 42s
2023-08-26 20:52:31 -04:00
imcompiling 22ea29e8ea Merge branch '10-name-change-request'
build_develop / build (push) Successful in 42s
build_latest / build (push) Successful in 42s
2023-07-29 21:13:59 -04:00
imcompiling 8c20641041 Added doc strings for the approve and deny buttons.
build_develop / build (push) Successful in 1m24s
2023-07-29 21:13:14 -04:00
imcompiling fa3fc6bd75 Added logic to handle bot being unable to change a nickname due to role hierarchy. 2023-07-29 21:08:16 -04:00
imcompiling 271cf14697 Need to be able to manage nicknames. 2023-07-29 19:21:11 -04:00
imcompiling ee2aaba421 Must await the edit. 2023-07-29 19:16:59 -04:00
imcompiling 6ae722beb9 Need member intent. 2023-07-29 19:16:47 -04:00
imcompiling 757a753cf5 I want to edit the message, not send a new one. 2023-07-29 19:05:52 -04:00
imcompiling b292fc09fe Log into gitea registry.
build_develop / build (push) Successful in 1m41s
2023-07-29 18:30:05 -04:00
imcompiling 7789e679ce Flush out view with approve and deny actions. 2023-07-29 18:18:31 -04:00
imcompiling 9cae7d002a Create initial command action. 2023-07-29 16:35:03 -04:00
imcompiling f71df0b7f4 Add the admin channel id to config. 2023-07-29 15:41:12 -04:00
imcompiling 2d2631bfac Create request class. 2023-07-29 15:39:53 -04:00
imcompiling f9d6227db1 Add the birthday table. 2023-07-29 15:16:23 -04:00
imcompiling c9946c9afe Merge branch '20-finish-daily-loop' 2023-07-26 16:27:01 -04:00
imcompiling c5d148ad49 Daily loop is complete. Properly maintains the current election. 2023-07-26 16:24:57 -04:00
imcompiling 198d5acea0 Add an assumption to readme. 2023-07-25 16:06:27 -04:00
imcompiling 7e3b59ea59 clean up election_cycle constructor. 2023-07-25 16:00:49 -04:00
imcompiling babc5d95ab Merge branch '15-term-start-action' 2023-07-25 15:30:45 -04:00
imcompiling 0eae8afa2a Fill out term_start_action 2023-07-25 15:25:35 -04:00
imcompiling d79c990558 Remove term end action. 2023-07-25 15:11:33 -04:00
imcompiling 3465698b85 Merge branch '14-high-council-election-action' 2023-07-25 14:52:57 -04:00
imcompiling 1731bd46a6 Fill out highcouncil_election_start_action
build_develop / build (push) Has been cancelled
2023-07-25 14:52:48 -04:00
imcompiling 1b4c5780df Refactor other function to use get_election_info. 2023-07-25 14:51:17 -04:00
imcompiling d67940aa26 Create new function for getting election information. 2023-07-25 14:50:44 -04:00
imcompiling 9867f91268 Add the highcouncil vote start message. 2023-07-25 14:41:16 -04:00
imcompiling 4763651e93 Merge branch '13-voting-end-action' 2023-07-25 14:38:50 -04:00
37 changed files with 3203 additions and 309 deletions
+15 -3
View File
@@ -1,6 +1,10 @@
name: build_develop
on: [push]
on:
push:
branches:
- '*'
- '!main'
jobs:
build:
@@ -12,7 +16,15 @@ jobs:
fetch-depth: 0
- name: Install Docker
uses: https://github.com/papodaca/install-docker-action@v1
- name: Log into container registry
uses: docker/login-action@v2
with:
registry: gitea.cheezers.org
username: imcompiling
password: ${{ secrets.PASSWORD }}
- name: set env
run: echo "GIT_BRANCH=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_ENV
- name: Build Docker Image
run: docker build -t gitea.cheezers.org/imcompiling/dakiny-bot:develop .
run: docker build -t gitea.cheezers.org/imcompiling/dakiny-bot:${{env.GIT_BRANCH}} .
- name: Push Docker Image
run: docker push gitea.cheezers.org/imcompiling/dakiny-bot:develop
run: docker push gitea.cheezers.org/imcompiling/dakiny-bot:${{env.GIT_BRANCH}}
+6
View File
@@ -14,6 +14,12 @@ jobs:
fetch-depth: 0
- name: Install Docker
uses: https://github.com/papodaca/install-docker-action@v1
- name: Log into container registry
uses: docker/login-action@v2
with:
registry: gitea.cheezers.org
username: imcompiling
password: ${{ secrets.PASSWORD }}
- name: Build Docker Image
run: docker build -t gitea.cheezers.org/imcompiling/dakiny-bot:latest .
- name: Push Docker Image
+28
View File
@@ -0,0 +1,28 @@
name: build_release
on:
push:
tags: ["v[0-9].[0-9].[0-9]"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install Docker
uses: https://github.com/papodaca/install-docker-action@v1
- name: Log into container registry
uses: docker/login-action@v2
with:
registry: gitea.cheezers.org
username: imcompiling
password: ${{ secrets.PASSWORD }}
- name: Set env
run: echo "GIT_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Build Docker Image
run: docker build -t gitea.cheezers.org/imcompiling/dakiny-bot:${{env.GIT_TAG}} .
- name: Push Tagged Image
run: docker push gitea.cheezers.org/imcompiling/dakiny-bot:${{env.GIT_TAG}}
+3 -2
View File
@@ -130,7 +130,7 @@ dmypy.json
# logging
nextcord.log
# data file for bot
# bot files
highscores_data.json
highscores_config.json
deleted_backups/
@@ -139,4 +139,5 @@ TODO.txt
sql_tester.py
conf
outputs
dakiny.db
database
dakiny-messages/
+11 -9
View File
@@ -1,21 +1,23 @@
FROM python:3.8.10-slim
FROM python:3.10.6-slim
# install dependencies for mysqlclient
# RUN apt-get update && apt-get -y upgrade && apt-get -y install python3-dev default-libmysqlclient-dev build-essential
# install dependencies
RUN apt-get update && apt-get -y upgrade && apt-get -y install git
RUN pip install --upgrade pip
WORKDIR /home
COPY cogs cogs/
COPY dakiny_bot.py dakiny_bot.py
COPY requirements.txt requirements.txt
COPY config.py config.py
COPY main.py main.py
COPY utils.py utils.py
COPY bot_messages bot_messages/
COPY classes classes/
COPY cogs cogs/
COPY database_setup database_setup/
RUN pip install -r requirements.txt
# remove build deps that aren't needed to run
# RUN apt-get -y remove python3-dev build-essential && apt-get -y autoremove
CMD ["python3", "-u", "./dakiny_bot.py"]
CMD ["python3", "-u", "./main.py"]
+6 -47
View File
@@ -1,50 +1,9 @@
# Discord Bot Template
# Dakiny Bot
[![Discord](https://img.shields.io/discord/819650821314052106?color=7289DA&logo=discord&logoColor=white)](https://discord.gg/fPrdqh3Zfu "Dev Pro Tips Discussion & Support Server")
[![Powered by Nextcord](https://custom-icon-badges.herokuapp.com/badge/-Powered%20by%20Nextcord-0d1620?logo=nextcord)](https://github.com/nextcord/nextcord "Powered by Nextcord Python API Wrapper")
Dakiny clan's own discord bot!
This repo is a template for easy creation of maintainable Python Discord bots.
The library used is [Nextcord](https://github.com/nextcord/nextcord), a maintained fork of Discord.py.
Nextcord documentation: https://nextcord.readthedocs.io/en/latest/
📺 Python Discord Tutorial: https://www.youtube.com/playlist?list=PL9YUC9AZJGFG6larkQJYio_f0V-O1NRjy
## How to use
Click "use this template" at the top of this repo and follow the instructions, or alternatively, initialize a git repo and copy the template files into the directory.
```bash
# Create a new folder (replace my-discord-bot with your bot's name)
mkdir my-discord-bot && cd my-discord-bot
# Initialize the folder as a git repository and copy the template files
git init
cp ../discord-bot-template/* .
```
## Environment variables
To run your bot, you'll need a token and other secrets set in a `.env` file.
Create a file called `.env` and place it in the root of your project.
(You can do this by creating a copy of `.env.sample` and renaming it to `.env`)
The contents should look something like this (where the part after `=` is the token you received from the Discord Developer Portal)
```
DISCORD_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
```
If you require additional API keys or variables specific to an enviroment, they should also be added here. You can access them by adding a line such as `GUILD_ID = os.getenv("GUILD_ID", "")` to `config.py`.
## Heroku
`runtime.txt` and `Procfile` are used for Heroku configuration and can be deleted in case you do not plan on hosting there.
Heroku setup tutorial: https://www.youtube.com/watch?v=EreE-0hQibM
## IDE Configuration
IDE config such as the `.vscode` folder do not normally belong on GitHub since they are often specific to a particular environment. To make sure GitHub will ignore the `.vscode` folder uncomment the line at the end of the `.gitignore`.
### 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
exist in the database.
-7
View File
@@ -1,7 +0,0 @@
Here's the {quarter} Clan Council:
Dakiny Council:
{council}
Dakiny High Council:
{high_council}
-50
View File
@@ -1,50 +0,0 @@
```fix
The Dakiny's admin team 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 enough time to have participated in at least one election;
**ii)** have no warnings under their name
and
**iii)** have been active (hosted 1 clan event or participate in 1 clan event) in the clan in the past 12 weeks before current election.
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_date}
Term: {term_start_date} - {term_end_date}
**{election_start_date}**: Post here to announce candidacy
**{voting_start_date}**: Voting begins
**{voting_end_date}**: Votes are tallied and winners are announced
**{highcouncil_election_date}**: The new Clan Council votes who among them shall receive the two High Council positions of: Clan Leader and Discord Server Manager.
**{term_start_date}**: 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_date} with the inauguration scheduled for {term_end_date}.
---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 apply to all candidates of that category.
Votes can be cast until the {highcouncil_election_end}, so we can announce and have everyone set up to start the term by {term_start}
-14
View File
@@ -1,14 +0,0 @@
====================
<@&{clan_member_id}>, it's time to vote for clan council.
React with <{voting_emote_id}> to vote.
Voting ends next {voting_end_day}, {voting_end_date}.
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
+346 -83
View File
@@ -2,16 +2,34 @@
Contains the election cycle class
"""
import collections
from datetime import datetime, timedelta
import nextcord
from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy import select, true, update
from sqlalchemy.orm import Session
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 config import DakinyConfig
from database.sql_data_classes import ElectionInformation, engine
from utils import get_datetime
from database_setup.sql_data_classes import (
CouncilMembers,
ElectionInformation,
Votes,
engine,
)
from utils import (
TEMPLATES_DIR,
build_highcouncil_votes_str,
build_votes_str,
edit_clan_council_info,
get_cl_election_name,
get_datetime,
get_dm_election_name,
get_election_cycle_name,
)
class ElectionCycle:
@@ -20,28 +38,35 @@ class ElectionCycle:
the cycle.
"""
def __init__(self, cycle: int, bot: commands.Bot) -> None:
def __init__(self, cycle: int, config: DakinyConfig, guild: nextcord.Guild) -> None:
"""
Generates all dates relevant to the election cycle from input election start, term start and end dates.
Generates all dates relevant to the election cycle determined by the config and cycle.
Args:
election_start (datetime): Date the election cycle starts.
term_start (datetime): Date the term being elected starts.
term_end (datetime): Date the term being elected ends.
bot: the discord bot instance
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 = bot.config
self.config: DakinyConfig = config
self.cycle = cycle
self.elecion_cycle = self.config.election_cycles[self.cycle]
self.election_start: datetime = get_datetime(self.elecion_cycle.election_start)
self.term_start: datetime = get_datetime(self.elecion_cycle.term_start)
self.term_end: datetime = get_datetime(self.elecion_cycle.term_end)
self.voting_start: datetime = get_datetime(self.elecion_cycle.voting_start)
self.voting_end: datetime = get_datetime(self.elecion_cycle.voting_end)
self.highcouncil_election_announce: datetime = self.voting_end + timedelta(days=1)
self.highcouncil_election_start: datetime = self.highcouncil_election_announce + timedelta(days=3)
self.highcouncil_election_end: datetime = self.term_start - timedelta(days=1)
self.guild: nextcord.Guild = bot.get_guild(self.config.params.guild_id)
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]:
"""
@@ -52,21 +77,27 @@ class ElectionCycle:
long.
"""
with open("bot_messages/election_announcement.txt", mode="r", encoding="utf8") as file:
with open(
f"{TEMPLATES_DIR}/election_announcement.txt", mode="r", encoding="utf8"
) as file:
message = file.read()
next_election = get_datetime(self.config.election_cycles[(self.cycle + 1) % 4].election_start)
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_date=self.election_start.strftime("%B %d"),
term_start_date=self.term_start.strftime("%B %d"),
term_end_date=self.term_end.strftime("%B %d"),
voting_start_date=self.voting_start.strftime("%B %d"),
voting_end_date=self.voting_end.strftime("%B %d"),
highcouncil_election_date=self.highcouncil_election_start.strftime("%B %d"),
next_election_start_date=next_election.strftime("%B %d"),
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---")
@@ -79,15 +110,41 @@ class ElectionCycle:
str: Message to send.
"""
with open("bot_messages/highcouncil_election.txt", mode="r", encoding="utf8") as file:
with open(
f"{TEMPLATES_DIR}/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_date=self.term_start.strftime("%B %d"),
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(
f"{TEMPLATES_DIR}/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:
@@ -98,14 +155,15 @@ class ElectionCycle:
str: Message to send
"""
with open("bot_messages/vote_start.txt", mode="r", encoding="utf8") as file:
with open(f"{TEMPLATES_DIR}/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)
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_emote_id=self.config.params.voting_emote_id,
voting_end_date=vote_end.strftime("%B %d"),
voting_end=vote_end.strftime("%B %d"),
voting_end_day=vote_end.strftime("%A"),
)
@@ -119,90 +177,295 @@ class ElectionCycle:
if today.date() == self.election_start.date():
await self.election_start_action()
elif today.date() == self.voting_start.date():
await self.voting_end_action()
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_action()
await self.highcouncil_election_start_action()
elif today.date() == self.term_start.date():
await self.term_start_action()
elif today.date() == self.term_end.date():
await self.term_end_action()
async def election_start_action(self) -> None:
"""
Performs required actions for when the election cycle starts.
"""
# create new channel for election
category: nextcord.CategoryChannel = self.guild.get_channel(self.config.params.active_category_id)
quarter_name: str = self.get_election_cycle_name()
parent: nextcord.TextChannel = self.guild.get_channel(
self.config.params.voting_parent_id
)
quarter_name: str = get_election_cycle_name(self.config)
# create new entry into database.
election_channel = await category.create_text_channel(quarter_name)
with Session(engine) as session:
new_cycle = ElectionInformation(ElectionCycle=quarter_name, ElectionChannel=election_channel.id)
session.add(new_cycle)
session.commit()
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 message in messages:
await election_channel.send(message)
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.config, self.guild)
)
else:
await thread.send(message)
# create new entry into database.
with Session(engine) as session:
new_cycle = ElectionInformation(
ElectionCycle=quarter_name,
ThreadId=thread.id,
CouncilNominationMsgId=nominate_msg.id,
)
session.add(new_cycle)
session.commit()
async def voting_start_action(self) -> None:
"""
Sends message informing members that voting has started.
Removes write permission from the general clan member role.
Adds the voting emote to all messages not sent by the vote. (This is temporary until voting is managed.)
"""
message = self.build_vote_start()
clan_member_role: nextcord.Role = self.guild.get_role(self.config.params.clan_member_role_id)
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, self.guild),
view=VoteView(self.config, self.guild),
)
with Session(engine) as session:
stmt = select(ElectionInformation).where(
ElectionInformation.ElectionCycle == self.get_election_cycle_name()
ElectionInformation.ElectionCycle
== get_election_cycle_name(self.config)
)
election_info = session.scalar(stmt)
election_channel = self.guild.get_channel(election_info.ElectionChannel)
await election_channel.send(message)
await election_channel.set_permissions(clan_member_role, send_messages=False)
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(self.config.params.clan_council_elect_role_id)
with Session(engine) as session:
stmt = select(ElectionInformation).where(
ElectionInformation.ElectionCycle == self.get_election_cycle_name()
)
election_info = session.scalar(stmt)
election_channel = self.guild.get_channel(election_info.ElectionChannel)
await election_channel.send(message)
await election_channel.set_permissions(council_elect_role, send_messages=True)
async def highcouncil_election_action(self) -> None:
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
)
nominate_msg: nextcord.Message = await election_thread.send(
message, view=NominateHighCouncilView(self.config, self.guild)
)
election_name = get_election_cycle_name(self.config)
with Session(engine) as session:
# remove vote button from vote message
stmt = select(ElectionInformation).where(
ElectionInformation.ElectionCycle == 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.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()
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.
"""
print("Highcouncil election has started.")
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.config, self.guild),
view=VoteHighcouncilView(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.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:
msg: str = "the term has started"
print(msg)
async def term_end_action(self):
msg: str = "the term has ended"
print(msg)
def get_election_cycle_name(self) -> str:
"""
Builds the unique name of the current election cycle
Marks the election as over.
"""
self.election_over = True
elect_role: nextcord.Role = self.guild.get_role(
self.config.params.clan_council_elect_role_id
)
council_role: nextcord.Role = self.guild.get_role(
self.config.params.clan_council_role_id
)
with Session(engine) as session:
# role management
stmt = (
select(CouncilMembers.MemberId)
.where(
CouncilMembers.ElectionCycle == get_election_cycle_name(self.config)
)
.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.
dm_election = get_dm_election_name(self.config)
cl_election = get_cl_election_name(self.config)
stmt = select(Votes.VotedForId).where(Votes.ElectionCycle == dm_election)
dm_votes = collections.Counter(session.scalars(stmt))
stmt = select(Votes.VotedForId).where(Votes.ElectionCycle == cl_election)
cl_votes = collections.Counter(session.scalars(stmt))
election_cycle = get_election_cycle_name(self.config)
try:
dm_winner = max(dm_votes, key=dm_votes.get)
stmt = (
update(CouncilMembers)
.where(CouncilMembers.ElectionCycle == election_cycle)
.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.ElectionCycle == election_cycle)
.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.ElectionCycle == get_election_cycle_name(self.config)
)
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!\n Clan 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, self.config)
def get_election_info(self) -> ElectionInformation:
"""
Gets information for current election.
Returns:
str: the name of the election cycle
ElectionInformation: The information from the database about the current election.
"""
return f"{self.election_start.year} {self.elecion_cycle.name} 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)
+266
View File
@@ -0,0 +1,266 @@
"""
Contains various views to be used in the high council nomination process.
"""
import nextcord
from nextcord.interactions import Interaction
from sqlalchemy import select
from sqlalchemy.orm import Session
from config import DakinyConfig
from database_setup.sql_data_classes import CouncilMembers, engine
from utils import (
NOMINEE_SPLITTER,
decorate_cl_nomination,
decorate_dm_nomination,
get_election_cycle_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,
config: DakinyConfig,
usr_id: int,
usr_nominated: bool,
speech_content: str,
):
super().__init__(
"Discord Manager Nomination",
timeout=5 * 60, # 5 minutes
)
self.usr_id: int = usr_id
self.config: DakinyConfig = config
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
cycle = get_election_cycle_name(self.config)
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionCycle == cycle)
.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,
config: DakinyConfig,
usr_id: int,
usr_nominated: bool,
speech_content: str,
):
super().__init__(
"Clan Leader Nomination",
timeout=5 * 60, # 5 minutes
)
self.usr_id: int = usr_id
self.config: DakinyConfig = config
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
cycle = get_election_cycle_name(self.config)
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionCycle == cycle)
.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, config: DakinyConfig, guild: nextcord.Guild):
super().__init__(timeout=None)
self.config: DakinyConfig = config
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:
"""
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(self.config.params.clan_council_elect_role_id)
not in interaction.user.roles
):
await interaction.response.send_message(
f"You must have the <@&{self.config.params.clan_council_elect_role_id}> role to vote.",
ephemeral=True,
)
return
cycle = get_election_cycle_name(self.config)
usr_nominated = False
speech_content = ""
with Session(engine) as session:
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionCycle == cycle)
.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(
self.config, 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:
"""
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(self.config.params.clan_council_elect_role_id)
not in interaction.user.roles
):
await interaction.response.send_message(
f"You must have the <@&{self.config.params.clan_council_elect_role_id}> role to vote.",
ephemeral=True,
)
return
cycle = get_election_cycle_name(self.config)
usr_nominated = False
speech_content = ""
with Session(engine) as session:
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionCycle == cycle)
.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(
self.config, interaction.user.id, usr_nominated, speech_content
)
)
+142
View File
@@ -0,0 +1,142 @@
"""
Contains various views to be used in the nomination process.
"""
import nextcord
from nextcord.interactions import Interaction
from sqlalchemy import select
from sqlalchemy.orm import Session
from config import DakinyConfig
from database_setup.sql_data_classes import CouncilMembers, engine
from utils import NOMINEE_SPLITTER, decorate_nomination, get_election_cycle_name
class NominateModal(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,
config: DakinyConfig,
usr_id: int,
usr_nominated: bool,
speech_content: str,
):
super().__init__(
"Nomination",
timeout=5 * 60, # 5 minutes
)
self.usr_id: int = usr_id
self.config: DakinyConfig = config
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_nomination(interaction.user.id, self.speech.value)
with Session(engine) as session:
if not self.usr_nominated:
msg = await interaction.channel.send(speech)
council_member = CouncilMembers(
ElectionCycle=get_election_cycle_name(self.config),
MemberId=interaction.user.id,
SpeechMsgId=msg.id,
)
session.add(council_member)
session.commit()
else:
# user already nominated, just need to update the election speech
cycle = get_election_cycle_name(self.config)
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionCycle == cycle)
.where(CouncilMembers.MemberId == self.usr_id)
)
obj = session.scalar(stmt)
msg: nextcord.Message = await interaction.channel.fetch_message(
obj.SpeechMsgId
)
await msg.edit(
content=decorate_nomination(interaction.user.id, self.speech.value)
)
class NominateView(nextcord.ui.View):
"""
The button to press to start your nomination.
"""
def __init__(self, config: DakinyConfig, guild: nextcord.Guild):
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",
)
async def nominate(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
) -> None:
"""
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 members only
if (
self.guild.get_role(self.config.params.clan_member_role_id)
not in interaction.user.roles
):
await interaction.response.send_message(
f"You must have the <@&{self.config.params.clan_member_role_id}> role to vote.",
ephemeral=True,
)
return
cycle = get_election_cycle_name(self.config)
usr_nominated = False
speech_content = ""
with Session(engine) as session:
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionCycle == cycle)
.where(CouncilMembers.MemberId == interaction.user.id)
)
obj = session.scalar(stmt)
if obj is not None:
usr_nominated = True
msg = await interaction.channel.fetch_message(obj.SpeechMsgId)
speech_content = msg.content.split(NOMINEE_SPLITTER)[1]
await interaction.response.send_modal(
NominateModal(
self.config, interaction.user.id, usr_nominated, speech_content
)
)
+259
View File
@@ -0,0 +1,259 @@
"""
Contains various views to be used while voting for high council. Controls the logic when performing the vote.
"""
from typing import List
import nextcord
from nextcord import SelectOption
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 (
CouncilMembers,
ElectionInformation,
Votes,
engine,
)
from utils import (
build_highcouncil_votes_str,
get_cl_election_name,
get_dm_election_name,
get_election_cycle_name,
)
class VoteSelect(nextcord.ui.StringSelect):
def __init__(
self, options: List["SelectOption"], config: DakinyConfig, election_type: str
) -> None:
super().__init__(
placeholder="Choose 1",
min_values=1,
max_values=1,
options=options,
)
self.config = config
self.election_type = election_type
async def callback(self, interaction: Interaction):
# decide on election type
match self.election_type:
case "cl":
election_name: str = get_cl_election_name(self.config)
case "dm":
election_name: str = get_dm_election_name(self.config)
case _:
# should never happen
raise ValueError('election type was not "cl" or "dm"')
cycle: str = get_election_cycle_name(self.config)
with Session(engine) as session:
stmt = select(ElectionInformation.HighCouncilVoteMsgId).where(
ElectionInformation.ElectionCycle == cycle
)
vote_msg_id = session.scalar(stmt)
if self.values[0] == "Abstain":
voted_for = None
else:
voted_for = int(self.values[0])
new_vote = Votes(
ElectionCycle=election_name,
VoterId=interaction.user.id,
VotedForId=voted_for,
)
try:
session.add(new_vote)
session.commit()
except exc.IntegrityError:
session.rollback()
stmt = (
update(Votes)
.where(Votes.ElectionCycle == election_name)
.where(Votes.VoterId == interaction.user.id)
.values(VotedForId=voted_for)
)
session.execute(stmt)
session.commit()
vote_message_str = build_highcouncil_votes_str(self.config, interaction.guild)
vote_msg: nextcord.Message = await interaction.channel.fetch_message(
vote_msg_id
)
await vote_msg.edit(content=vote_message_str)
if voted_for is not None:
msg = f"You have voted for <@{voted_for}>"
else:
msg = "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, config: DakinyConfig, guild: nextcord.Guild):
super().__init__(timeout=None)
self.config = config
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:
"""
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(self.config.params.clan_council_elect_role_id)
not in interaction.user.roles
):
await interaction.response.send_message(
f"You must have the <@&{self.config.params.clan_council_elect_role_id}> role to vote.",
ephemeral=True,
)
return
cycle: str = get_election_cycle_name(self.config)
election: str = get_cl_election_name(self.config)
with Session(engine) as session:
stmt = (
select(Votes.VotedForId)
.where(Votes.ElectionCycle == election)
.where(Votes.VoterId == interaction.user.id)
)
voted_for = session.scalar(stmt)
stmt = (
select(CouncilMembers.MemberId)
.where(CouncilMembers.ElectionCycle == cycle)
.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,
)
]
options.append(
SelectOption(
label="Abstain ☑️" if voted_for is None else "Abstain",
value="Abstain",
)
)
view = nextcord.ui.View()
view.add_item(
VoteSelect(options=options, config=self.config, election_type="cl")
)
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:
"""
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(self.config.params.clan_council_elect_role_id)
not in interaction.user.roles
):
await interaction.response.send_message(
f"You must have the <@&{self.config.params.clan_council_elect_role_id}> role to vote.",
ephemeral=True,
)
return
cycle: str = get_election_cycle_name(self.config)
election: str = get_dm_election_name(self.config)
with Session(engine) as session:
stmt = (
select(Votes.VotedForId)
.where(Votes.ElectionCycle == election)
.where(Votes.VoterId == interaction.user.id)
)
voted_for = session.scalar(stmt)
stmt = (
select(CouncilMembers.MemberId)
.where(CouncilMembers.ElectionCycle == cycle)
.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,
)
]
options.append(
SelectOption(
label="Abstain ☑️" if voted_for is None else "Abstain",
value="Abstain",
)
)
view = nextcord.ui.View()
view.add_item(
VoteSelect(options=options, config=self.config, election_type="dm")
)
await interaction.response.send_message(
content="Please vote for a Discord Manager!",
view=view,
ephemeral=True,
)
+157
View File
@@ -0,0 +1,157 @@
"""
Contains various views to be used while voting. Controls the logic when performing the vote.
"""
from typing import List
import nextcord
from nextcord import SelectOption
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 (
CouncilMembers,
ElectionInformation,
Votes,
engine,
)
from utils import build_votes_str, get_election_cycle_name
class VoteSelect(nextcord.ui.StringSelect):
def __init__(self, options: List["SelectOption"], config: DakinyConfig) -> None:
super().__init__(
placeholder="Choose 1",
min_values=1,
max_values=1,
options=options,
)
self.config = config
async def callback(self, interaction: Interaction):
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)
if self.values[0] == "Abstain":
voted_for = None
else:
voted_for = int(self.values[0])
new_vote = Votes(
ElectionCycle=election_name,
VoterId=interaction.user.id,
VotedForId=voted_for,
)
try:
session.add(new_vote)
session.commit()
except exc.IntegrityError:
session.rollback()
stmt = (
update(Votes)
.where(Votes.ElectionCycle == election_name)
.where(Votes.VoterId == interaction.user.id)
.values(VotedForId=voted_for)
)
session.execute(stmt)
session.commit()
vote_message_str = build_votes_str(self.config, interaction.guild)
vote_msg: nextcord.Message = await interaction.channel.fetch_message(
vote_msg_id
)
await vote_msg.edit(content=vote_message_str)
if voted_for is not None:
msg = f"You have voted for <@{voted_for}>"
else:
msg = "You have abstained from voting."
await interaction.edit(content=msg, view=None)
class VoteView(nextcord.ui.View):
"""
The button to press to start your nomination.
"""
def __init__(self, config: DakinyConfig, guild: nextcord.Guild):
super().__init__(timeout=None)
self.config = config
self.guild = guild
@nextcord.ui.button(
label="Vote", style=nextcord.ButtonStyle.primary, custom_id="vote"
)
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),
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 members only
if (
self.guild.get_role(self.config.params.clan_member_role_id)
not in interaction.user.roles
):
await interaction.response.send_message(
f"You must have the <@&{self.config.params.clan_member_role_id}> role to vote.",
ephemeral=True,
)
return
election: str = get_election_cycle_name(self.config)
with Session(engine) as session:
stmt = (
select(Votes.VotedForId)
.where(Votes.ElectionCycle == election)
.where(Votes.VoterId == interaction.user.id)
)
voted_for = session.scalar(stmt)
stmt = select(CouncilMembers.MemberId).where(
CouncilMembers.ElectionCycle == election
)
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,
)
]
options.append(
SelectOption(
label="Abstain ☑️" if voted_for is None else "Abstain",
value="Abstain",
)
)
view = nextcord.ui.View()
view.add_item(VoteSelect(options=options, config=self.config))
await interaction.response.send_message(
content="Please vote for a clan member!",
view=view,
ephemeral=True,
)
+82
View File
@@ -0,0 +1,82 @@
"""
File contains logic for creating a webhook listener, pulling from git, and updating all messages.
"""
import os
import nextcord
import uvicorn
from fastapi import FastAPI
from git import Repo
from nextcord.ext import commands
from config import DakinyConfig
from utils import REPO_DIR, update_clan_messages
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.config: DakinyConfig = self.bot.config
self.repo_setup()
@commands.Cog.listener()
async def on_ready(self):
"""
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"])
config = uvicorn.Config(app=app, host="0.0.0.0", port=5000, log_level="info")
server = uvicorn.Server(config)
await server.serve()
async def update_messages(self):
"""
Pulls from git and then calls method to update all the discord messages as described by the
files in the git repo.
"""
self.origin.pull()
guild: nextcord.Guild = self.bot.get_guild(self.config.params.guild_id)
await update_clan_messages(guild)
return {"message": "Successfully updated all discord messages."}
def repo_setup(self):
"""
Checks if the repo exists and creates repo object, clones if it does not.
"""
# check if repo has been cloned. open repo object
if os.path.isdir(REPO_DIR):
self.repo = Repo(REPO_DIR)
else:
self.repo = Repo.clone_from(REPO_URL, REPO_DIR)
self.origin = self.repo.remote("origin")
if self.config.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):
"""Adds function to bot"""
bot.add_cog(ClanMessages(bot))
+13
View File
@@ -4,7 +4,13 @@ Cog loader. Adds all command cogs to the bot.
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.set import setCog
from cogs.commands.test import TestCog
from cogs.commands.view import ViewCog
# This function will be called when this extension is loaded.
@@ -13,4 +19,11 @@ def setup(bot: commands.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(TestCog(bot))
bot.add_cog(ViewCog())
bot.add_cog(setCog())
+218
View File
@@ -0,0 +1,218 @@
"""
Contained commands and views for record commands.
"""
import nextcord
from discord import TextInputStyle
from nextcord.ext import commands
from config import DakinyConfig
from utils import record_event_host, record_event_participation
class RecordCog(commands.Cog, name="Record Commands"):
"""
Class holds record commands.
"""
def __init__(self, bot) -> None:
"""
Default constructor for RecordCog class. Does nothing.
"""
super().__init__()
self.bot: commands.Bot = bot
@nextcord.slash_command()
async def record(self, interaction: nextcord.Interaction):
"""
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.",
),
):
"""
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(self.bot.config.params.event_host_role_id)
not in interaction.user.roles
):
await interaction.response.send_message(
f"You must have the <@&{self.bot.config.params.event_host_role_id}> 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, self.bot.config, host.id),
ephemeral=True,
embed=embed,
)
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, config: DakinyConfig, host_id: int):
super().__init__()
self.name: str = name
self.config: DakinyConfig = config
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
):
"""
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):
"""
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
):
"""
Submits the event embed to the event channel. Removes view and embed from recording message.
Sends point information to database.
"""
discord_users = self.event_participants.values.members
if discord_users:
discord_users_list: list[str] = [
str(user.id) for user in discord_users if user.id is not self.host_id
]
record_event_participation(self.config, discord_users_list, True)
non_discord_users = interaction.message.embeds[0].fields[1].value
if non_discord_users:
non_discord_users_list: list[str] = non_discord_users.split("\n")
record_event_participation(self.config, non_discord_users_list, False)
record_event_host(self.config, self.host_id)
event_channel: nextcord.TextChannel = interaction.guild.get_channel(
self.config.params.event_post_channel_id
)
await event_channel.send(embed=interaction.message.embeds[0])
await interaction.response.edit_message(
content="Event has been submitted.", view=None, embed=None
)
self.stop()
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.
"""
# TODO turn this embed into a dict
def __init__(self, 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):
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):
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)
+54
View File
@@ -0,0 +1,54 @@
"""
Contained commands and views for refresh commands.
"""
import nextcord
from nextcord.ext import commands
from utils import edit_clan_council_info, update_clan_messages
class RefreshCog(commands.Cog, name="Refresh Commands"):
"""
Class holds refresh commands.
"""
def __init__(self, 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):
"""
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):
"""
Calls update_clan_messages() to update all messages.
Args:
interaction (nextcord.Interaction): Interaction with the command.
"""
await interaction.response.defer(ephemeral=True)
await update_clan_messages(interaction.guild)
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):
"""
Calls edit_clan_council_info().
Args:
interaction (nextcord.Interaction): Interaction with the command.
"""
await edit_clan_council_info(interaction.guild, self.bot.config)
await interaction.response.send_message(
"Clan Council Info message updated.", ephemeral=True
)
+86
View File
@@ -0,0 +1,86 @@
"""
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):
"""
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
),
):
"""
Deletes the given presence id from the database.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
presence (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
):
"""
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])
+202
View File
@@ -0,0 +1,202 @@
"""
Contains commands and views for request commands.
"""
import nextcord
from nextcord.ext import commands
from sqlalchemy.orm import Session
from config import DakinyConfig
from database_setup.sql_data_classes import Presences, engine
class RequestCog(commands.Cog, name="Request Commands"):
"""
Class holds request commands.
"""
def __init__(self, bot) -> None:
"""
Default constructor for RequestCog class. Does nothing.
"""
self.bot: commands.Bot = bot
@nextcord.slash_command()
async def request(self, interaction: nextcord.Interaction):
"""
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.",
),
):
"""
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.
"""
config: DakinyConfig = self.bot.config
content = f'<@{interaction.user.id}> would like to add presence "{presence}."'
view = PresenceChange(presence)
await self.bot.get_channel(config.params.admin_channel_id).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")
async def change_nickname(
self,
interaction: nextcord.Interaction,
new_nickname: str = nextcord.SlashOption(
required=True,
min_length=2,
max_length=32,
description="Your requested new nickname.",
),
):
"""
Sends a message to admins to request a nickname change.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
new_username(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}."'
view = UsernameRequest(interaction.user.id, new_nickname)
await self.bot.get_channel(config.params.admin_channel_id).send(
content=content, view=view
)
await interaction.response.send_message(
"Request sent to admins.", ephemeral=True
)
class UsernameRequest(nextcord.ui.View):
"""
View provides approve and deny buttons and action for a username change request.
"""
def __init__(
self,
user_id: int,
new_nickname: str,
):
super().__init__(timeout=None, auto_defer=True)
self.user_id = user_id
self.new_nickname = new_nickname
@nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green)
async def approve(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
):
"""
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.
Removes the view from the interaction message.
Args:
button (nextcord.ui.Button): Unused.
interaction (nextcord.Interaction): The interaction of the user clicking the button.
"""
user = interaction.guild.get_member(self.user_id)
if user is not None:
try:
await user.edit(nick=self.new_nickname)
content = (
interaction.message.content
+ f"\nApproved by <@{interaction.user.id}>."
)
await interaction.response.edit_message(content=content, view=None)
except nextcord.errors.Forbidden:
await interaction.response.send_message(
content="Please change manually.",
ephemeral=True,
)
content = (
interaction.message.content
+ "\n\nFailed. User has too high permissions for bot to change nickname."
)
await interaction.followup.edit_message(
interaction.message.id, content=content, view=None
)
else:
await interaction.response.edit_message(
content="An error occured while changing the user's nickname.",
view=None,
)
@nextcord.ui.button(label="Deny", style=nextcord.ButtonStyle.red)
async def deny(self, button: nextcord.ui.Button, interaction: nextcord.Interaction):
"""
Deny button for a nickname 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)
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
):
"""
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):
"""
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)
+101
View File
@@ -0,0 +1,101 @@
"""
Contains commands and views for set commands.
"""
from datetime import datetime
import nextcord
from nextcord.ext import commands
from sqlalchemy import delete, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from database_setup.sql_data_classes import UserBirthday, engine
from main import birthday_list
class setCog(commands.Cog, name="Set Commands"):
"""
Class holds set commands.
"""
def __init__(self) -> None:
"""
Default constructor for SetCog class. Does nothing.
"""
@nextcord.slash_command()
async def set(self, interaction: nextcord.Interaction):
"""
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."
)
async def birthday(
self,
interaction: nextcord.Interaction,
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."
),
):
"""
Sets a user's birthday in the database. Removes on input 0/0. If birthday is today, adds to the birthday_list.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
month (int, optional): The month 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.
await interaction.response.defer(ephemeral=True)
if month == 0 and day == 0:
stmt = delete(UserBirthday).where(
UserBirthday.UserId == interaction.user.id
)
with Session(engine) as session:
session.execute(stmt)
session.commit()
await interaction.followup.send("Removed birthday.", ephemeral=True)
return
# check if valid day
today = datetime.today()
try:
birthday = datetime(today.year, month, day)
except ValueError:
await interaction.followup.send(
f"Invalid date: {month}/{day}.", ephemeral=True
)
return
# If the birthday is today, add to birthday_list
if today.date() == birthday.date():
birthday_list.append(interaction.user.id)
new_birthday = UserBirthday(
UserId=interaction.user.id, Birthday=f"{month}/{day}"
)
with Session(engine) as session:
try:
session.add(new_birthday)
session.commit()
except IntegrityError:
# User already has birthday, change it instead.
session.rollback()
stmt = select(UserBirthday).where(
UserBirthday.UserId == interaction.user.id
)
new_birthday = session.scalar(stmt)
new_birthday.Birthday = f"{month}/{day}"
session.commit()
await interaction.followup.send(
f"Birthday set to {month}/{day}.", ephemeral=True
)
+96 -21
View File
@@ -3,9 +3,22 @@ Commands for testing.
"""
import nextcord
from nextcord.ext import commands
from nextcord.ext import application_checks, commands
from classes.election_cycle import ElectionCycle
from config import DakinyConfig
from utils import get_current_election_cycle
def is_me():
"""
predicate that makes sure only I can run the test commands.
"""
def predicate(interaction: nextcord.Interaction):
return interaction.user.id == 206602713511886849
return application_checks.check(predicate)
class TestCog(commands.Cog, name="Test commands."):
@@ -19,30 +32,14 @@ class TestCog(commands.Cog, name="Test commands."):
"""
self.bot = bot
@nextcord.slash_command()
@nextcord.slash_command(guild_ids=[1000247361639030804])
async def test(self, interaction: nextcord.Interaction):
"""
Parent slash command for help. Never called.
"""
@test.subcommand()
async def msg_builder(self, interaction: nextcord.Interaction):
"""
Tests the message builder.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
"""
cycle = ElectionCycle(0, self.bot)
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
@@ -51,10 +48,16 @@ class TestCog(commands.Cog, name="Test commands."):
interaction (nextcord.Interaction): _description_
"""
cycle = ElectionCycle(0, self.bot)
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(
get_current_election_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
await cycle.election_start_action()
@test.subcommand()
@is_me()
async def voting_start(self, interaction: nextcord.Interaction):
"""
tests the voting start action
@@ -62,5 +65,77 @@ class TestCog(commands.Cog, name="Test commands."):
Args:
interaction (nextcord.Interaction): _description_
"""
cycle = ElectionCycle(0, self.bot)
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(
get_current_election_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
await cycle.voting_start_action()
@test.subcommand()
@is_me()
async def voting_end(self, interaction: nextcord.Interaction):
"""
tests the voting start action
Args:
interaction (nextcord.Interaction): _description_
"""
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(
get_current_election_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
await cycle.voting_end_action()
@test.subcommand()
@is_me()
async def highcouncil_vote(self, interaction: nextcord.Interaction):
"""
tests the voting start action
Args:
interaction (nextcord.Interaction): _description_
"""
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(
get_current_election_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
await cycle.highcouncil_election_start_action()
@test.subcommand()
@is_me()
async def election_reminder(self, interaction: nextcord.Interaction):
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(
get_current_election_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
await cycle.election_reminder()
@test.subcommand()
@is_me()
async def daily(self, interaction: nextcord.Interaction):
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(
get_current_election_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
await cycle.check_date()
@test.subcommand()
@is_me()
async def term_start(self, interaction: nextcord.Interaction):
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(
get_current_election_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
await cycle.term_start_action()
+237
View File
@@ -0,0 +1,237 @@
"""
Commands for viewing information in the database.
"""
import nextcord
import pandas as pd
from nextcord.ext import commands
from sqlalchemy import select
from sqlalchemy.orm import Session
from database_setup.sql_data_classes import (
ParticipationPoints,
Presences,
Votes,
engine,
)
class ViewCog(commands.Cog, name="View Commands"):
"""
Holds the view commands.
"""
def __init__(self) -> None:
"""
Default constructor for ViewCog class. Does nothing.
"""
@nextcord.slash_command()
async def view(self, interaction: nextcord.Interaction):
"""
Parent slash command for set. Never called.
"""
@view.subcommand(description="Show event points.")
async def event_points(
self,
interaction: nextcord.Interaction,
quarter_name: str = nextcord.SlashOption(
description="The quarter to view",
required=False,
),
):
# just in case
await interaction.response.defer(ephemeral=True)
with Session(engine) as session:
if quarter_name:
df = pd.read_sql_query(
sql=session.query(ParticipationPoints)
.where(ParticipationPoints.Cycle.startswith(quarter_name))
.statement,
con=engine,
)
else:
df = pd.read_sql_query(
sql=session.query(ParticipationPoints).statement,
con=engine,
)
if df.empty:
await interaction.followup.send(
f'Quarter "{quarter_name}" is invalid.', ephemeral=True
)
return
cycles = df.groupby("Cycle")
content = ""
for cycle in cycles.groups:
content += f"**{cycle}**\n"
unsorted_all = cycles.get_group(cycle).groupby("DiscordMember")
members = unsorted_all.get_group(True)
non_members = unsorted_all.get_group(False)
sorted_members = members.sort_values(
by="UserId",
key=lambda user_ids: (
[
interaction.guild.get_member(int(id)).display_name
for id in user_ids
]
),
)
sorted_non_members = non_members.sort_values(by="UserId")
sorted_all = pd.concat(
[sorted_members, sorted_non_members], ignore_index=True, sort=False
)
for row in sorted_all.itertuples():
userid = row.UserId
points = row.Points
discord_member = row.DiscordMember
content += (
f"<@{userid}>: {points}\n"
if discord_member
else f"{userid}: {points}\n"
)
content += "\n"
if len(content) > 2000:
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,
),
):
"""
Gathers all presences from the database and responds with a message showing them.
Args:
interaction (nextcord.Integration): User's interaction with the command.
"""
# 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(
self,
interaction: nextcord.Interaction,
election_name: str = nextcord.SlashOption(
required=True, description="The election you want to view votes for."
),
):
"""
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
autocompletion to assist with getting elections.
Args:
interaction (nextcord.Interaction): User's interaction with the command.
election_name (str): Is SlashOption. The name of the election to view votes for.
"""
stmt = select(Votes).where(Votes.ElectionCycle == election_name)
with Session(engine) as session:
# Check if election exists.
if session.scalar(stmt) is None:
await interaction.response.send_message(
f"Invalid election: {election_name}.", ephemeral=True
)
return
votes = session.scalars(stmt)
content = ""
for vote in votes:
content += (
f"<@{vote.VoterId}> voted for <@{vote.VotedForId}>.\n"
if vote.VotedForId is not None
else f"<@{vote.VoterId}> abstained from voted.\n"
)
await interaction.response.send_message(content=content, ephemeral=True)
@votes.on_autocomplete("election_name")
async def election_autocomplete(
self, interaction: nextcord.Interaction, election_name: str
):
"""
Collects all elections that start with the election string.
Args:
interaction (nextcord.Interaction): Interaction with command.
election_name (str): String to run autocomplete on.
"""
with Session(engine) as session:
stmt = select(Votes.ElectionCycle).distinct()
elections = session.scalars(stmt)
# if empty compare string
if not election_name:
await interaction.response.send_autocomplete(list(elections)[:25])
return
autofilled_elections: "list[str]" = [
election
for election in elections
if election.lower().startswith(election_name.lower())
]
await interaction.response.send_autocomplete(autofilled_elections[:25])
@event_points.on_autocomplete("quarter_name")
async def event_points_autocomplete(
self, interaction: nextcord.Interaction, quarter_name: str
):
"""
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(ParticipationPoints.Cycle).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])
+3 -1
View File
@@ -4,7 +4,8 @@ Initializing code for all loops.
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):
@@ -12,4 +13,5 @@ def setup(bot: commands.Bot):
Adds functions to bot.
"""
bot.add_cog(BotPresence(bot))
bot.add_cog(DailyLoop(bot))
+48
View File
@@ -0,0 +1,48 @@
"""
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
): # pylint: disable=unused-argument
"""Listener for Guild Join. Updated presence on guild join."""
await self.change_presence()
@commands.Cog.listener()
async def on_ready(self):
"""Starts the loop when bot is ready."""
await self.bot.wait_until_ready()
# pylint doesn't recognize that periodic_change_presense is subclassed from tasks.loop.
self.periodic_change_presence.start() # pylint: disable=no-member
@tasks.loop(minutes=5)
async def periodic_change_presence(self):
"""Loops every 5 minutes and changes presence."""
await self.change_presence()
async def change_presence(self):
"""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(name="Custom Status", state=presence)
)
+108 -17
View File
@@ -1,13 +1,21 @@
"""
_summary_
Defines the daily loop that runs the Dakiny bot functions.
"""
from datetime import time
import random
from datetime import datetime, time
import nextcord
from nextcord.ext import commands, tasks
from pytz import timezone
from sqlalchemy import select
from sqlalchemy.orm import Session
from classes.election_cycle import ElectionCycle
from config import DakinyConfig
from database_setup.sql_data_classes import UserBirthday, engine
from main import birthday_list
from utils import get_current_election_cycle
class DailyLoop(commands.Cog, name="Daily Loop"):
@@ -17,21 +25,104 @@ class DailyLoop(commands.Cog, name="Daily Loop"):
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
# TODO need to figure out how to determine the current cycle
self.current_election = ElectionCycle(0, self.bot)
self.current_election = None
self.update_birthdays()
@commands.Cog.listener()
async def on_ready(self):
"""
Starts the loop when bot is ready.
"""
await self.bot.wait_until_ready()
# pylint doesn't recognize that periodic_update_guild_data is subclassed from tasks.loop.
await self.do_thing.start() # pylint: disable=no-member
# pylint doesn't recognize that daily_loop is subclassed from tasks.loop.
self.daily_loop.start() # pylint: disable=no-member
@tasks.loop(time=time(hour=0, tzinfo=timezone("GMT")))
async def do_thing(self):
""" """
print("loop")
await self.current_election.check_date()
# TODO need to figure out how to change the election cycle after current one is over
async def daily_loop(self):
"""
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
election is over to delete the current election object.
"""
if self.current_election is None:
self.current_election = self.get_election_cycle()
if self.current_election is not None:
await self.current_election.check_date()
if self.current_election.election_over:
self.current_election = None
self.update_birthdays()
if 239547993106284544 in birthday_list:
# inform kandy that it is her "birthday"
kandy: nextcord.User = await self.bot.fetch_user(239547993106284544)
await kandy.send('Happy "Birthday"!')
await self.cakedays()
@daily_loop.before_loop
async def before_loop(self):
"""
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()
# Bot needs to know guilds to get the current cycle.
self.current_election = self.get_election_cycle()
def get_election_cycle(self) -> ElectionCycle:
"""
Creates an object for the current election cycle if one is active. An election is considered active
between the election start and the term start.
Returns:
ElectionCycle: Current election cycle, None if there is no election happening.
"""
config: DakinyConfig = self.bot.config
cycle = get_current_election_cycle(config)
# prevent exception from using cycle as an Int in an array
if cycle is None:
return None
return ElectionCycle(cycle, config, self.bot.get_guild(config.params.guild_id))
def update_birthdays(self) -> None:
"""
Goes through database to collect all birthdays that are today. Adds user's id to birthday list.
"""
birthday_list.clear()
# kandy's 1% chance
if random.randint(0, 99) == 0:
birthday_list.append(239547993106284544)
today = datetime.today()
birthday = f"{today.month}/{today.day}"
stmt = select(UserBirthday.UserId).where(UserBirthday.Birthday == birthday)
with Session(engine) as session:
to_add = session.scalars(stmt)
birthday_list.extend(to_add.all())
async def cakedays(self) -> None:
"""
Loops through the guild's humans checking for joined_at dates that match today's date. Sends a message
saying happy cakeday.
"""
today = datetime.today()
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
)
cakedays: list[nextcord.Member] = [
human
for human in guild.humans
if human.joined_at.month == today.month and human.joined_at.day == today.day
]
for member in cakedays:
years = today.year - member.joined_at.year
year_str = "years" if years > 1 else "year"
msg = f"Happy Cake day! <@{member.id}> joined the discord {years} {year_str} ago!"
await announce_channel.send(msg)
+45
View File
@@ -0,0 +1,45 @@
"""
Defines cog for on message listener.
"""
import nextcord
from nextcord.ext import commands
from main import birthday_list
class SubmissionListener(commands.Cog, name="Message Listener"):
"""Sets up message listener."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.Cog.listener()
async def on_message(self, message: nextcord.Message):
"""Listener for message creation. Adds score and attachments to submission."""
# don't want to process any messages from bot.
if message.author.id == self.bot.user.id:
return
await birthday_action(message)
# 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):
"""Adds function to bot"""
bot.add_cog(SubmissionListener(bot))
async def birthday_action(message: nextcord.Message) -> None:
"""
Checks a message's auther and mentions to see if they are in the birthday list.
Args:
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)
):
await message.add_reaction("🎂")
+13 -4
View File
@@ -1,6 +1,7 @@
"""
File contains classes defining the Hydra configuraton structure.
"""
from dataclasses import dataclass
@@ -10,17 +11,22 @@ class Params:
Class defines the various configuration parameters.
"""
active_category_id: int
annoucement_category_id: int
archive_channel_group_id: int
admin_channel_id: int # might want to rename this...
announcement_category_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
event_host_points: int
event_host_role_id: int
event_participate_points: int
event_post_channel_id: int
general_channel_id: int
git_branch: str
guild_id: int
high_council_role_id: int
voting_emote_id: str
voting_parent_id: int
@dataclass
@@ -30,6 +36,9 @@ class ElectionCycle:
"""
election_start: str
highcouncil_election_announce: str
highcouncil_election_end: str
highcouncil_election_start: str
name: str
term_end: str
term_start: str
-27
View File
@@ -1,27 +0,0 @@
-- CREATE DATABASE dakiny;
-- use dakiny;
CREATE TABLE ElectionInformation(
ElectionCycle varchar(30) not null unique,
ElectionChannel bigint unsigned not null,
PRIMARY KEY (ElectionCycle)
);
CREATE TABLE CouncilMembers(
ElectionCycle varchar(30) not null unique,
MemberId bigint unsigned not null,
Elected boolean DEFAULT false,
HighCouncilElect boolean DEFAULT false,
DiscordManager boolean DEFAULT false,
ClanLeader boolean DEFAULT false,
FOREIGN KEY (ElectionCycle) REFERENCES ElectionInformation(ElectionCycle),
PRIMARY KEY (ElectionCycle, MemberId)
);
CREATE TABLE Votes(
ElectionCycle varchar(30) not null unique,
VoterId bigint unsigned not null,
VotedForId bigint unsigned not null,
FOREIGN KEY (ElectionCycle) REFERENCES ElectionInformation(ElectionCycle),
PRIMARY KEY (ElectionCycle, VoterId)
);
@@ -5,11 +5,11 @@ Small script to create the database if it doesn't already exist.
import sqlite3
from os.path import exists
if not exists("dakiny.db"):
with sqlite3.connect("dakiny.db") as con:
if not exists("database/dakiny.db"):
with sqlite3.connect("database/dakiny.db") as con:
cur = con.cursor()
with open("database/database.sql", mode="r", encoding="utf-8") as file:
with open("database_setup/database.sql", mode="r", encoding="utf-8") as file:
database_script = file.read()
cur.executescript(database_script)
con.commit()
+54
View File
@@ -0,0 +1,54 @@
CREATE TABLE ElectionInformation(
ElectionCycle varchar(30) not null unique,
ThreadId bigint unsigned not null,
CouncilVoteMsgId bigint unsigned,
HighCouncilVoteMsgId bigint unsigned,
CouncilNominationMsgId bigint unsigned,
HighCouncilNominationMsgId bigint unsigned,
PRIMARY KEY (ElectionCycle)
);
CREATE TABLE CouncilMembers(
ElectionCycle varchar(30) not null,
MemberId bigint unsigned not null,
SpeechMsgId bigint unsigned not null,
Elected boolean DEFAULT false,
DiscordManager boolean DEFAULT false,
ClanLeader boolean DEFAULT false,
DiscordManagerSpeechId bigint unsigned,
ClanLeaderSpeechId bigint unsigned,
FOREIGN KEY (ElectionCycle) REFERENCES ElectionInformation(ElectionCycle),
PRIMARY KEY (ElectionCycle, MemberId)
);
CREATE TABLE Votes(
ElectionCycle varchar(30) not null,
VoterId bigint unsigned not null,
VotedForId bigint unsigned,
FOREIGN KEY (ElectionCycle) REFERENCES ElectionInformation(ElectionCycle),
PRIMARY KEY (ElectionCycle, VoterId)
);
CREATE TABLE UserBirthday (
UserId bigint unsigned not null,
Birthday varchar(5) not null,
PRIMARY KEY (UserId)
);
CREATE TABLE Presences (
Id integer PRIMARY KEY,
Presence varchar(100) not null
);
CREATE TABLE ParticipationPoints (
Cycle varchar(30) not null,
UserId varchar(32) not null,
DiscordMember boolean not null,
Points int unsigned DEFAULT 0,
PRIMARY KEY (Cycle, UserId)
);
CREATE TABLE Messages (
Path varchar(100) PRIMARY KEY,
Id bigint unsigned not null
);
@@ -1,16 +1,21 @@
"""
Simple file to set up the sqlalchemy connection to database.
"""
from sqlalchemy import create_engine
from sqlalchemy.ext.automap import automap_base
# connect with sql alchemy and autoload schema
URL = "sqlite+pysqlite:///dakiny.db"
URL = "sqlite+pysqlite:///database/dakiny.db"
engine = create_engine(URL)
Base = automap_base()
Base.prepare(autoload_with=engine)
# define the classes
ElectionInformation = Base.classes.ElectionInformation
CouncilMembers = Base.classes.CouncilMembers
ElectionInformation = Base.classes.ElectionInformation
Messages = Base.classes.Messages
ParticipationPoints = Base.classes.ParticipationPoints
Presences = Base.classes.Presences
UserBirthday = Base.classes.UserBirthday
Votes = Base.classes.Votes
+24 -3
View File
@@ -1,3 +1,4 @@
import logging
import os
import sys
@@ -6,12 +7,26 @@ import nextcord
from hydra.core.config_store import ConfigStore
from nextcord.ext import commands
import database.create_database as create_database
import database_setup.create_database as create_database
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 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):
@@ -26,7 +41,7 @@ def main(cfg: DakinyConfig):
# intents.guilds = True
# To enable the members priveleged intent:
# intents.members = True
intents.members = True
bot = commands.Bot(intents=intents)
bot.config = cfg
@@ -40,6 +55,10 @@ def main(cfg: DakinyConfig):
"""When Discord is connected"""
assert bot.user is not None
create_database # pylint: disable=pointless-statement
bot.add_view(NominateView(cfg, bot.get_guild(cfg.params.guild_id)))
bot.add_view(NominateHighCouncilView(cfg, bot.get_guild(cfg.params.guild_id)))
bot.add_view(VoteView(cfg, bot.get_guild(cfg.params.guild_id)))
bot.add_view(VoteHighcouncilView(cfg, bot.get_guild(cfg.params.guild_id)))
print(f"{bot.user.name} has connected to Discord!")
# Run Discord bot
@@ -48,4 +67,6 @@ def main(cfg: DakinyConfig):
if __name__ == "__main__":
sys.argv.append("hydra.job.chdir=False")
main()
sys.argv.append("hydra/job_logging=none")
sys.argv.append("hydra/hydra_logging=none")
main() # pylint: disable=no-value-for-parameter
+3 -1
View File
@@ -1,4 +1,6 @@
Discord permissions required for bot to operate:
manage channels
manage roles
manage roles
manage nicknames
need member intent
+4 -1
View File
@@ -2,4 +2,7 @@ nextcord==2.4.2
python-dotenv==1.0.0
pytz==2022.7.1
hydra-core==1.3.2
sqlalchemy<2.0
sqlalchemy<2.0
pandas==1.5.2
fastapi==0.111.0
GitPython==3.1.43
+563 -1
View File
@@ -2,8 +2,27 @@
File contains util functions.
"""
import os
from datetime import datetime
import nextcord
from sqlalchemy import select, true, update
from sqlalchemy.orm import Session
from config import DakinyConfig
from database_setup.sql_data_classes import (
CouncilMembers,
Messages,
ParticipationPoints,
Votes,
engine,
)
NOMINEE_SPLITTER = "========================================\n\n"
REPO_DIR = "./dakiny-messages"
MESSAGES_DIR = f"{REPO_DIR}/messages"
TEMPLATES_DIR = f"{REPO_DIR}/message_templates"
def get_datetime(day: str) -> datetime:
"""
@@ -20,7 +39,550 @@ def get_datetime(day: str) -> datetime:
day = int(day.split("/")[1])
# 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 datetime.today().month == 12 and month < 12:
year += 1
if datetime.today().month == 1 and month > 1:
year -= 1
return datetime(year, month, day)
def get_election_cycle_name(config: DakinyConfig) -> str:
"""
Builds the unique name of the current election cycle
Args:
config (DakinyConfig): the config used to know the election cycle.
Returns:
str: the name of the election cycle
"""
cycle = get_current_election_cycle(config)
current_cycle = config.election_cycles[cycle]
return f"vote-{get_datetime(current_cycle.term_start).year}-{current_cycle.name}"
def get_cycle_name(config: DakinyConfig) -> str:
"""
Builds the name of the current cycle/quarter
Args:
config (DakinyConfig): the config used to know the election cycle.
Returns:
str: the name of the cycle.
"""
cycle = get_current_cycle(config)
current_cycle = config.election_cycles[cycle]
return f"{get_datetime(current_cycle.term_start).year}-{current_cycle.name}"
def get_dm_election_name(config: DakinyConfig) -> str:
"""
creates the name of the discord manager election
Args:
config (DakinyConfig): Config sent to get_election_cycle_name
Returns:
str: the name of the election
"""
return f"{get_election_cycle_name(config)}-dm"
def get_cl_election_name(config: DakinyConfig) -> str:
"""
creates the name of the clan leader election
Args:
config (DakinyConfig): Config sent to get_election_cycle_name
Returns:
str: the name of the election
"""
return f"{get_election_cycle_name(config)}-cl"
def get_current_election_cycle(config: DakinyConfig) -> int:
"""
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:
int: cycle index for active election. None for no active cycle
"""
today = datetime.today()
for cycle, election in enumerate(config.election_cycles):
if (
today.date() >= get_datetime(election.election_start).date()
and today.date() <= get_datetime(election.term_start).date()
):
return cycle
return None
def get_current_cycle(config: DakinyConfig) -> int:
"""
Returns the index for the current cycle/quarter. Defined as between term start and end. Shouldn't return none
Args:
config (DakinyConfig): Config object to use
Returns:
int: cycle index for current cycle.
"""
today = datetime.today()
for cycle, election in enumerate(config.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(config: DakinyConfig, guild: nextcord.Guild) -> str:
"""
Creates a string that contains all nominees and their votes to be sent as a discord message.
Args:
config (DakinyConfig): Daking config object
guild (nextcord.Guild): The guild object. Used to get display names for sorting.
Returns:
str: String that contains all nominees and their votes.
"""
votes: dict[int, int] = {}
with Session(engine) as session:
stmt = select(CouncilMembers).where(
CouncilMembers.ElectionCycle == get_election_cycle_name(config)
)
for nominee in sorted(
session.scalars(stmt),
key=lambda nominee: guild.get_member(nominee.MemberId).display_name,
):
votes[nominee.MemberId] = 0
stmt = select(Votes).where(
Votes.ElectionCycle == get_election_cycle_name(config)
)
for vote in session.scalars(stmt):
if vote.VotedForId is not None:
votes[vote.VotedForId] += 1
votes_str: str = "===================="
votes_str += user_vote_str(votes)
return votes_str
def build_highcouncil_votes_str(config: DakinyConfig, guild: nextcord.Guild) -> str:
"""
Creates a string that contains all high council nominees and their votes to be sent as a discord message.
Args:
config (DakinyConfig): Daking config object
Returns:
str: String that contains all nominees and their votes.
guild (nextcord.Guild): The guild object. Used to get display names for sorting.
"""
cl_votes: dict[int, int] = {}
dm_votes: dict[int, int] = {}
with Session(engine) as session:
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionCycle == get_election_cycle_name(config))
.filter(CouncilMembers.ClanLeaderSpeechId.is_not(None))
)
for nominee in sorted(
session.scalars(stmt),
key=lambda nominee: guild.get_member(nominee.MemberId).display_name,
):
cl_votes[nominee.MemberId] = 0
stmt = select(Votes).where(Votes.ElectionCycle == get_cl_election_name(config))
for vote in session.scalars(stmt):
if vote.VotedForId is not None:
cl_votes[vote.VotedForId] += 1
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionCycle == get_election_cycle_name(config))
.filter(CouncilMembers.DiscordManagerSpeechId.is_not(None))
)
for nominee in sorted(
session.scalars(stmt),
key=lambda nominee: guild.get_member(nominee.MemberId).display_name,
):
dm_votes[nominee.MemberId] = 0
stmt = select(Votes).where(Votes.ElectionCycle == get_dm_election_name(config))
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_event_participation(config: DakinyConfig, users: list[str], is_member: bool):
"""
Adds participation 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.
Args:
config (DakinyConfig): Dakiny config for getting points
users (list[int]): users that participated and need points.
is_member (bool): whether the users are a discord member or not
"""
cycle = get_cycle_name(config)
participation_point_value = config.params.event_participate_points
with Session(engine) as session:
for user in users:
stmt = (
select(ParticipationPoints)
.where(ParticipationPoints.UserId == user)
.where(ParticipationPoints.Cycle == cycle)
)
points = session.scalar(stmt)
if points is None:
new_points = ParticipationPoints(
Cycle=cycle,
UserId=user,
DiscordMember=is_member,
Points=participation_point_value,
)
session.add(new_points)
else:
points.Points += participation_point_value
session.commit()
def record_event_host(config: DakinyConfig, user: int):
"""
Adds host points to the given user. Checks if they don't exist in order to make a new entry, otherwise adds points
to the existing value.
Args:
config (DakinyConfig): Dakiny config for getting points
user (int): Id of user that hosted.
"""
cycle = get_cycle_name(config)
host_point_value = config.params.event_host_points
with Session(engine) as session:
stmt = (
select(ParticipationPoints)
.where(ParticipationPoints.UserId == str(user))
.where(ParticipationPoints.Cycle == cycle)
)
points = session.scalar(stmt)
if points is None:
new_points = ParticipationPoints(
Cycle=cycle,
UserId=str(user),
DiscordMember=True,
Points=host_point_value,
)
session.add(new_points)
else:
points.Points += host_point_value
session.commit()
async def update_clan_messages(guild: nextcord.Guild):
"""
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 ever 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.
"""
dirs = [
dir_name
for dir_name in os.listdir(MESSAGES_DIR)
if os.path.isdir(f"{MESSAGES_DIR}/{dir_name}")
]
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}"
with open(file_path, "r", encoding="utf8") as reader:
await edit_clan_message(
session, channel, message_name, reader.read(2000)
)
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,
):
"""
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, config: DakinyConfig):
"""
Gets the winners of the elections and updates the perminant message that states the current council.
Args:
guild (nextcord.Guild): Guild object that to use to get channel.
config (DakinyConfig): Configs
"""
clan_info_channel: nextcord.TextChannel = await guild.fetch_channel(
config.params.clan_info_channel_id
)
election = get_election_cycle_name(config)
with Session(engine) as session:
stmt = (
select(CouncilMembers.MemberId)
.where(CouncilMembers.ElectionCycle == election)
.where(CouncilMembers.DiscordManager == true())
)
discord_manager = session.scalar(stmt)
stmt = (
select(CouncilMembers.MemberId)
.where(CouncilMembers.ElectionCycle == election)
.where(CouncilMembers.ClanLeader == true())
)
clan_leader = session.scalar(stmt)
stmt = (
select(CouncilMembers.MemberId)
.where(CouncilMembers.ElectionCycle == election)
.where(CouncilMembers.Elected == true())
)
council_members_strs = [
f"<@{id}>"
for id in session.scalars(stmt)
if id not in (discord_manager, clan_leader)
]
quarter = get_cycle_name(config)
with open(
f"{TEMPLATES_DIR}/council_members.txt", mode="r", encoding="utf8"
) as file:
message = 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
)