Compare commits

...

326 commits

Author SHA1 Message Date
TSRBerry 37651883bc
Fix broken build id regex & Return a set from get_filepaths (#110)
* log_analyser: Match application or title when searching for build IDs

* log_analyser: Return a set from get_filepaths
2024-09-24 20:09:33 +02:00
TSR Berry fcde1f365c
Update flake.lock 2024-09-01 15:54:58 +02:00
TSRBerry 147011eba1
Automatically block analysis of logs containing blocked contents in paths (#108)
* Extract paths from logs and check for blocked content

* Extract paths in command line analyzer

* Split disabled paths message if necessary

* Log the blocked path that caused a warning

* Remove duplicate command alias

* Remove bad characters from extracted filepaths

* Fix is_path_disabled() only checking the full path

* Apply formatting

* Improve wording for the warning embeds

* Apply formatting
2024-09-01 15:53:44 +02:00
ekuland 76fe1dbbd4
Add Note for Rosetta (#109)
* Add get_cpu_notes

Check cpu for Virtual Apple and Note to disable Rosetta

* Update ryujinx_log_analyser.py

fix misspelling

* Apply formatting
2024-08-27 19:34:24 +02:00
TSRBerry 7b5b9fc104
log_analyzer: Fix cheat detection (#101) 2024-05-18 18:24:03 +02:00
TSR Berry 86c8bc87b1
Update dependencies 2024-05-15 17:30:55 +02:00
Piplup abcf1c7c9f
Better Slowmode command (#95)
* Update mod.py

* Handle optional slowmode parameter correctly

---------

Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com>
2024-05-15 17:21:21 +02:00
TSRBerry 2daca3a4b7
Add Mako workflow (#98) 2024-05-15 17:19:24 +02:00
WilliamWsyHK 95971e5359
Simple fix for time elapsed (#94) 2024-05-15 17:04:32 +02:00
WilliamWsyHK 9e395ffa10
Add log parsing capability to capture only enabled mods (#93)
* Add log parsing capability to capture only enabled mods

* Fix Python Black format complain

* Add fallback so that old logs without mod status can still be recognized

* stop Python Black complain again
2024-05-15 16:59:37 +02:00
Jerome A 41bc653bea
Add hypervisor in Logfile outputs (#92)
* Add hypervisor in logfile outputs

* fix typings

* moved setting

* Check if macos or not

* Replace "Not applicable" with "N/A"

* Add hypervisor setting to embed

---------

Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com>
2024-05-15 16:58:20 +02:00
TSRBerry ac3829f508
remind: Add message references and don't remove reminder confirmations (#97)
* Don't remove reminder confirmation messages

* Add message references to reminders
2024-04-14 15:34:12 +02:00
TSRBerry b2cda3fe85
vanity_url: Fix crash when editing a guild (#96) 2024-04-14 15:17:18 +02:00
TSR Berry 7c4a57d289
Update flake.lock 2024-03-11 16:48:16 +01:00
TSRBerry 97f534eb17
Automatically update configured vanity URLs (#91) 2024-03-11 16:47:15 +01:00
TSR Berry e4ee55a1fd
Update dependencies 2024-03-09 02:34:46 +01:00
TSR Berry 77fb8f402a
Avoid infinite recursion caused by poetry2nix.cleanPythonSources 2024-03-09 02:22:49 +01:00
TSRBerry 7c4bf15c93
Log purged messages, Fix macro usage in DMs & Fix reading empty files (#88)
* Log purged messages

* Fix CommandInvokeError for macros in DMs

* Fix decoding empty files and simplify read json logic

* Apply black formatting
2024-03-02 09:56:41 +01:00
dependabot[bot] f981d9837d
Bump aiohttp from 3.9.1 to 3.9.3 (#87)
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.9.1 to 3.9.3.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/v3.9.3/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.9.1...v3.9.3)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-02 09:54:37 +01:00
TSRBerry 03e4fd3541
A small batch of fixes (#81)
* log_analyser: Use a set for notes

* Use sys.exc_info() instead of sys.exception()

sys.exception() only exists since python 3.11

* Create data dir if it doesn't exist
2024-01-06 15:33:01 +01:00
dependabot[bot] caa5cf55ed
Bump actions/setup-python from 4 to 5 (#86)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-31 23:49:50 +01:00
Mary Guillemard c5046a19c3 Add nix flakes configuration
Signed-off-by: Mary Guillemard <mary@mary.zone>
2023-12-31 21:42:14 +00:00
Mary Guillemard a412d74ef8 Revert "Advertise python 3.11 and update lock file"
This reverts commit 847e955ec1.
2023-11-20 20:04:30 +01:00
Mary Guillemard 847e955ec1 Advertise python 3.11 and update lock file 2023-11-20 19:55:58 +01:00
GabCoolGuy df77d6f4db
Update README.md to make contributing easier (#80)
* Update README.md

Made changes to README.md to mention ryuko-ng and also to make contributing easier by modifying "How to run manually".

* Update README.md

oops i missed a spot

* Update README.md

* Remove stuff about requirements.txt

Needs feedback

* how could i forget about marysaka

Add marysaka in Credits

* missed a spot again

* Improvements ?

* slow and steady

* Definitely improvements

* more markdown
2023-11-20 19:49:39 +01:00
TSRBerry 72fd725a94
logfilereader: Fix RAM parser issues (#82)
* Create size helper

* Replace fixed RAM units with size helper

* Rename CommonErrors to CommonError

Enum names should be singular

* Apply black formatting
2023-11-20 19:48:35 +01:00
TSRBerry 9669556a39
Handle various file related issues (#76)
* Create state files if they don't exist yet

* Add notifications helper to message bot managers

* Inform bot managers about errors if possible

* Handle JSONDecodeErrors including empty files
2023-10-09 22:56:13 +02:00
dependabot[bot] 31b9aeb436
Bump humanize from 4.7.0 to 4.8.0 (#72)
Bumps [humanize](https://github.com/python-humanize/humanize) from 4.7.0 to 4.8.0.
- [Release notes](https://github.com/python-humanize/humanize/releases)
- [Commits](https://github.com/python-humanize/humanize/compare/4.7.0...4.8.0)

---
updated-dependencies:
- dependency-name: humanize
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-09 22:55:45 +02:00
TSRBerry 96016ae7f0
Fix macro usage in DMs (#75) 2023-10-09 22:53:57 +02:00
TSRBerry 04add0f364
Remove RAM warning and show available RAM under general info as well (#77)
* Remove stub logs from default log levels

* Remove RAM warning

* Show available and total RAM under general info
2023-10-09 22:53:20 +02:00
TSRBerry c33f4f29a2
log_analyser: Fix homebrew detection (#78) 2023-10-09 22:52:29 +02:00
dependabot[bot] 97a26a76ad
Bump aiohttp from 3.8.4 to 3.8.6 (#79)
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.8.4 to 3.8.6.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.8.4...v3.8.6)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-09 22:52:01 +02:00
dependabot[bot] 19fe13e265
Bump discord-py from 2.3.1 to 2.3.2 (#69)
Bumps [discord-py](https://github.com/Rapptz/discord.py) from 2.3.1 to 2.3.2.
- [Commits](https://github.com/Rapptz/discord.py/compare/v2.3.1...v2.3.2)

---
updated-dependencies:
- dependency-name: discord-py
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-09 22:47:14 +02:00
dependabot[bot] 00b94e9826
Bump actions/checkout from 3 to 4 (#73)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-09 22:46:13 +02:00
TSRBerry 3aa1149b10
QoL: Automatically create bot invite URLs (#74)
* Automatically create an invite_url

* Change invite url log message
2023-09-11 21:24:23 +02:00
TSRBerry 351b9655e3
Allow analyze command to be executed by everyone (#71) 2023-09-11 21:22:44 +02:00
TSRBerry 492d43c608
Fix reply command targets (#70) 2023-09-11 21:22:09 +02:00
TSRBerry 69b74069af
Fix modified log detection for multi game logs (#67)
* Fix modified log detection for multi game logs

* Add app_info to analyse() output

* Add main standalone script for easy debugging

* Apply black formatting
2023-07-05 08:35:00 +02:00
dependabot[bot] 5c910ddba6
Bump humanize from 4.6.0 to 4.7.0 (#66)
Bumps [humanize](https://github.com/python-humanize/humanize) from 4.6.0 to 4.7.0.
- [Release notes](https://github.com/python-humanize/humanize/releases)
- [Commits](https://github.com/python-humanize/humanize/compare/4.6.0...4.7.0)

---
updated-dependencies:
- dependency-name: humanize
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-04 08:29:52 +02:00
dependabot[bot] ad85c365a3
Bump discord-py from 2.3.0 to 2.3.1 (#65)
Bumps [discord-py](https://github.com/Rapptz/discord.py) from 2.3.0 to 2.3.1.
- [Commits](https://github.com/Rapptz/discord.py/compare/v2.3.0...v2.3.1)

---
updated-dependencies:
- dependency-name: discord-py
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-04 08:27:38 +02:00
dependabot[bot] fc05f6cc88
Bump discord-py from 2.2.3 to 2.3.0 (#62)
Bumps [discord-py](https://github.com/Rapptz/discord.py) from 2.2.3 to 2.3.0.
- [Commits](https://github.com/Rapptz/discord.py/compare/v2.2.3...v2.3.0)

---
updated-dependencies:
- dependency-name: discord-py
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-26 08:02:17 +02:00
TSRBerry 0419eba042
Fix .dockerignore patterns (#63)
* Fix .dockerignore patterns

* Remove config_template.py exclusion
2023-06-26 08:01:52 +02:00
Piplup 3faa6c5bd4
Update mod.py with .slowmode command (#56)
* dirty command

* fixed slowmode
2023-06-26 08:01:36 +02:00
ealekseychik 2711e80529
Fix __get_app_name logic to take last game name on multiple games launch (#60)
Co-authored-by: Egor Alekseychik <e.alekseychik@syberry.com>
2023-06-26 08:00:53 +02:00
TSRBerry 2ca94dd377
macro: Allow users to target multiple members when invoking macros (#64)
* macro: Use Greedy type annotation to optionally match multiple members

* Apply black formatting
2023-06-26 07:47:57 +02:00
Aceofgods ed26c0f552
Update meme.py with .lick meme command (#53)
* Update meme.py with .lick meme command

Never done this before.

* Update meme.py with lick meme v2

I used a web-based version of Black to fix the parsing error but couldn't get it to register the commands while indented?

* Add black formatting

* Apply suggestions from code review

Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>

---------

Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com>
2023-06-21 09:09:42 +02:00
TSRBerry 50c8b11f26
Finally fix list_macros completely (#61) 2023-06-12 09:45:51 +02:00
TSRBerry cbd1f55dc3
Fix list_macros command still not working properly (#57)
* Properly split the message

* Apply black formatting
2023-06-09 16:28:11 +02:00
SamusAranX 7bcf3c28fe
Updated RAM regex to work with both MiB and GB units (#58)
* Updated RAM regex to work with both MiB and GB units

* Set RAM values to something useful in the event of a parsing error

* Ran python3 -m black .
2023-06-09 16:27:23 +02:00
dependabot[bot] 151844fb3e
Bump gidgethub from 5.2.1 to 5.3.0 (#59)
Bumps [gidgethub](https://github.com/brettcannon/gidgethub) from 5.2.1 to 5.3.0.
- [Release notes](https://github.com/brettcannon/gidgethub/releases)
- [Changelog](https://github.com/brettcannon/gidgethub/blob/main/docs/changelog.rst)
- [Commits](https://github.com/brettcannon/gidgethub/compare/v5.2.1...v5.3.0)

---
updated-dependencies:
- dependency-name: gidgethub
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-09 16:26:52 +02:00
TSRBerry ec48a71b9e
Add exception for homebrew applications (#54) 2023-05-28 10:36:54 +02:00
TSRBerry 8f25f13eff
macro: Add paging for list_macros() (#55)
* macro: Add paging for list_macros()

* Apply black formatting
2023-05-28 10:36:37 +02:00
TSRBerry fa431c1d4c
Fix cheat names and last error snippet (#52) 2023-05-28 10:33:58 +02:00
dependabot[bot] c79e1628cb
Bump discord-py from 2.2.2 to 2.2.3 (#51)
Bumps [discord-py](https://github.com/Rapptz/discord.py) from 2.2.2 to 2.2.3.
- [Commits](https://github.com/Rapptz/discord.py/compare/v2.2.2...v2.2.3)

---
updated-dependencies:
- dependency-name: discord-py
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-28 10:33:35 +02:00
TSR Berry 1462e0a36a
Fix another minor spelling mistake 2023-05-05 20:00:25 +02:00
TSRBerry 317a29db75
Refactor logfilereader (#50)
* Add disabled_ids as an alias

* Fix possible AttributeErrors in get_app_info()

* Refactor logfilereader.py

* Refactor disabled_ids and fix a few bugs

* Only add active cheats to the list
2023-05-05 18:38:01 +02:00
TSRBerry df2f3b7ac5
Hotfix clearreactsbyuser() 2023-05-05 18:11:32 +02:00
TSR Berry 52d7c08324
Fix minor spelling mistake v2 2023-05-04 15:39:10 +02:00
TSRBerry 9cd2d6550d
Small logfilereader fixes and improvements (#49)
* Fix cheat_information regex

* Add warning about default user profiles to notes

* Fix disable_ro_section issues

* Apply black formatting
2023-05-04 00:40:41 +02:00
TSR Berry 1eacb849f5
Convert all ids to uppercase 2023-05-03 00:53:48 +02:00
TSR Berry f72afbca29
Fix minor spelling mistake 2023-05-02 22:03:40 +02:00
TSRBerry 77fc2040a2
Delete all logs with blocked ids & Add cheat analysis (#48)
* Rename disabled_tids to disabled_ids and start adding support for bids

* Add cheat information

* Add analysis block for build ids

Always remove blocked game logs

* Add ro_section to disabled_ids

* Search all potential log files for blocked games

* Add commands to block ro_sections of games

* Change order of macro command arguments

* Add new disabled_ids key to wanted_jsons
2023-05-02 21:38:22 +02:00
TSRBerry 18970723e7
Invoke warn with all positional args (#47) 2023-05-01 22:14:35 +02:00
TSRBerry 2506aa6437
Small set of hotfixes to make blocked_tids behave properly (#46)
* Revert log.exception() call to working state

* Await add_roles call and invoke warn command correctly

* Apply black formatting
2023-05-01 21:02:37 +02:00
TSRBerry 5be9915501
logfilereader: Fix macOS version detection & Reply to analyzed log (#45)
* Fix macOS version detection correctly this time

* Reply to the message with the uploaded log

* Apply black formatting
2023-04-28 22:38:23 +02:00
TSRBerry 45538eec6f
logfilereader: Fix analysing every attachment (#44)
* logfilereader: Fix analysing every attachment

Now Ryuko will only analyse logs again

* logfilereader: Stop analysing message.txt

Most of the time these won't contain all the info we need anyway.

* Apply black formatting
2023-04-27 20:56:57 +02:00
TSRBerry e937abb41c
Fix a few macro and logfilereader issues (#43)
* Try to improve exception logging

* Fix KeyError for new aliases

* List aliases and macros together

* Fix AttributeError when reading logs
2023-04-26 20:38:52 +02:00
TSRBerry 994438d3fa
Add commands to block log analysis of specific TIDs (#42)
* Small styling changes

* Add disallowed_roles for logfilereader

* macros: Fix naming and missing bot parameter

* Add disabled_tids helper

* Add pirate role to named role examples

* logfilereader: Add commands to block specific tids

* Add black formatting

* Add command to manually analyse logs

And some minor cleanup
2023-04-24 08:21:04 +02:00
Mary 2f4990d64f
Revert "Bump charset-normalizer from 2.1.0 to 3.1.0 (#36)" (#41)
This reverts commit 5515443ff5.
2023-04-20 07:47:13 +02:00
dependabot[bot] 4394ecd808
Bump yarl from 1.8.1 to 1.8.2 (#37)
Bumps [yarl](https://github.com/aio-libs/yarl) from 1.8.1 to 1.8.2.
- [Release notes](https://github.com/aio-libs/yarl/releases)
- [Changelog](https://github.com/aio-libs/yarl/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/yarl/compare/v1.8.1...v1.8.2)

---
updated-dependencies:
- dependency-name: yarl
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-20 07:42:06 +02:00
dependabot[bot] 5515443ff5
Bump charset-normalizer from 2.1.0 to 3.1.0 (#36)
Bumps [charset-normalizer](https://github.com/Ousret/charset_normalizer) from 2.1.0 to 3.1.0.
- [Release notes](https://github.com/Ousret/charset_normalizer/releases)
- [Changelog](https://github.com/Ousret/charset_normalizer/blob/master/CHANGELOG.md)
- [Upgrade guide](https://github.com/Ousret/charset_normalizer/blob/master/UPGRADE.md)
- [Commits](https://github.com/Ousret/charset_normalizer/compare/2.1.0...3.1.0)

---
updated-dependencies:
- dependency-name: charset-normalizer
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-20 07:41:55 +02:00
dependabot[bot] cc9e55057a
Bump frozenlist from 1.3.1 to 1.3.3 (#35)
Bumps [frozenlist](https://github.com/aio-libs/frozenlist) from 1.3.1 to 1.3.3.
- [Release notes](https://github.com/aio-libs/frozenlist/releases)
- [Changelog](https://github.com/aio-libs/frozenlist/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/frozenlist/compare/v1.3.1...v1.3.3)

---
updated-dependencies:
- dependency-name: frozenlist
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-20 07:41:42 +02:00
dependabot[bot] 90b6f45005
Bump discord-py from 2.0.0 to 2.2.2 (#34)
Bumps [discord-py](https://github.com/Rapptz/discord.py) from 2.0.0 to 2.2.2.
- [Release notes](https://github.com/Rapptz/discord.py/releases)
- [Commits](https://github.com/Rapptz/discord.py/compare/v2.0.0...v2.2.2)

---
updated-dependencies:
- dependency-name: discord-py
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-20 07:41:33 +02:00
TSRBerry 20a7d0506c
Fix logfilereader not detecting games (#40) 2023-04-20 07:41:18 +02:00
TSRBerry 3a0230259a
reply-targets: Await the result of fetch_message() (#39)
* Await the result of fetch_message()

* Apply black formatting
2023-04-05 18:57:46 +02:00
Mary bf59634898 Fix broken get_userlog in logs 2023-04-05 18:33:24 +02:00
Mary dea6cf5f38 Remove imagemanip 2023-04-05 15:03:54 +02:00
Mary f642c953e8 Remove requirements.txt 2023-04-05 12:24:02 +02:00
Mary f1e5c34fb3 Fix hackwarn using target_user instead of the target_user name 2023-04-05 12:19:21 +02:00
Mary 48f9cc5cde Change robocop_ng to have state directory differ from working directory 2023-04-05 12:10:18 +02:00
TSRBerry 8463b9b2fb
Add hackwarn command (#20)
* Add hackwarn command

* Fix command usage message
2023-04-02 15:15:49 +02:00
TSRBerry 4ad2527a59
Use reply message to target a specific member & Add aliases for macros (#26)
* Remove command message when using macros

* macro: Reply to the same message as the author invoking the command

* logfilereader: Increase size of empty logs

* meme: Add reply targets

* mod: Add reply targets

* mod_timed: Add reply targets

* macro: Add aliases for macros

* macro: Fix list_macros not listing any macros

* Apply black formatting
2023-04-02 13:56:49 +02:00
dependabot[bot] 9912df774d
Bump pillow from 9.2.0 to 9.5.0 (#33)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 9.2.0 to 9.5.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/9.2.0...9.5.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-02 13:53:24 +02:00
dependabot[bot] 8c8ca5a883
Bump humanize from 3.14.0 to 4.6.0 (#32)
Bumps [humanize](https://github.com/python-humanize/humanize) from 3.14.0 to 4.6.0.
- [Release notes](https://github.com/python-humanize/humanize/releases)
- [Commits](https://github.com/python-humanize/humanize/compare/3.14.0...4.6.0)

---
updated-dependencies:
- dependency-name: humanize
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-02 13:51:02 +02:00
dependabot[bot] ec02588ade
Bump gidgethub from 5.2.0 to 5.2.1 (#31)
Bumps [gidgethub](https://github.com/brettcannon/gidgethub) from 5.2.0 to 5.2.1.
- [Release notes](https://github.com/brettcannon/gidgethub/releases)
- [Changelog](https://github.com/brettcannon/gidgethub/blob/main/docs/changelog.rst)
- [Commits](https://github.com/brettcannon/gidgethub/compare/v5.2.0...v5.2.1)

---
updated-dependencies:
- dependency-name: gidgethub
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-02 13:45:17 +02:00
dependabot[bot] 08ded71a91
Bump aiohttp from 3.8.1 to 3.8.4 (#30)
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.8.1 to 3.8.4.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.8.1...v3.8.4)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-02 13:41:57 +02:00
dependabot[bot] db73c59e20
Bump multidict from 6.0.2 to 6.0.4 (#29)
Bumps [multidict](https://github.com/aio-libs/multidict) from 6.0.2 to 6.0.4.
- [Release notes](https://github.com/aio-libs/multidict/releases)
- [Changelog](https://github.com/aio-libs/multidict/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/multidict/compare/v6.0.2...v6.0.4)

---
updated-dependencies:
- dependency-name: multidict
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-02 13:41:06 +02:00
TSRBerry 5e2ce2abaa
Allow formatter to run on forks (#28)
* Allow formatter to run on forks

* Sometimes RTFM does save some time

* Automatically fix formatting for same repo PRs

* Apply black formatting
2023-04-01 18:43:56 +02:00
TSRBerry 9caa1f6b72
Add workflow to automatically apply black formatting & Add dependabot (#27)
* Add automatic formatting for pull requests

* Add dependabot.yml
2023-04-01 17:51:07 +02:00
Mary c82fc0e78c Fix macros command 2023-03-30 20:38:03 +02:00
Mary 29b2412513 Fix missing setup function for persistence role module 2023-03-30 19:44:43 +02:00
TSRBerry fc5b959558
Call Cog.listener (#25) 2023-03-30 19:37:49 +02:00
Mary 85595256c3 Fix load/unload/pull commands with module name changes 2023-03-30 19:29:29 +02:00
TSRBerry af92876835 Add macro cog (#24)
* Add macro cog

* Adjust macro cooldown

* Add macros.json to wanted_jsons

---------

Co-authored-by: Mary <mary@mary.zone>
2023-03-30 19:05:29 +02:00
TSRBerry f10ab7bb50
Add role persistence cog (#23) 2023-03-30 19:01:04 +02:00
Mary 2f2475985d Fix module support 2023-03-09 23:19:53 +01:00
TSRBerry bd5f37086e
Fix errors when trying to access an avatar and some cleanup (#22)
* Replace avatar_url with str(display_avatar)

* Rewrite imports and resolve a few warnings
2023-03-09 23:01:10 +01:00
Mary 950b3f577b Update move logic after rebase 2023-03-08 08:29:56 +01:00
Mary 5d43842e3c Fixes after rebase 2023-03-03 22:20:05 +01:00
TSR Berry d7852c2307
reactionroles: Wait until the bot is ready 2023-03-03 20:14:48 +01:00
TSR Berry 7f7a4c4707
cron: Wait until bot is ready 2023-03-03 20:10:15 +01:00
TSR Berry 3418c206d9
Apply black formatting 2023-03-03 19:58:51 +01:00
Mary b2bd3142a9
Adjust for support channel changes and macos support 2023-03-03 19:58:51 +01:00
Mark 367cc8fe9d
Added more warnings in log reader (#17)
* Added warnings:

- File permissions fix
- File not found fix
- Missing services fix
- Graphics Backend threading warning
- Software memory manager warning severity upgraded

* Small fixes:
- Use file size to track duplicate files
- Use logging module for exception warning

* Fix log analysis not showing on no game boot

* Formatting for clarity on mobile, minor rewording

* Add graphics backend to visible output

* Suggest Vulkan for AMD and Intel GPU

* Reword pr-testing message

* Remove anisotropic filtering warning

* Deduplicate mods information

* Nit: spelling error in comment

* Shorten log time message

* Increase bytes read to handle large amounts of DLC

* Add Texture Recompression to output

* Fix shadowed variables

* Recompression suggestion for Vulkan memory error

* Improve regex to deal with MB/MiB RAM measurement

* Rename Expand DRAM warning to match rewording

* Update LDN regex to detect new versions
2023-03-03 19:58:51 +01:00
Mary ade0917985
Upstream reaction roles changes 2023-03-03 19:58:51 +01:00
Mary b4d95b5635
Fix last commit again 2023-03-03 19:58:51 +01:00
Ayato (Shahil) 5ee601bda5
Update mod.py (#18) 2023-03-03 19:58:51 +01:00
Mary c8a9603bb0
Fix build issues 2023-03-03 19:58:51 +01:00
Mark 0015a812f9
Further fixes/improvements (#16)
* Fix AF warning when Unknown

* Flag FS integrity check being disabled
2023-03-03 19:58:51 +01:00
VocalFan ada0e92d32
Added many more error codes for .serr (#12)
* All the modules added, and all FS Error codes!

* Added common errors, plus all HIDBUS errors.

* Added all error codes for new modules!

* 0x2A2 fatal error added.

* Fixed accidental paste

* Added erpt error codes + new Devmenu module.
2023-03-03 19:58:51 +01:00
Mark b98928f801
Update missing key error since no libhac used (#14) 2023-03-03 19:58:50 +01:00
Mark d0acba564f
Log reading improvements (#13)
* Handles new Ryujinx version numbering

* Add detection of ResultFsPermissionDenied and ResultFsTargetNotFound errors

* Warns on detection of old Ryujinx version
2023-03-03 19:58:50 +01:00
SS 0874e367c1
Add LDN/Tester reaction-roles select support in Ryujinx guild with Ryuko (#9)
* Add LDN/Tester reaction-roles select support in Ryujinx guild with Ryuko

* Fixed Mario Kart role name

* Add "How to remove reaction" message in embed

* 1) organised to classes 2) moved a message to embed footer
And start.sh is a file i will delete it later

* Footer

* Change emoji of Splatoon 2 (paintbrush => 🦑)

* Removed a test func

* Indian English, its not my problem

* Removed  attribute and use m.guild in handle_offline_reaction_*()

* More stricter type checking

* equal sign comma

Co-authored-by: Mary <thog@protonmail.com>

* Address Thog comment 1/?

* Address Thog comment 2/?

* bye bye start.sh (Address Thog comment 3/?)

* Fix a loop constant confusion

* Formated with

* A fix

* One more fix & added names loop variables

* Improve embed description once more

* Removed black bracket formatting

* add dynamic embed description

* Use the f strings

* Fix embed footer desc

* Added message editing on new game add, and generate embed in a seperate function

* Test

* added a comment

* Code refactor, bug fixes

* use get() for recieving role name

* Add pokemon/mario party superstars

* Embed changes

* Addded Pokemon BD/SP. Bruh how many games I will add in future.

* Fix P BD/SP.

Co-authored-by: choppymaster <>
Co-authored-by: Mary <thog@protonmail.com>
2023-03-03 19:58:50 +01:00
Mark 22f81b449b
Log reading improvements (#11)
* Added warnings for several settings:
- Expand DRAM hack
- Memory Manager Mode
- Ignore Missing Services
- Anisotropic Filtering set to not Auto
- Debug logs enabled
- New severity level for PPTC and Shader cache warnings

* Various fixes:
- Warn for outdated keys/firmware,
- Error snippet fix when no game boots
- Embed improvements
- Improve duplicate log upload tracking to link to last uploaded file

* Larger download header range, handle larger files.

* Move notes visibility to show on startup crash

* Added vsync disabled warning and dump hash error

* Clean up controller warning to declutter empty log message

* Add .NET 6 shader warning and genericise shader init error
2023-03-03 19:58:50 +01:00
Mark Araujo 32a8b6b431
Fix error not being displayed if game name Unknown (#7)
* Log reading capabilities to Ryuko bot (#3)

* Add log reading capabilities
- User hardware specs
- Game info
- Controller configuration
- Last error snippet in log
- Warnings when using macOS or Intel iGPU
- Warning of logs not turned on

* Allowed log reading channels moved to config
This is easier for contributors to change their config file for testing

* Fixes large files not showing error snippet
- Large files are partially downloaded and show header information which
messes with log analysis, this gets stripped.
- Finding error messages function improved

* Default logs enabled shows a green checkmark

* Better feedback with log parsing message
- Bot prints `Log parsing...` and update message once log analysis done
- Added better error logging to console

* Better handling of invalid files warning
- Also fixed typo with bot message edit function

* Refactored embed generation to make more sense
- Embed is now based off a generic json assuming Unknown values at first
- Embed fields moved closer together
- Fields with newlines joined instead of manually separated
- Intel iGPU message changed to show preference for discrete GPU's

* Refactor to be simpler and easier to read.
- Hardware, ryujinx and log analysis split into separate functions
- Regex explicitly defined for each property instead of confusing map
- Added user settings reported, shows PPTC enabled or disabled

* Game notes sorted by order of severity
Notes will appear with most severe warnings first as follows:  ⚠️

* Analyses toggleable settings that appear in log
Currently these are: PPTC, audio backed, docked/handheld and vsync.
- Formatting change so these settings are more visible in bot embed

* Refactored user_settings, rewording of bot embed
- User settings reading handles missing log info for older versions
- `Switch Mode` (docked/handheld info) changed to `Console Mode`
- Missing firmware warning if firmware not installed

* Warning when shader cache collision detected

* Notes time elapsed in log file
- Error handling for no notes to log

* Show values for some user settings: audio, docked, missing services, resolution, shader cache and vsync

* Analyse user changeable settings
- Restructed embed to allow easier settings handling
- Changed embed formatting to deal with inline colums more cleanly

* Log file is now default function parameter

* Better sorting of analysis messages
- Now sorted alphabetically and by severity for consistency
- Show available RAM in low RAM warning
- Fix variable name misspelling

* Logging level changed to info

* Warn if bad dump in error message

* Add warning for no custom build support

* Warn user to post log in correct channel
- Warn about not supporting custom builds
- Warn to post in pr build if detected
- Warn about channels to post logs if detected in #general

* Fix logfilereader logic

* Ryuko bot fixes and improvements (#4)

* Fixes HTTPException by properly handling newline regex

* Improves information display:
- Shows settings info if no game is detected running
- Empty log warning takes up less space
- Error, Mods and Notes not shown on empty log

* Improved empty log message
- Also allow logs parsing in linux channel, as well as mentioning
when posting in non-allowed channels

* Fix ResScale parse error, better empty log message

* DM users about correct channels instead of in chat

* Fix variable spelling,  clearer bad dump warning

* Shows error snippet on empty log (#6)

* Error snippet shown on empty log
- Shader cache corruption warning

* Loop to get missing info in log

* Error search handles multiple terms
- Minor spelling correction for resolution value
- User settings visible on empty log

* Fix error not being displayed if game name Unknown

* Warns about PPTC and shader caches being disabled.

- Warns about audio backend being set to Dummy
- Shows PPTC cache and shader cache enabled/disabled

Co-authored-by: Mary <1760003+Thog@users.noreply.github.com>
2023-03-03 19:58:50 +01:00
Mark Araujo 82958d47cb
Shows error snippet on empty log (#6)
* Error snippet shown on empty log
- Shader cache corruption warning

* Loop to get missing info in log

* Error search handles multiple terms
- Minor spelling correction for resolution value
- User settings visible on empty log
2023-03-03 19:58:50 +01:00
Mark Araujo 9bb4aeed9a
Ryuko bot fixes and improvements (#4)
* Fixes HTTPException by properly handling newline regex

* Improves information display:
- Shows settings info if no game is detected running
- Empty log warning takes up less space
- Error, Mods and Notes not shown on empty log

* Improved empty log message
- Also allow logs parsing in linux channel, as well as mentioning
when posting in non-allowed channels

* Fix ResScale parse error, better empty log message

* DM users about correct channels instead of in chat

* Fix variable spelling,  clearer bad dump warning
2023-03-03 19:58:50 +01:00
Mary 40fce5e354
Fix logfilereader logic 2023-03-03 19:58:50 +01:00
Mark Araujo 8550271af6
Log reading capabilities to Ryuko bot (#3)
* Add log reading capabilities
- User hardware specs
- Game info
- Controller configuration
- Last error snippet in log
- Warnings when using macOS or Intel iGPU
- Warning of logs not turned on

* Allowed log reading channels moved to config
This is easier for contributors to change their config file for testing

* Fixes large files not showing error snippet
- Large files are partially downloaded and show header information which
messes with log analysis, this gets stripped.
- Finding error messages function improved

* Default logs enabled shows a green checkmark

* Better feedback with log parsing message
- Bot prints `Log parsing...` and update message once log analysis done
- Added better error logging to console

* Better handling of invalid files warning
- Also fixed typo with bot message edit function

* Refactored embed generation to make more sense
- Embed is now based off a generic json assuming Unknown values at first
- Embed fields moved closer together
- Fields with newlines joined instead of manually separated
- Intel iGPU message changed to show preference for discrete GPU's

* Refactor to be simpler and easier to read.
- Hardware, ryujinx and log analysis split into separate functions
- Regex explicitly defined for each property instead of confusing map
- Added user settings reported, shows PPTC enabled or disabled

* Game notes sorted by order of severity
Notes will appear with most severe warnings first as follows:  ⚠️

* Analyses toggleable settings that appear in log
Currently these are: PPTC, audio backed, docked/handheld and vsync.
- Formatting change so these settings are more visible in bot embed

* Refactored user_settings, rewording of bot embed
- User settings reading handles missing log info for older versions
- `Switch Mode` (docked/handheld info) changed to `Console Mode`
- Missing firmware warning if firmware not installed

* Warning when shader cache collision detected

* Notes time elapsed in log file
- Error handling for no notes to log

* Show values for some user settings: audio, docked, missing services, resolution, shader cache and vsync

* Analyse user changeable settings
- Restructed embed to allow easier settings handling
- Changed embed formatting to deal with inline colums more cleanly

* Log file is now default function parameter

* Better sorting of analysis messages
- Now sorted alphabetically and by severity for consistency
- Show available RAM in low RAM warning
- Fix variable name misspelling

* Logging level changed to info

* Warn if bad dump in error message

* Add warning for no custom build support

* Warn user to post log in correct channel
- Warn about not supporting custom builds
- Warn to post in pr build if detected
- Warn about channels to post logs if detected in #general
2023-03-03 19:58:43 +01:00
Ave Ozkal 0877351cca
Rewrite the verification code to the one required by Ryujinx Guild 2022-11-10 14:41:26 +01:00
ave 3309ad6a23
Merge pull request #90 from reswitched/dpy2
discord.py v2 support
2022-08-18 09:52:27 +02:00
ave bafcaf313f Revamp readme for dpy2 merge 2022-08-18 09:51:52 +02:00
ave cc1a91c67b eh fuck hashes 2022-08-18 09:26:36 +02:00
ave 5de6d53201 Switch to stable dpy 2 2022-08-18 09:24:43 +02:00
ave d1cb1a334b Improve docker readme 2022-08-10 21:36:20 +02:00
ave 4783f61b23 docker improvements 2022-08-10 21:31:04 +02:00
ave 9f9fae34c1 further fixes 2022-08-10 21:27:47 +02:00
ave d2842c2cd6 drop hashes 2022-08-10 21:25:40 +02:00
ave 8077a587d5 Update requirements.txt 2022-08-10 20:08:45 +02:00
ave f794792ecc Update discord.py version 2022-08-10 20:08:09 +02:00
ave afd2423394 Fix !quit, closes #89 2022-08-10 20:05:54 +02:00
ave 9907b19424 Update deps 2022-08-10 20:02:45 +02:00
ave 01f2cf7751 black pass 2022-05-24 23:29:46 +02:00
ave 4f027a7d9b update readme 2022-05-24 23:29:40 +02:00
ave 51f69df254 fix intents 2022-05-24 23:26:24 +02:00
ave a7967b9f97 fix robocronp 2022-05-24 23:17:55 +02:00
ave 8415e1f787 Fix robocronp, make bot running better
progress!
2022-05-24 23:12:20 +02:00
ave 2d25ab5601 make more of the code dpy2 friendly
- robocronp is completely broken
- The overall init should be unfucked, example here: https://discordpy.readthedocs.io/en/latest/migrating.html#extension-and-cog-loading-unloading-is-now-asynchronous
2022-05-24 20:35:42 +02:00
ave 07f53753c3 update requirements.txt 2022-05-24 19:54:37 +02:00
ave 47130feb31 Basic changes to maybe make the code discord.py 2 compatible? 2022-05-24 19:53:40 +02:00
ave b02b3fdbd4
Merge pull request #87 from tastymeatball/patch-1
Update basic.py
2021-12-08 20:50:28 +03:00
tastymeatball b752d238fe
Update basic.py 2021-12-08 17:50:37 +01:00
ave 50d2c4f99b
Merge pull request #81 from Ryujinx/fix/bandel
bandel: Fix missing command marker
2021-08-06 20:40:12 +03:00
Mary e1fc841323 bandel: Fix missing command marker
This make the bandel command effectively work yay.
2021-08-06 19:39:11 +02:00
ave 4a5561aa18 Bump to 1.0.1 2021-08-01 01:33:08 +03:00
ave d7db790bc7 fuck 2021-08-01 01:31:29 +03:00
ave 908d701a27 fucking nix 2021-08-01 01:29:34 +03:00
ave 58db1e1e6c Allow humanize 3.9.0 2021-08-01 01:27:02 +03:00
ave 5ef0f42967 poetryify robocop 2021-08-01 00:28:20 +03:00
ave 7dfc2c427b mod: introduce bandel command 2021-07-28 00:04:58 +03:00
ave 8e5e2dc01d
Merge pull request #80 from DavidBuchanan314/master
Fix broken links to reswitched.team
2021-07-15 11:37:51 +00:00
David Buchanan fa7d5f2db5 Fix broken links: reswitched.team -> reswitched.github.io 2021-07-15 12:38:18 +01:00
Ave but on a massive iMac be71f129d9 massban fixes 2021-06-08 19:07:23 +03:00
Ave but on a massive iMac edacaaef5b massban fixes 2021-06-08 19:04:06 +03:00
Ave but on a massive iMac c1ffd8b57b massban fixes 2021-06-08 19:03:29 +03:00
Ave but on a massive iMac cff5a3f80d massban fixes 2021-06-08 19:02:58 +03:00
Ave but on a massive iMac 1ebb049f09 Add massban command 2021-06-08 19:00:39 +03:00
Ave 6a6c88cdb8
Merge pull request #79 from Ryujinx/feature/docker
Add Dockerfile
2021-02-28 16:47:05 +03:00
Mary 51a4bf948b Add Dockerfile 2021-02-28 14:45:31 +01:00
Ave 4450573013
Move some count commands over to a separate cog 2021-02-13 01:47:00 +03:00
Ave cdd98f49b5
mod: hotfix 2021-01-29 23:13:22 +03:00
Ave f39d6fa4ae
mod: Don't ping 2021-01-29 23:11:42 +03:00
Ave 586c6239b2
Merge pull request #77 from noirscape/roles-accessed-too-early
Fix a variable 'roles' referenced before assignment error when relying on default fallback.
2021-01-09 01:43:55 +03:00
noirscape 9b3a442699 Fix a variable 'roles' referenced before assignment error when relying on default fallback. 2021-01-08 23:37:37 +01:00
Ave 24182b11ca mod_reswitched: Also allow togglemod 2020-11-04 19:01:19 +03:00
Ave 161caf30e6 yubicootp: one more bugfix 2020-10-13 18:15:18 +03:00
Ave f5ece42b15 yubicootp: bugfix 2020-10-13 18:12:55 +03:00
Ave adc4931a57 yubicootp: Hackily fix issue of misreading padded signatures from
responses :)
2020-10-13 18:10:23 +03:00
Ave 0487031974
yubicootp: Add signature support
Works both ways! Optional too!
2020-10-13 18:04:21 +03:00
Ave 490916a1ca
yubicootp: Limit to one OTP per message 2020-10-13 17:37:13 +03:00
Ave 8d1ef828f0
yubicootp: Properly fetch mid-message OTPs, allow 1+ OTPs per msg 2020-10-13 17:32:55 +03:00
Ave 6f30585c96
yubicootp: Attempt to make it work mid-sentence 2020-10-13 17:29:00 +03:00
Ave 44518e810e
Add automated Yubico OTP revoke support
Thanks @linuxgemini for all your help with this!
2020-10-13 17:13:24 +03:00
Ave b2dd805f2d
Merge pull request #76 from leo60228/remove-asyncio
Remove asyncio from requirements.txt
2020-10-02 03:29:15 +03:00
Ave ff7ba6790a
Merge pull request #75 from leo60228/sanitize
Escape Markdown in clean_content calls
2020-10-02 03:28:56 +03:00
leo60228 d6aa8f7e67 Remove asyncio from requirements.txt
asyncio is bundled with Python 3.4+, and the version on PyPI may have
issues with newer versions of Python on some configurations.
2020-10-01 20:15:33 -04:00
leo60228 8d899cf9a7 Escape Markdown in clean_content calls
Fixes #74
2020-10-01 20:03:41 -04:00
Ave 165ce2f442
Fix privileged intents 2020-10-01 22:07:50 +03:00
Ave d905db5666
Support privileged intents 2020-10-01 22:05:40 +03:00
Ave 4b0eae89b2
Year of linux 2020-09-04 01:05:07 +03:00
Ave 2c967764c5
mod_reswitched/pingmod: change wording 2020-09-01 21:18:17 +03:00
Ave ffb129770d
mod_reswitched: Push 2020-09-01 10:58:35 +03:00
Ave 7cb112cf5a
Warn on commands where user often leaves (by force orotherwise) if it happens 2020-06-27 02:01:38 +03:00
Ave 009cf95ae5
robocronp: hotfix 2020-06-13 13:34:37 +03:00
Ave a98bcfb4e9
Fix robocronp fail when verificaiton isn't used
Closes #73
2020-06-13 13:33:00 +03:00
pixel-stuck d8ba6b42d6
Update verification.py
fix missing paren (hopefully this was the intended behavior)
2020-06-13 01:57:11 -04:00
Ave a583240a93
verification: Fix issue with wrong messages
Closes #72

Bit of a hack, heh.
2020-06-09 04:24:47 +03:00
Ave 057b3c81a9
robocop: extend whitelist 2020-06-04 23:42:28 +03:00
Ave 52028b3bd9
Mark "deepsea" as potential piracy violation 2020-06-02 03:52:46 +03:00
Ave 6a703f0f7d
mod_userlog: add jump links 2020-05-29 18:12:55 +03:00
Ave ca9ccde0e5
mod: even more jump link stylization 2020-05-29 18:10:52 +03:00
Ave 4bf7d8db98
mod: Stylize the jump link 2020-05-29 18:09:47 +03:00
Ave 944e4256b3
mod: Put jump links to mod actions
Closes #43
2020-05-29 18:06:45 +03:00
Ave 3696bcef5e
sar: Improve .sar's text 2020-05-29 17:55:27 +03:00
Ave fd3fbbcc53
sar: hotfix 2020-05-29 17:54:29 +03:00
Ave 78f6bc6811
sar: add sar
Closes #71
2020-05-29 17:52:08 +03:00
Ave 0c2cd9744e
Merge pull request #70 from ThatNerdyPikachu/patch-2
config_template: Extend blacklist
2020-05-29 01:46:42 +03:00
Pika 9ff9bda62c
config_template: Extend whitelist 2020-05-28 18:43:10 -04:00
Ave 44e05bccf3
config_template: Extend whitelist 2020-05-28 22:39:11 +00:00
Ave 110f50e335
Merge pull request #69 (nice) from ThatNerdyPikachu/patch-2
Update suspect wordlist
2020-05-29 01:37:07 +03:00
Pika 048de3320e
Update suspect wordlist 2020-05-28 18:35:41 -04:00
Ave 6cc6f540bf
admin/pull: if autopulling, don't autoload modules that aren't in
initial_cogs
2020-05-25 14:10:10 +03:00
Ave 3d4c0ecacf
mod/nickname: Handle permission issues
I vape
2020-05-25 14:06:29 +03:00
Ave Ozkal 341e7e61c9
Fix black linting fuckups 2020-05-17 23:44:15 +03:00
Ave Ozkal b041a47c3d
unban: add unban 2020-05-17 23:40:23 +03:00
Ave 09d96591ed
Merge pull request #68 from NicholeMattera/master
Fixed bug in lists cog.
2020-05-06 03:43:20 +03:00
Nichole Mattera 151a2b4a2a Changed how file extensions are checked. 2020-05-05 20:39:31 -04:00
Nichole Mattera 6bc7a0c4aa Fixed bug in lists cog. 2020-05-05 19:43:23 -04:00
Ave Ozkal ce74bbe138
verification: use the proper participant role 2020-04-21 16:17:44 +03:00
Ave 206b8b812e
Merge pull request #59 from Jan200101/formatting-edit
improve log formatting
2020-04-21 15:49:19 +03:00
Jan 9f53bcca4e
Merge branch 'master' into formatting-edit 2020-04-21 12:59:33 +02:00
Jan200101 19408cd163
use before_ and after_content where they were intended to be used 2020-04-21 12:58:17 +02:00
Ave Ozkal 1282a40986
verification: fix some bugs 2020-04-21 09:08:56 +03:00
Ave Ozkal f219d4411d
config_template: Move optional cog configs to the bottom 2020-04-21 01:31:59 +03:00
Ave Ozkal 8027a5a169
links: Move guide text to config too
Final change of today, I promise.
2020-04-21 01:31:04 +03:00
Ave Ozkal 287cdd0cc3
Structure config_template.py further 2020-04-21 01:17:20 +03:00
Ave Ozkal fd845ecb40
Do a black pass and add black to contributing guidelines
FORK MAINTAINERS: I'm so, so sorry, but this was planned since forever.
If you need help integrating this, feel free to contact me.
2020-04-21 01:05:32 +03:00
Ave Ozkal 38d8a4ce5f
BREAKING: Move verification lines to config
FORK MAINTAINERS: Merging this without updating your config WILL lead to
your bot not starting properly.

This is done to make future upstream merges easier.
2020-04-21 00:56:34 +03:00
Ave Ozkal c5c5b45741
BREAKING: Move initial cogs to config
FORK MAINTAINERS: Merging this without updating your config WILL lead to
your bot not starting properly.
2020-04-21 00:42:49 +03:00
Ave Ozkal f454219ecc
Replace dkp link as requested 2020-04-19 21:00:41 +03:00
Ave 534085fb5e
Merge pull request #67 from Ryujinx/feature/configurable-suspect-words
logs: Make suspect words configurable
2020-04-09 23:54:52 +03:00
Thog 95dd28d7a6 logs: Make suspect words configurable
This commit move the hardcoded suspect words to the configuration.
2020-04-09 20:54:27 +02:00
Ave 9433380a69
Merge pull request #66 from Ryujinx/fix/logs-on_member_join
logs: fix on_member_join crash
2020-04-08 21:25:18 +03:00
Thog 06c5c63a6f logs: fix on_member_join crash 2020-04-08 20:16:08 +02:00
Ave Ozkal 88f1d5200d
cox: unfox 2020-04-05 23:53:20 +03:00
Ave Ozkal 69ecfc5f62
Cox: Clean content before showing 2020-04-05 23:50:58 +03:00
Ave Ozkal c05811e8f4
Add new highlight word 2020-03-26 10:22:27 +03:00
Ave Ozkal 0505977cb5
Remove an old guide 2020-03-19 22:30:17 +03:00
Ave Ozkal 1e0ed86b4c
Add official dev env thing 2020-03-19 22:19:38 +03:00
Ave Ozkal e2076fd414
Remove a guide 2020-03-19 21:33:52 +03:00
Ave Ozkal d04a4e8faa
Update list of maintainers 2020-03-14 16:26:47 +03:00
Ave 10716f2a39
Merge pull request #65 from NicholeMattera/master
List Management Cog
2020-02-26 18:40:27 +03:00
Nichole Mattera 1fe4dab6cd Forgot to remove logs after debugging. 2020-02-26 08:46:11 -05:00
Nichole Mattera fd3a26c80e Ran code through autopep8. 2020-02-25 20:08:55 -05:00
Nichole Mattera 4058612e96 Added cog to manage channels dedicated towards lists. 2020-02-25 18:15:49 -05:00
Ave Ozkal 40570ee112
noteid: fix a bug where it wouldn't work 2020-02-03 20:18:30 +03:00
Ave 2055a63eec
Merge pull request #62 from Mz49/patch-1
Condense repeated code to a function
2020-01-08 15:51:54 +00:00
Mz 656dbb1a0e
condensed a few lines to a new function 2020-01-08 02:08:38 -08:00
Ave (High Sec Drive) d056752e98 logs: Only allow events from whitelsited guilds 2019-12-27 16:43:47 +01:00
Ave Ozkal eeb2689bae
Impose rate limits on .cox
Currently 1 per 3 hours: https://discordapp.com/channels/269333940928512010/286612533757083648/658069981698981899
2019-12-22 01:20:21 +03:00
Ave Ozkal b6f6b98480
Revert ".trump"
This reverts commit e53a6341a5.
2019-12-22 01:19:28 +03:00
Ave Ozkal fac3ac3605
Revert ".trump: make image size be... better"
This reverts commit 83b18d7a0d.
2019-12-22 01:18:59 +03:00
Ave Ozkal 83b18d7a0d
.trump: make image size be... better 2019-12-19 16:43:37 +03:00
Ave Ozkal e53a6341a5
.trump
fml this bot became not only a liberal but also a bloated mess
2019-12-19 16:38:24 +03:00
Ave 1359e51a84
Merge pull request #61 from lucyyyyyyy/patch-6
i too am a bitch
2019-12-19 15:34:11 +02:00
Lucy fc18f4a83c
im a bitch 2019-12-19 23:30:27 +10:00
Ave Ozkal 1c5ab9c63a
add .ttfs .gitignore 2019-12-19 11:37:33 +03:00
Ave Ozkal 5174da59aa
Limit .cox to ot and staff 2019-12-19 04:35:17 +03:00
Ave Ozkal ee2ec67c17
fix cox 2019-12-19 04:31:48 +03:00
Ave Ozkal e4fcc81a13
add .cox
Literal bloat
2019-12-19 04:17:36 +03:00
Ave Ozkal f44e1ea17b
Verif: add rule 11 2019-12-07 13:31:59 +03:00
Ave 0629a09d69
Fix my gitlab url 2019-11-26 08:52:23 +02:00
Ave Ozkal 11d264cbd8
basic: Add hackercount 2019-11-11 14:29:30 +03:00
Ave Ozkal a0fd90fabd
Make robocop *actually* dm on help 2019-11-06 02:06:21 +03:00
Ave Ozkal 3e70ede154
Make robocop dm on help 2019-11-06 02:01:22 +03:00
Ave Ozkal 527f55e6dd
Fix usage not showing command name 2019-11-06 01:59:29 +03:00
Ave Ozkal 3ed010a4ce
blackalabi
blackalabi
2019-11-05 12:43:17 +03:00
Ave Ozkal 9d65235687
kick: add boot. 2019-10-30 23:04:59 +03:00
Jan200101 623f841faa
replace codeblock and insert zero width joiner next to stray backticks
- replace simple codeblocks (`) with multiline codeblocks (```)
- add a zero width joiner (U+200D) after every backtick in the message content effectively canceling any formatting done by it

Should fix #17
2019-10-22 00:12:09 +02:00
Ave Ozkal d97f948e9b
:) 2019-10-07 10:19:01 +03:00
Ave 79dbfc8611
Merge pull request #56 from Jan200101/mention-fix
escape user and display name
2019-09-21 21:04:54 +03:00
Jan200101 6ca37b2335 escape user and display name
escaping the user name might be a bit overkill but its better to be safe than sorry I guess
2019-09-21 19:47:19 +02:00
Ave f5acc27ccd
Merge pull request #54 from leo60228/zalgomeme
Add zalgo to hedgeberg bam meme
2019-09-19 16:28:02 +03:00
leo60228 2122e0b217 Add zalgo to hedgeberg bam meme 2019-09-19 09:27:09 -04:00
Ave b9940e507f
Merge pull request #53 from leo60228/selfbam
Add selfbam memes
2019-09-19 00:25:02 +03:00
leo60228 2bcb9ca394 Add selfbam memes 2019-09-18 17:19:05 -04:00
Ave 4d935aa257
mod: bike meme 2019-09-19 00:12:12 +03:00
Ave Ozkal 1b89752651
mod: hedge-proofing but this time actually hardcode hedge's id lmao 2019-09-18 16:20:15 +03:00
Ave 1febd31635
Merge pull request #50 from thedax/master
Escape Nintendo Homebrew invite link with < >s.
2019-09-18 06:16:39 +03:00
The Dax bb98d0606a Swap lines as per tart's request 2019-09-17 18:38:07 -04:00
Ave Ozkal a595baa2ad
mod: Improve hedgeproofing by also mod blocking actions on bot 2019-09-17 13:26:41 +03:00
Ave Ozkal c91284cd92
mod: meme. 2019-09-17 13:23:21 +03:00
The Dax 4408850017 Escape Nintendo Homebrew invite link with < >s.
This prevents it from picking up the full stop at the end of the sentence as part of the URL and displaying an annoying second information box.
2019-09-14 20:07:30 -04:00
Ave 922f2ac37d
Merge pull request #49 from suppai/master
Don't advertise a bot ReSwitched doesn't have
2019-09-14 18:40:55 +03:00
Noëlle Mercer 6ad14de9a7
Update err.py 2019-09-14 11:39:06 -04:00
Noëlle Mercer 7d8e39c900
Don't advertise a bot ReSwitched doesn't have 2019-09-14 11:37:23 -04:00
Ave adc9ccb60b
Merge pull request #47 from kitlith/patch-1
Add a link to NH in the newcomers text.
2019-09-14 18:36:19 +03:00
tomGER 25cecff058
Update ERR to include warning
I've had enough dms to be annoyed at this point ._.
2019-09-11 17:45:44 +02:00
Ave Ozkal 5f21ecbcb7
meme: add yotld 2019-08-12 13:09:36 +03:00
Kitlith af0e91197e
Change to use roblabla's wording before I forget. 2019-08-08 13:52:51 -07:00
Kitlith abf68e86b9
Add a link to NH in the newcomers text. 2019-08-07 17:47:33 -07:00
Ave Ozkal 40c49caca4
Add a tiny note to README about role position 2019-08-06 12:54:23 +03:00
Ave Ozkal 3bc58e6e11
get_user_info -> fetch_user #46 2019-07-01 23:14:51 +03:00
Ave aafcdc30f2
Merge pull request #45 from roblabla/master
Specify hex encoding and digest size
2019-06-30 11:11:54 +03:00
roblabla c86edbfe48 Specify hex encoding and digest size 2019-06-29 18:29:19 +00:00
Ave 18b98692f6
Merge pull request #44 from suppai/master
Update mod.py
2019-06-28 12:23:42 +03:00
Noëlle Mercer 215d3034bc
Update mod.py
https://cdn.discordapp.com/emojis/591448135075889204.png?v=1
2019-06-28 05:17:53 -04:00
Ave Ozkal 67728ccbd1
Don't force our event loop, use dpy's one
Also use stable dpy instead of master

Closes #38
2019-06-25 11:39:00 +03:00
Ave 5861a76674
Merge pull request #41 from leo60228/patch-1
Add "cracked" to susp_words
2019-06-22 20:46:10 +03:00
Ave 9c44ed04c7
Merge pull request #42 from noahc3/master
Change guide.sdsetup.com links to switch.homebrew.guide
2019-06-22 20:45:47 +03:00
noahc3 0f9b9ef0bd
Change links to switch.homebrew.guide 2019-06-22 12:25:27 -05:00
Ave Ozkal 6ac7ce0ad6
Pleasing my neurodiverse traits 2019-06-22 19:50:10 +03:00
tomGER 7de8d19b4a
added more errcodes by m4xw 2019-06-21 16:38:11 +02:00
tomGER fe0779a871
Update errcodes.py 2019-06-21 16:32:03 +02:00
/u/leo60228 6ca7516312
Add "cracked" to susp_words 2019-06-19 19:44:04 -04:00
Ave 2b83b355e9
Merge pull request #40 from thedax/typo-fix
Fix typo/copy-paste error
2019-06-18 07:57:35 +03:00
The Dax a58bcb0589 Fix typo/copy-paste error 2019-06-18 00:54:07 -04:00
Ave Ozkal a1991921a2
dec: fix 2019-06-17 19:18:33 +03:00
Ave Ozkal 831e44d56b
dec command + ratelimit for hex 2019-06-17 19:17:13 +03:00
Ave Ozkal 9ca55a5461
hex: better uppercase 2019-06-17 19:11:08 +03:00
Ave Ozkal 941270a586
add hex command 2019-06-17 19:10:01 +03:00
Ave edc7442532
Merge pull request #39 from jakibaki/master
Vital fix to meme-command
2019-06-15 11:34:58 +03:00
jakibaki 0536941075 Vital fix to meme-command 2019-06-15 10:33:33 +02:00
Tyler True 8a511cd109 Fix requirements.txt (#37)
The `rewrite` branch of discord.py no longer exists, presumably it has been merged into `master` since then.
Remove `@rewrite` from discord.py line in the pip requirements file.
2019-06-13 17:50:00 +02:00
tumGER 93f2a694c9 Update [ERR] 2019-06-13 17:44:14 +02:00
Ave 8a242956ac
Create SECURITY.md
closes #36
2019-06-12 23:05:47 +00:00
Ave Ozkal 6007959119
hotfix, wrap link in <> 2019-06-04 01:14:54 +03:00
Ave Ozkal a7c3f52893
links/guide: add the serial checker 2019-06-04 01:14:19 +03:00
Ave Ozkal 46d0a5246f
Drop the hashalg() thing as it was confusing everyone 2019-06-04 01:03:06 +03:00
Ave Ozkal 997cd80bf9
Extend the list of whitelisted commands on newcomers
Also, hopefully the new logic is (at least slightly) better.
2019-04-25 10:17:06 +03:00
Ave Ozkal 4ce90624c3
Fix resetalgo
I'm dumb
2019-04-25 10:14:12 +03:00
Ave Ozkal 2ca99a8fcb
Readme changes
Made it so that there's no confusion as to who "I" refers to after to
after the move from my own repo to reswitched org.
2019-04-25 10:10:56 +03:00
Ave Ozkal 0a73902dff
Remove plailect's guide
(as requested by tomger, and as approved by plailect)

Add back when it's updated.
2019-04-24 16:35:42 +03:00
Ave Ozkal 06df732c80
incorrect documentation is just 👌 2019-04-24 12:08:44 +03:00
Ave Ozkal fca8684f34
Add cog load actions, solve the bug we found on verification reload 2019-04-24 12:07:29 +03:00
Ave Ozkal 319385f100
Log invite code instead of invite link (untested) 2019-04-24 12:03:57 +03:00
Ave Ozkal 8a152ed8ef
testing on prod 2019-04-24 11:59:04 +03:00
Ave Ozkal 170116a641
Align text (it bugs me)
Literal, medical autism 👌
2019-04-24 10:50:46 +03:00
Ave Ozkal 72fd1c78bb
Fix typos (or else people will be unhappy) 2019-04-24 10:49:45 +03:00
Ave Ozkal 4cf7a6575c
Clean up verification a bit, modularize it, reset on start and daily 2019-04-24 10:47:07 +03:00
Ave Ozkal 81f8fd875f
Fix the randomized hash algorithm 2019-04-24 10:24:59 +03:00
結城イヴ a1bc115621 hash_choice needs to change every reset, not every boot 2019-04-24 01:24:26 -04:00
結城イヴ cccfe27310 oops v2 2019-04-24 01:03:03 -04:00
結城イヴ 95d69d7979 Fix the wrong hash algo check and make it a case of the existing elif chain 2019-04-24 01:00:33 -04:00
結城イヴ a7e4575049 Oops 2019-04-23 23:58:17 -04:00
結城イヴ 20374bb5b8 Randomize hash choice 2019-04-23 23:37:58 -04:00
81 changed files with 8600 additions and 2879 deletions

8
.dockerignore Normal file
View file

@ -0,0 +1,8 @@
**/__pycache__/
.git/
.github/
.gitignore
.dockerignore
**/config.py
**/data/

18
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,18 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
open-pull-requests-limit: 5
reviewers:
- marysaka
schedule:
interval: weekly
- package-ecosystem: pip
directory: /
open-pull-requests-limit: 5
reviewers:
- marysaka
schedule:
interval: weekly

2
.github/reviewers.yml vendored Normal file
View file

@ -0,0 +1,2 @@
default:
- TSRBerry

59
.github/workflows/formatting.yml vendored Normal file
View file

@ -0,0 +1,59 @@
name: Check formatting
on:
pull_request:
permissions:
contents: write
pull-requests: write
checks: write
jobs:
black:
name: Python Black
runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name == github.repository
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
- uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install black
run: pip install black
- name: Configure git
run: |
git config --global user.name github-actions[bot]
git config --global user.email 41898282+github-actions[bot]@users.noreply.github.com
- name: Run black
run: python -m black .
- name: Check if files have been modified
id: mod_check
run: |
[[ $(git status -s | wc -l) -le 1 ]] \
&& echo "is-dirty=false" >> "$GITHUB_OUTPUT" \
|| echo "is-dirty=true" >> "$GITHUB_OUTPUT"
- name: Commit and push changes
if: steps.mod_check.outputs.is-dirty == 'true'
run: |
git add .
git commit -m "Apply black formatting"
git push
fork-black:
name: Python Black
runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name != github.repository
steps:
- uses: actions/checkout@v4
- uses: psf/black@stable

41
.github/workflows/mako.yml vendored Normal file
View file

@ -0,0 +1,41 @@
name: Mako
on:
discussion:
types: [created, edited, answered, unanswered, category_changed]
discussion_comment:
types: [created, edited]
gollum:
issue_comment:
types: [created, edited]
issues:
types: [opened, edited, reopened, pinned, milestoned, demilestoned, assigned, unassigned, labeled, unlabeled]
pull_request_target:
types: [opened, edited, reopened, synchronize, ready_for_review, assigned, unassigned]
jobs:
tasks:
name: Run Ryujinx tasks
permissions:
actions: read
contents: read
discussions: write
issues: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
if: github.event_name == 'pull_request_target'
with:
# Ensure we pin the source origin as pull_request_target run under forks.
fetch-depth: 0
repository: Ryujinx/ryuko-ng
ref: master
- name: Run Mako command
uses: Ryujinx/Ryujinx-Mako@master
with:
command: exec-ryujinx-tasks
args: --event-name "${{ github.event_name }}" --event-path "${{ github.event_path }}" -w "${{ github.workspace }}" "${{ github.repository }}" "${{ github.run_id }}"
app_id: ${{ secrets.MAKO_APP_ID }}
private_key: ${{ secrets.MAKO_PRIVATE_KEY }}
installation_id: ${{ secrets.MAKO_INSTALLATION_ID }}

2
.gitignore vendored
View file

@ -98,7 +98,7 @@ files/*
*.ttf
priv-*
config.py
# Prevent data files from being committed
data/
robocop_ng/config.py

12
Dockerfile Normal file
View file

@ -0,0 +1,12 @@
FROM python:3.10-alpine
WORKDIR /usr/src/app
COPY poetry.lock pyproject.toml ./
RUN apk add --no-cache gcc musl-dev python3-dev libffi-dev openssl-dev cargo && pip install --no-cache-dir poetry && poetry config virtualenvs.create false && poetry install --no-root --no-interaction --no-ansi -vvv && apk del gcc musl-dev python3-dev libffi-dev openssl-dev cargo
COPY . .
WORKDIR /usr/src/app
CMD [ "python", "-m", "robocop_ng", "/state" ]

145
README.md
View file

@ -1,19 +1,43 @@
# Robocop-ng
# ryuko-ng
Next-gen rewrite of Kurisu/Robocop bot used on ReSwitched bot with discord.py rewrite, designed to be relatively clean, consistent and un-bloated.
Discord bot for handling Ryujinx moderation tasks and such, (n)ext-(g)en rewrite of Robocop
Code is based on https://gitlab.com/ao/dpybotbase and https://github.com/916253/Kurisu-Reswitched.
Code is based on https://github.com/reswitched/robocop-ng.
---
## How to migrate from discord.py v1 to v2
As of 18.08.2022 this repo is based on discord.py v2.
Only changes needed are updating your cogs and ensuring that all privileged intents are enabled for your bot.
You can find the privileged intents guide here: https://discordpy.readthedocs.io/en/latest/intents.html?highlight=intents#privileged-intents
You can see the migration instructions for your cogs here: https://discordpy.readthedocs.io/en/latest/migrating.html
---
## How to run
- Copy `config.py.template` to `config.py`, configure all necessary parts to your server.
- Install python3.6+.
- Install python dependencies (`pip3 install -Ur requirements.txt`, you might need to put `sudo -H` before that)
- If you're moving from Kurisu or Robocop: Follow `Tips for people moving from Kurisu/Robocop` below.
- Run `Robocop.py` (`python3 Robocop.py`)
- Copy `robocop_ng/config_template.py` to `robocop_ng/config.py` and **configure all necessary parts for your server**.
- Enable all privileged intents ([guide here](https://discordpy.readthedocs.io/en/latest/intents.html?highlight=intents#privileged-intents)) for the bot. You don't need to give Discord your passport as Ryuko-NG is not designed to run in >1 guild at once, let alone >100.
- Add the bot to your guild. There are many resources about this online.
- If you haven't already done this already, **move the bot's role above the roles it'll need to manage, or else it won't function properly**, this is especially important for verification as it doesn't work otherwise.
- If you're moving from Kurisu or Robocop: Follow [Tips for people moving from Kurisu/Robocop](https://github.com/Ryujinx/ryuko-ng#tips-for-people-moving-from-kurisurobocop) below.
### Running with docker
- `docker build . -t robocopng`
- Assuming your robocop-ng repo is on `~/docker/`: `docker run --restart=always -v ~/docker/robocop-ng:/usr/src/app --name robocop_ng robocopng:latest`
For updates, run `git pull;docker rm -f robocop_ng` then run the two commands above again.
### Running manually
- Install python3.8+.
- Install dependencies with [poetry](https://python-poetry.org/) using `poetry install`.
- Run `robocop_ng/__main__.py` (`cd robocop_ng;python3 __main__.py`).
To keep the bot running, you might want to use pm2 or a systemd service.
@ -23,115 +47,30 @@ To keep the bot running, you might want to use pm2 or a systemd service.
If you're moving from Kurisu/Robocop, and want to preserve your data, you'll want to do the following steps:
- Copy your `data` folder over.
- Copy your `data` folder over into the `robocop_ng` folder.
- Rename your `data/warnsv2.json` file to `data/userlog.json`.
- Edit `data/restrictions.json` and replace role names (`"Muted"` etc) with role IDs (`526500080879140874` etc). Make sure to have it as int, not as str (don't wrap role id with `"` or `'`).
---
## TODO
## Contributing
All Robocop features are now supported.
Contributions are welcome. If you're unsure if your PR would be merged or not, ask in the [Ryujinx discord guild](https://discord.gg/ryujinx) pinging Berry.
<details>
<summary>List of added Kurisu/Robocop features</summary>
<p>
- [x] .py configs
- [x] membercount command
- [x] Meme commands and pegaswitch (honestly the easiest part)
- [x] source command
- [x] robocop command
- [x] Verification: Actual verification system
- [x] Verification: Reset command
- [x] Logging: joins
- [x] Logging: leaves
- [x] Logging: role changes
- [x] Logging: bans
- [x] Logging: kicks
- [x] Moderation: speak
- [x] Moderation: ban
- [x] Moderation: silentban
- [x] Moderation: kick
- [x] Moderation: userinfo
- [x] Moderation: approve-revoke (community)
- [x] Moderation: addhacker-removehacker (hacker)
- [x] Moderation: probate-unprobate (participant)
- [x] Moderation: lock-softlock-unlock (channel lockdown)
- [x] Moderation: mute-unmute
- [x] Moderation: playing
- [x] Moderation: botnickname
- [x] Moderation: nickname
- [x] Moderation: clear/purge
- [x] Moderation: restrictions (people who leave with muted role will get muted role on join)
- [x] Warns: warn
- [x] Warns: listwarns-listwarnsid
- [x] Warns: clearwarns-clearwarnsid
- [x] Warns: delwarnid-delwarn
- [x] .serr and .err (thanks tomger!)
</p>
</details>
You're expected to use [black](https://github.com/psf/black) for code formatting before sending a PR. Simply install it with pip (`pip3 install black`), and run it with `black .`.
---
The main goal of this project, to get Robocop functionality done, is complete.
## Credits
Secondary goal is adding new features:
Ryuko-NG is a fork of [Robocop-NG](https://github.com/reswitched/robocop-ng) that is mainly maintained by [@TSRBerry](https://github.com/TSRBerry) and [@marysaka](https://github.com/marysaka).
- [ ] Purge: On purge, send logs in form of txt file to server logs
- [ ] New verification feature: Using log module from akbbot for logging attempts and removing old attempts
- [ ] New feature: Modmail
- [ ] New feature: Submiterr (relies on modmail)
- [ ] New feature: Highlights (problematic words automatically get posted to modmail channel, relies on modmail)
- [ ] Feature creep: Shortlink completion (gl/ao/etc)
- [ ] New moderation feature: timelock (channel lockdown with time, relies on robocronp)
[Robocop-NG](https://github.com/reswitched/robocop-ng) was initially developed by [@aveao](https://github.com/aveao) and @tumGER. It is currently maintained by [@aveao](https://github.com/aveao). Similarly, the official robocop-ng on the ReSwitched discord guild is hosted by [@aveao](https://github.com/aveao) too.
<details>
<summary>Completed features</summary>
<p>
- [x] Better security, better checks and better guild whitelisting
- [x] Feature creep: Reminds
- [x] A system for running jobs in background with an interval (will be called robocronp)
- [x] Commands to list said jobs and remove them
- [x] New moderation feature: timemute (mute with time, relies on robocronp)
- [x] New moderation feature: timeban (ban with expiry, relies on robocronp)
- [x] Improvements to lockdown to ensure that staff can talk
- [x] New moderation feature: Display of mutes, bans and kicks on listwarns (.userlog now)
- [x] New moderation feature: User notes
- [x] New moderation feature: Reaction removing features (thanks misson20000!)
- [x] New moderation feature: User nickname change
- [x] New moderation feature: watch-unwatch
- [x] New moderation feature: tracking suspicious keywords
- [x] New moderation feature: tracking invites posted
- [x] New self-moderation feature: .mywarns
</p>
</details>
<details>
<summary>TODO for robocronp</summary>
<p>
- [ ] Reduce code repetition on mod_timed.py
- [x] Allow non-hour values on timed bans
the following require me to rethink some of the lockdown code, which I don't feel like
- [ ] lockdown in helper
- [ ] timelock command
- [ ] working cronjob for unlock
</p>
</details>
---
## Thanks to
I would like to thank the following, in no particular order:
- ReSwitched community, for being amazing
- ihaveamac/ihaveahax and f916253 for the original kurisu/robocop
- tomGER for working hard on rewriting the .err/.serr commands, those were a nightmare
- misson20000 for adding in reaction removal feature and putting up with my many BS requests on PR reviews
- linuxgemini for helping out with Yubico OTP revocation code (which is based on their work)
- Everyone who contributed to robocop-ng/ryuko-ng in any way (reporting a bug, sending a PR, forking and hosting their own at their own guild, etc).

View file

@ -1,209 +0,0 @@
import os
import asyncio
import sys
import logging
import logging.handlers
import traceback
import aiohttp
import config
import discord
from discord.ext import commands
script_name = os.path.basename(__file__).split('.')[0]
log_file_name = f"{script_name}.log"
# Limit of discord (non-nitro) is 8MB (not MiB)
max_file_size = 1000 * 1000 * 8
backup_count = 3
file_handler = logging.handlers.RotatingFileHandler(
filename=log_file_name, maxBytes=max_file_size, backupCount=backup_count)
stdout_handler = logging.StreamHandler(sys.stdout)
log_format = logging.Formatter(
'[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s')
file_handler.setFormatter(log_format)
stdout_handler.setFormatter(log_format)
log = logging.getLogger('discord')
log.setLevel(logging.INFO)
log.addHandler(file_handler)
log.addHandler(stdout_handler)
def get_prefix(bot, message):
prefixes = config.prefixes
return commands.when_mentioned_or(*prefixes)(bot, message)
wanted_jsons = ["data/restrictions.json",
"data/robocronptab.json",
"data/userlog.json",
"data/invites.json"]
initial_extensions = ['cogs.common',
'cogs.admin',
'cogs.verification',
'cogs.mod',
'cogs.mod_note',
'cogs.mod_reacts',
'cogs.mod_userlog',
'cogs.mod_timed',
'cogs.mod_watch',
'cogs.basic',
'cogs.logs',
'cogs.err',
'cogs.lockdown',
'cogs.legacy',
'cogs.links',
'cogs.remind',
'cogs.robocronp',
'cogs.meme',
'cogs.pin',
'cogs.invites']
bot = commands.Bot(command_prefix=get_prefix,
description=config.bot_description, pm_help=True)
bot.log = log
bot.loop = asyncio.get_event_loop()
bot.config = config
bot.script_name = script_name
bot.wanted_jsons = wanted_jsons
if __name__ == '__main__':
for extension in initial_extensions:
try:
bot.load_extension(extension)
except Exception as e:
log.error(f'Failed to load extension {extension}.')
log.error(traceback.print_exc())
@bot.event
async def on_ready():
aioh = {"User-Agent": f"{script_name}/1.0'"}
bot.aiosession = aiohttp.ClientSession(headers=aioh)
bot.app_info = await bot.application_info()
bot.botlog_channel = bot.get_channel(config.botlog_channel)
log.info(f'\nLogged in as: {bot.user.name} - '
f'{bot.user.id}\ndpy version: {discord.__version__}\n')
game_name = f"{config.prefixes[0]}help"
# Send "Robocop has started! x has y members!"
guild = bot.botlog_channel.guild
msg = f"{bot.user.name} has started! "\
f"{guild.name} has {guild.member_count} members!"
data_files = [discord.File(fpath) for fpath in wanted_jsons]
await bot.botlog_channel.send(msg, files=data_files)
activity = discord.Activity(name=game_name,
type=discord.ActivityType.listening)
await bot.change_presence(activity=activity)
@bot.event
async def on_command(ctx):
log_text = f"{ctx.message.author} ({ctx.message.author.id}): "\
f"\"{ctx.message.content}\" "
if ctx.guild: # was too long for tertiary if
log_text += f"on \"{ctx.channel.name}\" ({ctx.channel.id}) "\
f"at \"{ctx.guild.name}\" ({ctx.guild.id})"
else:
log_text += f"on DMs ({ctx.channel.id})"
log.info(log_text)
@bot.event
async def on_error(event_method, *args, **kwargs):
log.error(f"Error on {event_method}: {sys.exc_info()}")
@bot.event
async def on_command_error(ctx, error):
error_text = str(error)
err_msg = f"Error with \"{ctx.message.content}\" from "\
f"\"{ctx.message.author} ({ctx.message.author.id}) "\
f"of type {type(error)}: {error_text}"
log.error(err_msg)
if not isinstance(error, commands.CommandNotFound):
err_msg = bot.escape_message(err_msg)
await bot.botlog_channel.send(err_msg)
if isinstance(error, commands.NoPrivateMessage):
return await ctx.send("This command doesn't work on DMs.")
elif isinstance(error, commands.MissingPermissions):
roles_needed = '\n- '.join(error.missing_perms)
return await ctx.send(f"{ctx.author.mention}: You don't have the right"
" permissions to run this command. You need: "
f"```- {roles_needed}```")
elif isinstance(error, commands.BotMissingPermissions):
roles_needed = '\n-'.join(error.missing_perms)
return await ctx.send(f"{ctx.author.mention}: Bot doesn't have "
"the right permissions to run this command. "
"Please add the following roles: "
f"```- {roles_needed}```")
elif isinstance(error, commands.CommandOnCooldown):
return await ctx.send(f"{ctx.author.mention}: You're being "
"ratelimited. Try in "
f"{error.retry_after:.1f} seconds.")
elif isinstance(error, commands.CheckFailure):
return await ctx.send(f"{ctx.author.mention}: Check failed. "
"You might not have the right permissions "
"to run this command, or you may not be able "
"to run this command in the current channel.")
elif isinstance(error, commands.CommandInvokeError) and\
("Cannot send messages to this user" in error_text):
return await ctx.send(f"{ctx.author.mention}: I can't DM you.\n"
"You might have me blocked or have DMs "
f"blocked globally or for {ctx.guild.name}.\n"
"Please resolve that, then "
"run the command again.")
elif isinstance(error, commands.CommandNotFound):
# Nothing to do when command is not found.
return
help_text = f"Usage of this command is: ```{ctx.prefix}"\
f"{ctx.command.signature}```\nPlease see `{ctx.prefix}help "\
f"{ctx.command.name}` for more info about this command."
if isinstance(error, commands.BadArgument):
return await ctx.send(f"{ctx.author.mention}: You gave incorrect "
f"arguments. {help_text}")
elif isinstance(error, commands.MissingRequiredArgument):
return await ctx.send(f"{ctx.author.mention}: You gave incomplete "
f"arguments. {help_text}")
@bot.event
async def on_message(message):
if message.author.bot:
return
if (message.guild) and (message.guild.id not in config.guild_whitelist):
return
# Ignore messages in newcomers channel, unless it's potentially reset
if message.channel.id == config.welcome_channel and\
"reset" not in message.content:
return
ctx = await bot.get_context(message)
await bot.invoke(ctx)
if not os.path.exists("data"):
os.makedirs("data")
for wanted_json in wanted_jsons:
if not os.path.exists(wanted_json):
with open(wanted_json, "w") as f:
f.write("{}")
bot.run(config.token, bot=True, reconnect=True, loop=bot.loop)

22
SECURITY.md Normal file
View file

@ -0,0 +1,22 @@
# Security Policy
PRs to this file to improve wording are welcome.
Please do not try to exploit public instances if it's going to cause harm, instead, set up your own instance of robocop-ng.
Breaking "database" files, running arbitrary code, using an unprivileged user to do something user can't normally do (editing channels or guild, deleting others' messages, making bot do an @e or @h mention, reading channels that user can't read, writing to channels that user can't write to, etc.) are all considered harmful.
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------------ | ------------------ |
| Latest git | :white_check_mark: |
## Reporting a Vulnerability
If the vulnerability fits into the "harmful" category specified above, then please email arcab [at] ave [dot] zone with details, as creating a public issue may cause it to be abused on public instances.
If not, please open an issue.

View file

@ -1,160 +0,0 @@
import discord
from discord.ext import commands
from discord.ext.commands import Cog
import traceback
import inspect
import re
from helpers.checks import check_if_bot_manager
class Admin(Cog):
def __init__(self, bot):
self.bot = bot
self.last_eval_result = None
self.previous_eval_code = None
@commands.guild_only()
@commands.check(check_if_bot_manager)
@commands.command(name='exit', aliases=["quit", "bye"])
async def _exit(self, ctx):
"""Shuts down the bot, bot manager only."""
await ctx.send(":wave: Goodbye!")
await self.bot.logout()
@commands.guild_only()
@commands.check(check_if_bot_manager)
@commands.command()
async def load(self, ctx, ext: str):
"""Loads a cog, bot manager only."""
try:
self.bot.load_extension("cogs." + ext)
except:
await ctx.send(f':x: Cog loading failed, traceback: '
f'```\n{traceback.format_exc()}\n```')
return
self.bot.log.info(f'Loaded ext {ext}')
await ctx.send(f':white_check_mark: `{ext}` successfully loaded.')
@commands.guild_only()
@commands.check(check_if_bot_manager)
@commands.command()
async def fetchlog(self, ctx):
"""Returns log"""
await ctx.send("Here's the current log file:",
file=discord.File(f"{self.bot.script_name}.log"))
@commands.guild_only()
@commands.check(check_if_bot_manager)
@commands.command()
async def fetchdata(self, ctx):
"""Returns data files"""
data_files = [discord.File(fpath) for fpath in self.bot.wanted_jsons]
await ctx.send("Here you go:", files=data_files)
@commands.guild_only()
@commands.check(check_if_bot_manager)
@commands.command(name='eval')
async def _eval(self, ctx, *, code: str):
"""Evaluates some code, bot manager only."""
try:
code = code.strip('` ')
env = {
'bot': self.bot,
'ctx': ctx,
'message': ctx.message,
'server': ctx.guild,
'guild': ctx.guild,
'channel': ctx.message.channel,
'author': ctx.message.author,
# modules
'discord': discord,
'commands': commands,
# utilities
'_get': discord.utils.get,
'_find': discord.utils.find,
# last result
'_': self.last_eval_result,
'_p': self.previous_eval_code,
}
env.update(globals())
self.bot.log.info(f"Evaling {repr(code)}:")
result = eval(code, env)
if inspect.isawaitable(result):
result = await result
if result is not None:
self.last_eval_result = result
self.previous_eval_code = code
sliced_message = await self.bot.slice_message(repr(result),
prefix="```",
suffix="```")
for msg in sliced_message:
await ctx.send(msg)
except:
sliced_message = \
await self.bot.slice_message(traceback.format_exc(),
prefix="```",
suffix="```")
for msg in sliced_message:
await ctx.send(msg)
@commands.guild_only()
@commands.check(check_if_bot_manager)
@commands.command()
async def pull(self, ctx, auto=False):
"""Does a git pull, bot manager only."""
tmp = await ctx.send('Pulling...')
git_output = await self.bot.async_call_shell("git pull")
await tmp.edit(content=f"Pull complete. Output: ```{git_output}```")
if auto:
cogs_to_reload = re.findall(r'cogs/([a-z_]*).py[ ]*\|', git_output)
for cog in cogs_to_reload:
try:
self.bot.unload_extension("cogs." + cog)
self.bot.load_extension("cogs." + cog)
self.bot.log.info(f'Reloaded ext {cog}')
await ctx.send(f':white_check_mark: `{cog}` '
'successfully reloaded.')
except:
await ctx.send(f':x: Cog reloading failed, traceback: '
f'```\n{traceback.format_exc()}\n```')
return
@commands.guild_only()
@commands.check(check_if_bot_manager)
@commands.command()
async def unload(self, ctx, ext: str):
"""Unloads a cog, bot manager only."""
self.bot.unload_extension("cogs." + ext)
self.bot.log.info(f'Unloaded ext {ext}')
await ctx.send(f':white_check_mark: `{ext}` successfully unloaded.')
@commands.check(check_if_bot_manager)
@commands.command()
async def reload(self, ctx, ext="_"):
"""Reloads a cog, bot manager only."""
if ext == "_":
ext = self.lastreload
else:
self.lastreload = ext
try:
self.bot.unload_extension("cogs." + ext)
self.bot.load_extension("cogs." + ext)
except:
await ctx.send(f':x: Cog reloading failed, traceback: '
f'```\n{traceback.format_exc()}\n```')
return
self.bot.log.info(f'Reloaded ext {ext}')
await ctx.send(f':white_check_mark: `{ext}` successfully reloaded.')
def setup(bot):
bot.add_cog(Admin(bot))

View file

@ -1,62 +0,0 @@
import time
import config
import discord
from discord.ext import commands
from discord.ext.commands import Cog
class Basic(Cog):
def __init__(self, bot):
self.bot = bot
@commands.command()
async def hello(self, ctx):
"""Says hello. Duh."""
await ctx.send(f"Hello {ctx.author.mention}!")
@commands.guild_only()
@commands.command()
async def communitycount(self, ctx):
"""Prints the community member count of the server."""
community = ctx.guild.get_role(config.named_roles["community"])
await ctx.send(f"{ctx.guild.name} has "
f"{len(community.members)} community members!")
@commands.guild_only()
@commands.command()
async def membercount(self, ctx):
"""Prints the member count of the server."""
await ctx.send(f"{ctx.guild.name} has "
f"{ctx.guild.member_count} members!")
@commands.command(aliases=["robocopng", "robocop-ng"])
async def robocop(self, ctx):
"""Shows a quick embed with bot info."""
embed = discord.Embed(title="Robocop-NG",
url=config.source_url,
description=config.embed_desc)
embed.set_thumbnail(url=self.bot.user.avatar_url)
await ctx.send(embed=embed)
@commands.command(aliases=['p'])
async def ping(self, ctx):
"""Shows ping values to discord.
RTT = Round-trip time, time taken to send a message to discord
GW = Gateway Ping"""
before = time.monotonic()
tmp = await ctx.send('Calculating ping...')
after = time.monotonic()
rtt_ms = (after - before) * 1000
gw_ms = self.bot.latency * 1000
message_text = f":ping_pong:\n"\
f"rtt: `{rtt_ms:.1f}ms`\n"\
f"gw: `{gw_ms:.1f}ms`"
self.bot.log.info(message_text)
await tmp.edit(content=message_text)
def setup(bot):
bot.add_cog(Basic(bot))

View file

@ -1,43 +0,0 @@
from discord.ext import commands
from discord.ext.commands import Cog
from helpers.checks import check_if_collaborator
import config
import json
class Invites(Cog):
def __init__(self, bot):
self.bot = bot
@commands.command()
@commands.guild_only()
@commands.check(check_if_collaborator)
async def invite(self, ctx):
welcome_channel = self.bot.get_channel(config.welcome_channel)
author = ctx.message.author
reason = f"Created by {str(author)} ({author.id})"
invite = await welcome_channel.create_invite(max_age = 0,
max_uses = 1, temporary = True, unique = True, reason = reason)
with open("data/invites.json", "r") as f:
invites = json.load(f)
invites[invite.id] = {
"uses": 0,
"url": invite.url,
"max_uses": 1,
"code": invite.code
}
with open("data/invites.json", "w") as f:
f.write(json.dumps(invites))
await ctx.message.add_reaction("🆗")
try:
await ctx.author.send(f"Created single-use invite {invite.url}")
except discord.errors.Forbidden:
await ctx.send(f"{ctx.author.mention} I could not send you the \
invite. Send me a DM so I can reply to you.")
def setup(bot):
bot.add_cog(Invites(bot))

View file

@ -1,30 +0,0 @@
from discord.ext import commands
from discord.ext.commands import Cog
class Legacy(Cog):
def __init__(self, bot):
self.bot = bot
@commands.command(hidden=True, aliases=["removehacker"])
async def probate(self, ctx):
"""Use .revoke <user> <role>"""
await ctx.send("This command was replaced with `.revoke <user> <role>`"
" on Robocop-NG, please use that instead.")
@commands.command(hidden=True)
async def softlock(self, ctx):
"""Use .lock True"""
await ctx.send("This command was replaced with `.lock True`"
" on Robocop-NG, please use that instead.\n"
"Also... good luck, and sorry for taking your time. "
"Lockdown rarely means anything good.")
@commands.command(hidden=True, aliases=["addhacker"])
async def unprobate(self, ctx):
"""Use .approve <user> <role>"""
await ctx.send("This command was replaced with `.approve <user> <role>`"
" on Robocop-NG, please use that instead.")
def setup(bot):
bot.add_cog(Legacy(bot))

View file

@ -1,87 +0,0 @@
import discord
import config
from discord.ext import commands
from discord.ext.commands import Cog
class Links(Cog):
"""
Commands for easily linking to projects.
"""
def __init__(self, bot):
self.bot = bot
@commands.command(hidden=True)
async def pegaswitch(self, ctx):
"""Link to the Pegaswitch repo"""
await ctx.send("https://github.com/reswitched/pegaswitch")
@commands.command(hidden=True, aliases=["atmos"])
async def atmosphere(self, ctx):
"""Link to the Atmosphere repo"""
await ctx.send("https://github.com/atmosphere-nx/atmosphere")
@commands.command(hidden=True, aliases=["xyproblem"])
async def xy(self, ctx):
"""Link to the "What is the XY problem?" post from SE"""
await ctx.send("<https://meta.stackexchange.com/q/66377/285481>\n\n"
"TL;DR: It's asking about your attempted solution "
"rather than your actual problem.\n"
"It's perfectly okay to want to learn about a "
"solution, but please be clear about your intentions "
"if you're not actually trying to solve a problem.")
@commands.command(hidden=True, aliases=["guides", "link"])
async def guide(self, ctx):
"""Link to the guide(s)"""
await ctx.send("**Generic starter guides:**\n"
"Nintendo Homebrew's Guide: "
"<https://nh-server.github.io/switch-guide/>\n"
"AtlasNX's Guide: "
"<https://guide.teamatlasnx.com>\n"
"Pegaswitch Guide: <https://switch.hacks.guide/> "
"(outdated for anything but Pegaswitch/3.0.0)\n\n"
"**Specific guides:**\n"
"Manually Updating/Downgrading (with HOS): "
"<https://guide.sdsetup.com/usingcfw/manualupgrade>\n"
"Manually Repairing/Downgrading (without HOS): "
"<https://guide.sdsetup.com/usingcfw/manualchoiupgrade>\n"
"How to get started developing Homebrew: "
"<https://gbatemp.net/threads/"
"tutorial-switch-homebrew-development.507284/>\n"
"Getting full RAM in homebrew without NSPs: "
"as of Atmosphere 0.8.6, hold R while opening any game.")
@commands.command()
async def source(self, ctx):
"""Gives link to source code."""
await ctx.send(f"You can find my source at {config.source_url}. "
"Serious PRs and issues welcome!")
@commands.command()
async def rules(self, ctx, *, targetuser: discord.Member = None):
"""Post a link to the Rules"""
if not targetuser:
targetuser = ctx.author
await ctx.send(f"{targetuser.mention}: A link to the rules "
f"can be found here: {config.rules_url}")
@commands.command()
async def community(self, ctx, *, targetuser: discord.Member = None):
"""Post a link to the community section of the rules"""
if not targetuser:
targetuser = ctx.author
await ctx.send(f"{targetuser.mention}: "
"https://reswitched.team/discord/#member-roles-breakdown"
"\n\n"
"Community role allows access to the set of channels "
"on the community category (#off-topic, "
"#homebrew-development, #switch-hacking-general etc)."
"\n\n"
"What you need to get the role is to be around, "
"be helpful and nice to people and "
"show an understanding of rules.")
def setup(bot):
bot.add_cog(Links(bot))

View file

@ -1,132 +0,0 @@
import random
import discord
from discord.ext import commands
from discord.ext.commands import Cog
import math
import platform
from helpers.checks import check_if_staff_or_ot
class Meme(Cog):
"""
Meme commands.
"""
def __init__(self, bot):
self.bot = bot
def c_to_f(self, c):
"""this is where we take memes too far"""
return math.floor(9.0 / 5.0 * c + 32)
def c_to_k(self, c):
"""this is where we take memes REALLY far"""
return math.floor(c + 273.15)
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True, name="warm")
async def warm_member(self, ctx, user: discord.Member):
"""Warms a user :3"""
celsius = random.randint(15, 100)
fahrenheit = self.c_to_f(celsius)
kelvin = self.c_to_k(celsius)
await ctx.send(f"{user.mention} warmed."
f" User is now {celsius}°C "
f"({fahrenheit}°F, {kelvin}K).")
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True, name="chill", aliases=["cold"])
async def chill_member(self, ctx, user: discord.Member):
"""Chills a user >:3"""
celsius = random.randint(-50, 15)
fahrenheit = self.c_to_f(celsius)
kelvin = self.c_to_k(celsius)
await ctx.send(f"{user.mention} chilled."
f" User is now {celsius}°C "
f"({fahrenheit}°F, {kelvin}K).")
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True, aliases=["thank", "reswitchedgold"])
async def gild(self, ctx, user: discord.Member):
"""Gives a star to a user"""
await ctx.send(f"{user.mention} gets a :star:, yay!")
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True, aliases=["reswitchedsilver", "silv3r",
"reswitchedsilv3r"])
async def silver(self, ctx, user: discord.Member):
"""Gives a user ReSwitched Silver™"""
embed = discord.Embed(title="ReSwitched Silver™!",
description=f"Here's your ReSwitched Silver™,"
f"{user.mention}!")
embed.set_image(url="https://cdn.discordapp.com/emojis/"
"548623626916724747.png?v=1")
await ctx.send(embed=embed)
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True)
async def btwiuse(self, ctx):
"""btw i use arch"""
uname = platform.uname()
await ctx.send(f"BTW I use {platform.python_implementation()} "
f"{platform.python_version()} on {uname.system} "
f"{uname.release}")
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True)
async def yahaha(self, ctx):
"""secret command"""
await ctx.send(f"🍂 you found me 🍂")
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True)
async def peng(self, ctx):
"""heck tomger"""
await ctx.send(f"🐧")
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True, aliases=["outstanding"])
async def outstandingmove(self, ctx):
"""Posts the outstanding move meme"""
await ctx.send("https://cdn.discordapp.com/attachments"
"/371047036348268545/528413677007929344"
"/image0-5.jpg")
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True)
async def bones(self, ctx):
await ctx.send("https://cdn.discordapp.com/emojis/"
"443501365843591169.png?v=1")
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True)
async def headpat(self, ctx):
await ctx.send("https://cdn.discordapp.com/emojis/"
"465650811909701642.png?v=1")
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True, aliases=["when", "etawhen",
"emunand", "thermosphere"])
async def eta(self, ctx):
await ctx.send("June 15.")
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True, name="bam")
async def bam_member(self, ctx, target: discord.Member):
"""Bams a user owo"""
safe_name = await commands.clean_content().convert(ctx, str(target))
await ctx.send(f"{safe_name} is ̶n͢ow b̕&̡.̷ 👍̡")
@commands.command(hidden=True)
async def memebercount(self, ctx):
"""Checks memeber count, as requested by dvdfreitag"""
await ctx.send("There's like, uhhhhh a bunch")
@commands.command(hidden=True)
async def frolics(self, ctx):
"""test"""
await ctx.send("https://www.youtube.com/watch?v=VmarNEsjpDI")
def setup(bot):
bot.add_cog(Meme(bot))

View file

@ -1,440 +0,0 @@
import discord
from discord.ext import commands
from discord.ext.commands import Cog
import config
from helpers.checks import check_if_staff, check_if_bot_manager
from helpers.userlogs import userlog
from helpers.restrictions import add_restriction, remove_restriction
import io
class Mod(Cog):
def __init__(self, bot):
self.bot = bot
def check_if_target_is_staff(self, target):
return any(r.id in config.staff_role_ids for r in target.roles)
@commands.guild_only()
@commands.check(check_if_bot_manager)
@commands.command()
async def setguildicon(self, ctx, url):
"""Changes guild icon, bot manager only."""
img_bytes = await self.bot.aiogetbytes(url)
await ctx.guild.edit(icon=img_bytes, reason=str(ctx.author))
await ctx.send(f"Done!")
log_channel = self.bot.get_channel(config.modlog_channel)
log_msg = f"✏️ **Guild Icon Update**: {ctx.author} "\
"changed the guild icon."
img_filename = url.split("/")[-1].split("#")[0] # hacky
img_file = discord.File(io.BytesIO(img_bytes),
filename=img_filename)
await log_channel.send(log_msg, file=img_file)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command()
async def mute(self, ctx, target: discord.Member, *, reason: str = ""):
"""Mutes a user, staff only."""
# Hedge-proofing the code
if target == ctx.author:
return await ctx.send("You can't do mod actions on yourself.")
elif self.check_if_target_is_staff(target):
return await ctx.send("I can't mute this user as "
"they're a member of staff.")
userlog(target.id, ctx.author, reason, "mutes", target.name)
safe_name = await commands.clean_content().convert(ctx, str(target))
dm_message = f"You were muted!"
if reason:
dm_message += f" The given reason is: \"{reason}\"."
try:
await target.send(dm_message)
except discord.errors.Forbidden:
# Prevents kick issues in cases where user blocked bot
# or has DMs disabled
pass
mute_role = ctx.guild.get_role(config.mute_role)
await target.add_roles(mute_role, reason=str(ctx.author))
chan_message = f"🔇 **Muted**: {ctx.author.mention} muted "\
f"{target.mention} | {safe_name}\n"\
f"🏷 __User ID__: {target.id}\n"
if reason:
chan_message += f"✏️ __Reason__: \"{reason}\""
else:
chan_message += "Please add an explanation below. In the future, "\
"it is recommended to use `.mute <user> [reason]`"\
" as the reason is automatically sent to the user."
log_channel = self.bot.get_channel(config.modlog_channel)
await log_channel.send(chan_message)
await ctx.send(f"{target.mention} can no longer speak.")
add_restriction(target.id, config.mute_role)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command()
async def unmute(self, ctx, target: discord.Member):
"""Unmutes a user, staff only."""
safe_name = await commands.clean_content().convert(ctx, str(target))
mute_role = ctx.guild.get_role(config.mute_role)
await target.remove_roles(mute_role, reason=str(ctx.author))
chan_message = f"🔈 **Unmuted**: {ctx.author.mention} unmuted "\
f"{target.mention} | {safe_name}\n"\
f"🏷 __User ID__: {target.id}\n"
log_channel = self.bot.get_channel(config.modlog_channel)
await log_channel.send(chan_message)
await ctx.send(f"{target.mention} can now speak again.")
remove_restriction(target.id, config.mute_role)
@commands.guild_only()
@commands.bot_has_permissions(kick_members=True)
@commands.check(check_if_staff)
@commands.command()
async def kick(self, ctx, target: discord.Member, *, reason: str = ""):
"""Kicks a user, staff only."""
# Hedge-proofing the code
if target == ctx.author:
return await ctx.send("You can't do mod actions on yourself.")
elif self.check_if_target_is_staff(target):
return await ctx.send("I can't kick this user as "
"they're a member of staff.")
userlog(target.id, ctx.author, reason, "kicks", target.name)
safe_name = await commands.clean_content().convert(ctx, str(target))
dm_message = f"You were kicked from {ctx.guild.name}."
if reason:
dm_message += f" The given reason is: \"{reason}\"."
dm_message += "\n\nYou are able to rejoin the server,"\
" but please be sure to behave when participating again."
try:
await target.send(dm_message)
except discord.errors.Forbidden:
# Prevents kick issues in cases where user blocked bot
# or has DMs disabled
pass
await target.kick(reason=f"{ctx.author}, reason: {reason}")
chan_message = f"👢 **Kick**: {ctx.author.mention} kicked "\
f"{target.mention} | {safe_name}\n"\
f"🏷 __User ID__: {target.id}\n"
if reason:
chan_message += f"✏️ __Reason__: \"{reason}\""
else:
chan_message += "Please add an explanation below. In the future"\
", it is recommended to use "\
"`.kick <user> [reason]`"\
" as the reason is automatically sent to the user."
log_channel = self.bot.get_channel(config.modlog_channel)
await log_channel.send(chan_message)
@commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@commands.check(check_if_staff)
@commands.command()
async def ban(self, ctx, target: discord.Member, *, reason: str = ""):
"""Bans a user, staff only."""
# Hedge-proofing the code
if target == ctx.author:
return await ctx.send("You can't do mod actions on yourself.")
elif self.check_if_target_is_staff(target):
return await ctx.send("I can't ban this user as "
"they're a member of staff.")
userlog(target.id, ctx.author, reason, "bans", target.name)
safe_name = await commands.clean_content().convert(ctx, str(target))
dm_message = f"You were banned from {ctx.guild.name}."
if reason:
dm_message += f" The given reason is: \"{reason}\"."
dm_message += "\n\nThis ban does not expire."
try:
await target.send(dm_message)
except discord.errors.Forbidden:
# Prevents ban issues in cases where user blocked bot
# or has DMs disabled
pass
await target.ban(reason=f"{ctx.author}, reason: {reason}",
delete_message_days=0)
chan_message = f"⛔ **Ban**: {ctx.author.mention} banned "\
f"{target.mention} | {safe_name}\n"\
f"🏷 __User ID__: {target.id}\n"
if reason:
chan_message += f"✏️ __Reason__: \"{reason}\""
else:
chan_message += "Please add an explanation below. In the future"\
", it is recommended to use `.ban <user> [reason]`"\
" as the reason is automatically sent to the user."
log_channel = self.bot.get_channel(config.modlog_channel)
await log_channel.send(chan_message)
await ctx.send(f"{safe_name} is now b&. 👍")
@commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@commands.check(check_if_staff)
@commands.command(aliases=["softban"])
async def hackban(self, ctx, target: int, *, reason: str = ""):
"""Bans a user with their ID, doesn't message them, staff only."""
target_user = await self.bot.get_user_info(target)
target_member = ctx.guild.get_member(target)
# Hedge-proofing the code
if target == ctx.author.id:
return await ctx.send("You can't do mod actions on yourself.")
elif target_member and self.check_if_target_is_staff(target_member):
return await ctx.send("I can't ban this user as "
"they're a member of staff.")
userlog(target, ctx.author, reason, "bans", target_user.name)
safe_name = await commands.clean_content().convert(ctx, str(target))
await ctx.guild.ban(target_user,
reason=f"{ctx.author}, reason: {reason}",
delete_message_days=0)
chan_message = f"⛔ **Hackban**: {ctx.author.mention} banned "\
f"{target_user.mention} | {safe_name}\n"\
f"🏷 __User ID__: {target}\n"
if reason:
chan_message += f"✏️ __Reason__: \"{reason}\""
else:
chan_message += "Please add an explanation below. In the future"\
", it is recommended to use "\
"`.hackban <user> [reason]`."
log_channel = self.bot.get_channel(config.modlog_channel)
await log_channel.send(chan_message)
await ctx.send(f"{safe_name} is now b&. 👍")
@commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@commands.check(check_if_staff)
@commands.command()
async def silentban(self, ctx, target: discord.Member, *, reason: str = ""):
"""Bans a user, staff only."""
# Hedge-proofing the code
if target == ctx.author:
return await ctx.send("You can't do mod actions on yourself.")
elif self.check_if_target_is_staff(target):
return await ctx.send("I can't ban this user as "
"they're a member of staff.")
userlog(target.id, ctx.author, reason, "bans", target.name)
safe_name = await commands.clean_content().convert(ctx, str(target))
await target.ban(reason=f"{ctx.author}, reason: {reason}",
delete_message_days=0)
chan_message = f"⛔ **Silent ban**: {ctx.author.mention} banned "\
f"{target.mention} | {safe_name}\n"\
f"🏷 __User ID__: {target.id}\n"
if reason:
chan_message += f"✏️ __Reason__: \"{reason}\""
else:
chan_message += "Please add an explanation below. In the future"\
", it is recommended to use `.ban <user> [reason]`"\
" as the reason is automatically sent to the user."
log_channel = self.bot.get_channel(config.modlog_channel)
await log_channel.send(chan_message)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command()
async def approve(self, ctx, target: discord.Member,
role: str = "community"):
"""Add a role to a user (default: community), staff only."""
if role not in config.named_roles:
return await ctx.send("No such role! Available roles: " +
','.join(config.named_roles))
log_channel = self.bot.get_channel(config.modlog_channel)
target_role = ctx.guild.get_role(config.named_roles[role])
if target_role in target.roles:
return await ctx.send("Target already has this role.")
await target.add_roles(target_role, reason=str(ctx.author))
await ctx.send(f"Approved {target.mention} to `{role}` role.")
await log_channel.send(f"✅ Approved: {ctx.author.mention} added"
f" {role} to {target.mention}")
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command(aliases=["unapprove"])
async def revoke(self, ctx, target: discord.Member,
role: str = "community"):
"""Remove a role from a user (default: community), staff only."""
if role not in config.named_roles:
return await ctx.send("No such role! Available roles: " +
','.join(config.named_roles))
log_channel = self.bot.get_channel(config.modlog_channel)
target_role = ctx.guild.get_role(config.named_roles[role])
if target_role not in target.roles:
return await ctx.send("Target doesn't have this role.")
await target.remove_roles(target_role, reason=str(ctx.author))
await ctx.send(f"Un-approved {target.mention} from `{role}` role.")
await log_channel.send(f"❌ Un-approved: {ctx.author.mention} removed"
f" {role} from {target.mention}")
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command(aliases=["clear"])
async def purge(self, ctx, limit: int, channel: discord.TextChannel = None):
"""Clears a given number of messages, staff only."""
log_channel = self.bot.get_channel(config.modlog_channel)
if not channel:
channel = ctx.channel
await channel.purge(limit=limit)
msg = f"🗑 **Purged**: {ctx.author.mention} purged {limit} "\
f"messages in {channel.mention}."
await log_channel.send(msg)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command()
async def warn(self, ctx, target: discord.Member, *, reason: str = ""):
"""Warns a user, staff only."""
# Hedge-proofing the code
if target == ctx.author:
return await ctx.send("You can't do mod actions on yourself.")
elif self.check_if_target_is_staff(target):
return await ctx.send("I can't warn this user as "
"they're a member of staff.")
log_channel = self.bot.get_channel(config.modlog_channel)
warn_count = userlog(target.id, ctx.author, reason,
"warns", target.name)
safe_name = await commands.clean_content().convert(ctx, str(target))
chan_msg = f"⚠️ **Warned**: {ctx.author.mention} warned "\
f"{target.mention} (warn #{warn_count}) "\
f"| {safe_name}\n"
msg = f"You were warned on {ctx.guild.name}."
if reason:
msg += " The given reason is: " + reason
msg += f"\n\nPlease read the rules in {config.rules_url}. "\
f"This is warn #{warn_count}."
if warn_count == 2:
msg += " __The next warn will automatically kick.__"
if warn_count == 3:
msg += "\n\nYou were kicked because of this warning. "\
"You can join again right away. "\
"Two more warnings will result in an automatic ban."
if warn_count == 4:
msg += "\n\nYou were kicked because of this warning. "\
"This is your final warning. "\
"You can join again, but "\
"**one more warn will result in a ban**."
chan_msg += "**This resulted in an auto-kick.**\n"
if warn_count == 5:
msg += "\n\nYou were automatically banned due to five warnings."
chan_msg += "**This resulted in an auto-ban.**\n"
try:
await target.send(msg)
except discord.errors.Forbidden:
# Prevents log issues in cases where user blocked bot
# or has DMs disabled
pass
if warn_count == 3 or warn_count == 4:
await target.kick()
if warn_count >= 5: # just in case
await target.ban(reason="exceeded warn limit",
delete_message_days=0)
await ctx.send(f"{target.mention} warned. "
f"User has {warn_count} warning(s).")
if reason:
chan_msg += f"✏️ __Reason__: \"{reason}\""
else:
chan_msg += "Please add an explanation below. In the future"\
", it is recommended to use `.ban <user> [reason]`"\
" as the reason is automatically sent to the user."
await log_channel.send(chan_msg)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command(aliases=["setnick", "nick"])
async def nickname(self, ctx, target: discord.Member, *, nick: str = ""):
"""Sets a user's nickname, staff only.
Just send .nickname <user> to wipe the nickname."""
if nick:
await target.edit(nick=nick, reason=str(ctx.author))
else:
await target.edit(nick=None, reason=str(ctx.author))
await ctx.send("Successfully set nickname.")
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command(aliases=['echo'])
async def say(self, ctx, *, the_text: str):
"""Repeats a given text, staff only."""
await ctx.send(the_text)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command()
async def speak(self, ctx, channel: discord.TextChannel, *, the_text: str):
"""Repeats a given text in a given channel, staff only."""
await channel.send(the_text)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command(aliases=["setplaying", "setgame"])
async def playing(self, ctx, *, game: str = ""):
"""Sets the bot's currently played game name, staff only.
Just send .playing to wipe the playing state."""
if game:
await self.bot.change_presence(activity=discord.Game(name=game))
else:
await self.bot.change_presence(activity=None)
await ctx.send("Successfully set game.")
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command(aliases=["setbotnick", "botnick", "robotnick"])
async def botnickname(self, ctx, *, nick: str = ""):
"""Sets the bot's nickname, staff only.
Just send .botnickname to wipe the nickname."""
if nick:
await ctx.guild.me.edit(nick=nick, reason=str(ctx.author))
else:
await ctx.guild.me.edit(nick=None, reason=str(ctx.author))
await ctx.send("Successfully set bot nickname.")
def setup(bot):
bot.add_cog(Mod(bot))

View file

@ -1,137 +0,0 @@
import discord
import config
from datetime import datetime
from discord.ext import commands
from discord.ext.commands import Cog
from helpers.checks import check_if_staff
from helpers.robocronp import add_job
from helpers.userlogs import userlog
from helpers.restrictions import add_restriction
class ModTimed(Cog):
def __init__(self, bot):
self.bot = bot
def check_if_target_is_staff(self, target):
return any(r.id in config.staff_role_ids for r in target.roles)
@commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@commands.check(check_if_staff)
@commands.command()
async def timeban(self, ctx, target: discord.Member,
duration: str, *, reason: str = ""):
"""Bans a user for a specified amount of time, staff only."""
# Hedge-proofing the code
if target == ctx.author:
return await ctx.send("You can't do mod actions on yourself.")
elif self.check_if_target_is_staff(target):
return await ctx.send("I can't ban this user as "
"they're a member of staff.")
expiry_timestamp = self.bot.parse_time(duration)
expiry_datetime = datetime.utcfromtimestamp(expiry_timestamp)
duration_text = self.bot.get_relative_timestamp(time_to=expiry_datetime,
include_to=True,
humanized=True)
userlog(target.id, ctx.author, f"{reason} (Timed, until "
f"{duration_text})",
"bans", target.name)
safe_name = await commands.clean_content().convert(ctx, str(target))
dm_message = f"You were banned from {ctx.guild.name}."
if reason:
dm_message += f" The given reason is: \"{reason}\"."
dm_message += f"\n\nThis ban will expire {duration_text}."
try:
await target.send(dm_message)
except discord.errors.Forbidden:
# Prevents ban issues in cases where user blocked bot
# or has DMs disabled
pass
await target.ban(reason=f"{ctx.author}, reason: {reason}",
delete_message_days=0)
chan_message = f"⛔ **Timed Ban**: {ctx.author.mention} banned "\
f"{target.mention} for {duration_text} | {safe_name}\n"\
f"🏷 __User ID__: {target.id}\n"
if reason:
chan_message += f"✏️ __Reason__: \"{reason}\""
else:
chan_message += "Please add an explanation below. In the future"\
", it is recommended to use `.ban <user> [reason]`"\
" as the reason is automatically sent to the user."
add_job("unban", target.id, {"guild": ctx.guild.id}, expiry_timestamp)
log_channel = self.bot.get_channel(config.log_channel)
await log_channel.send(chan_message)
await ctx.send(f"{safe_name} is now b&. "
f"It will expire {duration_text}. 👍")
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command()
async def timemute(self, ctx, target: discord.Member,
duration: str, *, reason: str = ""):
"""Mutes a user for a specified amount of time, staff only."""
# Hedge-proofing the code
if target == ctx.author:
return await ctx.send("You can't do mod actions on yourself.")
elif self.check_if_target_is_staff(target):
return await ctx.send("I can't mute this user as "
"they're a member of staff.")
expiry_timestamp = self.bot.parse_time(duration)
expiry_datetime = datetime.utcfromtimestamp(expiry_timestamp)
duration_text = self.bot.get_relative_timestamp(time_to=expiry_datetime,
include_to=True,
humanized=True)
userlog(target.id, ctx.author, f"{reason} (Timed, until "
f"{duration_text})",
"mutes", target.name)
safe_name = await commands.clean_content().convert(ctx, str(target))
dm_message = f"You were muted!"
if reason:
dm_message += f" The given reason is: \"{reason}\"."
dm_message += f"\n\nThis mute will expire {duration_text}."
try:
await target.send(dm_message)
except discord.errors.Forbidden:
# Prevents kick issues in cases where user blocked bot
# or has DMs disabled
pass
mute_role = ctx.guild.get_role(config.mute_role)
await target.add_roles(mute_role, reason=str(ctx.author))
chan_message = f"🔇 **Timed Mute**: {ctx.author.mention} muted "\
f"{target.mention} for {duration_text} | {safe_name}\n"\
f"🏷 __User ID__: {target.id}\n"
if reason:
chan_message += f"✏️ __Reason__: \"{reason}\""
else:
chan_message += "Please add an explanation below. In the future, "\
"it is recommended to use `.mute <user> [reason]`"\
" as the reason is automatically sent to the user."
add_job("unmute", target.id, {"guild": ctx.guild.id}, expiry_timestamp)
log_channel = self.bot.get_channel(config.log_channel)
await log_channel.send(chan_message)
await ctx.send(f"{target.mention} can no longer speak. "
f"It will expire {duration_text}.")
add_restriction(target.id, config.mute_role)
def setup(bot):
bot.add_cog(ModTimed(bot))

View file

@ -1,153 +0,0 @@
import asyncio
import config
import time
import discord
import traceback
from discord.ext import commands
from discord.ext.commands import Cog
from helpers.robocronp import get_crontab, delete_job
from helpers.restrictions import remove_restriction
from helpers.checks import check_if_staff
class Robocronp(Cog):
def __init__(self, bot):
self.bot = bot
bot.loop.create_task(self.minutely())
bot.loop.create_task(self.hourly())
async def send_data(self):
data_files = [discord.File(fpath) for fpath in self.bot.wanted_jsons]
log_channel = self.bot.get_channel(config.botlog_channel)
await log_channel.send("Hourly data backups:", files=data_files)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command()
async def listjobs(self, ctx):
"""Lists timed robocronp jobs, staff only."""
ctab = get_crontab()
embed = discord.Embed(title=f"Active robocronp jobs")
for jobtype in ctab:
for jobtimestamp in ctab[jobtype]:
for job_name in ctab[jobtype][jobtimestamp]:
job_details = repr(ctab[jobtype][jobtimestamp][job_name])
embed.add_field(name=f"{jobtype} for {job_name}",
value=f"Timestamp: {jobtimestamp}, "
f"Details: {job_details}",
inline=False)
await ctx.send(embed=embed)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command(aliases=["removejob"])
async def deletejob(self, ctx, timestamp: str,
job_type: str, job_name: str):
"""Removes a timed robocronp job, staff only.
You'll need to supply:
- timestamp (like 1545981602)
- job type (like "unban")
- job name (userid, like 420332322307571713)
You can get all 3 from listjobs command."""
delete_job(timestamp, job_type, job_name)
await ctx.send(f"{ctx.author.mention}: Deleted!")
async def do_jobs(self, ctab, jobtype, timestamp):
log_channel = self.bot.get_channel(config.botlog_channel)
for job_name in ctab[jobtype][timestamp]:
try:
job_details = ctab[jobtype][timestamp][job_name]
if jobtype == "unban":
target_user = await self.bot.get_user_info(job_name)
target_guild = self.bot.get_guild(job_details["guild"])
delete_job(timestamp, jobtype, job_name)
await target_guild.unban(target_user,
reason="Robocronp: Timed "
"ban expired.")
elif jobtype == "unmute":
remove_restriction(job_name, config.mute_role)
target_guild = self.bot.get_guild(job_details["guild"])
target_member = target_guild.get_member(int(job_name))
target_role = target_guild.get_role(config.mute_role)
await target_member.remove_roles(target_role,
reason="Robocronp: Timed "
"mute expired.")
delete_job(timestamp, jobtype, job_name)
elif jobtype == "remind":
text = job_details["text"]
added_on = job_details["added"]
target = await self.bot.get_user_info(int(job_name))
if target:
await target.send("You asked to be reminded about"
f" `{text}` on {added_on}.")
delete_job(timestamp, jobtype, job_name)
except:
# Don't kill cronjobs if something goes wrong.
delete_job(timestamp, jobtype, job_name)
await log_channel.send("Crondo has errored, job deleted: ```"
f"{traceback.format_exc()}```")
async def clean_channel(self, channel_id):
log_channel = self.bot.get_channel(config.botlog_channel)
channel = self.bot.get_channel(channel_id)
try:
done_cleaning = False
count = 0
while not done_cleaning:
purge_res = await channel.purge(limit=100)
count += len(purge_res)
if len(purge_res) != 100:
done_cleaning = True
await log_channel.send(f"Wiped {count} messages from "
f"<#{channel.id}> automatically.")
except:
# Don't kill cronjobs if something goes wrong.
await log_channel.send("Cronclean has errored: ```"
f"{traceback.format_exc()}```")
async def minutely(self):
await self.bot.wait_until_ready()
log_channel = self.bot.get_channel(config.botlog_channel)
while not self.bot.is_closed():
try:
ctab = get_crontab()
timestamp = time.time()
for jobtype in ctab:
for jobtimestamp in ctab[jobtype]:
if timestamp > int(jobtimestamp):
await self.do_jobs(ctab, jobtype, jobtimestamp)
# Handle clean channels
for clean_channel in config.minutely_clean_channels:
await self.clean_channel(clean_channel)
except:
# Don't kill cronjobs if something goes wrong.
await log_channel.send("Cron-minutely has errored: ```"
f"{traceback.format_exc()}```")
await asyncio.sleep(60)
async def hourly(self):
await self.bot.wait_until_ready()
log_channel = self.bot.get_channel(config.botlog_channel)
while not self.bot.is_closed():
# Your stuff that should run at boot
# and after that every hour goes here
await asyncio.sleep(3600)
try:
await self.send_data()
# Handle clean channels
for clean_channel in config.hourly_clean_channels:
await self.clean_channel(clean_channel)
except:
# Don't kill cronjobs if something goes wrong.
await log_channel.send("Cron-hourly has errored: ```"
f"{traceback.format_exc()}```")
# Your stuff that should run an hour after boot
# and after that every hour goes here
def setup(bot):
bot.add_cog(Robocronp(bot))

View file

@ -1,306 +0,0 @@
import discord
from discord.ext import commands
from discord.ext.commands import Cog
import asyncio
import config
import random
from inspect import cleandoc
import hashlib
from helpers.checks import check_if_staff
welcome_header = """
<:ReSwitched:326421448543567872> __**Welcome to ReSwitched!**__
__**Be sure you read the following rules and information before participating. If you came here to ask about "backups", this is NOT the place.**__
__**Got questions about Nintendo Switch hacking? Before asking in the server, please see our FAQ at <https://reswitched.team/faq/> to see if your question has already been answered.**__
:bookmark_tabs:__Rules:__
"""
welcome_rules = (
# 1
"""
Read all the rules before participating in chat. Not reading the rules is *not* an excuse for breaking them.
It's suggested that you read channel topics and pins before asking questions as well, as some questions may have already been answered in those.
""",
# 2
"""
Be nice to each other. It's fine to disagree, it's not fine to insult or attack other people.
You may disagree with anyone or anything you like, but you should try to keep it to opinions, and not people. Avoid vitriol.
Constant antagonistic behavior is considered uncivil and appropriate action will be taken.
The use of derogatory slurs -- sexist, racist, homophobic, transphobic, or otherwise -- is unacceptable and may be grounds for an immediate ban.
""",
# 3
'If you have concerns about another user, please take up your concerns with a staff member (someone with the "mod" role in the sidebar) in private. Don\'t publicly call other users out.',
# 4
"""
From time to time, we may mention everyone in the server. We do this when we feel something important is going on that requires attention. Complaining about these pings may result in a ban.
To disable notifications for these pings, suppress them in "ReSwitched → Notification Settings".
""",
# 5
"""
Don't spam.
For excessively long text, use a service like <https://0bin.net/>.
""",
# 6
"Don't brigade, raid, or otherwise attack other people or communities. Don't discuss participation in these attacks. This may warrant an immediate permanent ban.",
# 7
'Off-topic content goes to #off-topic. Keep low-quality content like memes out.',
# 8
'Trying to evade, look for loopholes, or stay borderline within the rules will be treated as breaking them.',
# 9
"""
Absolutely no piracy or related discussion. This includes:
"Backups", even if you legally own a copy of the game.
"Installable" NSPs, XCIs, and NCAs; this **includes** installable homebrew (i.e. on the Home Menu instead of within nx-hbmenu).
Signature and ES patches, also known as "sigpatches"
Usage of piracy-focused groups' (Team Xecuter, etc.) hardware and software, such as SX OS.
This is a zero-tolerance, non-negotiable policy that is enforced strictly and swiftly, up to and including instant bans without warning.
""",
# 10
'The first character of your server nickname should be alphanumeric if you wish to talk in chat.'
)
welcome_footer = (
"""
:hash: __Channel Breakdown:__
#news - Used exclusively for updates on ReSwitched progress and community information. Most major announcements are passed through this channel and whenever something is posted there it's usually something you'll want to look at.
#switch-hacking-meta - For "meta-discussion" related to hacking the switch. This is where we talk *about* the switch hacking that's going on, and where you can get clarification about the hacks that exist and the work that's being done.
#user-support - End-user focused support, mainly between users. Ask your questions about using switch homebrew here.
#tool-support - Developer focused support. Ask your questions about using PegaSwitch, libtransistor, Mephisto, and other tools here.
#hack-n-all - General hacking, hardware and software development channel for hacking on things *other* than the switch. This is a great place to ask about hacking other systems-- and for the community to have technical discussions.
""",
"""
#switch-hacking-general - Channel for everyone working on hacking the switch-- both in an exploit and a low-level hardware sense. This is where a lot of our in-the-open development goes on. Note that this isn't the place for developing homebrew-- we have #homebrew-development for that!
#homebrew-development - Discussion about the development of homebrew goes there. Feel free to show off your latest creation here.
#off-topic - Channel for discussion of anything that doesn't belong in #general. Anything goes, so long as you make sure to follow the rules and be on your best behavior.
#toolchain-development - Discussion about the development of libtransistor itself goes there.
#cfw-development - Development discussion regarding custom firmware (CFW) projects, such as Atmosphère. This channel is meant for the discussion accompanying active development.
#bot-cmds - Channel for excessive/random use of Robocop's various commands.
**If you are still not sure how to get access to the other channels, please read the rules again.**
**If you have questions about the rules, feel free to ask here!**
**Note: This channel is completely automated (aside from responding to questions about the rules). If your message didn't give you access to the other channels, you failed the test. Feel free to try again.**
""",
)
hidden_term_line = ' • When you have finished reading all of the rules, send a message in this channel that includes the SHA1 hash of your discord "name#discriminator" (for example, SHA1(User#1234)), and we\'ll grant you access to the other channels. You can find your "name#discriminator" (your username followed by a # and four numbers) under the discord channel list.'
class Verification(Cog):
def __init__(self, bot):
self.bot = bot
@commands.check(check_if_staff)
@commands.command()
async def reset(self, ctx, limit: int = 100, force: bool = False):
"""Wipes messages and pastes the welcome message again. Staff only."""
if ctx.message.channel.id != config.welcome_channel and not force:
await ctx.send(f"This command is limited to"
f" <#{config.welcome_channel}>, unless forced.")
return
await ctx.channel.purge(limit=limit)
await ctx.send(welcome_header)
rules = ['**{}**. {}'.format(i, cleandoc(r)) for i, r in
enumerate(welcome_rules, 1)]
rule_choice = random.randint(2, len(rules))
rules[rule_choice - 1] += '\n' + hidden_term_line
msg = f"🗑 **Reset**: {ctx.author.mention} cleared {limit} messages "\
f" in {ctx.channel.mention}"
msg += f"\n💬 __Current challenge location__: under rule {rule_choice}"
log_channel = self.bot.get_channel(config.log_channel)
await log_channel.send(msg)
# find rule that puts us over 2,000 characters, if any
total = 0
messages = []
current_message = ""
for item in rules:
total += len(item) + 2 # \n\n
if total < 2000:
current_message += item + "\n\n"
else:
# we've hit the limit; split!
messages += [current_message]
current_message = "\n\u200B\n" + item + "\n\u200B\n"
total = 0
messages += [current_message]
for item in messages:
await ctx.send(item)
await asyncio.sleep(1)
for x in welcome_footer:
await ctx.send(cleandoc(x))
await asyncio.sleep(1)
async def process_message(self, message):
"""Big code that makes me want to shoot myself
Not really a rewrite but more of a port
Git blame tells me that I should blame/credit Robin Lambertz"""
if message.channel.id == config.welcome_channel:
# Assign common stuff into variables to make stuff less of a mess
member = message.author
full_name = str(member)
discrim = str(member.discriminator)
guild = message.guild
chan = message.channel
mcl = message.content.lower()
# Reply to users that insult the bot
oof = ["bad", "broken", "buggy", "bugged",
"stupid", "dumb", "silly", "fuck", "heck", "h*ck"]
if "bot" in mcl and any(insult in mcl for insult in oof):
snark = random.choice(["bad human",
"no u",
"no u, rtfm",
"pebkac"])
return await chan.send(snark)
# Get the role we will give in case of success
success_role = guild.get_role(config.participant_role)
# Get a list of stuff we'll allow and will consider close
allowed_names = [f"@{full_name}", full_name, str(member.id)]
close_names = [f"@{member.name}", member.name, discrim,
f"#{discrim}"]
# Now add the same things but with newlines at the end of them
allowed_names += [(an + '\n') for an in allowed_names]
close_names += [(cn + '\n') for cn in close_names]
allowed_names += [(an + '\r\n') for an in allowed_names]
close_names += [(cn + '\r\n') for cn in close_names]
# [ ͡° ͜ᔦ ͡°] 𝐖𝐞𝐥𝐜𝐨𝐦𝐞 𝐭𝐨 𝐌𝐚𝐜 𝐎𝐒 𝟗.
allowed_names += [(an + '\r') for an in allowed_names]
close_names += [(cn + '\r') for cn in close_names]
# Finally, hash the stuff so that we can access them later :)
sha1_allow = [hashlib.sha1(name.encode('utf-8')).hexdigest()
for name in allowed_names]
md5_allow = [hashlib.md5(name.encode('utf-8')).hexdigest()
for name in allowed_names]
sha256_allow = [hashlib.sha256(name.encode('utf-8')).hexdigest()
for name in allowed_names]
sha1_close = [hashlib.sha1(name.encode('utf-8')).hexdigest()
for name in close_names]
# I'm not even going to attempt to break those into lines jfc
if any(allow in mcl for allow in sha1_allow):
await member.add_roles(success_role)
await chan.purge(limit=100, check=lambda m: m.author == message.author or (m.author == self.bot.user and message.author.mention in m.content))
elif any(close in mcl for close in sha1_close):
await chan.send(f"{message.author.mention} :no_entry: Close, but incorrect. You got the process right, but you're not doing it on your name and discriminator properly. Please re-read the rules carefully and look up any terms you are not familiar with.")
elif any(allow in mcl for allow in md5_allow):
await chan.send(f"{message.author.mention} :no_entry: Close, but incorrect. You're processing your name and discriminator properly, but you're not using the right process. Please re-read the rules carefully and look up any terms you are not familiar with.")
elif any(allow in mcl for allow in sha256_allow):
await chan.send(f"{message.author.mention} :no_entry: Close, but incorrect. You're processing your name and discriminator properly, but you're not using the right process. Please re-read the rules carefully and look up any terms you are not familiar with.")
elif full_name in message.content or str(member.id) in message.content or member.name in message.content or discrim in message.content:
no_text = ":no_entry: Incorrect. You need to do something *specific* with your name and discriminator instead of just posting it. Please re-read the rules carefully and look up any terms you are not familiar with."
rand_num = random.randint(1, 100)
if rand_num == 42:
no_text = "you're doing it wrong"
elif rand_num == 43:
no_text = "ugh, wrong, read the rules."
elif rand_num == 44:
no_text = "\"The definition of insanity is doing the same thing over and over again, but expecting different results.\"\n-Albert Einstein"
await chan.send(f"{message.author.mention} {no_text}")
@Cog.listener()
async def on_message(self, message):
if message.author.bot:
return
try:
await self.process_message(message)
except discord.errors.Forbidden:
chan = self.bot.get_channel(message.channel)
await chan.send("💢 I don't have permission to do this.")
@Cog.listener()
async def on_message_edit(self, before, after):
if after.author.bot:
return
try:
await self.process_message(after)
except discord.errors.Forbidden:
chan = self.bot.get_channel(after.channel)
await chan.send("💢 I don't have permission to do this.")
# @commands.guild_only()
# @commands.command()
# async def verify(self, ctx, *, verification_string: str):
# """Does verification.
# See text on top of #verification for more info."""
# await ctx.message.delete()
# veriflogs_channel = ctx.guild.get_channel(config.veriflogs_chanid)
# verification_role = ctx.guild.get_role(config.read_rules_roleid)
# verification_wanted = config.verification_code\
# .replace("[discrim]", ctx.author.discriminator)
# # Do checks on if the user can even attempt to verify
# if ctx.channel.id != config.verification_chanid:
# resp = await ctx.send("This command can only be used "
# f"on <#{config.verification_chanid}>.")
# await asyncio.sleep(config.sleep_secs)
# return await resp.delete()
# if verification_role in ctx.author.roles:
# resp = await ctx.send("This command can only by those without "
# f"<@&{config.read_rules_roleid}> role.")
# await asyncio.sleep(config.sleep_secs)
# return await resp.delete()
# # Log verification attempt
# await self.bot.update_logs("Verification Attempt",
# ctx.author.id,
# veriflogs_channel,
# log_text=verification_string,
# digdepth=50, result=-1)
# # Check verification code
# if verification_string.lower().strip() == verification_wanted:
# resp = await ctx.send("Success! Welcome to the "
# f"club, {str(ctx.author)}.")
# await self.bot.update_logs("Verification Attempt",
# ctx.author.id,
# veriflogs_channel,
# digdepth=50, result=0)
# await asyncio.sleep(config.sleep_secs)
# await ctx.author.add_roles(verification_role)
# await resp.delete()
# else:
# resp = await ctx.send(f"Incorrect password, {str(ctx.author)}.")
# await asyncio.sleep(config.sleep_secs)
# await resp.delete()
def setup(bot):
bot.add_cog(Verification(bot))

View file

@ -1,99 +0,0 @@
import datetime
# Basic bot config, insert your token here, update description if you want
prefixes = [".", "!"]
token = "token-goes-here"
bot_description = "Robocop-NG, the moderation bot of ReSwitched."
# If you forked robocop-ng, put your repo here
source_url = "https://github.com/reswitched/robocop-ng"
rules_url = "https://reswitched.team/discord/#rules"
# The bot description to be used in .robocop embed
embed_desc = "Robocop-NG is developed by [Ave](https://github.com/aveao)"\
" and [tomGER](https://github.com/tumGER), and is a rewrite "\
"of Robocop.\nRobocop is based on Kurisu by 916253 and ihaveamac."
# Minimum account age required to join the guild
# If user's account creation is shorter than the time delta given here
# then user will be kicked and informed
min_age = datetime.timedelta(minutes=15)
# The bot will only work in these guilds
guild_whitelist = [
269333940928512010 # ReSwitched discord
]
# Named roles to be used with .approve and .revoke
# Example: .approve User hacker
named_roles = {
"community": 420010997877833731,
"hacker": 364508795038072833,
"participant": 434353085926866946
}
# The bot manager and staff roles
# Bot manager can run eval, exit and other destructive commands
# Staff can run administrative commands
bot_manager_role_id = 466447265863696394 # Bot management role in ReSwitched
staff_role_ids = [364647829248933888, # Team role in ReSwitched
360138431524765707, # Mod role in ReSwitched
466447265863696394, # Bot management role in ReSwitched
360138163156549632, # Admin role in ReSwitched
287289529986187266] # Wizard role in ReSwitched
# Various log channels used to log bot and guild's activity
# You can use same channel for multiple log types
# Spylog channel logs suspicious messages or messages by members under watch
# Invites created with .invite will direct to the welcome channel.
log_channel = 290958160414375946 # server-logs in ReSwitched
botlog_channel = 529070282409771048 # bot-logs channel in ReSwitched
modlog_channel = 542114169244221452 # mod-logs channel in ReSwitched
spylog_channel = 548304839294189579 # spy channel in ReSwitched
welcome_channel = 326416669058662401 # newcomers channel in ReSwitched
# These channel entries are used to determine which roles will be given
# access when we unmute on them
general_channels = [420029476634886144,
414949821003202562,
383368936466546698,
343244421044633602,
491316901692178432,
539212260350885908] # Channels everyone can access
community_channels = [269333940928512010,
438839875970662400,
404722395845361668,
435687501068501002,
286612533757083648] # Channels requiring community role
# Controls which roles are blocked during lockdown
lockdown_configs = {
# Used as a default value for channels without a config
"default": {
"channels": general_channels,
"roles": [named_roles["participant"]]
},
"community": {
"channels": community_channels,
"roles": [named_roles["community"], named_roles["hacker"]]
}
}
# Mute role is applied to users when they're muted
# As we no longer have mute role on ReSwitched, I set it to 0 here
mute_role = 0 # Mute role in ReSwitched
# Channels that will be cleaned every minute/hour
minutely_clean_channels = []
hourly_clean_channels = []
# Edited and deletes messages in these channels will be logged
spy_channels = general_channels
# Channels and roles where users can pin messages
allowed_pin_channels = []
allowed_pin_roles = []
# Used for the pinboard. Leave empty if you don't wish for a gist pinboard.
github_oauth_token = ""

4
default.nix Normal file
View file

@ -0,0 +1,4 @@
(import (fetchTarball
"https://github.com/edolstra/flake-compat/archive/master.tar.gz") {
src = builtins.fetchGit ./.;
}).defaultNix

175
flake.lock Normal file
View file

@ -0,0 +1,175 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nix-github-actions": {
"inputs": {
"nixpkgs": [
"poetry2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1703863825,
"narHash": "sha256-rXwqjtwiGKJheXB43ybM8NwWB8rO2dSRrEqes0S7F5Y=",
"owner": "nix-community",
"repo": "nix-github-actions",
"rev": "5163432afc817cf8bd1f031418d1869e4c9d5547",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-github-actions",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1720535198,
"narHash": "sha256-zwVvxrdIzralnSbcpghA92tWu2DV2lwv89xZc8MTrbg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "205fd4226592cc83fd4c0885a3e4c9c400efabb5",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-23.11",
"repo": "nixpkgs",
"type": "github"
}
},
"poetry2nix": {
"inputs": {
"flake-utils": "flake-utils_2",
"nix-github-actions": "nix-github-actions",
"nixpkgs": [
"nixpkgs"
],
"systems": "systems_3",
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1724417163,
"narHash": "sha256-gD0N0pnKxWJcKtbetlkKOIumS0Zovgxx/nMfOIJIzoI=",
"owner": "nix-community",
"repo": "poetry2nix",
"rev": "7619e43c2b48c29e24b88a415256f09df96ec276",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "poetry2nix",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"poetry2nix": "poetry2nix"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"id": "systems",
"type": "indirect"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"poetry2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1719749022,
"narHash": "sha256-ddPKHcqaKCIFSFc/cvxS14goUhCOAwsM1PbMr0ZtHMg=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "8df5ff62195d4e67e2264df0b7f5e8c9995fd0bd",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

133
flake.nix Normal file
View file

@ -0,0 +1,133 @@
{
description = "Application packaged using poetry2nix";
inputs = {
flake-utils.url = "github:numtide/flake-utils";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
poetry2nix = {
url = "github:nix-community/poetry2nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, flake-utils, poetry2nix }@inputs:
let
ryuko_overlay = final: prev:
let
pkgs = import nixpkgs { system = prev.system; };
poetry2nix = inputs.poetry2nix.lib.mkPoetry2Nix { inherit pkgs; };
in {
ryuko-ng = with final;
poetry2nix.mkPoetryApplication rec {
projectDir = self;
src = projectDir;
overrides = [ poetry2nix.defaultPoetryOverrides (self: super: {
cryptography = super.cryptography.overridePythonAttrs (old: {
cargoDeps = pkgs.rustPlatform.fetchCargoTarball {
src = old.src;
sourceRoot = "${old.pname}-${old.version}/src/rust";
name = "${old.pname}-${old.version}";
# cryptography-42.0.7
sha256 = "sha256-wAup/0sI8gYVsxr/vtcA+tNkBT8wxmp68FPbOuro1E4=";
};
});
}) ];
};
};
in flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [ self.overlays."${system}" ];
};
in {
packages = {
default = self.packages.${system}.ryuko-ng;
ryuko-ng = pkgs.ryuko-ng;
};
overlays = ryuko_overlay;
nixosModules.ryuko-ng = { pkgs, lib, config, ... }: {
options = let inherit (lib) mkEnableOption mkOption types;
in {
services.ryuko-ng = {
enable = mkEnableOption (lib.mdDoc "ryuko-ng discord bot");
};
};
config = let
inherit (lib) mkIf;
cfg = config.services.ryuko-ng;
in mkIf cfg.enable {
nixpkgs.overlays = [ self.overlays."${system}" ];
systemd.services.ryuko-ng = {
description = "ryuko-ng bot";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
script = ''
${pkgs.ryuko-ng.dependencyEnv}/bin/python3 -m robocop_ng /var/lib/ryuko-ng
'';
serviceConfig = rec {
Type = "simple";
User = "ryuko-ng";
Group = "ryuko-ng";
StateDirectory = "ryuko-ng";
StateDirectoryMode = "0700";
CacheDirectory = "ryuko-ng";
CacheDirectoryMode = "0700";
UMask = "0077";
WorkingDirectory = "/var/lib/ryuko-ng";
Restart = "on-failure";
};
};
users = {
users.ryuko-ng = {
group = "ryuko-ng";
isSystemUser = true;
};
extraUsers.ryuko-ng.uid = 989;
groups.ryuko-ng = { };
extraGroups.ryuko-ng = {
name = "ryuko-ng";
gid = 987;
};
};
};
};
devShells.default = pkgs.mkShell {
inputsFrom = [ self.packages.${system}.ryuko-ng ];
packages = [ pkgs.poetry ];
};
checks = {
vmTest = with import (nixpkgs + "/nixos/lib/testing-python.nix") {
inherit system;
};
makeTest {
name = "ryuko-ng nixos module testing ${system}";
nodes = {
client = { ... }: {
imports = [ self.nixosModules.${system}.ryuko-ng ];
services.ryuko-ng.enable = true;
};
};
testScript = ''
client.wait_for_unit("ryuko-ng.service")
'';
};
};
formatter = pkgs.nixfmt;
});
}

View file

@ -1,42 +0,0 @@
import json
def get_restrictions():
with open("data/restrictions.json", "r") as f:
return json.load(f)
def set_restrictions(contents):
with open("data/restrictions.json", "w") as f:
f.write(contents)
def get_user_restrictions(uid):
uid = str(uid)
with open("data/restrictions.json", "r") as f:
rsts = json.load(f)
if uid in rsts:
return rsts[uid]
return []
def add_restriction(uid, rst):
# mostly from kurisu source, credits go to ihaveamac
uid = str(uid)
rsts = get_restrictions()
if uid not in rsts:
rsts[uid] = []
if rst not in rsts[uid]:
rsts[uid].append(rst)
set_restrictions(json.dumps(rsts))
def remove_restriction(uid, rst):
# mostly from kurisu source, credits go to ihaveamac
uid = str(uid)
rsts = get_restrictions()
if uid not in rsts:
rsts[uid] = []
if rst in rsts[uid]:
rsts[uid].remove(rst)
set_restrictions(json.dumps(rsts))

View file

@ -1,37 +0,0 @@
import json
import math
def get_crontab():
with open("data/robocronptab.json", "r") as f:
return json.load(f)
def set_crontab(contents):
with open("data/robocronptab.json", "w") as f:
f.write(contents)
def add_job(job_type, job_name, job_details, timestamp):
timestamp = str(math.floor(timestamp))
job_name = str(job_name)
ctab = get_crontab()
if job_type not in ctab:
ctab[job_type] = {}
if timestamp not in ctab[job_type]:
ctab[job_type][timestamp] = {}
ctab[job_type][timestamp][job_name] = job_details
set_crontab(json.dumps(ctab))
def delete_job(timestamp, job_type, job_name):
timestamp = str(timestamp)
job_name = str(job_name)
ctab = get_crontab()
del ctab[job_type][timestamp][job_name]
set_crontab(json.dumps(ctab))

View file

@ -1,63 +0,0 @@
import json
import time
userlog_event_types = {"warns": "Warn",
"bans": "Ban",
"kicks": "Kick",
"mutes": "Mute",
"notes": "Note"}
def get_userlog():
with open("data/userlog.json", "r") as f:
return json.load(f)
def set_userlog(contents):
with open("data/userlog.json", "w") as f:
f.write(contents)
def userlog(uid, issuer, reason, event_type, uname: str = ""):
userlogs = get_userlog()
uid = str(uid)
if uid not in userlogs:
userlogs[uid] = {"warns": [],
"mutes": [],
"kicks": [],
"bans": [],
"notes": [],
"watch": False,
"name": "n/a"}
if uname:
userlogs[uid]["name"] = uname
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
log_data = {"issuer_id": issuer.id,
"issuer_name": f"{issuer}",
"reason": reason,
"timestamp": timestamp}
if event_type not in userlogs[uid]:
userlogs[uid][event_type] = []
userlogs[uid][event_type].append(log_data)
set_userlog(json.dumps(userlogs))
return len(userlogs[uid][event_type])
def setwatch(uid, issuer, watch_state, uname: str = ""):
userlogs = get_userlog()
uid = str(uid)
# Can we reduce code repetition here?
if uid not in userlogs:
userlogs[uid] = {"warns": [],
"mutes": [],
"kicks": [],
"bans": [],
"notes": [],
"watch": False,
"name": "n/a"}
if uname:
userlogs[uid]["name"] = uname
userlogs[uid]["watch"] = watch_state
set_userlog(json.dumps(userlogs))
return

686
poetry.lock generated Normal file
View file

@ -0,0 +1,686 @@
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
[[package]]
name = "aiohttp"
version = "3.9.5"
description = "Async http client/server framework (asyncio)"
optional = false
python-versions = ">=3.8"
files = [
{file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7"},
{file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c"},
{file = "aiohttp-3.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a"},
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430"},
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3"},
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b"},
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72"},
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0"},
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558"},
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db"},
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f"},
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832"},
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10"},
{file = "aiohttp-3.9.5-cp310-cp310-win32.whl", hash = "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb"},
{file = "aiohttp-3.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb"},
{file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342"},
{file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d"},
{file = "aiohttp-3.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424"},
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee"},
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2"},
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233"},
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595"},
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6"},
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d"},
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323"},
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9"},
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771"},
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75"},
{file = "aiohttp-3.9.5-cp311-cp311-win32.whl", hash = "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6"},
{file = "aiohttp-3.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a"},
{file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678"},
{file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c"},
{file = "aiohttp-3.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f"},
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4"},
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c"},
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa"},
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58"},
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf"},
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f"},
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81"},
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a"},
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a"},
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da"},
{file = "aiohttp-3.9.5-cp312-cp312-win32.whl", hash = "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59"},
{file = "aiohttp-3.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888"},
{file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8"},
{file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8"},
{file = "aiohttp-3.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78"},
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b"},
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de"},
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac"},
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11"},
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd"},
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1"},
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e"},
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156"},
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031"},
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe"},
{file = "aiohttp-3.9.5-cp38-cp38-win32.whl", hash = "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da"},
{file = "aiohttp-3.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a"},
{file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed"},
{file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a"},
{file = "aiohttp-3.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372"},
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb"},
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24"},
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2"},
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106"},
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b"},
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475"},
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed"},
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c"},
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46"},
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2"},
{file = "aiohttp-3.9.5-cp39-cp39-win32.whl", hash = "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09"},
{file = "aiohttp-3.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1"},
{file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"},
]
[package.dependencies]
aiosignal = ">=1.1.2"
attrs = ">=17.3.0"
frozenlist = ">=1.1.1"
multidict = ">=4.5,<7.0"
yarl = ">=1.0,<2.0"
[package.extras]
speedups = ["Brotli", "aiodns", "brotlicffi"]
[[package]]
name = "aiosignal"
version = "1.3.1"
description = "aiosignal: a list of registered asynchronous callbacks"
optional = false
python-versions = ">=3.7"
files = [
{file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"},
{file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"},
]
[package.dependencies]
frozenlist = ">=1.1.0"
[[package]]
name = "attrs"
version = "23.2.0"
description = "Classes Without Boilerplate"
optional = false
python-versions = ">=3.7"
files = [
{file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"},
{file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"},
]
[package.extras]
cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
dev = ["attrs[tests]", "pre-commit"]
docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
tests = ["attrs[tests-no-zope]", "zope-interface"]
tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"]
tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"]
[[package]]
name = "cffi"
version = "1.16.0"
description = "Foreign Function Interface for Python calling C code."
optional = false
python-versions = ">=3.8"
files = [
{file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"},
{file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"},
{file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"},
{file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"},
{file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"},
{file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"},
{file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"},
{file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"},
{file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"},
{file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"},
{file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"},
{file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"},
{file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"},
{file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"},
{file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"},
{file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"},
{file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"},
{file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"},
{file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"},
{file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"},
{file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"},
{file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"},
{file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"},
{file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"},
{file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"},
{file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"},
{file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"},
]
[package.dependencies]
pycparser = "*"
[[package]]
name = "cryptography"
version = "42.0.7"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
python-versions = ">=3.7"
files = [
{file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a987f840718078212fdf4504d0fd4c6effe34a7e4740378e59d47696e8dfb477"},
{file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bd13b5e9b543532453de08bcdc3cc7cebec6f9883e886fd20a92f26940fd3e7a"},
{file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a79165431551042cc9d1d90e6145d5d0d3ab0f2d66326c201d9b0e7f5bf43604"},
{file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a47787a5e3649008a1102d3df55424e86606c9bae6fb77ac59afe06d234605f8"},
{file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:02c0eee2d7133bdbbc5e24441258d5d2244beb31da5ed19fbb80315f4bbbff55"},
{file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5e44507bf8d14b36b8389b226665d597bc0f18ea035d75b4e53c7b1ea84583cc"},
{file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7f8b25fa616d8b846aef64b15c606bb0828dbc35faf90566eb139aa9cff67af2"},
{file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:93a3209f6bb2b33e725ed08ee0991b92976dfdcf4e8b38646540674fc7508e13"},
{file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e6b8f1881dac458c34778d0a424ae5769de30544fc678eac51c1c8bb2183e9da"},
{file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3de9a45d3b2b7d8088c3fbf1ed4395dfeff79d07842217b38df14ef09ce1d8d7"},
{file = "cryptography-42.0.7-cp37-abi3-win32.whl", hash = "sha256:789caea816c6704f63f6241a519bfa347f72fbd67ba28d04636b7c6b7da94b0b"},
{file = "cryptography-42.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:8cb8ce7c3347fcf9446f201dc30e2d5a3c898d009126010cbd1f443f28b52678"},
{file = "cryptography-42.0.7-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:a3a5ac8b56fe37f3125e5b72b61dcde43283e5370827f5233893d461b7360cd4"},
{file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:779245e13b9a6638df14641d029add5dc17edbef6ec915688f3acb9e720a5858"},
{file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d563795db98b4cd57742a78a288cdbdc9daedac29f2239793071fe114f13785"},
{file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:31adb7d06fe4383226c3e963471f6837742889b3c4caa55aac20ad951bc8ffda"},
{file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:efd0bf5205240182e0f13bcaea41be4fdf5c22c5129fc7ced4a0282ac86998c9"},
{file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a9bc127cdc4ecf87a5ea22a2556cab6c7eda2923f84e4f3cc588e8470ce4e42e"},
{file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3577d029bc3f4827dd5bf8bf7710cac13527b470bbf1820a3f394adb38ed7d5f"},
{file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2e47577f9b18723fa294b0ea9a17d5e53a227867a0a4904a1a076d1646d45ca1"},
{file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1a58839984d9cb34c855197043eaae2c187d930ca6d644612843b4fe8513c886"},
{file = "cryptography-42.0.7-cp39-abi3-win32.whl", hash = "sha256:e6b79d0adb01aae87e8a44c2b64bc3f3fe59515280e00fb6d57a7267a2583cda"},
{file = "cryptography-42.0.7-cp39-abi3-win_amd64.whl", hash = "sha256:16268d46086bb8ad5bf0a2b5544d8a9ed87a0e33f5e77dd3c3301e63d941a83b"},
{file = "cryptography-42.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2954fccea107026512b15afb4aa664a5640cd0af630e2ee3962f2602693f0c82"},
{file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:362e7197754c231797ec45ee081f3088a27a47c6c01eff2ac83f60f85a50fe60"},
{file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4f698edacf9c9e0371112792558d2f705b5645076cc0aaae02f816a0171770fd"},
{file = "cryptography-42.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5482e789294854c28237bba77c4c83be698be740e31a3ae5e879ee5444166582"},
{file = "cryptography-42.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e9b2a6309f14c0497f348d08a065d52f3020656f675819fc405fb63bbcd26562"},
{file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d8e3098721b84392ee45af2dd554c947c32cc52f862b6a3ae982dbb90f577f14"},
{file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c65f96dad14f8528a447414125e1fc8feb2ad5a272b8f68477abbcc1ea7d94b9"},
{file = "cryptography-42.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36017400817987670037fbb0324d71489b6ead6231c9604f8fc1f7d008087c68"},
{file = "cryptography-42.0.7.tar.gz", hash = "sha256:ecbfbc00bf55888edda9868a4cf927205de8499e7fabe6c050322298382953f2"},
]
[package.dependencies]
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
[package.extras]
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
nox = ["nox"]
pep8test = ["check-sdist", "click", "mypy", "ruff"]
sdist = ["build"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
test-randomorder = ["pytest-randomly"]
[[package]]
name = "discord-py"
version = "2.3.2"
description = "A Python wrapper for the Discord API"
optional = false
python-versions = ">=3.8.0"
files = [
{file = "discord.py-2.3.2-py3-none-any.whl", hash = "sha256:9da4679fc3cb10c64b388284700dc998663e0e57328283bbfcfc2525ec5960a6"},
{file = "discord.py-2.3.2.tar.gz", hash = "sha256:4560f70f2eddba7e83370ecebd237ac09fbb4980dc66507482b0c0e5b8f76b9c"},
]
[package.dependencies]
aiohttp = ">=3.7.4,<4"
[package.extras]
docs = ["sphinx (==4.4.0)", "sphinxcontrib-trio (==1.1.2)", "sphinxcontrib-websupport", "typing-extensions (>=4.3,<5)"]
speed = ["Brotli", "aiodns (>=1.1)", "cchardet (==2.1.7)", "orjson (>=3.5.4)"]
test = ["coverage[toml]", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mock", "typing-extensions (>=4.3,<5)"]
voice = ["PyNaCl (>=1.3.0,<1.6)"]
[[package]]
name = "frozenlist"
version = "1.4.1"
description = "A list-like structure which implements collections.abc.MutableSequence"
optional = false
python-versions = ">=3.8"
files = [
{file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"},
{file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"},
{file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"},
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"},
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"},
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"},
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"},
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"},
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"},
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"},
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"},
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"},
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"},
{file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"},
{file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"},
{file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"},
{file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"},
{file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"},
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"},
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"},
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"},
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"},
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"},
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"},
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"},
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"},
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"},
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"},
{file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"},
{file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"},
{file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"},
{file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"},
{file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"},
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"},
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"},
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"},
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"},
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"},
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"},
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"},
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"},
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"},
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"},
{file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"},
{file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"},
{file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"},
{file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"},
{file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"},
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"},
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"},
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"},
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"},
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"},
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"},
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"},
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"},
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"},
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"},
{file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"},
{file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"},
{file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"},
{file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"},
{file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"},
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"},
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"},
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"},
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"},
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"},
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"},
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"},
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"},
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"},
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"},
{file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"},
{file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"},
{file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"},
{file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"},
]
[[package]]
name = "gidgethub"
version = "5.3.0"
description = "An async GitHub API library"
optional = false
python-versions = ">=3.7"
files = [
{file = "gidgethub-5.3.0-py3-none-any.whl", hash = "sha256:4dd92f2252d12756b13f9dd15cde322bfb0d625b6fb5d680da1567ec74b462c0"},
{file = "gidgethub-5.3.0.tar.gz", hash = "sha256:9ece7d37fbceb819b80560e7ed58f936e48a65d37ec5f56db79145156b426a25"},
]
[package.dependencies]
PyJWT = {version = ">=2.4.0", extras = ["crypto"]}
uritemplate = ">=3.0.1"
[package.extras]
aiohttp = ["aiohttp"]
dev = ["aiohttp", "black", "coverage[toml] (>=5.0.3)", "httpx", "mypy", "pytest-cov", "pytest-xdist", "tornado"]
doc = ["sphinx (>=4.0.0)", "sphinx-rtd-theme (>=0.5.2)"]
httpx = ["httpx (>=0.16.1)"]
test = ["importlib-resources", "pytest (>=5.4.1)", "pytest-asyncio", "pytest-tornasync"]
tornado = ["tornado"]
[[package]]
name = "humanize"
version = "4.9.0"
description = "Python humanize utilities"
optional = false
python-versions = ">=3.8"
files = [
{file = "humanize-4.9.0-py3-none-any.whl", hash = "sha256:ce284a76d5b1377fd8836733b983bfb0b76f1aa1c090de2566fcf008d7f6ab16"},
{file = "humanize-4.9.0.tar.gz", hash = "sha256:582a265c931c683a7e9b8ed9559089dea7edcf6cc95be39a3cbc2c5d5ac2bcfa"},
]
[package.extras]
tests = ["freezegun", "pytest", "pytest-cov"]
[[package]]
name = "idna"
version = "3.7"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.5"
files = [
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
]
[[package]]
name = "multidict"
version = "6.0.5"
description = "multidict implementation"
optional = false
python-versions = ">=3.7"
files = [
{file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"},
{file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"},
{file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"},
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"},
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"},
{file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"},
{file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"},
{file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"},
{file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"},
{file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"},
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"},
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"},
{file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"},
{file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"},
{file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"},
{file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"},
{file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"},
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"},
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"},
{file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"},
{file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"},
{file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"},
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"},
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"},
{file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"},
{file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"},
{file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"},
{file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"},
{file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"},
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"},
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"},
{file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"},
{file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"},
{file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"},
{file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"},
{file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"},
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"},
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"},
{file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"},
{file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"},
{file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"},
{file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"},
]
[[package]]
name = "parsedatetime"
version = "2.6"
description = "Parse human-readable date/time text."
optional = false
python-versions = "*"
files = [
{file = "parsedatetime-2.6-py3-none-any.whl", hash = "sha256:cb96edd7016872f58479e35879294258c71437195760746faffedb692aef000b"},
{file = "parsedatetime-2.6.tar.gz", hash = "sha256:4cb368fbb18a0b7231f4d76119165451c8d2e35951455dfee97c62a87b04d455"},
]
[[package]]
name = "pycparser"
version = "2.22"
description = "C parser in Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
]
[[package]]
name = "pyjwt"
version = "2.8.0"
description = "JSON Web Token implementation in Python"
optional = false
python-versions = ">=3.7"
files = [
{file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"},
{file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"},
]
[package.dependencies]
cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""}
[package.extras]
crypto = ["cryptography (>=3.4.0)"]
dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
description = "Extensions to the standard Python datetime module"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
files = [
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
]
[package.dependencies]
six = ">=1.5"
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
[[package]]
name = "uritemplate"
version = "4.1.1"
description = "Implementation of RFC 6570 URI Templates"
optional = false
python-versions = ">=3.6"
files = [
{file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"},
{file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"},
]
[[package]]
name = "yarl"
version = "1.9.4"
description = "Yet another URL library"
optional = false
python-versions = ">=3.7"
files = [
{file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"},
{file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"},
{file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"},
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"},
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"},
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"},
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"},
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"},
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"},
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"},
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"},
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"},
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"},
{file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"},
{file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"},
{file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"},
{file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"},
{file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"},
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"},
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"},
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"},
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"},
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"},
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"},
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"},
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"},
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"},
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"},
{file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"},
{file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"},
{file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"},
{file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"},
{file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"},
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"},
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"},
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"},
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"},
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"},
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"},
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"},
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"},
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"},
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"},
{file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"},
{file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"},
{file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"},
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"},
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"},
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"},
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"},
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"},
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"},
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"},
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"},
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"},
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"},
{file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"},
{file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"},
{file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"},
{file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"},
{file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"},
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"},
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"},
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"},
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"},
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"},
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"},
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"},
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"},
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"},
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"},
{file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"},
{file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"},
{file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"},
{file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"},
{file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"},
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"},
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"},
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"},
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"},
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"},
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"},
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"},
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"},
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"},
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"},
{file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"},
{file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"},
{file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"},
{file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"},
]
[package.dependencies]
idna = ">=2.0"
multidict = ">=4.0"
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "1453fcd147cbf31437ecaa4b37deb0f5d880579a2b93b5f668c28c154ed299d8"

23
pyproject.toml Normal file
View file

@ -0,0 +1,23 @@
[tool.poetry]
name = "robocop_ng"
version = "1.0.1"
description = "Discord bot for handling ReSwitched moderation tasks and such, (n)ext-(g)en rewrite of Robocop"
authors = ["ReSwitched Team"]
license = "MIT"
readme = "README.md"
repository = "https://github.com/reswitched/robocop-ng"
[tool.poetry.dependencies]
python = "^3.11"
python-dateutil = "^2.8.2"
humanize = "^4.8.0"
parsedatetime = "^2.6"
aiohttp = "^3.9.3"
gidgethub = "^5.3.0"
"discord.py" = "^2.3.2"
[tool.poetry.dev-dependencies]
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View file

@ -1,8 +0,0 @@
git+https://github.com/Rapptz/discord.py@rewrite
asyncio
python-dateutil
humanize
parsedatetime
aiohttp
gidgethub

296
robocop_ng/__main__.py Executable file
View file

@ -0,0 +1,296 @@
import asyncio
import logging.handlers
import os
import sys
import aiohttp
import discord
from discord.ext import commands
from discord.ext.commands import CommandError, Context
from robocop_ng.helpers.notifications import report_critical_error
if len(sys.argv[1:]) != 1:
sys.stderr.write("usage: <state_dir>")
sys.exit(1)
state_dir = os.path.abspath(sys.argv[1])
sys.path.append(state_dir)
import config
script_name = os.path.basename(__file__).split(".")[0]
log_file_name = f"{script_name}.log"
# Limit of discord (non-nitro) is 8MB (not MiB)
max_file_size = 1000 * 1000 * 8
backup_count = 3
file_handler = logging.handlers.RotatingFileHandler(
filename=log_file_name, maxBytes=max_file_size, backupCount=backup_count
)
stdout_handler = logging.StreamHandler(sys.stdout)
log_format = logging.Formatter(
"[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s"
)
file_handler.setFormatter(log_format)
stdout_handler.setFormatter(log_format)
log = logging.getLogger("discord")
log.setLevel(logging.INFO)
log.addHandler(file_handler)
log.addHandler(stdout_handler)
def get_prefix(bot, message):
prefixes = config.prefixes
return commands.when_mentioned_or(*prefixes)(bot, message)
wanted_jsons = [
"data/restrictions.json",
"data/robocronptab.json",
"data/userlog.json",
"data/invites.json",
"data/macros.json",
"data/persistent_roles.json",
"data/disabled_ids.json",
]
if not os.path.exists(os.path.join(state_dir, "data")):
os.makedirs(os.path.join(state_dir, "data"))
for wanted_json_idx in range(len(wanted_jsons)):
wanted_jsons[wanted_json_idx] = os.path.join(
state_dir, wanted_jsons[wanted_json_idx]
)
if not os.path.isfile(wanted_jsons[wanted_json_idx]):
with open(wanted_jsons[wanted_json_idx], "w") as file:
file.write("{}")
intents = discord.Intents.all()
intents.typing = False
bot = commands.Bot(
command_prefix=get_prefix, description=config.bot_description, intents=intents
)
bot.help_command = commands.DefaultHelpCommand(dm_help=True)
bot.log = log
bot.config = config
bot.script_name = script_name
bot.state_dir = state_dir
bot.wanted_jsons = wanted_jsons
async def get_channel_safe(self, channel_id: int):
res = self.get_channel(channel_id)
if res is None:
res = await self.fetch_channel(channel_id)
return res
commands.Bot.get_channel_safe = get_channel_safe
@bot.event
async def on_ready():
aioh = {"User-Agent": f"{script_name}/1.0'"}
bot.aiosession = aiohttp.ClientSession(headers=aioh)
bot.app_info = await bot.application_info()
bot.botlog_channel = await bot.get_channel_safe(config.botlog_channel)
log.info(
f"\nLogged in as: {bot.user.name} - "
f"{bot.user.id}\ndpy version: {discord.__version__}\n"
)
game_name = f"{config.prefixes[0]}help"
# Send "Robocop has started! x has y members!"
guild = bot.botlog_channel.guild
msg = (
f"{bot.user.name} has started! "
f"{guild.name} has {guild.member_count} members!"
)
data_files = [discord.File(fpath) for fpath in wanted_jsons]
await bot.botlog_channel.send(msg, files=data_files)
activity = discord.Activity(name=game_name, type=discord.ActivityType.listening)
await bot.change_presence(activity=activity)
@bot.event
async def on_command(ctx):
log_text = (
f"{ctx.message.author} ({ctx.message.author.id}): " f'"{ctx.message.content}" '
)
if ctx.guild: # was too long for tertiary if
log_text += (
f'on "{ctx.channel.name}" ({ctx.channel.id}) '
f'at "{ctx.guild.name}" ({ctx.guild.id})'
)
else:
log_text += f"on DMs ({ctx.channel.id})"
log.info(log_text)
@bot.event
async def on_error(event: str, *args, **kwargs):
log.exception(f"Error on {event}:")
exception = sys.exc_info()[1]
is_report_allowed = any(
[
not isinstance(exception, x)
for x in [
discord.RateLimited,
discord.GatewayNotFound,
discord.InteractionResponded,
discord.LoginFailure,
]
]
)
if exception is not None and is_report_allowed:
await report_critical_error(
bot,
exception,
additional_info={"Event": event, "args": args, "kwargs": kwargs},
)
@bot.event
async def on_command_error(ctx: Context, error: CommandError):
error_text = str(error)
err_msg = (
f'Error with "{ctx.message.content}" from '
f'"{ctx.message.author} ({ctx.message.author.id}) '
f"of type {type(error)}: {error_text}"
)
log.exception(err_msg)
if not isinstance(error, commands.CommandNotFound):
err_msg = bot.escape_message(err_msg)
await bot.botlog_channel.send(err_msg)
if isinstance(error, commands.NoPrivateMessage):
return await ctx.send("This command doesn't work on DMs.")
elif isinstance(error, commands.MissingPermissions):
roles_needed = "\n- ".join(error.missing_perms)
return await ctx.send(
f"{ctx.author.mention}: You don't have the right"
" permissions to run this command. You need: "
f"```- {roles_needed}```"
)
elif isinstance(error, commands.BotMissingPermissions):
roles_needed = "\n-".join(error.missing_perms)
return await ctx.send(
f"{ctx.author.mention}: Bot doesn't have "
"the right permissions to run this command. "
"Please add the following roles: "
f"```- {roles_needed}```"
)
elif isinstance(error, commands.CommandOnCooldown):
return await ctx.send(
f"{ctx.author.mention}: You're being "
"ratelimited. Try in "
f"{error.retry_after:.1f} seconds."
)
elif isinstance(error, commands.CheckFailure):
return await ctx.send(
f"{ctx.author.mention}: Check failed. "
"You might not have the right permissions "
"to run this command, or you may not be able "
"to run this command in the current channel."
)
elif isinstance(error, commands.CommandInvokeError) and (
"Cannot send messages to this user" in error_text
):
return await ctx.send(
f"{ctx.author.mention}: I can't DM you.\n"
"You might have me blocked or have DMs "
f"blocked globally or for {ctx.guild.name}.\n"
"Please resolve that, then "
"run the command again."
)
elif isinstance(error, commands.CommandNotFound):
# Nothing to do when command is not found.
return
help_text = (
f"Usage of this command is: ```{ctx.prefix}{ctx.command.name} "
f"{ctx.command.signature}```\nPlease see `{ctx.prefix}help "
f"{ctx.command.name}` for more info about this command."
)
# Keep a list of commands that involve mentioning users
# and can involve users leaving/getting banned
# noinspection NonAsciiCharacters,PyPep8Naming
ಠ_ಠ = ["warn", "kick", "ban"]
if isinstance(error, commands.BadArgument):
# and if said commands get used, add a specific notice.
if ctx.command.name in ಠ_ಠ:
help_text = (
"This probably means that user left (or already got kicked/banned).\n"
+ help_text
)
return await ctx.send(
f"{ctx.author.mention}: You gave incorrect arguments. {help_text}"
)
elif isinstance(error, commands.MissingRequiredArgument):
return await ctx.send(
f"{ctx.author.mention}: You gave incomplete arguments. {help_text}"
)
@bot.event
async def on_message(message):
if message.author.bot:
return
if (message.guild) and (message.guild.id not in config.guild_whitelist):
return
# Ignore messages in newcomers channel, unless it's potentially
# an allowed command
welcome_allowed = ["reset", "kick", "ban", "warn"]
if message.channel.id == config.welcome_channel and not any(
cmd in message.content for cmd in welcome_allowed
):
return
ctx = await bot.get_context(message)
await bot.invoke(ctx)
async def main():
async with bot:
if len(config.guild_whitelist) == 1:
invite_url = discord.utils.oauth_url(
config.client_id,
guild=discord.Object(config.guild_whitelist[0]),
disable_guild_select=True,
)
else:
invite_url = discord.utils.oauth_url(config.client_id)
log.info(f"\nInvite URL: {invite_url}\n")
for cog in config.initial_cogs:
try:
await bot.load_extension(f"robocop_ng.{cog}")
except Exception as e:
log.exception(f"Failed to load cog {cog}:", e)
await bot.start(config.token)
if __name__ == "__main__":
asyncio.run(main())

2
robocop_ng/assets/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*.otf
*.ttf

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

177
robocop_ng/cogs/admin.py Normal file
View file

@ -0,0 +1,177 @@
import inspect
import re
import traceback
import discord
from discord.ext import commands
from discord.ext.commands import Cog
from robocop_ng.helpers.checks import check_if_bot_manager
class Admin(Cog):
def __init__(self, bot):
self.bot = bot
self.last_eval_result = None
self.previous_eval_code = None
@commands.guild_only()
@commands.check(check_if_bot_manager)
@commands.command(name="exit", aliases=["quit", "bye"])
async def _exit(self, ctx):
"""Shuts down the bot, bot manager only."""
await ctx.send(":wave: Goodbye!")
await self.bot.close()
@commands.guild_only()
@commands.check(check_if_bot_manager)
@commands.command()
async def fetchlog(self, ctx):
"""Returns log"""
await ctx.send(
"Here's the current log file:",
file=discord.File(f"{self.bot.script_name}.log"),
)
@commands.guild_only()
@commands.check(check_if_bot_manager)
@commands.command()
async def fetchdata(self, ctx):
"""Returns data files"""
data_files = [discord.File(fpath) for fpath in self.bot.wanted_jsons]
await ctx.send("Here you go:", files=data_files)
@commands.guild_only()
@commands.check(check_if_bot_manager)
@commands.command(name="eval")
async def _eval(self, ctx, *, code: str):
"""Evaluates some code, bot manager only."""
try:
code = code.strip("` ")
env = {
"bot": self.bot,
"ctx": ctx,
"message": ctx.message,
"server": ctx.guild,
"guild": ctx.guild,
"channel": ctx.message.channel,
"author": ctx.message.author,
# modules
"discord": discord,
"commands": commands,
# utilities
"_get": discord.utils.get,
"_find": discord.utils.find,
# last result
"_": self.last_eval_result,
"_p": self.previous_eval_code,
}
env.update(globals())
self.bot.log.info(f"Evaling {repr(code)}:")
result = eval(code, env)
if inspect.isawaitable(result):
result = await result
if result is not None:
self.last_eval_result = result
self.previous_eval_code = code
sliced_message = await self.bot.slice_message(
repr(result), prefix="```", suffix="```"
)
for msg in sliced_message:
await ctx.send(msg)
except:
sliced_message = await self.bot.slice_message(
traceback.format_exc(), prefix="```", suffix="```"
)
for msg in sliced_message:
await ctx.send(msg)
async def cog_load_actions(self, cog_name):
if cog_name == "verification":
verif_channel = self.bot.get_channel(self.bot.config.welcome_channel)
await self.bot.do_resetalgo(verif_channel, "cog load")
@commands.guild_only()
@commands.check(check_if_bot_manager)
@commands.command()
async def pull(self, ctx, auto=False):
"""Does a git pull, bot manager only."""
tmp = await ctx.send("Pulling...")
git_output = await self.bot.async_call_shell("git pull")
await tmp.edit(content=f"Pull complete. Output: ```{git_output}```")
if auto:
cogs_to_reload = re.findall(r"cogs/([a-z_]*).py[ ]*\|", git_output)
for cog in cogs_to_reload:
cog_name = "robocop_ng.cogs." + cog
if cog_name not in self.bot.config.initial_cogs:
continue
try:
await self.bot.unload_extension(cog_name)
await self.bot.load_extension(cog_name)
self.bot.log.info(f"Reloaded ext {cog}")
await ctx.send(f":white_check_mark: `{cog}` successfully reloaded.")
await self.cog_load_actions(cog)
except:
await ctx.send(
f":x: Cog reloading failed, traceback: "
f"```\n{traceback.format_exc()}\n```"
)
return
@commands.guild_only()
@commands.check(check_if_bot_manager)
@commands.command()
async def load(self, ctx, ext: str):
"""Loads a cog, bot manager only."""
try:
await self.bot.load_extension("robocop_ng.cogs." + ext)
await self.cog_load_actions(ext)
except:
await ctx.send(
f":x: Cog loading failed, traceback: "
f"```\n{traceback.format_exc()}\n```"
)
return
self.bot.log.info(f"Loaded ext {ext}")
await ctx.send(f":white_check_mark: `{ext}` successfully loaded.")
@commands.guild_only()
@commands.check(check_if_bot_manager)
@commands.command()
async def unload(self, ctx, ext: str):
"""Unloads a cog, bot manager only."""
await self.bot.unload_extension("robocop_ng.cogs." + ext)
self.bot.log.info(f"Unloaded ext {ext}")
await ctx.send(f":white_check_mark: `{ext}` successfully unloaded.")
@commands.check(check_if_bot_manager)
@commands.command()
async def reload(self, ctx, ext="_"):
"""Reloads a cog, bot manager only."""
if ext == "_":
ext = self.lastreload
else:
self.lastreload = ext
try:
await self.bot.unload_extension("robocop_ng.cogs." + ext)
await self.bot.load_extension("robocop_ng.cogs." + ext)
await self.cog_load_actions(ext)
except:
await ctx.send(
f":x: Cog reloading failed, traceback: "
f"```\n{traceback.format_exc()}\n```"
)
return
self.bot.log.info(f"Reloaded ext {ext}")
await ctx.send(f":white_check_mark: `{ext}` successfully reloaded.")
async def setup(bot):
await bot.add_cog(Admin(bot))

67
robocop_ng/cogs/basic.py Normal file
View file

@ -0,0 +1,67 @@
import time
import discord
from discord.ext import commands
from discord.ext.commands import Cog
class Basic(Cog):
def __init__(self, bot):
self.bot = bot
@commands.command()
async def hello(self, ctx):
"""Says hello. Duh."""
await ctx.send(f"Hello {ctx.author.mention}!")
@commands.cooldown(1, 10, type=commands.BucketType.user)
@commands.command(name="hex")
async def _hex(self, ctx, num: int):
"""Converts base 10 to 16 (for emummc sector calculation)"""
hex_val = hex(num).upper().replace("0X", "0x")
await ctx.send(f"{ctx.author.mention}: {hex_val}")
@commands.cooldown(1, 10, type=commands.BucketType.user)
@commands.command(name="dec")
async def _dec(self, ctx, num):
"""Converts base 16 to 10"""
await ctx.send(f"{ctx.author.mention}: {int(num, 16)}")
@commands.guild_only()
@commands.command()
async def membercount(self, ctx):
"""Prints the member count of the server."""
await ctx.send(f"{ctx.guild.name} has {ctx.guild.member_count} members!")
@commands.command(aliases=["robocopng", "robocop-ng"])
async def robocop(self, ctx):
"""Shows a quick embed with bot info."""
embed = discord.Embed(
title="Robocop-NG",
url=self.bot.config.source_url,
description=self.bot.config.embed_desc,
)
embed.set_thumbnail(url=str(self.bot.user.display_avatar))
await ctx.send(embed=embed)
@commands.command(aliases=["p"])
async def ping(self, ctx):
"""Shows ping values to discord.
RTT = Round-trip time, time taken to send a message to discord
GW = Gateway Ping"""
before = time.monotonic()
tmp = await ctx.send("Calculating ping...")
after = time.monotonic()
rtt_ms = (after - before) * 1000
gw_ms = self.bot.latency * 1000
message_text = f":ping_pong:\nrtt: `{rtt_ms:.1f}ms`\ngw: `{gw_ms:.1f}ms`"
self.bot.log.info(message_text)
await tmp.edit(content=message_text)
async def setup(bot):
await bot.add_cog(Basic(bot))

View file

@ -0,0 +1,29 @@
from discord.ext import commands
from discord.ext.commands import Cog
class BasicReswitched(Cog):
def __init__(self, bot):
self.bot = bot
@commands.guild_only()
@commands.command()
async def communitycount(self, ctx):
"""Prints the community member count of the server."""
community = ctx.guild.get_role(self.bot.config.named_roles["community"])
await ctx.send(
f"{ctx.guild.name} has {len(community.members)} community members!"
)
@commands.guild_only()
@commands.command()
async def hackercount(self, ctx):
"""Prints the hacker member count of the server."""
h4x0r = ctx.guild.get_role(self.bot.config.named_roles["hacker"])
await ctx.send(
f"{ctx.guild.name} has {len(h4x0r.members)} people with hacker role!"
)
async def setup(bot):
await bot.add_cog(BasicReswitched(bot))

View file

@ -7,6 +7,7 @@ import math
import parsedatetime
from discord.ext.commands import Cog
class Common(Cog):
def __init__(self, bot):
self.bot = bot
@ -30,9 +31,14 @@ class Common(Cog):
res_timestamp = math.floor(time.mktime(time_struct))
return res_timestamp
def get_relative_timestamp(self, time_from=None, time_to=None,
humanized=False, include_from=False,
include_to=False):
def get_relative_timestamp(
self,
time_from=None,
time_to=None,
humanized=False,
include_from=False,
include_to=False,
):
# Setting default value to utcnow() makes it show time from cog load
# which is not what we want
if not time_from:
@ -42,17 +48,19 @@ class Common(Cog):
if humanized:
humanized_string = humanize.naturaltime(time_from - time_to)
if include_from and include_to:
str_with_from_and_to = f"{humanized_string} "\
f"({str(time_from).split('.')[0]} "\
f"- {str(time_to).split('.')[0]})"
str_with_from_and_to = (
f"{humanized_string} "
f"({str(time_from).split('.')[0]} "
f"- {str(time_to).split('.')[0]})"
)
return str_with_from_and_to
elif include_from:
str_with_from = f"{humanized_string} "\
f"({str(time_from).split('.')[0]})"
str_with_from = (
f"{humanized_string} " f"({str(time_from).split('.')[0]})"
)
return str_with_from
elif include_to:
str_with_to = f"{humanized_string} "\
f"({str(time_to).split('.')[0]})"
str_with_to = f"{humanized_string} " f"({str(time_to).split('.')[0]})"
return str_with_to
return humanized_string
else:
@ -60,8 +68,7 @@ class Common(Cog):
epoch_from = (time_from - epoch).total_seconds()
epoch_to = (time_to - epoch).total_seconds()
second_diff = epoch_to - epoch_from
result_string = str(datetime.timedelta(
seconds=second_diff)).split('.')[0]
result_string = str(datetime.timedelta(seconds=second_diff)).split(".")[0]
return result_string
async def aioget(self, url):
@ -72,11 +79,12 @@ class Common(Cog):
self.bot.log.info(f"Data from {url}: {text_data}")
return text_data
else:
self.bot.log.error(f"HTTP Error {data.status} "
"while getting {url}")
self.bot.log.error(f"HTTP Error {data.status} while getting {url}")
except:
self.bot.log.error(f"Error while getting {url} "
f"on aiogetbytes: {traceback.format_exc()}")
self.bot.log.error(
f"Error while getting {url} "
f"on aiogetbytes: {traceback.format_exc()}"
)
async def aiogetbytes(self, url):
try:
@ -86,11 +94,12 @@ class Common(Cog):
self.bot.log.debug(f"Data from {url}: {byte_data}")
return byte_data
else:
self.bot.log.error(f"HTTP Error {data.status} "
"while getting {url}")
self.bot.log.error(f"HTTP Error {data.status} while getting {url}")
except:
self.bot.log.error(f"Error while getting {url} "
f"on aiogetbytes: {traceback.format_exc()}")
self.bot.log.error(
f"Error while getting {url} "
f"on aiogetbytes: {traceback.format_exc()}"
)
async def aiojson(self, url):
try:
@ -98,18 +107,19 @@ class Common(Cog):
if data.status == 200:
text_data = await data.text()
self.bot.log.info(f"Data from {url}: {text_data}")
content_type = data.headers['Content-Type']
content_type = data.headers["Content-Type"]
return await data.json(content_type=content_type)
else:
self.bot.log.error(f"HTTP Error {data.status} "
"while getting {url}")
self.bot.log.error(f"HTTP Error {data.status} while getting {url}")
except:
self.bot.log.error(f"Error while getting {url} "
f"on aiogetbytes: {traceback.format_exc()}")
self.bot.log.error(
f"Error while getting {url} "
f"on aiogetbytes: {traceback.format_exc()}"
)
def hex_to_int(self, color_hex: str):
"""Turns a given hex color into an integer"""
return int("0x" + color_hex.strip('#'), 16)
return int("0x" + color_hex.strip("#"), 16)
def escape_message(self, text: str):
"""Escapes unfun stuff from messages"""
@ -129,10 +139,12 @@ class Common(Cog):
"""Slices a message into multiple messages"""
if len(text) > size * self.max_split_length:
haste_url = await self.haste(text)
return [f"Message is too long ({len(text)} > "
f"{size * self.max_split_length} "
f"({size} * {self.max_split_length}))"
f", go to haste: <{haste_url}>"]
return [
f"Message is too long ({len(text)} > "
f"{size * self.max_split_length} "
f"({size} * {self.max_split_length}))"
f", go to haste: <{haste_url}>"
]
reply_list = []
size_wo_fix = size - len(prefix) - len(suffix)
while len(text) > size_wo_fix:
@ -141,28 +153,28 @@ class Common(Cog):
reply_list.append(f"{prefix}{text}{suffix}")
return reply_list
async def haste(self, text, instance='https://mystb.in/'):
response = await self.bot.aiosession.post(f"{instance}documents",
data=text)
async def haste(self, text, instance="https://mystb.in/"):
response = await self.bot.aiosession.post(f"{instance}documents", data=text)
if response.status == 200:
result_json = await response.json()
return f"{instance}{result_json['key']}"
else:
return f"Error {response.status}: {response.text}"
async def async_call_shell(self, shell_command: str,
inc_stdout=True, inc_stderr=True):
async def async_call_shell(
self, shell_command: str, inc_stdout=True, inc_stderr=True
):
pipe = asyncio.subprocess.PIPE
proc = await asyncio.create_subprocess_shell(str(shell_command),
stdout=pipe,
stderr=pipe)
proc = await asyncio.create_subprocess_shell(
str(shell_command), stdout=pipe, stderr=pipe
)
if not (inc_stdout or inc_stderr):
return "??? you set both stdout and stderr to False????"
proc_result = await proc.communicate()
stdout_str = proc_result[0].decode('utf-8').strip()
stderr_str = proc_result[1].decode('utf-8').strip()
stdout_str = proc_result[0].decode("utf-8").strip()
stderr_str = proc_result[1].decode("utf-8").strip()
if inc_stdout and not inc_stderr:
return stdout_str
@ -170,8 +182,7 @@ class Common(Cog):
return stderr_str
if stdout_str and stderr_str:
return f"stdout:\n\n{stdout_str}\n\n"\
f"======\n\nstderr:\n\n{stderr_str}"
return f"stdout:\n\n{stdout_str}\n\n" f"======\n\nstderr:\n\n{stderr_str}"
elif stdout_str:
return f"stdout:\n\n{stdout_str}"
elif stderr_str:
@ -180,5 +191,5 @@ class Common(Cog):
return "No output."
def setup(bot):
bot.add_cog(Common(bot))
async def setup(bot):
await bot.add_cog(Common(bot))

View file

@ -1,23 +1,26 @@
import re
import discord
import discord
from discord.ext import commands
from discord.ext.commands import Cog
from helpers.errcodes import *
from robocop_ng.helpers.errcodes import *
class Err(Cog):
"""Everything related to Nintendo 3DS, Wii U and Switch error codes"""
def __init__(self, bot):
self.bot = bot
self.dds_re = re.compile(r'0\d{2}\-\d{4}')
self.wiiu_re = re.compile(r'1\d{2}\-\d{4}')
self.switch_re = re.compile(r'2\d{3}\-\d{4}')
self.no_err_desc = "It seems like your error code is unknown. "\
"You should report relevant details to "\
"<@141532589725974528> (tomGER#7462) "\
"so it can be added to the bot."
self.rickroll = "https://www.youtube.com/watch?v=4uj896lr3-E"
self.dds_re = re.compile(r"0\d{2}\-\d{4}")
self.wiiu_re = re.compile(r"1\d{2}\-\d{4}")
self.switch_re = re.compile(r"2\d{3}\-\d{4}")
self.no_err_desc = (
"It seems like your error code is unknown. "
"You can check on Switchbrew for your error code at "
"<https://switchbrew.org/wiki/Error_codes>"
)
self.rickroll = "https://www.youtube.com/watch?v=z3ZiVn5L9vM"
@commands.command(aliases=["3dserr", "3err", "dserr"])
async def dderr(self, ctx, err: str):
@ -29,9 +32,9 @@ class Err(Cog):
else:
err_description = self.no_err_desc
# Make a nice Embed out of it
embed = discord.Embed(title=err,
url=self.rickroll,
description=err_description)
embed = discord.Embed(
title=err, url=self.rickroll, description=err_description
)
embed.set_footer(text="Console: 3DS")
# Send message, crazy
@ -48,8 +51,7 @@ class Err(Cog):
level = (rc >> 27) & 0x1F
embed = discord.Embed(title=f"0x{rc:X}")
embed.add_field(name="Module", value=dds_modules.get(mod, mod))
embed.add_field(name="Description",
value=dds_descriptions.get(desc, desc))
embed.add_field(name="Description", value=dds_descriptions.get(desc, desc))
embed.add_field(name="Summary", value=dds_summaries.get(summ, summ))
embed.add_field(name="Level", value=dds_levels.get(level, level))
embed.set_footer(text="Console: 3DS")
@ -57,13 +59,15 @@ class Err(Cog):
await ctx.send(embed=embed)
return
else:
await ctx.send("Unknown Format - This is either "
"no error code or you made some mistake!")
await ctx.send(
"Unknown Format - This is either "
"no error code or you made some mistake!"
)
@commands.command(aliases=["wiiuserr", "uerr", "wuerr", "mochaerr"])
async def wiiuerr(self, ctx, err: str):
"""Searches for Wii U error codes!
Usage: .wiiuserr/.uerr/.wuerr/.mochaerr <Error Code>"""
Usage: .wiiuserr/.uerr/.wuerr/.mochaerr <Error Code>"""
if self.wiiu_re.match(err): # Wii U
module = err[2:3] # Is that even true, idk just guessing
desc = err[5:8]
@ -73,9 +77,9 @@ class Err(Cog):
err_description = self.no_err_desc
# Make a nice Embed out of it
embed = discord.Embed(title=err,
url=self.rickroll,
description=err_description)
embed = discord.Embed(
title=err, url=self.rickroll, description=err_description
)
embed.set_footer(text="Console: Wii U")
embed.add_field(name="Module", value=module, inline=True)
embed.add_field(name="Description", value=desc, inline=True)
@ -83,16 +87,17 @@ class Err(Cog):
# Send message, crazy
await ctx.send(embed=embed)
else:
await ctx.send("Unknown Format - This is either "
"no error code or you made some mistake!")
await ctx.send(
"Unknown Format - This is either "
"no error code or you made some mistake!"
)
@commands.command(aliases=["nxerr", "serr"])
async def err(self, ctx, err: str):
"""Searches for Switch error codes!
Usage: .serr/.nxerr/.err <Error Code>"""
Usage: .serr/.nxerr/.err <Error Code>"""
if self.switch_re.match(err) or err.startswith("0x"): # Switch
if err.startswith("0x"):
err = err[2:]
errcode = int(err, 16)
@ -103,7 +108,7 @@ class Err(Cog):
desc = int(err[5:9])
errcode = (desc << 9) + module
str_errcode = f'{(module + 2000):04}-{desc:04}'
str_errcode = f"{(module + 2000):04}-{desc:04}"
# Searching for Modules in list
if module in switch_modules:
@ -126,19 +131,21 @@ class Err(Cog):
err_description = errcode_range[2]
# Make a nice Embed out of it
embed = discord.Embed(title=f"{str_errcode} / {hex(errcode)}",
url=self.rickroll,
description=err_description)
embed.add_field(name="Module",
value=f"{err_module} ({module})",
inline=True)
embed = discord.Embed(
title=f"{str_errcode} / {hex(errcode)}",
url=self.rickroll,
description=err_description,
)
embed.add_field(
name="Module", value=f"{err_module} ({module})", inline=True
)
embed.add_field(name="Description", value=desc, inline=True)
if "ban" in err_description:
embed.set_footer("F to you | Console: Switch")
embed.set_footer(text="F to you | Console: Switch")
else:
embed.set_footer(text="Console: Switch")
await ctx.send(embed=embed)
# Special case handling because Nintendo feels like
@ -146,45 +153,46 @@ class Err(Cog):
elif err in switch_game_err:
game, desc = switch_game_err[err].split(":")
embed = discord.Embed(title=err,
url=self.rickroll,
description=desc)
embed = discord.Embed(title=err, url=self.rickroll, description=desc)
embed.set_footer(text="Console: Switch")
embed.add_field(name="Game", value=game, inline=True)
await ctx.send(embed=embed)
else:
await ctx.send("Unknown Format - This is either "
"no error code or you made some mistake!")
await ctx.send(
"Unknown Format - This is either "
"no error code or you made some mistake!"
)
@commands.command(aliases=["e2h"])
async def err2hex(self, ctx, err: str):
"""Converts Nintendo Switch errors to hex
Usage: .err2hex <Error Code>"""
Usage: .err2hex <Error Code>"""
if self.switch_re.match(err):
module = int(err[0:4]) - 2000
desc = int(err[5:9])
errcode = (desc << 9) + module
await ctx.send(hex(errcode))
else:
await ctx.send("This doesn't follow the typical"
" Nintendo Switch 2XXX-XXXX format!")
await ctx.send(
"This doesn't follow the typical Nintendo Switch 2XXX-XXXX format!"
)
@commands.command(aliases=["h2e"])
async def hex2err(self, ctx, err: str):
"""Converts Nintendo Switch errors to hex
Usage: .hex2err <Hex>"""
Usage: .hex2err <Hex>"""
if err.startswith("0x"):
err = err[2:]
err = int(err, 16)
module = err & 0x1FF
desc = (err >> 9) & 0x3FFF
errcode = f'{(module + 2000):04}-{desc:04}'
errcode = f"{(module + 2000):04}-{desc:04}"
await ctx.send(errcode)
else:
await ctx.send("This doesn't look like typical hex!")
def setup(bot):
bot.add_cog(Err(bot))
async def setup(bot):
await bot.add_cog(Err(bot))

View file

@ -0,0 +1,40 @@
import json
import os
import discord
from discord.ext import commands
from discord.ext.commands import Cog
from robocop_ng.helpers.checks import check_if_collaborator
from robocop_ng.helpers.invites import add_invite
class Invites(Cog):
def __init__(self, bot):
self.bot = bot
@commands.command()
@commands.guild_only()
@commands.check(check_if_collaborator)
async def invite(self, ctx):
welcome_channel = self.bot.get_channel(self.bot.config.welcome_channel)
author = ctx.message.author
reason = f"Created by {str(author)} ({author.id})"
invite = await welcome_channel.create_invite(
max_age=0, max_uses=1, temporary=True, unique=True, reason=reason
)
add_invite(self.bot, invite.id, invite.url, 1, invite.code)
await ctx.message.add_reaction("🆗")
try:
await ctx.author.send(f"Created single-use invite {invite.url}")
except discord.errors.Forbidden:
await ctx.send(
f"{ctx.author.mention} I could not send you the \
invite. Send me a DM so I can reply to you."
)
async def setup(bot):
await bot.add_cog(Invites(bot))

37
robocop_ng/cogs/legacy.py Normal file
View file

@ -0,0 +1,37 @@
from discord.ext import commands
from discord.ext.commands import Cog
class Legacy(Cog):
def __init__(self, bot):
self.bot = bot
@commands.command(hidden=True, aliases=["removehacker"])
async def probate(self, ctx):
"""Use .revoke <user> <role>"""
await ctx.send(
"This command was replaced with `.revoke <user> <role>`"
" on Robocop-NG, please use that instead."
)
@commands.command(hidden=True)
async def softlock(self, ctx):
"""Use .lock True"""
await ctx.send(
"This command was replaced with `.lock True`"
" on Robocop-NG, please use that instead.\n"
"Also... good luck, and sorry for taking your time. "
"Lockdown rarely means anything good."
)
@commands.command(hidden=True, aliases=["addhacker"])
async def unprobate(self, ctx):
"""Use .approve <user> <role>"""
await ctx.send(
"This command was replaced with `.approve <user> <role>`"
" on Robocop-NG, please use that instead."
)
async def setup(bot):
await bot.add_cog(Legacy(bot))

79
robocop_ng/cogs/links.py Normal file
View file

@ -0,0 +1,79 @@
import discord
from discord.ext import commands
from discord.ext.commands import Cog
class Links(Cog):
"""
Commands for easily linking to projects.
"""
def __init__(self, bot):
self.bot = bot
@commands.command(hidden=True)
async def pegaswitch(self, ctx):
"""Link to the Pegaswitch repo"""
await ctx.send("https://github.com/reswitched/pegaswitch")
@commands.command(hidden=True, aliases=["atmos"])
async def atmosphere(self, ctx):
"""Link to the Atmosphere repo"""
await ctx.send("https://github.com/atmosphere-nx/atmosphere")
@commands.command(hidden=True, aliases=["xyproblem"])
async def xy(self, ctx):
"""Link to the "What is the XY problem?" post from SE"""
await ctx.send(
"<https://meta.stackexchange.com/q/66377/285481>\n\n"
"TL;DR: It's asking about your attempted solution "
"rather than your actual problem.\n"
"It's perfectly okay to want to learn about a "
"solution, but please be clear about your intentions "
"if you're not actually trying to solve a problem."
)
@commands.command(hidden=True, aliases=["guides", "link"])
async def guide(self, ctx):
"""Link to the guides"""
await ctx.send(self.bot.config.links_guide_text)
@commands.command()
async def source(self, ctx):
"""Gives link to source code."""
await ctx.send(
f"You can find my source at {self.bot.config.source_url}. "
"Serious PRs and issues welcome!"
)
@commands.command()
async def rules(self, ctx, *, targetuser: discord.Member = None):
"""Post a link to the Rules"""
if not targetuser:
targetuser = ctx.author
await ctx.send(
f"{targetuser.mention}: A link to the rules "
f"can be found here: {self.bot.config.rules_url}"
)
@commands.command()
async def community(self, ctx, *, targetuser: discord.Member = None):
"""Post a link to the community section of the rules"""
if not targetuser:
targetuser = ctx.author
await ctx.send(
f"{targetuser.mention}: "
"https://reswitched.github.io/discord/#member-roles-breakdown"
"\n\n"
"Community role allows access to the set of channels "
"on the community category (#off-topic, "
"#homebrew-development, #switch-hacking-general etc)."
"\n\n"
"What you need to get the role is to be around, "
"be helpful and nice to people and "
"show an understanding of rules."
)
async def setup(bot):
await bot.add_cog(Links(bot))

392
robocop_ng/cogs/lists.py Normal file
View file

@ -0,0 +1,392 @@
import io
import os.path
import discord
from discord.ext import commands
from discord.ext.commands import Cog
class Lists(Cog):
"""
Manages channels that are dedicated to lists.
"""
def __init__(self, bot):
self.bot = bot
# Helpers
def check_if_target_is_staff(self, target):
return any(r.id in self.bot.config.staff_role_ids for r in target.roles)
def is_edit(self, emoji):
return str(emoji)[0] == "" or str(emoji)[0] == "📝"
def is_delete(self, emoji):
return str(emoji)[0] == "" or str(emoji)[0] == ""
def is_recycle(self, emoji):
return str(emoji)[0] == ""
def is_insert_above(self, emoji):
return str(emoji)[0] == "⤴️" or str(emoji)[0] == ""
def is_insert_below(self, emoji):
return str(emoji)[0] == "⤵️" or str(emoji)[0] == ""
def is_reaction_valid(self, reaction):
allowed_reactions = [
"",
"📝",
"",
"",
"",
"⤴️",
"",
"",
"⤵️",
]
return str(reaction.emoji)[0] in allowed_reactions
async def find_reactions(self, user_id, channel_id, limit=None):
reactions = []
channel = self.bot.get_channel(channel_id)
async for message in channel.history(limit=limit):
if len(message.reactions) == 0:
continue
for reaction in message.reactions:
users = await reaction.users().flatten()
user_ids = map(lambda user: user.id, users)
if user_id in user_ids:
reactions.append(reaction)
return reactions
def create_log_message(self, emoji, action, user, channel, reason=""):
msg = (
f"{emoji} **{action}** \n"
f"from {self.bot.escape_message(user.name)} ({user.id}), in {channel.mention}"
)
if reason != "":
msg += f":\n`{reason}`"
return msg
async def clean_up_raw_text_file_message(self, message):
embeds = message.embeds
if len(embeds) == 0:
return
fields = embeds[0].fields
for field in fields:
if field.name == "Message ID":
files_channel = self.bot.get_channel(self.bot.config.list_files_channel)
file_message = await files_channel.fetch_message(int(field.value))
await file_message.delete()
await message.edit(embed=None)
async def cache_message(self, message):
msg = {
"has_attachment": False,
"attachment_filename": "",
"attachment_data": b"",
"content": message.content,
}
if len(message.attachments) != 0:
attachment = next(
(
a
for a in message.attachments
if os.path.splitext(a.filename)[1] in [".png", ".jpg", ".jpeg"]
),
None,
)
if attachment is not None:
msg["has_attachment"] = True
msg["attachment_filename"] = attachment.filename
msg["attachment_data"] = await attachment.read()
return msg
async def send_cached_message(self, channel, message):
if message["has_attachment"] == True:
file = discord.File(
io.BytesIO(message["attachment_data"]),
filename=message["attachment_filename"],
)
await channel.send(content=message["content"], file=file)
else:
await channel.send(content=message["content"])
# Commands
@commands.command(aliases=["list"])
async def listitem(self, ctx, channel: discord.TextChannel, number: int):
"""Link to a specific list item."""
if number <= 0:
await ctx.send(f"Number must be greater than 0.")
return
if channel.id not in self.bot.config.list_channels:
await ctx.send(f"{channel.mention} is not a list channel.")
return
counter = 0
async for message in channel.history(limit=None, oldest_first=True):
if message.content.strip():
counter += 1
if counter == number:
embed = discord.Embed(
title=f"Item #{number} in #{channel.name}",
description=message.content,
url=message.jump_url,
)
await ctx.send(content="", embed=embed)
return
await ctx.send(f"Unable to find item #{number} in {channel.mention}.")
# Listeners
@Cog.listener()
async def on_raw_reaction_add(self, payload):
await self.bot.wait_until_ready()
# We only care about reactions in Rules, and Support FAQ
if payload.channel_id not in self.bot.config.list_channels:
return
channel = self.bot.get_channel(payload.channel_id)
message = await channel.fetch_message(payload.message_id)
member = channel.guild.get_member(payload.user_id)
user = self.bot.get_user(payload.user_id)
reaction = next(
(
reaction
for reaction in message.reactions
if str(reaction.emoji) == str(payload.emoji)
),
None,
)
if reaction is None:
return
# Only staff can add reactions in these channels.
if not self.check_if_target_is_staff(member):
await reaction.remove(user)
return
# Reactions are only allowed on messages from the bot.
if not message.author.bot:
await reaction.remove(user)
return
# Only certain reactions are allowed.
if not self.is_reaction_valid(reaction):
await reaction.remove(user)
return
# Remove all other reactions from user in this channel.
for r in await self.find_reactions(payload.user_id, payload.channel_id):
if r.message.id != message.id or (
r.message.id == message.id and str(r.emoji) != str(reaction.emoji)
):
await r.remove(user)
# When editing we want to provide the user a copy of the raw text.
if self.is_edit(reaction.emoji) and self.bot.config.list_files_channel != 0:
files_channel = self.bot.get_channel(self.bot.config.list_files_channel)
file = discord.File(
io.BytesIO(message.content.encode("utf-8")),
filename=f"{message.id}.txt",
)
file_message = await files_channel.send(file=file)
embed = discord.Embed(
title="Click here to get the raw text to modify.",
url=f"{file_message.attachments[0].url}?",
)
embed.add_field(name="Message ID", value=file_message.id, inline=False)
await message.edit(embed=embed)
@Cog.listener()
async def on_raw_reaction_remove(self, payload):
await self.bot.wait_until_ready()
# We only care about reactions in Rules, and Support FAQ
if payload.channel_id not in self.bot.config.list_channels:
return
channel = self.bot.get_channel(payload.channel_id)
message = await channel.fetch_message(payload.message_id)
# Reaction was removed from a message we don"t care about.
if not message.author.bot:
return
# We want to remove the embed we added.
if self.is_edit(payload.emoji) and self.bot.config.list_files_channel != 0:
await self.clean_up_raw_text_file_message(message)
@Cog.listener()
async def on_message(self, message):
await self.bot.wait_until_ready()
# We only care about messages in Rules, and Support FAQ
if message.channel.id not in self.bot.config.list_channels:
return
# We don"t care about messages from bots.
if message.author.bot:
return
# Only staff can modify lists.
if not self.check_if_target_is_staff(message.author):
await message.delete()
return
log_channel = self.bot.get_channel(self.bot.config.log_channel)
channel = message.channel
content = message.content
user = message.author
attachment_filename = None
attachment_data = None
if len(message.attachments) != 0:
# Lists will only reupload the first image.
attachment = next(
(
a
for a in message.attachments
if os.path.splitext(a.filename)[1] in [".png", ".jpg", ".jpeg"]
),
None,
)
if attachment is not None:
attachment_filename = attachment.filename
attachment_data = await attachment.read()
await message.delete()
reactions = await self.find_reactions(user.id, channel.id)
# Add to the end of the list if there is no reactions or somehow more
# than one.
if len(reactions) != 1:
if attachment_filename is not None and attachment_data is not None:
file = discord.File(
io.BytesIO(attachment_data), filename=attachment_filename
)
await channel.send(content=content, file=file)
else:
await channel.send(content)
for reaction in reactions:
await reaction.remove(user)
await log_channel.send(
self.create_log_message("💬", "List item added:", user, channel)
)
return
targeted_reaction = reactions[0]
targeted_message = targeted_reaction.message
if self.is_edit(targeted_reaction):
if self.bot.config.list_files_channel != 0:
await self.clean_up_raw_text_file_message(targeted_message)
await targeted_message.edit(content=content)
await targeted_reaction.remove(user)
await log_channel.send(
self.create_log_message("📝", "List item edited:", user, channel)
)
elif self.is_delete(targeted_reaction):
await targeted_message.delete()
await log_channel.send(
self.create_log_message(
"", "List item deleted:", user, channel, content
)
)
elif self.is_recycle(targeted_reaction):
messages = [await self.cache_message(targeted_message)]
for message in await channel.history(
limit=None, after=targeted_message, oldest_first=True
).flatten():
messages.append(await self.cache_message(message))
await channel.purge(limit=len(messages) + 1, bulk=True)
for message in messages:
await self.send_cached_message(channel, message)
await log_channel.send(
self.create_log_message(
"", "List item recycled:", user, channel, content
)
)
elif self.is_insert_above(targeted_reaction):
messages = [await self.cache_message(targeted_message)]
for message in await channel.history(
limit=None, after=targeted_message, oldest_first=True
).flatten():
messages.append(await self.cache_message(message))
await channel.purge(limit=len(messages) + 1, bulk=True)
if attachment_filename is not None and attachment_data is not None:
file = discord.File(
io.BytesIO(attachment_data), filename=attachment_filename
)
await channel.send(content=content, file=file)
else:
await channel.send(content)
for message in messages:
await self.send_cached_message(channel, message)
await log_channel.send(
self.create_log_message("💬", "List item added:", user, channel)
)
elif self.is_insert_below(targeted_reaction):
await targeted_reaction.remove(user)
messages = []
for message in await channel.history(
limit=None, after=targeted_message, oldest_first=True
).flatten():
messages.append(await self.cache_message(message))
await channel.purge(limit=len(messages), bulk=True)
if attachment_filename is not None and attachment_data is not None:
file = discord.File(
io.BytesIO(attachment_data), filename=attachment_filename
)
await channel.send(content=content, file=file)
else:
await channel.send(content)
for message in messages:
await self.send_cached_message(channel, message)
await log_channel.send(
self.create_log_message("💬", "List item added:", user, channel)
)
async def setup(bot):
await bot.add_cog(Lists(bot))

View file

@ -1,47 +1,49 @@
import discord
from discord.ext import commands
from discord.ext.commands import Cog
import config
import discord
from helpers.checks import check_if_staff
from robocop_ng.helpers.checks import check_if_staff
class Lockdown(Cog):
def __init__(self, bot):
self.bot = bot
async def set_sendmessage(self, channel: discord.TextChannel,
role, allow_send, issuer):
async def set_sendmessage(
self, channel: discord.TextChannel, role, allow_send, issuer
):
try:
roleobj = channel.guild.get_role(role)
overrides = channel.overwrites_for(roleobj)
overrides.send_messages = allow_send
await channel.set_permissions(roleobj,
overwrite=overrides,
reason=str(issuer))
await channel.set_permissions(
roleobj, overwrite=overrides, reason=str(issuer)
)
except:
pass
async def unlock_for_staff(self, channel: discord.TextChannel, issuer):
for role in config.staff_role_ids:
for role in self.bot.config.staff_role_ids:
await self.set_sendmessage(channel, role, True, issuer)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command()
async def lock(self, ctx, channel: discord.TextChannel = None,
soft: bool = False):
async def lock(self, ctx, channel: discord.TextChannel = None, soft: bool = False):
"""Prevents people from speaking in a channel, staff only.
Defaults to current channel."""
if not channel:
channel = ctx.channel
log_channel = self.bot.get_channel(config.modlog_channel)
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
for key, lockdown_conf in config.lockdown_configs.items():
roles = None
for key, lockdown_conf in self.bot.config.lockdown_configs.items():
if channel.id in lockdown_conf["channels"]:
roles = lockdown_conf["roles"]
if roles is None:
roles = config.lockdown_configs["default"]["roles"]
roles = self.bot.config.lockdown_configs["default"]["roles"]
for role in roles:
await self.set_sendmessage(channel, role, False, ctx.author)
@ -50,14 +52,20 @@ class Lockdown(Cog):
public_msg = "🔒 Channel locked down. "
if not soft:
public_msg += "Only staff members may speak. "\
"Do not bring the topic to other channels or risk "\
"disciplinary actions."
public_msg += (
"Only staff members may speak. "
"Do not bring the topic to other channels or risk "
"disciplinary actions."
)
await ctx.send(public_msg)
safe_name = await commands.clean_content().convert(ctx, str(ctx.author))
msg = f"🔒 **Lockdown**: {ctx.channel.mention} by {ctx.author.mention} "\
f"| {safe_name}"
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(ctx.author)
)
msg = (
f"🔒 **Lockdown**: {ctx.channel.mention} by {ctx.author.mention} "
f"| {safe_name}"
)
await log_channel.send(msg)
@commands.guild_only()
@ -67,26 +75,31 @@ class Lockdown(Cog):
"""Unlocks speaking in current channel, staff only."""
if not channel:
channel = ctx.channel
log_channel = self.bot.get_channel(config.modlog_channel)
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
for key, lockdown_conf in config.lockdown_configs.items():
roles = None
for key, lockdown_conf in self.bot.config.lockdown_configs.items():
if channel.id in lockdown_conf["channels"]:
roles = lockdown_conf["roles"]
if roles is None:
roles = config.lockdown_configs["default"]["roles"]
roles = self.bot.config.lockdown_configs["default"]["roles"]
await self.unlock_for_staff(channel, ctx.author)
for role in roles:
await self.set_sendmessage(channel, role, True, ctx.author)
safe_name = await commands.clean_content().convert(ctx, str(ctx.author))
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(ctx.author)
)
await ctx.send("🔓 Channel unlocked.")
msg = f"🔓 **Unlock**: {ctx.channel.mention} by {ctx.author.mention} "\
f"| {safe_name}"
msg = (
f"🔓 **Unlock**: {ctx.channel.mention} by {ctx.author.mention} "
f"| {safe_name}"
)
await log_channel.send(msg)
def setup(bot):
bot.add_cog(Lockdown(bot))
async def setup(bot):
await bot.add_cog(Lockdown(bot))

View file

@ -0,0 +1,710 @@
import logging
import re
from typing import Optional
import aiohttp
from discord import Colour, Embed, Message, Attachment
from discord.ext import commands
from discord.ext.commands import Cog, Context, BucketType
from robocop_ng.helpers.checks import check_if_staff
from robocop_ng.helpers.disabled_ids import (
add_disabled_app_id,
is_app_id_valid,
remove_disabled_app_id,
get_disabled_ids,
is_app_id_disabled,
is_build_id_valid,
add_disabled_build_id,
remove_disabled_build_id,
is_build_id_disabled,
is_ro_section_disabled,
is_ro_section_valid,
add_disabled_ro_section,
remove_disabled_ro_section,
remove_disable_id,
)
from robocop_ng.helpers.disabled_paths import (
is_path_disabled,
get_disabled_paths,
add_disabled_path,
remove_disabled_path,
)
from robocop_ng.helpers.ryujinx_log_analyser import LogAnalyser
logging.basicConfig(
format="%(asctime)s (%(levelname)s) %(message)s (Line %(lineno)d)",
level=logging.INFO,
)
class LogFileReader(Cog):
@staticmethod
def is_valid_log_name(attachment: Attachment) -> tuple[bool, bool]:
filename = attachment.filename
ryujinx_log_file_regex = re.compile(r"^Ryujinx_.*\.log$")
log_file = re.compile(r"^.*\.log|.*\.txt$")
is_ryujinx_log_file = re.match(ryujinx_log_file_regex, filename) is not None
is_log_file = re.match(log_file, filename) is not None
return is_log_file, is_ryujinx_log_file
def __init__(self, bot):
self.bot = bot
self.bot_log_allowed_channels = self.bot.config.bot_log_allowed_channels
self.disallowed_named_roles = ["pirate"]
self.ryujinx_blue = Colour(0x4A90E2)
self.uploaded_log_info = []
self.disallowed_roles = [
self.bot.config.named_roles[x] for x in self.disallowed_named_roles
]
@staticmethod
async def download_file(log_url):
async with aiohttp.ClientSession() as session:
# Grabs first and last few bytes of log file to prevent abuse from large files
headers = {"Range": "bytes=0-60000, -6000"}
async with session.get(log_url, headers=headers) as response:
return await response.text("UTF-8")
@staticmethod
def is_log_valid(log_file: str) -> bool:
app_info = LogAnalyser.get_app_info(log_file)
is_homebrew = LogAnalyser.is_homebrew(log_file)
if app_info is None or is_homebrew:
return True
game_name, app_id, another_app_id, build_ids, main_ro_section = app_info
if (
game_name is None
or app_id is None
or another_app_id is None
or build_ids is None
or main_ro_section is None
):
return False
return app_id == another_app_id
def is_game_blocked(self, log_file: str) -> bool:
app_info = LogAnalyser.get_app_info(log_file)
if app_info is None:
return False
game_name, app_id, another_app_id, build_ids, main_ro_section = app_info
if is_app_id_disabled(self.bot, app_id) or is_app_id_disabled(
self.bot, another_app_id
):
return True
for bid in build_ids:
if is_build_id_disabled(self.bot, bid):
return True
return is_ro_section_disabled(self.bot, main_ro_section)
def contains_blocked_paths(self, log_file: str) -> Optional[str]:
filepaths = LogAnalyser.get_filepaths(log_file)
if filepaths is None:
return None
for filepath in filepaths:
if is_path_disabled(self.bot, filepath):
return filepath
return None
async def blocked_game_action(self, message: Message) -> Embed:
warn_command = self.bot.get_command("warn")
if warn_command is not None:
warn_message = await message.reply(
".warn This log contains a blocked game."
)
warn_context = await self.bot.get_context(warn_message)
await warn_context.invoke(
warn_command,
target=None,
reason="This log contains a blocked game.",
)
else:
logging.error(
f"Couldn't find 'warn' command. Unable to warn {message.author} for uploading a log of a blocked game."
)
pirate_role = message.guild.get_role(self.bot.config.named_roles["pirate"])
await message.author.add_roles(pirate_role)
embed = Embed(
title="⛔ Blocked game detected ⛔",
colour=Colour(0xFF0000),
description="This log contains a blocked game and has been removed.\n"
"The user has been warned and the pirate role has been applied.",
)
embed.set_footer(text=f"Log uploaded by @{message.author.name}")
await message.delete()
return embed
async def blocked_path_action(self, message: Message, blocked_path: str) -> Embed:
warn_command = self.bot.get_command("warn")
if warn_command is not None:
warn_message = await message.reply(
".warn This log contains blocked content in paths."
)
warn_context = await self.bot.get_context(warn_message)
await warn_context.invoke(
warn_command,
target=None,
reason=f"This log contains blocked content in paths: '{blocked_path}'",
)
else:
logging.error(
f"Couldn't find 'warn' command. Unable to warn {message.author} for uploading a log "
f"containing a blocked content in paths."
)
pirate_role = message.guild.get_role(self.bot.config.named_roles["pirate"])
await message.author.add_roles(pirate_role)
embed = Embed(
title="⛔ Blocked content in path detected ⛔",
colour=Colour(0xFF0000),
description="This log contains paths containing blocked content and has been removed.\n"
"The user has been warned and the pirate role has been applied.",
)
embed.set_footer(text=f"Log uploaded by @{message.author.name}")
await message.delete()
return embed
def format_analysed_log(self, author_name: str, analysed_log):
cleaned_game_name = re.sub(
r"\s\[(64|32)-bit\]$", "", analysed_log["game_info"]["game_name"]
)
analysed_log["game_info"]["game_name"] = cleaned_game_name
hardware_info = " | ".join(
(
f"**CPU:** {analysed_log['hardware_info']['cpu']}",
f"**GPU:** {analysed_log['hardware_info']['gpu']}",
f"**RAM:** {analysed_log['hardware_info']['ram']}",
f"**OS:** {analysed_log['hardware_info']['os']}",
)
)
system_settings_info = "\n".join(
(
f"**Audio Backend:** `{analysed_log['settings']['audio_backend']}`",
f"**Console Mode:** `{analysed_log['settings']['docked']}`",
f"**PPTC Cache:** `{analysed_log['settings']['pptc']}`",
f"**Shader Cache:** `{analysed_log['settings']['shader_cache']}`",
f"**V-Sync:** `{analysed_log['settings']['vsync']}`",
f"**Hypervisor:** `{analysed_log['settings']['hypervisor']}`",
)
)
graphics_settings_info = "\n".join(
(
f"**Graphics Backend:** `{analysed_log['settings']['graphics_backend']}`",
f"**Resolution:** `{analysed_log['settings']['resolution_scale']}`",
f"**Anisotropic Filtering:** `{analysed_log['settings']['anisotropic_filtering']}`",
f"**Aspect Ratio:** `{analysed_log['settings']['aspect_ratio']}`",
f"**Texture Recompression:** `{analysed_log['settings']['texture_recompression']}`",
)
)
ryujinx_info = " | ".join(
(
f"**Version:** {analysed_log['emu_info']['ryu_version']}",
f"**Firmware:** {analysed_log['emu_info']['ryu_firmware']}",
)
)
log_embed = Embed(title=f"{cleaned_game_name}", colour=self.ryujinx_blue)
log_embed.set_footer(text=f"Log uploaded by {author_name}")
log_embed.add_field(
name="General Info",
value=" | ".join((ryujinx_info, hardware_info)),
inline=False,
)
log_embed.add_field(
name="System Settings",
value=system_settings_info,
inline=True,
)
log_embed.add_field(
name="Graphics Settings",
value=graphics_settings_info,
inline=True,
)
if (
cleaned_game_name == "Unknown"
and analysed_log["game_info"]["errors"] == "No errors found in log"
):
log_embed.add_field(
name="Empty Log",
value=f"""The log file appears to be empty. To get a proper log, follow these steps:
1) In Logging settings, ensure `Enable Logging to File` is checked.
2) Ensure the following default logs are enabled: `Info`, `Warning`, `Error`, `Guest` and `Stub`.
3) Start a game up.
4) Play until your issue occurs.
5) Upload the latest log file which is larger than 3KB.""",
inline=False,
)
if (
cleaned_game_name == "Unknown"
and analysed_log["game_info"]["errors"] != "No errors found in log"
):
log_embed.add_field(
name="Latest Error Snippet",
value=analysed_log["game_info"]["errors"],
inline=False,
)
log_embed.add_field(
name="No Game Boot Detected",
value=f"""No game boot has been detected in log file. To get a proper log, follow these steps:
1) In Logging settings, ensure `Enable Logging to File` is checked.
2) Ensure the following default logs are enabled: `Info`, `Warning`, `Error`, `Guest` and `Stub`.
3) Start a game up.
4) Play until your issue occurs.
5) Upload the latest log file which is larger than 3KB.""",
inline=False,
)
else:
log_embed.add_field(
name="Latest Error Snippet",
value=analysed_log["game_info"]["errors"],
inline=False,
)
log_embed.add_field(
name="Mods", value=analysed_log["game_info"]["mods"], inline=False
)
log_embed.add_field(
name="Cheats", value=analysed_log["game_info"]["cheats"], inline=False
)
log_embed.add_field(
name="Notes",
value=analysed_log["game_info"]["notes"],
inline=False,
)
return log_embed
async def log_file_read(self, message):
attached_log = message.attachments[0]
author_name = f"@{message.author.name}"
log_file = await self.download_file(attached_log.url)
if self.is_game_blocked(log_file):
return await self.blocked_game_action(message)
blocked_path = self.contains_blocked_paths(log_file)
if blocked_path:
return await self.blocked_path_action(message, blocked_path)
for role in message.author.roles:
if role.id in self.disallowed_roles:
embed = Embed(
colour=Colour(0xFF0000),
description="I'm not allowed to analyse this log.",
)
embed.set_footer(text=f"Log uploaded by {author_name}")
return embed
if not self.is_log_valid(log_file):
embed = Embed(
title="⚠️ Modified log detected ⚠️",
colour=Colour(0xFCFC00),
description="This log contains manually modified information and won't be analysed.",
)
embed.set_footer(text=f"Log uploaded by {author_name}")
return embed
try:
analyser = LogAnalyser(log_file)
except ValueError:
return Embed(
colour=self.ryujinx_blue,
description="This log file appears to be invalid. Please make sure to upload a Ryujinx log file.",
)
is_channel_allowed = False
for allowed_channel_id in self.bot.config.bot_log_allowed_channels.values():
if message.channel.id == allowed_channel_id:
is_channel_allowed = True
break
return self.format_analysed_log(
author_name,
analyser.analyse_discord(
is_channel_allowed,
self.bot.config.bot_log_allowed_channels["pr-testing"],
),
)
@commands.check(check_if_staff)
@commands.command(
aliases=["disallow_log_id", "forbid_log_id", "block_id", "blockid"]
)
async def disable_log_id(
self, ctx: Context, disable_id: str, block_id_type: str, *, block_id: str
):
match block_id_type.lower():
case "app" | "app_id" | "appid" | "tid" | "title_id":
if not is_app_id_valid(block_id):
return await ctx.send("The specified app id is invalid.")
if add_disabled_app_id(self.bot, disable_id, block_id):
return await ctx.send(
f"Application id '{block_id}' is now blocked!"
)
else:
return await ctx.send(
f"Application id '{block_id}' is already blocked."
)
case "build" | "build_id" | "bid":
if not is_build_id_valid(block_id):
return await ctx.send("The specified build id is invalid.")
if add_disabled_build_id(self.bot, disable_id, block_id):
return await ctx.send(f"Build id '{block_id}' is now blocked!")
else:
return await ctx.send(f"Build id '{block_id}' is already blocked.")
case "ro_section" | "rosection":
ro_section_snippet = block_id.strip("`").splitlines()
ro_section_snippet = [
line for line in ro_section_snippet if len(line.strip()) > 0
]
ro_section_info_regex = re.search(
r"PrintRoSectionInfo: main:", ro_section_snippet[0]
)
if ro_section_info_regex is None:
ro_section_snippet.insert(0, "PrintRoSectionInfo: main:")
ro_section = LogAnalyser.get_main_ro_section(
"\n".join(ro_section_snippet)
)
if ro_section is not None and is_ro_section_valid(ro_section):
if add_disabled_ro_section(self.bot, disable_id, ro_section):
return await ctx.send(
f"The specified read-only section for '{disable_id}' is now blocked."
)
else:
return await ctx.send(
f"The specified read-only section for '{disable_id}' is already blocked."
)
case _:
return await ctx.send(
"The specified id type is invalid. Valid id types are: ['app_id', 'build_id', 'ro_section']"
)
@commands.check(check_if_staff)
@commands.command(
aliases=[
"allow_log_id",
"unblock_log_id",
"unblock_id",
"allow_id",
"unblockid",
]
)
async def enable_log_id(self, ctx: Context, disable_id: str, block_id_type="all"):
match block_id_type.lower():
case "all":
if remove_disable_id(self.bot, disable_id):
return await ctx.send(
f"All ids for '{disable_id}' are now unblocked!"
)
else:
return await ctx.send(f"No blocked ids for '{disable_id}' found.")
case "app" | "app_id" | "appid" | "tid" | "title_id":
if remove_disabled_app_id(self.bot, disable_id):
return await ctx.send(
f"Application id for '{disable_id}' is now unblocked!"
)
else:
return await ctx.send(
f"No blocked application id for '{disable_id}' found."
)
case "build" | "build_id" | "bid":
if remove_disabled_build_id(self.bot, disable_id):
return await ctx.send(
f"Build id for '{disable_id}' is now unblocked!"
)
else:
return await ctx.send(f"No blocked build id '{disable_id}' found.")
case "ro_section" | "rosection":
if remove_disabled_ro_section(self.bot, disable_id):
return await ctx.send(
f"Read-only section for '{disable_id}' is now unblocked!"
)
else:
return await ctx.send(
f"No blocked read-only section for '{disable_id}' found."
)
case _:
return await ctx.send(
"The specified id type is invalid. Valid id types are: ['all', 'app_id', 'build_id', 'ro_section']"
)
@commands.check(check_if_staff)
@commands.command(
aliases=[
"disabled_ids",
"blocked_ids",
"listblockedids",
"list_blocked_log_ids",
"list_blocked_ids",
]
)
async def list_disabled_ids(self, ctx: Context):
disabled_ids = get_disabled_ids(self.bot)
id_types = {"app_id": "AppID", "build_id": "BID", "ro_section": "RoSection"}
message = "**Blocking analysis of the following IDs:**\n"
for name, entry in disabled_ids.items():
message += f"- {name}:\n"
for id_type, title in id_types.items():
if len(entry[id_type]) > 0:
if id_type != "ro_section":
message += f" - __{title}__: {entry[id_type]}\n"
else:
message += f" - __{title}__\n"
message += "\n"
return await ctx.send(message)
@commands.check(check_if_staff)
@commands.command(
aliases=[
"get_blocked_ro_section",
"disabled_ro_section",
"blocked_ro_section",
"list_disabled_ro_section",
"list_blocked_ro_section",
]
)
async def get_disabled_ro_section(self, ctx: Context, disable_id: str):
disabled_ids = get_disabled_ids(self.bot)
disable_id = disable_id.lower()
if (
disable_id in disabled_ids.keys()
and len(disabled_ids[disable_id]["ro_section"]) > 0
):
message = f"**Blocked read-only section for '{disable_id}'**:\n"
message += "```\n"
for key, content in disabled_ids[disable_id]["ro_section"].items():
match key:
case "module":
message += f"Module: {content}\n"
case "sdk_libraries":
message += f"SDK Libraries: \n"
for entry in content:
message += f" SDK {entry}\n"
message += "\n"
message += "```"
return await ctx.send(message)
else:
return await ctx.send(f"No read-only section blocked for '{disable_id}'.")
@commands.check(check_if_staff)
@commands.command(
aliases=["disallow_path", "forbid_path", "block_path", "blockpath"]
)
async def disable_path(self, ctx: Context, block_path: str):
if add_disabled_path(self.bot, block_path):
return await ctx.send(f"Path content `{block_path}` is now blocked!")
else:
return await ctx.send(f"Path content `{block_path}` is already blocked.")
@commands.check(check_if_staff)
@commands.command(
aliases=[
"allow_path",
"unblock_path",
"unblockpath",
]
)
async def enable_path(self, ctx: Context, block_path: str):
if remove_disabled_path(self.bot, block_path):
return await ctx.send(f"Path content `{block_path}` is now unblocked!")
else:
return await ctx.send(f"No blocked path content '{block_path}' found.")
@commands.check(check_if_staff)
@commands.command(
aliases=[
"disabled_paths",
"blocked_paths",
"listdisabledpaths",
"listblockedpaths",
"list_blocked_paths",
]
)
async def list_disabled_paths(self, ctx: Context):
messages = []
disabled_paths = get_disabled_paths(self.bot)
message = (
"**Blocking analysis of logs containing the following content in paths:**\n"
)
for entry in disabled_paths:
if len(message) >= 1500:
messages.append(message)
message = f"- `{entry}`\n"
else:
message += f"- `{entry}`\n"
if message not in messages:
# Add the last message as well
messages.append(message)
for msg in messages:
await ctx.send(msg)
async def analyse_log_message(self, message: Message, attachment_index=0):
author_id = message.author.id
author_mention = message.author.mention
filename = message.attachments[attachment_index].filename
filesize = message.attachments[attachment_index].size
# Any message over 2000 chars is uploaded as message.txt, so this is accounted for
log_file_link = message.jump_url
uploaded_logs_exist = [
True for elem in self.uploaded_log_info if filename in elem.values()
]
if not any(uploaded_logs_exist):
reply_message = await message.channel.send(
"Log detected, parsing...", reference=message
)
try:
embed = await self.log_file_read(message)
if "Ryujinx_" in filename:
self.uploaded_log_info.append(
{
"filename": filename,
"file_size": filesize,
"link": log_file_link,
"author": author_id,
}
)
# Avoid duplicate log file analysis, at least temporarily; keep track of the last few filenames of uploaded logs
# this should help support channels not be flooded with too many log files
# fmt: off
self.uploaded_log_info = self.uploaded_log_info[-5:]
# fmt: on
return await reply_message.edit(content=None, embed=embed)
except UnicodeDecodeError as error:
await reply_message.edit(
content=author_mention,
embed=Embed(
description="This log file appears to be invalid. Please re-check and re-upload your log file.",
colour=self.ryujinx_blue,
),
)
logging.warning(error)
except Exception as error:
await reply_message.edit(
content=f"Error: Couldn't parse log; parser threw `{type(error).__name__}` exception."
)
logging.warning(error)
else:
duplicate_log_file = next(
(
elem
for elem in self.uploaded_log_info
if elem["filename"] == filename
and elem["file_size"] == filesize
and elem["author"] == author_id
),
None,
)
await message.channel.send(
content=author_mention,
embed=Embed(
description=f"The log file `{filename}` appears to be a duplicate [already uploaded here]({duplicate_log_file['link']}). Please upload a more recent file.",
colour=self.ryujinx_blue,
),
)
@commands.cooldown(3, 30, BucketType.channel)
@commands.command(
aliases=["analyselog", "analyse_log", "analyze", "analyzelog", "analyze_log"]
)
async def analyse(self, ctx: Context, attachment_number=1):
await ctx.message.delete()
if ctx.message.reference is not None:
message = await ctx.fetch_message(ctx.message.reference.message_id)
if len(message.attachments) >= attachment_number:
attachment = message.attachments[attachment_number - 1]
is_log_file, _ = self.is_valid_log_name(attachment)
if is_log_file:
return await self.analyse_log_message(
message, attachment_number - 1
)
else:
return await ctx.send(
f"The attached log file '{attachment.filename}' is not valid.",
reference=ctx.message.reference,
)
return await ctx.send(
"Please use `.analyse` as a reply to a message with an attached log file."
)
@Cog.listener()
async def on_message(self, message: Message):
await self.bot.wait_until_ready()
if message.author.bot:
return
for attachment in message.attachments:
is_log_file, is_ryujinx_log_file = self.is_valid_log_name(attachment)
if is_log_file and not is_ryujinx_log_file:
attached_log = message.attachments[0]
log_file = await self.download_file(attached_log.url)
# Large files show a header value when not downloaded completely
# this regex makes sure that the log text to read starts from the first timestamp, ignoring headers
log_file_header_regex = re.compile(
r"\d{2}:\d{2}:\d{2}\.\d{3}.*", re.DOTALL
)
log_file_match = re.search(log_file_header_regex, log_file)
if log_file_match:
log_file = log_file_match.group(0)
if self.is_game_blocked(log_file):
return await message.channel.send(
content=None, embed=await self.blocked_game_action(message)
)
blocked_path = self.contains_blocked_paths(log_file)
if blocked_path:
return await message.channel.send(
content=None,
embed=await self.blocked_path_action(message, blocked_path),
)
elif (
is_log_file
and is_ryujinx_log_file
and message.channel.id in self.bot_log_allowed_channels.values()
):
return await self.analyse_log_message(
message, message.attachments.index(attachment)
)
elif (
is_log_file
and is_ryujinx_log_file
and message.channel.id not in self.bot_log_allowed_channels.values()
):
return await message.author.send(
content=message.author.mention,
embed=Embed(
description="\n".join(
(
f"Please upload Ryujinx log files to the correct location:\n",
f'<#{self.bot.config.bot_log_allowed_channels["windows-support"]}>: Windows help and troubleshooting',
f'<#{self.bot.config.bot_log_allowed_channels["linux-support"]}>: Linux help and troubleshooting',
f'<#{self.bot.config.bot_log_allowed_channels["macos-support"]}>: macOS help and troubleshooting',
f'<#{self.bot.config.bot_log_allowed_channels["patreon-support"]}>: Help and troubleshooting for Patreon subscribers',
f'<#{self.bot.config.bot_log_allowed_channels["development"]}>: Ryujinx development discussion',
f'<#{self.bot.config.bot_log_allowed_channels["pr-testing"]}>: Discussion of in-progress pull request builds',
)
),
colour=self.ryujinx_blue,
),
)
async def setup(bot):
await bot.add_cog(LogFileReader(bot))

View file

@ -1,10 +1,14 @@
import json
import os
import re
import discord
from discord.ext.commands import Cog
import json
import re
import config
from helpers.restrictions import get_user_restrictions
from helpers.checks import check_if_staff
from robocop_ng.helpers.checks import check_if_staff
from robocop_ng.helpers.invites import get_invites, set_invites
from robocop_ng.helpers.restrictions import get_user_restrictions
from robocop_ng.helpers.userlogs import get_userlog
class Logs(Cog):
@ -14,32 +18,30 @@ class Logs(Cog):
def __init__(self, bot):
self.bot = bot
self.invite_re = re.compile(r"((discord\.gg|discordapp\.com/"
r"+invite)/+[a-zA-Z0-9-]+)",
re.IGNORECASE)
self.invite_re = re.compile(
r"((discord\.gg|discordapp\.com/" r"+invite)/+[a-zA-Z0-9-]+)", re.IGNORECASE
)
self.name_re = re.compile(r"[a-zA-Z0-9].*")
self.clean_re = re.compile(r'[^a-zA-Z0-9_ ]+', re.UNICODE)
self.clean_re = re.compile(r"[^a-zA-Z0-9_ ]+", re.UNICODE)
# All lower case, no spaces, nothing non-alphanumeric
self.susp_words = ["sx", "tx", "reinx", # piracy-enabling cfws
"tinfoil", "dz", # title managers
"goldleaf", "lithium", # title managers
"xci"] # "backup" format
susp_hellgex = "|".join([r"\W*".join(list(word)) for
word in self.susp_words])
susp_hellgex = "|".join(
[r"\W*".join(list(word)) for word in self.bot.config.suspect_words]
)
self.susp_hellgex = re.compile(susp_hellgex, re.IGNORECASE)
self.ok_words = []
@Cog.listener()
async def on_member_join(self, member):
await self.bot.wait_until_ready()
log_channel = self.bot.get_channel(config.log_channel)
if member.guild.id not in self.bot.config.guild_whitelist:
return
log_channel = self.bot.get_channel(self.bot.config.log_channel)
# We use this a lot, might as well get it once
escaped_name = self.bot.escape_message(member)
# Attempt to correlate the user joining with an invite
with open("data/invites.json", "r") as f:
invites = json.load(f)
invites = get_invites(self.bot)
real_invites = await member.guild.invites()
@ -50,7 +52,7 @@ class Logs(Cog):
"uses": 0,
"url": invite.url,
"max_uses": invite.max_uses,
"code": invite.code
"code": invite.code,
}
probable_invites_used = []
@ -73,12 +75,11 @@ class Logs(Cog):
del invites[id]
# Save invites data.
with open("data/invites.json", "w") as f:
f.write(json.dumps(invites))
set_invites(self.bot, invites)
# Prepare the invite correlation message
if len(probable_invites_used) == 1:
invite_used = probable_invites_used[0]["url"]
invite_used = probable_invites_used[0]["code"]
elif len(probable_invites_used) == 0:
invite_used = "Unknown"
else:
@ -87,54 +88,64 @@ class Logs(Cog):
# Check if user account is older than 15 minutes
age = member.joined_at - member.created_at
if age < config.min_age:
if age < self.bot.config.min_age:
try:
await member.send(f"Your account is too new to "
f"join {member.guild.name}."
" Please try again later.")
await member.send(
f"Your account is too new to "
f"join {member.guild.name}."
" Please try again later."
)
sent = True
except discord.errors.Forbidden:
sent = False
await member.kick(reason="Too new")
msg = f"🚨 **Account too new**: {member.mention} | "\
f"{escaped_name}\n"\
f"🗓 __Creation__: {member.created_at}\n"\
f"🕓 Account age: {age}\n"\
f"✉ Joined with: {invite_used}\n"\
f"🏷 __User ID__: {member.id}"
msg = (
f"🚨 **Account too new**: {member.mention} | "
f"{escaped_name}\n"
f"🗓 __Creation__: {member.created_at}\n"
f"🕓 Account age: {age}\n"
f"✉ Joined with: {invite_used}\n"
f"🏷 __User ID__: {member.id}"
)
if not sent:
msg += "\nThe user has disabled direct messages,"\
" so the reason was not sent."
msg += (
"\nThe user has disabled direct messages, "
"so the reason was not sent."
)
await log_channel.send(msg)
return
msg = f"✅ **Join**: {member.mention} | "\
f"{escaped_name}\n"\
f"🗓 __Creation__: {member.created_at}\n"\
f"🕓 Account age: {age}\n"\
f"✉ Joined with: {invite_used}\n"\
f"🏷 __User ID__: {member.id}"
msg = (
f"✅ **Join**: {member.mention} | "
f"{escaped_name}\n"
f"🗓 __Creation__: {member.created_at}\n"
f"🕓 Account age: {age}\n"
f"✉ Joined with: {invite_used}\n"
f"🏷 __User ID__: {member.id}"
)
# Handles user restrictions
# Basically, gives back muted role to users that leave with it.
rsts = get_user_restrictions(member.id)
rsts = get_user_restrictions(self.bot, member.id)
roles = [discord.utils.get(member.guild.roles, id=rst) for rst in rsts]
await member.add_roles(*roles)
# Real hell zone.
with open("data/userlog.json", "r") as f:
warns = json.load(f)
warns = get_userlog(self.bot)
try:
if len(warns[str(member.id)]["warns"]) == 0:
await log_channel.send(msg)
else:
embed = discord.Embed(color=discord.Color.dark_red(),
title=f"Warns for {escaped_name}")
embed.set_thumbnail(url=member.avatar_url)
embed = discord.Embed(
color=discord.Color.dark_red(), title=f"Warns for {escaped_name}"
)
embed.set_thumbnail(url=str(member.display_avatar))
for idx, warn in enumerate(warns[str(member.id)]["warns"]):
embed.add_field(name=f"{idx + 1}: {warn['timestamp']}",
value=f"Issuer: {warn['issuer_name']}"
f"\nReason: {warn['reason']}")
embed.add_field(
name=f"{idx + 1}: {warn['timestamp']}",
value=f"Issuer: {warn['issuer_name']}"
f"\nReason: {warn['reason']}",
)
await log_channel.send(msg, embed=embed)
except KeyError: # if the user is not in the file
await log_channel.send(msg)
@ -147,34 +158,41 @@ class Logs(Cog):
return
alert = False
cleancont = self.clean_re.sub('', message.content).lower()
msg = f"🚨 Suspicious message by {message.author.mention} "\
f"({message.author.id}):"
cleancont = self.clean_re.sub("", message.content).lower()
msg = (
f"🚨 Suspicious message by {message.author.mention} "
f"({message.author.id}):"
)
invites = self.invite_re.findall(message.content)
for invite in invites:
msg += f"\n- Has invite: https://{invite[0]}"
alert = True
for susp_word in self.susp_words:
if susp_word in cleancont and\
not any(ok_word in cleancont for ok_word in self.ok_words):
for susp_word in self.bot.config.suspect_words:
if susp_word in cleancont and not any(
ok_word in cleancont
for ok_word in self.bot.config.suspect_ignored_words
):
msg += f"\n- Contains suspicious word: `{susp_word}`"
alert = True
if alert:
msg += f"\n\nJump: <{message.jump_url}>"
spy_channel = self.bot.get_channel(config.spylog_channel)
spy_channel = self.bot.get_channel(self.bot.config.spylog_channel)
# Bad Code :tm:, blame retr0id
message_clean = message.content.replace("*", "").replace("_", "")
regd = self.susp_hellgex.sub(lambda w: "**{}**".format(w.group(0)),
message_clean)
regd = self.susp_hellgex.sub(
lambda w: "**{}**".format(w.group(0)), message_clean
)
# Show a message embed
embed = discord.Embed(description=regd)
embed.set_author(name=message.author.display_name,
icon_url=message.author.avatar_url)
embed.set_author(
name=message.author.display_name,
icon_url=str(message.author.display_avatar),
)
await spy_channel.send(msg, embed=embed)
@ -183,16 +201,17 @@ class Logs(Cog):
if compliant:
return
msg = f"R11 violating name by {message.author.mention} "\
f"({message.author.id})."
msg = (
f"R11 violating name by {message.author.mention} " f"({message.author.id})."
)
spy_channel = self.bot.get_channel(config.spylog_channel)
spy_channel = self.bot.get_channel(self.bot.config.spylog_channel)
await spy_channel.send(msg)
@Cog.listener()
async def on_message(self, message):
await self.bot.wait_until_ready()
if message.channel.id not in config.spy_channels:
if message.channel.id not in self.bot.config.spy_channels:
return
await self.do_spy(message)
@ -200,7 +219,7 @@ class Logs(Cog):
@Cog.listener()
async def on_message_edit(self, before, after):
await self.bot.wait_until_ready()
if after.channel.id not in config.spy_channels or after.author.bot:
if after.channel.id not in self.bot.config.spy_channels or after.author.bot:
return
# If content is the same, just skip over it
@ -210,11 +229,18 @@ class Logs(Cog):
await self.do_spy(after)
log_channel = self.bot.get_channel(config.log_channel)
msg = "📝 **Message edit**: \n"\
f"from {self.bot.escape_message(after.author.name)} "\
f"({after.author.id}), in {after.channel.mention}:\n"\
f"`{before.clean_content}` → `{after.clean_content}`"
# U+200D is a Zero Width Joiner stopping backticks from breaking the formatting
before_content = before.clean_content.replace("`", "`\u200d")
after_content = after.clean_content.replace("`", "`\u200d")
log_channel = self.bot.get_channel(self.bot.config.log_channel)
msg = (
"📝 **Message edit**: \n"
f"from {self.bot.escape_message(after.author.name)} "
f"({after.author.id}), in {after.channel.mention}:\n"
f"```{before_content}``` → ```{after_content}```"
)
# If resulting message is too long, upload to hastebin
if len(msg) > 2000:
@ -226,14 +252,16 @@ class Logs(Cog):
@Cog.listener()
async def on_message_delete(self, message):
await self.bot.wait_until_ready()
if message.channel.id not in config.spy_channels or message.author.bot:
if message.channel.id not in self.bot.config.spy_channels or message.author.bot:
return
log_channel = self.bot.get_channel(config.log_channel)
msg = "🗑️ **Message delete**: \n"\
f"from {self.bot.escape_message(message.author.name)} "\
f"({message.author.id}), in {message.channel.mention}:\n"\
f"`{message.clean_content}`"
log_channel = self.bot.get_channel(self.bot.config.log_channel)
msg = (
"🗑️ **Message delete**: \n"
f"from {self.bot.escape_message(message.author.name)} "
f"({message.author.id}), in {message.channel.mention}:\n"
f"`{message.clean_content}`"
)
# If resulting message is too long, upload to hastebin
if len(msg) > 2000:
@ -245,28 +273,46 @@ class Logs(Cog):
@Cog.listener()
async def on_member_remove(self, member):
await self.bot.wait_until_ready()
log_channel = self.bot.get_channel(config.log_channel)
msg = f"⬅️ **Leave**: {member.mention} | "\
f"{self.bot.escape_message(member)}\n"\
f"🏷 __User ID__: {member.id}"
if member.guild.id not in self.bot.config.guild_whitelist:
return
log_channel = self.bot.get_channel(self.bot.config.log_channel)
msg = (
f"⬅️ **Leave**: {member.mention} | "
f"{self.bot.escape_message(member)}\n"
f"🏷 __User ID__: {member.id}"
)
await log_channel.send(msg)
@Cog.listener()
async def on_member_ban(self, guild, member):
await self.bot.wait_until_ready()
log_channel = self.bot.get_channel(config.modlog_channel)
msg = f"⛔ **Ban**: {member.mention} | "\
f"{self.bot.escape_message(member)}\n"\
f"🏷 __User ID__: {member.id}"
if guild.id not in self.bot.config.guild_whitelist:
return
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
msg = (
f"⛔ **Ban**: {member.mention} | "
f"{self.bot.escape_message(member)}\n"
f"🏷 __User ID__: {member.id}"
)
await log_channel.send(msg)
@Cog.listener()
async def on_member_unban(self, guild, user):
await self.bot.wait_until_ready()
log_channel = self.bot.get_channel(config.modlog_channel)
msg = f"⚠️ **Unban**: {user.mention} | "\
f"{self.bot.escape_message(user)}\n"\
f"🏷 __User ID__: {user.id}"
if guild.id not in self.bot.config.guild_whitelist:
return
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
msg = (
f"⚠️ **Unban**: {user.mention} | "
f"{self.bot.escape_message(user)}\n"
f"🏷 __User ID__: {user.id}"
)
# if user.id in self.bot.timebans:
# msg += "\nTimeban removed."
# self.bot.timebans.pop(user.id)
@ -281,8 +327,12 @@ class Logs(Cog):
@Cog.listener()
async def on_member_update(self, member_before, member_after):
await self.bot.wait_until_ready()
if member_after.guild.id not in self.bot.config.guild_whitelist:
return
msg = ""
log_channel = self.bot.get_channel(config.log_channel)
log_channel = self.bot.get_channel(self.bot.config.log_channel)
if member_before.roles != member_after.roles:
# role removal
role_removal = []
@ -310,9 +360,11 @@ class Logs(Cog):
msg += ", ".join(roles)
if member_before.name != member_after.name:
msg += "\n📝 __Username change__: "\
f"{self.bot.escape_message(member_before)}"\
f"{self.bot.escape_message(member_after)}"
msg += (
"\n📝 __Username change__: "
f"{self.bot.escape_message(member_before)}"
f"{self.bot.escape_message(member_after)}"
)
if member_before.nick != member_after.nick:
if not member_before.nick:
msg += "\n🏷 __Nickname addition__"
@ -320,13 +372,17 @@ class Logs(Cog):
msg += "\n🏷 __Nickname removal__"
else:
msg += "\n🏷 __Nickname change__"
msg += f": {self.bot.escape_message(member_before.nick)}"\
f"{self.bot.escape_message(member_after.nick)}"
msg += (
f": {self.bot.escape_message(member_before.nick)}"
f"{self.bot.escape_message(member_after.nick)}"
)
if msg:
msg = f" **Member update**: {member_after.mention} | "\
f"{self.bot.escape_message(member_after)}{msg}"
msg = (
f" **Member update**: {member_after.mention} | "
f"{self.bot.escape_message(member_after)}{msg}"
)
await log_channel.send(msg)
def setup(bot):
bot.add_cog(Logs(bot))
async def setup(bot):
await bot.add_cog(Logs(bot))

179
robocop_ng/cogs/macro.py Normal file
View file

@ -0,0 +1,179 @@
import discord
from discord.ext import commands
from discord.ext.commands import Cog, Context, BucketType, Greedy
from robocop_ng.helpers.checks import check_if_staff, check_if_staff_or_dm
from robocop_ng.helpers.macros import (
get_macro,
add_macro,
edit_macro,
remove_macro,
get_macros_dict,
add_aliases,
remove_aliases,
clear_aliases,
)
class Macro(Cog):
def __init__(self, bot):
self.bot = bot
@commands.cooldown(3, 30, BucketType.user)
@commands.command(aliases=["m"])
async def macro(self, ctx: Context, key: str, targets: Greedy[discord.User] = None):
if ctx.guild:
await ctx.message.delete()
if len(key) > 0:
text = get_macro(self.bot, key)
if text is not None:
if targets is not None:
await ctx.send(
f"{', '.join(target.mention for target in targets)}:\n{text}"
)
else:
if ctx.message.reference is not None:
await ctx.send(
text, reference=ctx.message.reference, mention_author=True
)
else:
await ctx.send(text)
else:
await ctx.send(
f"{ctx.author.mention}: The macro '{key}' doesn't exist."
)
@commands.check(check_if_staff)
@commands.command(name="macroadd", aliases=["ma", "addmacro", "add_macro"])
async def add_macro(self, ctx: Context, key: str, *, text: str):
if add_macro(self.bot, key, text):
await ctx.send(f"Macro '{key}' added!")
else:
await ctx.send(f"Error: Macro '{key}' already exists.")
@commands.check(check_if_staff)
@commands.command(name="aliasadd", aliases=["addalias", "add_alias"])
async def add_alias_macro(self, ctx: Context, existing_key: str, *new_keys: str):
if len(new_keys) == 0:
await ctx.send("Error: You need to add at least one alias.")
else:
if add_aliases(self.bot, existing_key, list(new_keys)):
await ctx.send(
f"Added {len(new_keys)} aliases to macro '{existing_key}'!"
)
else:
await ctx.send(f"Error: No new and unique aliases found.")
@commands.check(check_if_staff)
@commands.command(name="macroedit", aliases=["me", "editmacro", "edit_macro"])
async def edit_macro(self, ctx: Context, key: str, *, text: str):
if edit_macro(self.bot, key, text):
await ctx.send(f"Macro '{key}' edited!")
else:
await ctx.send(f"Error: Macro '{key}' not found.")
@commands.check(check_if_staff)
@commands.command(
name="aliasremove",
aliases=[
"aliasdelete",
"delalias",
"aliasdel",
"removealias",
"remove_alias",
"delete_alias",
],
)
async def remove_alias_macro(
self, ctx: Context, existing_key: str, *remove_keys: str
):
if len(remove_keys) == 0:
await ctx.send("Error: You need to remove at least one alias.")
else:
if remove_aliases(self.bot, existing_key, list(remove_keys)):
await ctx.send(
f"Removed {len(remove_keys)} aliases from macro '{existing_key}'!"
)
else:
await ctx.send(
f"Error: None of the specified aliases were found for macro '{existing_key}'."
)
@commands.check(check_if_staff)
@commands.command(
name="macroremove",
aliases=[
"mr",
"md",
"removemacro",
"remove_macro",
"macrodel",
"delmacro",
"delete_macro",
],
)
async def remove_macro(self, ctx: Context, key: str):
if remove_macro(self.bot, key):
await ctx.send(f"Macro '{key}' removed!")
else:
await ctx.send(f"Error: Macro '{key}' not found.")
@commands.check(check_if_staff)
@commands.command(name="aliasclear", aliases=["clearalias", "clear_alias"])
async def clear_alias_macro(self, ctx: Context, existing_key: str):
if clear_aliases(self.bot, existing_key):
await ctx.send(f"Removed all aliases of macro '{existing_key}'!")
else:
await ctx.send(f"Error: No aliases found for macro '{existing_key}'.")
@commands.check(check_if_staff_or_dm)
@commands.cooldown(3, 30, BucketType.channel)
@commands.command(name="macros", aliases=["ml", "listmacros", "list_macros"])
async def list_macros(self, ctx: Context, macros_only=False):
macros = get_macros_dict(self.bot)
if len(macros["macros"]) > 0:
messages = []
macros_formatted = []
for key in sorted(macros["macros"].keys()):
message = f"- {key}"
if not macros_only and key in macros["aliases"]:
for alias in macros["aliases"][key]:
message += f", {alias}"
macros_formatted.append(message)
message = f"📝 **Macros**:\n"
for macro in macros_formatted:
if len(message) >= 1500:
messages.append(message)
message = f"{macro}\n"
else:
message += f"{macro}\n"
if message not in messages:
# Add the last message as well
messages.append(message)
for msg in messages:
await ctx.send(msg)
else:
await ctx.send("Couldn't find any macros.")
@commands.check(check_if_staff_or_dm)
@commands.cooldown(3, 30, BucketType.channel)
@commands.command(name="aliases", aliases=["listaliases", "list_aliases"])
async def list_aliases(self, ctx: Context, existing_key: str):
macros = get_macros_dict(self.bot)
existing_key = existing_key.lower()
if existing_key in macros["aliases"].keys():
message = f"📝 **Aliases for '{existing_key}'**:\n"
for alias in sorted(macros["aliases"][existing_key]):
message += f"- {alias}\n"
await ctx.send(message)
else:
await ctx.send(f"Couldn't find any aliases for macro '{existing_key}'.")
async def setup(bot):
await bot.add_cog(Macro(bot))

246
robocop_ng/cogs/meme.py Normal file
View file

@ -0,0 +1,246 @@
import datetime
import math
import platform
import random
from typing import Optional
import discord
from discord.ext import commands
from discord.ext.commands import Cog
from robocop_ng.helpers.checks import check_if_staff_or_ot
class Meme(Cog):
"""
Meme commands.
"""
def __init__(self, bot):
self.bot = bot
def c_to_f(self, c):
"""this is where we take memes too far"""
return math.floor(9.0 / 5.0 * c + 32)
def c_to_k(self, c):
"""this is where we take memes REALLY far"""
return math.floor(c + 273.15)
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True, name="warm")
async def warm_member(self, ctx, user: Optional[discord.Member]):
"""Warms a user :3"""
if user is None and ctx.message.reference is None:
celsius = random.randint(15, 20)
fahrenheit = self.c_to_f(celsius)
kelvin = self.c_to_k(celsius)
await ctx.send(
f"{ctx.author.mention} tries to warm themself."
f" User is now {celsius}°C "
f"({fahrenheit}°F, {kelvin}K).\n"
"You might have more success warming someone else :3"
)
else:
if user is None:
user = (
await ctx.channel.fetch_message(ctx.message.reference.message_id)
).author
celsius = random.randint(15, 100)
fahrenheit = self.c_to_f(celsius)
kelvin = self.c_to_k(celsius)
await ctx.send(
f"{user.mention} warmed."
f" User is now {celsius}°C "
f"({fahrenheit}°F, {kelvin}K)."
)
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True)
async def lick(self, ctx, user: Optional[discord.Member]):
"""licks a user :?"""
if user is None and ctx.message.reference is None:
await ctx.send(f"{ctx.author.mention} licks their lips! 👅")
else:
if user is None:
user = (
await ctx.channel.fetch_message(ctx.message.reference.message_id)
).author
await ctx.send(f"{user.mention} has been licked! 👅")
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True, name="chill", aliases=["cold"])
async def chill_member(self, ctx, user: Optional[discord.Member]):
"""Chills a user >:3"""
if user is None and ctx.message.reference is None:
celsius = random.randint(-75, 10)
fahrenheit = self.c_to_f(celsius)
kelvin = self.c_to_k(celsius)
await ctx.send(
f"{ctx.author.mention} chills themself."
f" User is now {celsius}°C "
f"({fahrenheit}°F, {kelvin}K).\n"
"🧊 Don't be so hard on yourself. 😔"
)
else:
if user is None:
user = (
await ctx.channel.fetch_message(ctx.message.reference.message_id)
).author
celsius = random.randint(-50, 15)
fahrenheit = self.c_to_f(celsius)
kelvin = self.c_to_k(celsius)
await ctx.send(
f"{user.mention} chilled."
f" User is now {celsius}°C "
f"({fahrenheit}°F, {kelvin}K)."
)
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True, aliases=["thank", "reswitchedgold"])
async def gild(self, ctx, user: Optional[discord.Member]):
"""Gives a star to a user"""
if user is None and ctx.message.reference is None:
await ctx.send(f"No stars for you, {ctx.author.mention}!")
else:
if user is None:
user = (
await ctx.channel.fetch_message(ctx.message.reference.message_id)
).author
await ctx.send(f"{user.mention} gets a :star:, yay!")
@commands.check(check_if_staff_or_ot)
@commands.command(
hidden=True, aliases=["reswitchedsilver", "silv3r", "reswitchedsilv3r"]
)
async def silver(self, ctx, user: Optional[discord.Member]):
"""Gives a user ReSwitched Silver™"""
if user is None and ctx.message.reference is None:
await ctx.send(f"{ctx.author.mention}, you can't reward yourself.")
else:
if user is None:
user = (
await ctx.channel.fetch_message(ctx.message.reference.message_id)
).author
embed = discord.Embed(
title="ReSwitched Silver™!",
description=f"Here's your ReSwitched Silver™," f"{user.mention}!",
)
embed.set_image(
url="https://cdn.discordapp.com/emojis/548623626916724747.png?v=1"
)
await ctx.send(embed=embed)
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True)
async def btwiuse(self, ctx):
"""btw i use arch"""
uname = platform.uname()
await ctx.send(
f"BTW I use {platform.python_implementation()} "
f"{platform.python_version()} on {uname.system} "
f"{uname.release}"
)
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True)
async def yahaha(self, ctx):
"""secret command"""
await ctx.send(f"🍂 you found me 🍂")
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True)
async def blackalabi(self, ctx):
"""secret command"""
await ctx.send("https://elixi.re/i/discord.png")
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True)
async def peng(self, ctx):
"""heck tomger"""
await ctx.send(f"🐧")
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True, aliases=["outstanding"])
async def outstandingmove(self, ctx):
"""Posts the outstanding move meme"""
await ctx.send(
"https://cdn.discordapp.com/attachments"
"/371047036348268545/528413677007929344"
"/image0-5.jpg"
)
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True)
async def bones(self, ctx):
await ctx.send("https://cdn.discordapp.com/emojis/443501365843591169.png?v=1")
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True)
async def headpat(self, ctx):
await ctx.send("https://cdn.discordapp.com/emojis/465650811909701642.png?v=1")
@commands.check(check_if_staff_or_ot)
@commands.command(
hidden=True, aliases=["when", "etawhen", "emunand", "emummc", "thermosphere"]
)
async def eta(self, ctx):
await ctx.send("June 15.")
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True, name="bam")
async def bam_member(self, ctx, target: Optional[discord.Member]):
"""Bams a user owo"""
if target is None and ctx.message.reference is None:
await ctx.reply("https://tenor.com/view/bonk-gif-26414884")
else:
if ctx.message.reference is not None:
target = (
await ctx.channel.fetch_message(ctx.message.reference.message_id)
).author
if target == ctx.author:
if target.id == 181627658520625152:
return await ctx.send(
"https://cdn.discordapp.com/attachments/286612533757083648/403080855402315796/rehedge.PNG"
)
return await ctx.send("hedgeberg#7337 is ̶n͢ow b̕&̡.̷ 👍̡")
elif target == self.bot.user:
return await ctx.send(
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
)
await ctx.send(f"{safe_name} is ̶n͢ow b̕&̡.̷ 👍̡")
@commands.command(hidden=True)
async def memebercount(self, ctx):
"""Checks memeber count, as requested by dvdfreitag"""
await ctx.send("There's like, uhhhhh a bunch")
@commands.command(hidden=True)
async def frolics(self, ctx):
"""test"""
await ctx.send("https://www.youtube.com/watch?v=VmarNEsjpDI")
@commands.command(
hidden=True,
aliases=[
"yotld",
"yold",
"yoltd",
"yearoflinuxondesktop",
"yearoflinuxonthedesktop",
],
)
async def yearoflinux(self, ctx):
"""Shows the year of Linux on the desktop"""
await ctx.send(
f"{datetime.datetime.now().year} is the year of Linux on the Desktop"
)
async def setup(bot):
await bot.add_cog(Meme(bot))

933
robocop_ng/cogs/mod.py Normal file
View file

@ -0,0 +1,933 @@
import io
from typing import Optional
import discord
from discord.ext import commands
from discord.ext.commands import Cog, Context
from robocop_ng.helpers.checks import check_if_staff, check_if_bot_manager
from robocop_ng.helpers.restrictions import add_restriction, remove_restriction
from robocop_ng.helpers.userlogs import userlog
class Mod(Cog):
def __init__(self, bot):
self.bot = bot
def check_if_target_is_staff(self, target):
return any(r.id in self.bot.config.staff_role_ids for r in target.roles)
@commands.guild_only()
@commands.check(check_if_bot_manager)
@commands.command()
async def setguildicon(self, ctx, url):
"""Changes guild icon, bot manager only."""
img_bytes = await self.bot.aiogetbytes(url)
await ctx.guild.edit(icon=img_bytes, reason=str(ctx.author))
await ctx.send(f"Done!")
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
log_msg = (
f"✏️ **Guild Icon Update**: {ctx.author} changed the guild icon."
f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
)
img_filename = url.split("/")[-1].split("#")[0] # hacky
img_file = discord.File(io.BytesIO(img_bytes), filename=img_filename)
await log_channel.send(log_msg, file=img_file)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command()
async def mute(self, ctx, target: Optional[discord.Member], *, reason: str = ""):
"""Mutes a user, staff only."""
if target is None and ctx.message.reference is None:
return await ctx.send(
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
)
else:
if ctx.message.reference is not None:
if target is not None:
reason = str(target) + reason
target = (
await ctx.channel.fetch_message(ctx.message.reference.message_id)
).author
# Hedge-proofing the code
if target == ctx.author:
return await ctx.send("You can't do mod actions on yourself.")
elif target == self.bot.user:
return await ctx.send(
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
)
elif self.check_if_target_is_staff(target):
return await ctx.send(
"I can't mute this user as they're a member of staff."
)
userlog(self.bot, target.id, ctx.author, reason, "mutes", target.name)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
)
dm_message = f"You were muted!"
if reason:
dm_message += f' The given reason is: "{reason}".'
try:
await target.send(dm_message)
except discord.errors.Forbidden:
# Prevents kick issues in cases where user blocked bot
# or has DMs disabled
pass
mute_role = ctx.guild.get_role(self.bot.config.mute_role)
await target.add_roles(mute_role, reason=str(ctx.author))
chan_message = (
f"🔇 **Muted**: {str(ctx.author)} muted "
f"{target.mention} | {safe_name}\n"
f"🏷 __User ID__: {target.id}\n"
)
if reason:
chan_message += f'✏️ __Reason__: "{reason}"'
else:
chan_message += (
"Please add an explanation below. In the future, "
"it is recommended to use `.mute <user> [reason]`"
" as the reason is automatically sent to the user."
)
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
await log_channel.send(chan_message)
await ctx.send(f"{target.mention} can no longer speak.")
add_restriction(self.bot, target.id, self.bot.config.mute_role)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command()
async def unmute(self, ctx, target: discord.Member):
"""Unmutes a user, staff only."""
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
)
mute_role = ctx.guild.get_role(self.bot.config.mute_role)
await target.remove_roles(mute_role, reason=str(ctx.author))
chan_message = (
f"🔈 **Unmuted**: {str(ctx.author)} unmuted "
f"{target.mention} | {safe_name}\n"
f"🏷 __User ID__: {target.id}\n"
)
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
await log_channel.send(chan_message)
await ctx.send(f"{target.mention} can now speak again.")
remove_restriction(self.bot, target.id, self.bot.config.mute_role)
@commands.guild_only()
@commands.bot_has_permissions(kick_members=True)
@commands.check(check_if_staff)
@commands.command()
async def kick(self, ctx, target: Optional[discord.Member], *, reason: str = ""):
"""Kicks a user, staff only."""
if target is None and ctx.message.reference is None:
return await ctx.send(
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
)
else:
if ctx.message.reference is not None:
if target is not None:
reason = str(target) + reason
target = (
await ctx.channel.fetch_message(ctx.message.reference.message_id)
).author
# Hedge-proofing the code
if target == ctx.author:
return await ctx.send("You can't do mod actions on yourself.")
elif target == self.bot.user:
return await ctx.send(
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
)
elif self.check_if_target_is_staff(target):
return await ctx.send(
"I can't kick this user as they're a member of staff."
)
userlog(self.bot, target.id, ctx.author, reason, "kicks", target.name)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
)
dm_message = f"You were kicked from {ctx.guild.name}."
if reason:
dm_message += f' The given reason is: "{reason}".'
dm_message += (
"\n\nYou are able to rejoin the server,"
" but please be sure to behave when participating again."
)
try:
await target.send(dm_message)
except discord.errors.Forbidden:
# Prevents kick issues in cases where user blocked bot
# or has DMs disabled
pass
await target.kick(reason=f"{ctx.author}, reason: {reason}")
chan_message = (
f"👢 **Kick**: {str(ctx.author)} kicked "
f"{target.mention} | {safe_name}\n"
f"🏷 __User ID__: {target.id}\n"
)
if reason:
chan_message += f'✏️ __Reason__: "{reason}"'
else:
chan_message += (
"Please add an explanation below. In the future"
", it is recommended to use "
"`.kick <user> [reason]`"
" as the reason is automatically sent to the user."
)
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
await log_channel.send(chan_message)
await ctx.send(f"👢 {safe_name}, 👍.")
@commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@commands.check(check_if_staff)
@commands.command(aliases=["yeet"])
async def ban(self, ctx, target: Optional[discord.Member], *, reason: str = ""):
"""Bans a user, staff only."""
if target is None and ctx.message.reference is None:
return await ctx.send(
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
)
else:
if ctx.message.reference is not None:
if target is not None:
reason = str(target) + reason
target = (
await ctx.channel.fetch_message(ctx.message.reference.message_id)
).author
# Hedge-proofing the code
if target == ctx.author:
if target.id == 181627658520625152:
return await ctx.send(
"https://cdn.discordapp.com/attachments/286612533757083648/403080855402315796/rehedge.PNG"
)
return await ctx.send("hedgeberg#7337 is now b&. 👍")
elif target == self.bot.user:
return await ctx.send(
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
)
elif self.check_if_target_is_staff(target):
return await ctx.send("I can't ban this user as they're a member of staff.")
userlog(self.bot, target.id, ctx.author, reason, "bans", target.name)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
)
dm_message = f"You were banned from {ctx.guild.name}."
if reason:
dm_message += f' The given reason is: "{reason}".'
dm_message += "\n\nThis ban does not expire."
try:
await target.send(dm_message)
except discord.errors.Forbidden:
# Prevents ban issues in cases where user blocked bot
# or has DMs disabled
pass
await target.ban(
reason=f"{ctx.author}, reason: {reason}", delete_message_days=0
)
chan_message = (
f"⛔ **Ban**: {str(ctx.author)} banned "
f"{target.mention} | {safe_name}\n"
f"🏷 __User ID__: {target.id}\n"
)
if reason:
chan_message += f'✏️ __Reason__: "{reason}"'
else:
chan_message += (
"Please add an explanation below. In the future"
", it is recommended to use `.ban <user> [reason]`"
" as the reason is automatically sent to the user."
)
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
await log_channel.send(chan_message)
await ctx.send(f"{safe_name} is now b&. 👍")
@commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@commands.check(check_if_staff)
@commands.command()
async def bandel(
self, ctx, day_count: int, target: Optional[discord.Member], *, reason: str = ""
):
"""Bans a user for a given number of days, staff only."""
if target is None and ctx.message.reference is None:
return await ctx.send(
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
)
else:
if ctx.message.reference is not None:
if target is not None:
reason = str(target) + reason
target = (
await ctx.channel.fetch_message(ctx.message.reference.message_id)
).author
# Hedge-proofing the code
if target == ctx.author:
if target.id == 181627658520625152:
return await ctx.send(
"https://cdn.discordapp.com/attachments/286612533757083648/403080855402315796/rehedge.PNG"
)
return await ctx.send("hedgeberg#7337 is now b&. 👍")
elif target == self.bot.user:
return await ctx.send(
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
)
elif self.check_if_target_is_staff(target):
return await ctx.send("I can't ban this user as they're a member of staff.")
if day_count < 0 or day_count > 7:
return await ctx.send(
"Message delete day count needs to be between 0 and 7 days."
)
userlog(self.bot, target.id, ctx.author, reason, "bans", target.name)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
)
dm_message = f"You were banned from {ctx.guild.name}."
if reason:
dm_message += f' The given reason is: "{reason}".'
dm_message += "\n\nThis ban does not expire."
try:
await target.send(dm_message)
except discord.errors.Forbidden:
# Prevents ban issues in cases where user blocked bot
# or has DMs disabled
pass
await target.ban(
reason=f"{ctx.author}, days of message deletions: {day_count}, reason: {reason}",
delete_message_days=day_count,
)
chan_message = (
f"⛔ **Ban**: {str(ctx.author)} banned with {day_count} of messages deleted "
f"{target.mention} | {safe_name}\n"
f"🏷 __User ID__: {target.id}\n"
)
if reason:
chan_message += f'✏️ __Reason__: "{reason}"'
else:
chan_message += (
"Please add an explanation below. In the future"
", it is recommended to use `.bandel <daycount> <user> [reason]`"
" as the reason is automatically sent to the user."
)
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
await log_channel.send(chan_message)
await ctx.send(
f"{safe_name} is now b&, with {day_count} days of messages deleted. 👍"
)
@commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@commands.check(check_if_staff)
@commands.command(aliases=["softban"])
async def hackban(self, ctx, target: int, *, reason: str = ""):
"""Bans a user with their ID, doesn't message them, staff only."""
target_user = await self.bot.fetch_user(target)
target_member = ctx.guild.get_member(target)
# Hedge-proofing the code
if target == ctx.author.id:
return await ctx.send("You can't do mod actions on yourself.")
elif target == self.bot.user:
return await ctx.send(
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
)
elif target_member and self.check_if_target_is_staff(target_member):
return await ctx.send("I can't ban this user as they're a member of staff.")
userlog(self.bot, target, ctx.author, reason, "bans", target_user.name)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
)
await ctx.guild.ban(
target_user, reason=f"{ctx.author}, reason: {reason}", delete_message_days=0
)
chan_message = (
f"⛔ **Hackban**: {str(ctx.author)} banned "
f"{target_user.mention} | {safe_name}\n"
f"🏷 __User ID__: {target}\n"
)
if reason:
chan_message += f'✏️ __Reason__: "{reason}"'
else:
chan_message += (
"Please add an explanation below. In the future"
", it is recommended to use "
"`.hackban <user> [reason]`."
)
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
await log_channel.send(chan_message)
await ctx.send(f"{safe_name} is now b&. 👍")
@commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@commands.check(check_if_staff)
@commands.command()
async def massban(self, ctx, *, targets: str):
"""Bans users with their IDs, doesn't message them, staff only."""
targets_int = [int(target) for target in targets.strip().split(" ")]
for target in targets_int:
target_user = await self.bot.fetch_user(target)
target_member = ctx.guild.get_member(target)
# Hedge-proofing the code
if target == ctx.author.id:
await ctx.send(f"(re: {target}) You can't do mod actions on yourself.")
continue
elif target == self.bot.user:
await ctx.send(
f"(re: {target}) I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
)
continue
elif target_member and self.check_if_target_is_staff(target_member):
await ctx.send(
f"(re: {target}) I can't ban this user as they're a member of staff."
)
continue
userlog(self.bot, target, ctx.author, f"massban", "bans", target_user.name)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
)
await ctx.guild.ban(
target_user,
reason=f"{ctx.author}, reason: massban",
delete_message_days=0,
)
chan_message = (
f"⛔ **Massban**: {str(ctx.author)} banned "
f"{target_user.mention} | {safe_name}\n"
f"🏷 __User ID__: {target}\n"
"Please add an explanation below."
)
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
await log_channel.send(chan_message)
await ctx.send(f"All {len(targets_int)} users are now b&. 👍")
@commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@commands.check(check_if_staff)
@commands.command()
async def unban(self, ctx, target: int, *, reason: str = ""):
"""Unbans a user with their ID, doesn't message them, staff only."""
target_user = await self.bot.fetch_user(target)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
)
await ctx.guild.unban(target_user, reason=f"{ctx.author}, reason: {reason}")
chan_message = (
f"⚠️ **Unban**: {str(ctx.author)} unbanned "
f"{target_user.mention} | {safe_name}\n"
f"🏷 __User ID__: {target}\n"
)
if reason:
chan_message += f'✏️ __Reason__: "{reason}"'
else:
chan_message += (
"Please add an explanation below. In the future"
", it is recommended to use "
"`.unban <user id> [reason]`."
)
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
await log_channel.send(chan_message)
await ctx.send(f"{safe_name} is now unb&.")
@commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@commands.check(check_if_staff)
@commands.command()
async def silentban(self, ctx, target: discord.Member, *, reason: str = ""):
"""Bans a user, staff only."""
# Hedge-proofing the code
if target == ctx.author:
return await ctx.send("You can't do mod actions on yourself.")
elif target == self.bot.user:
return await ctx.send(
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
)
elif self.check_if_target_is_staff(target):
return await ctx.send("I can't ban this user as they're a member of staff.")
userlog(self.bot, target.id, ctx.author, reason, "bans", target.name)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
)
await target.ban(
reason=f"{ctx.author}, reason: {reason}", delete_message_days=0
)
chan_message = (
f"⛔ **Silent ban**: {str(ctx.author)} banned "
f"{target.mention} | {safe_name}\n"
f"🏷 __User ID__: {target.id}\n"
)
if reason:
chan_message += f'✏️ __Reason__: "{reason}"'
else:
chan_message += (
"Please add an explanation below. In the future"
", it is recommended to use `.ban <user> [reason]`"
" as the reason is automatically sent to the user."
)
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
await log_channel.send(chan_message)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command()
async def approve(
self, ctx, target: Optional[discord.Member], role: str = "community"
):
"""Add a role to a user (default: community), staff only."""
if role not in self.bot.config.named_roles:
return await ctx.send(
"No such role! Available roles: "
+ ",".join(self.bot.config.named_roles)
)
if target is None and ctx.message.reference is None:
return await ctx.send(
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
)
else:
if ctx.message.reference is not None:
target = (
await ctx.channel.fetch_message(ctx.message.reference.message_id)
).author
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
target_role = ctx.guild.get_role(self.bot.config.named_roles[role])
if target_role in target.roles:
return await ctx.send("Target already has this role.")
await target.add_roles(target_role, reason=str(ctx.author))
await ctx.send(f"Approved {target.mention} to `{role}` role.")
await log_channel.send(
f"✅ Approved: {str(ctx.author)} added"
f" {role} to {target.mention}"
f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command(aliases=["unapprove"])
async def revoke(
self, ctx, target: Optional[discord.Member], role: str = "community"
):
"""Remove a role from a user (default: community), staff only."""
if role not in self.bot.config.named_roles:
return await ctx.send(
"No such role! Available roles: "
+ ",".join(self.bot.config.named_roles)
)
if target is None and ctx.message.reference is None:
return await ctx.send(
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
)
else:
if ctx.message.reference is not None:
target = (
await ctx.channel.fetch_message(ctx.message.reference.message_id)
).author
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
target_role = ctx.guild.get_role(self.bot.config.named_roles[role])
if target_role not in target.roles:
return await ctx.send("Target doesn't have this role.")
await target.remove_roles(target_role, reason=str(ctx.author))
await ctx.send(f"Un-approved {target.mention} from `{role}` role.")
await log_channel.send(
f"❌ Un-approved: {str(ctx.author)} removed"
f" {role} from {target.mention}"
f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command(aliases=["clear"])
async def purge(self, ctx, limit: int, channel: discord.TextChannel = None):
"""Clears a given number of messages, staff only."""
modlog_channel = self.bot.get_channel(self.bot.config.modlog_channel)
log_channel = self.bot.get_channel(self.bot.config.log_channel)
if not channel:
channel = ctx.channel
purged_log_jump_url = ""
for deleted_message in await channel.purge(limit=limit):
msg = (
"🗑️ **Message purged**: \n"
f"from {self.bot.escape_message(deleted_message.author.name)} "
f"({deleted_message.author.id}), in {deleted_message.channel.mention}:\n"
f"`{deleted_message.clean_content}`"
)
if len(purged_log_jump_url) == 0:
purged_log_jump_url = (await log_channel.send(msg)).jump_url
else:
await log_channel.send(msg)
msg = (
f"🗑 **Purged**: {str(ctx.author)} purged {limit} "
f"messages in {channel.mention}."
f"\n🔗 __Jump__: <{purged_log_jump_url}>"
)
await modlog_channel.send(msg)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command()
async def warn(self, ctx, target: Optional[discord.Member], *, reason: str = ""):
"""Warns a user, staff only."""
if target is None and ctx.message.reference is None:
return await ctx.send(
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
)
else:
if ctx.message.reference is not None:
if target is not None:
reason = str(target) + reason
target = (
await ctx.channel.fetch_message(ctx.message.reference.message_id)
).author
# Hedge-proofing the code
if target == ctx.author:
return await ctx.send("You can't do mod actions on yourself.")
elif target == self.bot.user:
return await ctx.send(
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
)
elif self.check_if_target_is_staff(target):
return await ctx.send(
"I can't warn this user as they're a member of staff."
)
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
warn_count = userlog(
self.bot, target.id, ctx.author, reason, "warns", target.name
)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
)
chan_msg = (
f"⚠️ **Warned**: {str(ctx.author)} warned "
f"{target.mention} (warn #{warn_count}) "
f"| {safe_name}\n"
)
msg = f"You were warned on {ctx.guild.name}."
if reason:
msg += " The given reason is: " + reason
msg += (
f"\n\nPlease read the rules in {self.bot.config.rules_url}. "
f"This is warn #{warn_count}."
)
if warn_count == 2:
msg += " __The next warn will automatically kick.__"
if warn_count == 3:
msg += (
"\n\nYou were kicked because of this warning. "
"This is your final warning. "
"You can join again, but "
"**one more warn will result in a ban**."
)
chan_msg += "**This resulted in an auto-kick.**\n"
if warn_count == 4:
msg += "\n\nYou were automatically banned due to four warnings."
chan_msg += "**This resulted in an auto-ban.**\n"
try:
await target.send(msg)
except discord.errors.Forbidden:
# Prevents log issues in cases where user blocked bot
# or has DMs disabled
pass
if warn_count == 3:
await target.kick()
if warn_count >= 4: # just in case
await target.ban(reason="exceeded warn limit", delete_message_days=0)
await ctx.send(
f"{target.mention} warned. " f"User has {warn_count} warning(s)."
)
if reason:
chan_msg += f'✏️ __Reason__: "{reason}"'
else:
chan_msg += (
"Please add an explanation below. In the future"
", it is recommended to use `.warn <user> [reason]`"
" as the reason is automatically sent to the user."
)
chan_msg += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
await log_channel.send(chan_msg)
@commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@commands.check(check_if_staff)
@commands.command(aliases=["softwarn"])
async def hackwarn(self, ctx, target: int, *, reason: str = ""):
"""Warns a user with their ID, doesn't message them, staff only."""
target_user = await self.bot.fetch_user(target)
target_member = ctx.guild.get_member(target)
# Hedge-proofing the code
if target == ctx.author.id:
return await ctx.send("You can't do mod actions on yourself.")
elif target == self.bot.user:
return await ctx.send(
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
)
elif target_member and self.check_if_target_is_staff(target_member):
return await ctx.send(
"I can't warn this user as they're a member of staff."
)
warn_count = userlog(
self.bot, target, ctx.author, reason, "warns", target_user.name
)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
)
chan_msg = (
f"⚠️ **Hackwarned**: {str(ctx.author)} warned "
f"{target_user.mention} (warn #{warn_count}) | {safe_name}\n"
f"🏷 __User ID__: {target}\n"
)
if warn_count == 4:
userlog(
self.bot,
target,
ctx.author,
"exceeded warn limit",
"bans",
target_user.name,
)
chan_msg += "**This resulted in an auto-hackban.**\n"
await ctx.guild.ban(
target_user,
reason=f"{ctx.author}, reason: exceeded warn limit",
delete_message_days=0,
)
if reason:
chan_msg += f'✏️ __Reason__: "{reason}"'
else:
chan_msg += (
"Please add an explanation below. In the future"
", it is recommended to use "
"`.hackwarn <user> [reason]`."
)
chan_msg += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
await log_channel.send(chan_msg)
await ctx.send(f"{safe_name} warned. " f"User has {warn_count} warning(s).")
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command(aliases=["setnick", "nick"])
async def nickname(self, ctx, target: Optional[discord.Member], *, nick: str = ""):
"""Sets a user's nickname, staff only.
Just send .nickname <user> to wipe the nickname."""
if target is None and ctx.message.reference is None:
return await ctx.send(
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
)
else:
if ctx.message.reference is not None:
target = (
await ctx.channel.fetch_message(ctx.message.reference.message_id)
).author
try:
if nick:
await target.edit(nick=nick, reason=str(ctx.author))
else:
await target.edit(nick=None, reason=str(ctx.author))
await ctx.send("Successfully set nickname.")
except discord.errors.Forbidden:
await ctx.send(
"I don't have the permission to set that user's nickname.\n"
"User's top role may be above mine, or I may lack Manage Nicknames permission."
)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command(aliases=["echo"])
async def say(self, ctx, *, the_text: str):
"""Repeats a given text, staff only."""
await ctx.send(the_text)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command()
async def speak(self, ctx, channel: discord.TextChannel, *, the_text: str):
"""Repeats a given text in a given channel, staff only."""
await channel.send(the_text)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command(aliases=["setplaying", "setgame"])
async def playing(self, ctx, *, game: str = ""):
"""Sets the bot's currently played game name, staff only.
Just send .playing to wipe the playing state."""
if game:
await self.bot.change_presence(activity=discord.Game(name=game))
else:
await self.bot.change_presence(activity=None)
await ctx.send("Successfully set game.")
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command(aliases=["setbotnick", "botnick", "robotnick"])
async def botnickname(self, ctx, *, nick: str = ""):
"""Sets the bot's nickname, staff only.
Just send .botnickname to wipe the nickname."""
if nick:
await ctx.guild.me.edit(nick=nick, reason=str(ctx.author))
else:
await ctx.guild.me.edit(nick=None, reason=str(ctx.author))
await ctx.send("Successfully set bot nickname.")
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command()
async def move(self, ctx, channelTo: discord.TextChannel, *, limit: int):
"""Move a user to another channel, staff only.
!move {channel to move to} {number of messages}"""
# get a list of the messages
fetchedMessages = []
async for message in ctx.channel.history(limit=limit + 1):
fetchedMessages.append(message)
# delete all of those messages from the channel
for i in fetchedMessages:
await i.delete()
# invert the list and remove the last message (gets rid of the command message)
fetchedMessages = fetchedMessages[::-1]
fetchedMessages = fetchedMessages[:-1]
# Loop over the messages fetched
for message in fetchedMessages:
# if the message is embedded already
if message.embeds:
# set the embed message to the old embed object
embedMessage = message.embeds[0]
# else
else:
# Create embed message object and set content to original
embedMessage = discord.Embed(description=message.content)
avatar_url = None
if message.author.display_avatar is not None:
avatar_url = str(message.author.display_avatar)
# set the embed message author to original author
embedMessage.set_author(name=message.author, icon_url=avatar_url)
# if message has attachments add them
if message.attachments:
for i in message.attachments:
embedMessage.set_image(url=i.proxy_url)
# Send to the desired channel
await channelTo.send(embed=embedMessage)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command(aliases=["slow"])
async def slowmode(
self, ctx: Context, seconds: int, channel: Optional[discord.TextChannel] = None
):
if channel is None:
channel = ctx.channel
if seconds > 21600 or seconds < 0:
return await ctx.send("Seconds can't be above '21600' or less then '0'")
await channel.edit(
slowmode_delay=seconds, reason=f"{str(ctx.author)} set the slowmode"
)
await ctx.send(f"Set the slowmode delay in this channel to {seconds} seconds!")
async def setup(bot):
await bot.add_cog(Mod(bot))

View file

@ -1,8 +1,9 @@
import discord
from discord.ext import commands
from discord.ext.commands import Cog
from helpers.checks import check_if_staff
from helpers.userlogs import userlog
from robocop_ng.helpers.checks import check_if_staff
from robocop_ng.helpers.userlogs import userlog
class ModNote(Cog):
@ -14,8 +15,7 @@ class ModNote(Cog):
@commands.command(aliases=["addnote"])
async def note(self, ctx, target: discord.Member, *, note: str = ""):
"""Adds a note to a user, staff only."""
userlog(target.id, ctx.author, note,
"notes", target.name)
userlog(self.bot, target.id, ctx.author, note, "notes", target.name)
await ctx.send(f"{ctx.author.mention}: noted!")
@commands.guild_only()
@ -23,10 +23,9 @@ class ModNote(Cog):
@commands.command(aliases=["addnoteid"])
async def noteid(self, ctx, target: int, *, note: str = ""):
"""Adds a note to a user by userid, staff only."""
userlog(target, ctx.author, note,
"notes")
await ctx.send(f"{target.mention}: noted!")
userlog(self.bot, target, ctx.author, note, "notes")
await ctx.send(f"{ctx.author.mention}: noted!")
def setup(bot):
bot.add_cog(ModNote(bot))
async def setup(bot):
await bot.add_cog(ModNote(bot))

View file

@ -1,9 +1,10 @@
import asyncio
import discord
from discord.ext import commands
from discord.ext.commands import Cog
import config
from helpers.checks import check_if_staff
from robocop_ng.helpers.checks import check_if_staff
class ModReact(Cog):
@ -13,34 +14,41 @@ class ModReact(Cog):
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command()
async def clearreactsbyuser(self, ctx, user: discord.Member, *,
channel: discord.TextChannel = None,
limit: int = 50):
async def clearreactsbyuser(
self,
ctx,
user: discord.Member,
*,
channel: discord.TextChannel = None,
limit: int = 50,
):
"""Clears reacts from a given user in the given channel, staff only."""
log_channel = self.bot.get_channel(config.modlog_channel)
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
if not channel:
channel = ctx.channel
count = 0
async for msg in channel.history(limit=limit):
for react in msg.reactions:
if await react.users().find(lambda u: u == user):
count += 1
async for u in react.users():
await msg.remove_reaction(react, u)
msg = f"✏️ **Cleared reacts**: {ctx.author.mention} cleared "\
f"{user.mention}'s reacts from the last {limit} messages "\
f"in {channel.mention}."
async for react_user in react.users():
if react_user == user:
count += 1
await react.remove(user)
msg = (
f"✏️ **Cleared reacts**: {ctx.author.mention} cleared "
f"{user.mention}'s reacts from the last {limit} messages "
f"in {channel.mention}."
)
await ctx.channel.send(f"Cleared {count} unique reactions")
await log_channel.send(msg)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command()
async def clearallreacts(self, ctx, *,
limit: int = 50,
channel: discord.TextChannel = None):
async def clearallreacts(
self, ctx, *, limit: int = 50, channel: discord.TextChannel = None
):
"""Clears all reacts in a given channel, staff only. Use with care."""
log_channel = self.bot.get_channel(config.modlog_channel)
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
if not channel:
channel = ctx.channel
count = 0
@ -48,8 +56,10 @@ class ModReact(Cog):
if msg.reactions:
count += 1
await msg.clear_reactions()
msg = f"✏️ **Cleared reacts**: {ctx.author.mention} cleared all "\
f"reacts from the last {limit} messages in {channel.mention}."
msg = (
f"✏️ **Cleared reacts**: {ctx.author.mention} cleared all "
f"reacts from the last {limit} messages in {channel.mention}."
)
await ctx.channel.send(f"Cleared reacts from {count} messages!")
await log_channel.send(msg)
@ -58,8 +68,10 @@ class ModReact(Cog):
@commands.command()
async def clearreactsinteractive(self, ctx):
"""Clears reacts interactively, staff only. Use with care."""
msg_text = f"{ctx.author.mention}, react to the reactions you want "\
f"to remove. React to this message when you're done."
msg_text = (
f"{ctx.author.mention}, react to the reactions you want "
f"to remove. React to this message when you're done."
)
msg = await ctx.channel.send(msg_text)
tasks = []
@ -74,10 +86,11 @@ class ModReact(Cog):
else:
# remove a reaction
async def impl():
msg = await self.bot \
.get_guild(event.guild_id) \
.get_channel(event.channel_id) \
.get_message(event.message_id)
msg = (
await self.bot.get_guild(event.guild_id)
.get_channel(event.channel_id)
.get_message(event.message_id)
)
def check_emoji(r):
if event.emoji.is_custom_emoji() == r.custom_emoji:
@ -88,17 +101,17 @@ class ModReact(Cog):
return event.emoji.name == r.emoji
else:
return False
for reaction in filter(check_emoji, msg.reactions):
async for u in reaction.users():
await reaction.message.remove_reaction(reaction, u)
# schedule immediately
tasks.append(asyncio.create_task(impl()))
return False
try:
await self.bot.wait_for("raw_reaction_add",
timeout=120.0,
check=check)
await self.bot.wait_for("raw_reaction_add", timeout=120.0, check=check)
except asyncio.TimeoutError:
await msg.edit(content=f"{msg_text} Timed out.")
else:
@ -106,5 +119,5 @@ class ModReact(Cog):
await msg.edit(content=f"{msg_text} Done!")
def setup(bot):
bot.add_cog(ModReact(bot))
async def setup(bot):
await bot.add_cog(ModReact(bot))

View file

@ -0,0 +1,45 @@
from discord.ext import commands
from discord.ext.commands import Cog
from robocop_ng.helpers.checks import check_if_staff
class ModReswitched(Cog):
def __init__(self, bot):
self.bot = bot
@commands.guild_only()
@commands.command(aliases=["pingmods", "summonmods"])
async def pingmod(self, ctx):
"""Pings mods, only use when there's an emergency."""
can_ping = any(r.id in self.bot.config.pingmods_allow for r in ctx.author.roles)
if can_ping:
await ctx.send(
f"<@&{self.bot.config.pingmods_role}>: {ctx.author.mention} needs assistance."
)
else:
await ctx.send(
f"{ctx.author.mention}: You need the community role to be able to ping the entire mod team, please pick an online mod (not staff, please!), and ping them instead."
)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command(aliases=["togglemod"])
async def modtoggle(self, ctx):
"""Toggles your mod role, staff only."""
target_role = ctx.guild.get_role(self.bot.config.modtoggle_role)
if target_role in ctx.author.roles:
await ctx.author.remove_roles(
target_role, reason="Staff self-unassigned mod role"
)
await ctx.send(f"{ctx.author.mention}: Removed your mod role.")
else:
await ctx.author.add_roles(
target_role, reason="Staff self-assigned mod role"
)
await ctx.send(f"{ctx.author.mention}: Gave you mod role.")
async def setup(bot):
await bot.add_cog(ModReswitched(bot))

View file

@ -0,0 +1,188 @@
from datetime import datetime
from typing import Optional
import discord
from discord.ext import commands
from discord.ext.commands import Cog
from robocop_ng.helpers.checks import check_if_staff
from robocop_ng.helpers.restrictions import add_restriction
from robocop_ng.helpers.robocronp import add_job
from robocop_ng.helpers.userlogs import userlog
class ModTimed(Cog):
def __init__(self, bot):
self.bot = bot
def check_if_target_is_staff(self, target):
return any(r.id in self.bot.config.staff_role_ids for r in target.roles)
@commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@commands.check(check_if_staff)
@commands.command()
async def timeban(
self, ctx, target: Optional[discord.Member], duration: str, *, reason: str = ""
):
"""Bans a user for a specified amount of time, staff only."""
if target is None and ctx.message.reference is None:
return await ctx.send(
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
)
else:
if ctx.message.reference is not None:
if target is not None:
duration = str(target) + duration
target = (
await ctx.channel.fetch_message(ctx.message.reference.message_id)
).author
# Hedge-proofing the code
if target == ctx.author:
return await ctx.send("You can't do mod actions on yourself.")
elif self.check_if_target_is_staff(target):
return await ctx.send("I can't ban this user as they're a member of staff.")
expiry_timestamp = self.bot.parse_time(duration)
expiry_datetime = datetime.utcfromtimestamp(expiry_timestamp)
duration_text = self.bot.get_relative_timestamp(
time_to=expiry_datetime, include_to=True, humanized=True
)
userlog(
self.bot,
target.id,
ctx.author,
f"{reason} (Timed, until " f"{duration_text})",
"bans",
target.name,
)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
)
dm_message = f"You were banned from {ctx.guild.name}."
if reason:
dm_message += f' The given reason is: "{reason}".'
dm_message += f"\n\nThis ban will expire {duration_text}."
try:
await target.send(dm_message)
except discord.errors.Forbidden:
# Prevents ban issues in cases where user blocked bot
# or has DMs disabled
pass
await target.ban(
reason=f"{ctx.author}, reason: {reason}", delete_message_days=0
)
chan_message = (
f"⛔ **Timed Ban**: {ctx.author.mention} banned "
f"{target.mention} for {duration_text} | {safe_name}\n"
f"🏷 __User ID__: {target.id}\n"
)
if reason:
chan_message += f'✏️ __Reason__: "{reason}"'
else:
chan_message += (
"Please add an explanation below. In the future"
", it is recommended to use `.ban <user> [reason]`"
" as the reason is automatically sent to the user."
)
add_job(self.bot, "unban", target.id, {"guild": ctx.guild.id}, expiry_timestamp)
log_channel = self.bot.get_channel(self.bot.config.log_channel)
await log_channel.send(chan_message)
await ctx.send(f"{safe_name} is now b&. " f"It will expire {duration_text}. 👍")
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command()
async def timemute(
self, ctx, target: Optional[discord.Member], duration: str, *, reason: str = ""
):
"""Mutes a user for a specified amount of time, staff only."""
if target is None and ctx.message.reference is None:
return await ctx.send(
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
)
else:
if ctx.message.reference is not None:
if target is not None:
duration = str(target) + duration
target = (
await ctx.channel.fetch_message(ctx.message.reference.message_id)
).author
# Hedge-proofing the code
if target == ctx.author:
return await ctx.send("You can't do mod actions on yourself.")
elif self.check_if_target_is_staff(target):
return await ctx.send(
"I can't mute this user as they're a member of staff."
)
expiry_timestamp = self.bot.parse_time(duration)
expiry_datetime = datetime.utcfromtimestamp(expiry_timestamp)
duration_text = self.bot.get_relative_timestamp(
time_to=expiry_datetime, include_to=True, humanized=True
)
userlog(
self.bot,
target.id,
ctx.author,
f"{reason} (Timed, until " f"{duration_text})",
"mutes",
target.name,
)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
)
dm_message = f"You were muted!"
if reason:
dm_message += f' The given reason is: "{reason}".'
dm_message += f"\n\nThis mute will expire {duration_text}."
try:
await target.send(dm_message)
except discord.errors.Forbidden:
# Prevents kick issues in cases where user blocked bot
# or has DMs disabled
pass
mute_role = ctx.guild.get_role(self.bot.config.mute_role)
await target.add_roles(mute_role, reason=str(ctx.author))
chan_message = (
f"🔇 **Timed Mute**: {ctx.author.mention} muted "
f"{target.mention} for {duration_text} | {safe_name}\n"
f"🏷 __User ID__: {target.id}\n"
)
if reason:
chan_message += f'✏️ __Reason__: "{reason}"'
else:
chan_message += (
"Please add an explanation below. In the future, "
"it is recommended to use `.mute <user> [reason]`"
" as the reason is automatically sent to the user."
)
add_job(
self.bot, "unmute", target.id, {"guild": ctx.guild.id}, expiry_timestamp
)
log_channel = self.bot.get_channel(self.bot.config.log_channel)
await log_channel.send(chan_message)
await ctx.send(
f"{target.mention} can no longer speak. " f"It will expire {duration_text}."
)
add_restriction(self.bot, target.id, self.bot.config.mute_role)
async def setup(bot):
await bot.add_cog(ModTimed(bot))

View file

@ -1,25 +1,27 @@
import json
import discord
from discord.ext import commands
from discord.ext.commands import Cog
import config
import json
from helpers.checks import check_if_staff
from helpers.userlogs import get_userlog, set_userlog, userlog_event_types
from robocop_ng.helpers.checks import check_if_staff
from robocop_ng.helpers.userlogs import get_userlog, set_userlog, userlog_event_types
class ModUserlog(Cog):
def __init__(self, bot):
self.bot = bot
def get_userlog_embed_for_id(self, uid: str, name: str, own: bool = False,
event=""):
def get_userlog_embed_for_id(
self, uid: str, name: str, own: bool = False, event=""
):
own_note = " Good for you!" if own else ""
wanted_events = ["warns", "bans", "kicks", "mutes"]
if event and not isinstance(event, list):
wanted_events = [event]
embed = discord.Embed(color=discord.Color.dark_red())
embed.set_author(name=f"Userlog for {name}")
userlog = get_userlog()
userlog = get_userlog(self.bot)
if uid not in userlog:
embed.description = f"There are none!{own_note} (no entry)"
@ -30,12 +32,17 @@ class ModUserlog(Cog):
if event_type in userlog[uid] and userlog[uid][event_type]:
event_name = userlog_event_types[event_type]
for idx, event in enumerate(userlog[uid][event_type]):
issuer = "" if own else f"Issuer: {event['issuer_name']} "\
f"({event['issuer_id']})\n"
embed.add_field(name=f"{event_name} {idx + 1}: "
f"{event['timestamp']}",
value=issuer + f"Reason: {event['reason']}",
inline=False)
issuer = (
""
if own
else f"Issuer: {event['issuer_name']} "
f"({event['issuer_id']})\n"
)
embed.add_field(
name=f"{event_name} {idx + 1}: " f"{event['timestamp']}",
value=issuer + f"Reason: {event['reason']}",
inline=False,
)
if not own and "watch" in userlog[uid]:
watch_state = "" if userlog[uid]["watch"] else "NOT "
@ -47,37 +54,37 @@ class ModUserlog(Cog):
return embed
def clear_event_from_id(self, uid: str, event_type):
userlog = get_userlog()
userlog = get_userlog(self.bot)
if uid not in userlog:
return f"<@{uid}> has no {event_type}!"
event_count = len(userlog[uid][event_type])
if not event_count:
return f"<@{uid}> has no {event_type}!"
userlog[uid][event_type] = []
set_userlog(json.dumps(userlog))
set_userlog(self.bot, json.dumps(userlog))
return f"<@{uid}> no longer has any {event_type}!"
def delete_event_from_id(self, uid: str, idx: int, event_type):
userlog = get_userlog()
userlog = get_userlog(self.bot)
if uid not in userlog:
return f"<@{uid}> has no {event_type}!"
event_count = len(userlog[uid][event_type])
if not event_count:
return f"<@{uid}> has no {event_type}!"
if idx > event_count:
return "Index is higher than "\
f"count ({event_count})!"
return "Index is higher than " f"count ({event_count})!"
if idx < 1:
return "Index is below 1!"
event = userlog[uid][event_type][idx - 1]
event_name = userlog_event_types[event_type]
embed = discord.Embed(color=discord.Color.dark_red(),
title=f"{event_name} {idx} on "
f"{event['timestamp']}",
description=f"Issuer: {event['issuer_name']}\n"
f"Reason: {event['reason']}")
embed = discord.Embed(
color=discord.Color.dark_red(),
title=f"{event_name} {idx} on " f"{event['timestamp']}",
description=f"Issuer: {event['issuer_name']}\n"
f"Reason: {event['reason']}",
)
del userlog[uid][event_type][idx - 1]
set_userlog(json.dumps(userlog))
set_userlog(self.bot, json.dumps(userlog))
return embed
@commands.guild_only()
@ -85,21 +92,18 @@ class ModUserlog(Cog):
@commands.command(aliases=["events"])
async def eventtypes(self, ctx):
"""Lists the available event types, staff only."""
event_list = [f"{et} ({userlog_event_types[et]})" for et in
userlog_event_types]
event_text = ("Available events:\n``` - " +
"\n - ".join(event_list) +
"```")
event_list = [f"{et} ({userlog_event_types[et]})" for et in userlog_event_types]
event_text = "Available events:\n``` - " + "\n - ".join(event_list) + "```"
await ctx.send(event_text)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command(name="userlog",
aliases=["listwarns", "getuserlog", "listuserlog"])
@commands.command(
name="userlog", aliases=["listwarns", "getuserlog", "listuserlog"]
)
async def userlog_cmd(self, ctx, target: discord.Member, event=""):
"""Lists the userlog events for a user, staff only."""
embed = self.get_userlog_embed_for_id(str(target.id), str(target),
event=event)
embed = self.get_userlog_embed_for_id(str(target.id), str(target), event=event)
await ctx.send(embed=embed)
@commands.guild_only()
@ -107,16 +111,16 @@ class ModUserlog(Cog):
@commands.command(aliases=["listnotes", "usernotes"])
async def notes(self, ctx, target: discord.Member):
"""Lists the notes for a user, staff only."""
embed = self.get_userlog_embed_for_id(str(target.id), str(target),
event="notes")
embed = self.get_userlog_embed_for_id(
str(target.id), str(target), event="notes"
)
await ctx.send(embed=embed)
@commands.guild_only()
@commands.command(aliases=["mywarns"])
async def myuserlog(self, ctx):
"""Lists your userlog events (warns etc)."""
embed = self.get_userlog_embed_for_id(str(ctx.author.id),
str(ctx.author), True)
embed = self.get_userlog_embed_for_id(str(ctx.author.id), str(ctx.author), True)
await ctx.send(embed=embed)
@commands.guild_only()
@ -130,16 +134,20 @@ class ModUserlog(Cog):
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command(aliases=["clearwarns"])
async def clearevent(self, ctx, target: discord.Member,
event="warns"):
async def clearevent(self, ctx, target: discord.Member, event="warns"):
"""Clears all events of given type for a user, staff only."""
log_channel = self.bot.get_channel(config.modlog_channel)
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
msg = self.clear_event_from_id(str(target.id), event)
safe_name = await commands.clean_content().convert(ctx, str(target))
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
)
await ctx.send(msg)
msg = f"🗑 **Cleared {event}**: {ctx.author.mention} cleared"\
f" all {event} events of {target.mention} | "\
f"{safe_name}"
msg = (
f"🗑 **Cleared {event}**: {ctx.author.mention} cleared"
f" all {event} events of {target.mention} | "
f"{safe_name}"
f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
)
await log_channel.send(msg)
@commands.guild_only()
@ -147,29 +155,36 @@ class ModUserlog(Cog):
@commands.command(aliases=["clearwarnsid"])
async def cleareventid(self, ctx, target: int, event="warns"):
"""Clears all events of given type for a userid, staff only."""
log_channel = self.bot.get_channel(config.modlog_channel)
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
msg = self.clear_event_from_id(str(target), event)
await ctx.send(msg)
msg = f"🗑 **Cleared {event}**: {ctx.author.mention} cleared"\
f" all {event} events of <@{target}> "
msg = (
f"🗑 **Cleared {event}**: {ctx.author.mention} cleared"
f" all {event} events of <@{target}> "
f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
)
await log_channel.send(msg)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command(aliases=["delwarn"])
async def delevent(self, ctx, target: discord.Member, idx: int,
event="warns"):
async def delevent(self, ctx, target: discord.Member, idx: int, event="warns"):
"""Removes a specific event from a user, staff only."""
log_channel = self.bot.get_channel(config.modlog_channel)
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
del_event = self.delete_event_from_id(str(target.id), idx, event)
event_name = userlog_event_types[event].lower()
# This is hell.
if isinstance(del_event, discord.Embed):
await ctx.send(f"{target.mention} has a {event_name} removed!")
safe_name = await commands.clean_content().convert(ctx, str(target))
msg = f"🗑 **Deleted {event_name}**: "\
f"{ctx.author.mention} removed "\
f"{event_name} {idx} from {target.mention} | {safe_name}"
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
)
msg = (
f"🗑 **Deleted {event_name}**: "
f"{ctx.author.mention} removed "
f"{event_name} {idx} from {target.mention} | {safe_name}"
f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
)
await log_channel.send(msg, embed=del_event)
else:
await ctx.send(del_event)
@ -179,15 +194,18 @@ class ModUserlog(Cog):
@commands.command(aliases=["delwarnid"])
async def deleventid(self, ctx, target: int, idx: int, event="warns"):
"""Removes a specific event from a userid, staff only."""
log_channel = self.bot.get_channel(config.modlog_channel)
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
del_event = self.delete_event_from_id(str(target), idx, event)
event_name = userlog_event_types[event].lower()
# This is hell.
if isinstance(del_event, discord.Embed):
await ctx.send(f"<@{target}> has a {event_name} removed!")
msg = f"🗑 **Deleted {event_name}**: "\
f"{ctx.author.mention} removed "\
f"{event_name} {idx} from <@{target}> "
msg = (
f"🗑 **Deleted {event_name}**: "
f"{ctx.author.mention} removed "
f"{event_name} {idx} from <@{target}> "
f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
)
await log_channel.send(msg, embed=del_event)
else:
await ctx.send(del_event)
@ -202,21 +220,30 @@ class ModUserlog(Cog):
role = "@ everyone"
event_types = ["warns", "bans", "kicks", "mutes", "notes"]
embed = self.get_userlog_embed_for_id(str(user.id), str(user),
event=event_types)
embed = self.get_userlog_embed_for_id(
str(user.id), str(user), event=event_types
)
await ctx.send(f"user = {user}\n"
f"id = {user.id}\n"
f"avatar = {user.avatar_url}\n"
f"bot = {user.bot}\n"
f"created_at = {user.created_at}\n"
f"display_name = {user.display_name}\n"
f"joined_at = {user.joined_at}\n"
f"activities = `{user.activities}`\n"
f"color = {user.colour}\n"
f"top_role = {role}\n",
embed=embed)
user_name = await commands.clean_content(escape_markdown=True).convert(
ctx, user.name
)
display_name = await commands.clean_content(escape_markdown=True).convert(
ctx, user.display_name
)
await ctx.send(
f"user = {user_name}\n"
f"id = {user.id}\n"
f"avatar = {user.display_avatar}\n"
f"bot = {user.bot}\n"
f"created_at = {user.created_at}\n"
f"display_name = {display_name}\n"
f"joined_at = {user.joined_at}\n"
f"color = {user.colour}\n"
f"top_role = {role}\n",
embed=embed,
)
def setup(bot):
bot.add_cog(ModUserlog(bot))
async def setup(bot):
await bot.add_cog(ModUserlog(bot))

View file

@ -1,8 +1,9 @@
import discord
from discord.ext import commands
from discord.ext.commands import Cog
from helpers.checks import check_if_staff
from helpers.userlogs import setwatch
from robocop_ng.helpers.checks import check_if_staff
from robocop_ng.helpers.userlogs import setwatch
class ModWatch(Cog):
@ -14,7 +15,7 @@ class ModWatch(Cog):
@commands.command()
async def watch(self, ctx, target: discord.Member, *, note: str = ""):
"""Puts a user under watch, staff only."""
setwatch(target.id, ctx.author, True, target.name)
setwatch(self.bot, target.id, ctx.author, True, target.name)
await ctx.send(f"{ctx.author.mention}: user is now on watch.")
@commands.guild_only()
@ -22,7 +23,7 @@ class ModWatch(Cog):
@commands.command()
async def watchid(self, ctx, target: int, *, note: str = ""):
"""Puts a user under watch by userid, staff only."""
setwatch(target, ctx.author, True, target.name)
setwatch(self.bot, target, ctx.author, True, target.name)
await ctx.send(f"{target.mention}: user is now on watch.")
@commands.guild_only()
@ -30,7 +31,7 @@ class ModWatch(Cog):
@commands.command()
async def unwatch(self, ctx, target: discord.Member, *, note: str = ""):
"""Removes a user from watch, staff only."""
setwatch(target.id, ctx.author, False, target.name)
setwatch(self.bot, target.id, ctx.author, False, target.name)
await ctx.send(f"{ctx.author.mention}: user is now not on watch.")
@commands.guild_only()
@ -38,9 +39,9 @@ class ModWatch(Cog):
@commands.command()
async def unwatchid(self, ctx, target: int, *, note: str = ""):
"""Removes a user from watch by userid, staff only."""
setwatch(target, ctx.author, False, target.name)
setwatch(self.bot, target, ctx.author, False, target.name)
await ctx.send(f"{target.mention}: user is now not on watch.")
def setup(bot):
bot.add_cog(ModWatch(bot))
async def setup(bot):
await bot.add_cog(ModWatch(bot))

View file

@ -1,12 +1,13 @@
import config
from discord.ext import commands
from discord.ext.commands import Cog
from discord.enums import MessageType
from discord import Embed
import aiohttp
import gidgethub.aiohttp
from helpers.checks import check_if_collaborator
from helpers.checks import check_if_pin_channel
from discord import Embed
from discord.enums import MessageType
from discord.ext import commands
from discord.ext.commands import Cog
from robocop_ng.helpers.checks import check_if_collaborator
from robocop_ng.helpers.checks import check_if_pin_channel
class Pin(Cog):
"""
@ -17,9 +18,11 @@ class Pin(Cog):
self.bot = bot
def is_pinboard(self, msg):
return msg.author == self.bot.user and \
len(msg.embeds) > 0 and \
msg.embeds[0].title == "Pinboard"
return (
msg.author == self.bot.user
and len(msg.embeds) > 0
and msg.embeds[0].title == "Pinboard"
)
async def get_pinboard(self, gh, channel):
# Find pinboard pin
@ -32,43 +35,44 @@ class Pin(Cog):
return (id, data["files"]["pinboard.md"]["content"])
# Create pinboard pin if it does not exist
data = await gh.post("/gists", data={
"files": {
"pinboard.md": {
"content": "Old pins are available here:\n\n"
}
data = await gh.post(
"/gists",
data={
"files": {
"pinboard.md": {"content": "Old pins are available here:\n\n"}
},
"description": f"Pinboard for SwitchRoot #{channel.name}",
"public": True,
},
"description": f"Pinboard for SwitchRoot #{channel.name}",
"public": True
})
)
msg = await channel.send(embed=Embed(
title="Pinboard",
description="Old pins are moved to the pinboard to make space for \
msg = await channel.send(
embed=Embed(
title="Pinboard",
description="Old pins are moved to the pinboard to make space for \
new ones. Check it out!",
url=data["html_url"]))
url=data["html_url"],
)
)
await msg.pin()
return (data["id"], data["files"]["pinboard.md"]["content"])
async def add_pin_to_pinboard(self, channel, data):
if config.github_oauth_token == "":
if self.bot.config.github_oauth_token == "":
# Don't add to gist pinboard if we don't have an oauth token
return
async with aiohttp.ClientSession() as session:
gh = gidgethub.aiohttp.GitHubAPI(session, "RoboCop-NG",
oauth_token=config.github_oauth_token)
gh = gidgethub.aiohttp.GitHubAPI(
session, "RoboCop-NG", oauth_token=self.bot.config.github_oauth_token
)
(id, content) = await self.get_pinboard(gh, channel)
content += "- " + data + "\n"
await gh.patch(f"/gists/{id}", data={
"files": {
"pinboard.md": {
"content": content
}
}
})
await gh.patch(
f"/gists/{id}", data={"files": {"pinboard.md": {"content": content}}}
)
@commands.command()
@commands.guild_only()
@ -98,7 +102,7 @@ class Pin(Cog):
return
# Check that reaction pinning is allowd in this channel
if payload.channel_id not in config.allowed_pin_channels:
if payload.channel_id not in self.bot.config.allowed_pin_channels:
return
target_guild = self.bot.get_guild(payload.guild_id)
@ -107,7 +111,7 @@ class Pin(Cog):
# Check that the user is allowed to reaction-pin
target_user = target_guild.get_member(payload.user_id)
for role in config.staff_role_ids + config.allowed_pin_roles:
for role in self.bot.config.staff_role_ids + self.bot.config.allowed_pin_roles:
if role in [role.id for role in target_user.roles]:
target_chan = self.bot.get_channel(payload.channel_id)
target_msg = await target_chan.get_message(payload.message_id)
@ -136,7 +140,7 @@ class Pin(Cog):
break
# Wait for the automated "Pinned" message so we can delete it
waitable = self.bot.wait_for('message', check=check)
waitable = self.bot.wait_for("message", check=check)
# Pin the message
await target_msg.pin()
@ -153,5 +157,5 @@ def check(msg):
return msg.type is MessageType.pins_add
def setup(bot):
bot.add_cog(Pin(bot))
async def setup(bot):
await bot.add_cog(Pin(bot))

View file

@ -1,10 +1,12 @@
import discord
import asyncio
import time
from datetime import datetime
import discord
from discord.ext import commands
from discord.ext.commands import Cog
from helpers.robocronp import add_job, get_crontab
from robocop_ng.helpers.robocronp import add_job, get_crontab
class Remind(Cog):
@ -15,55 +17,72 @@ class Remind(Cog):
@commands.command()
async def remindlist(self, ctx):
"""Lists your reminders."""
ctab = get_crontab()
ctab = get_crontab(self.bot)
uid = str(ctx.author.id)
embed = discord.Embed(title=f"Active robocronp jobs")
for jobtimestamp in ctab["remind"]:
if uid not in ctab["remind"][jobtimestamp]:
continue
job_details = ctab["remind"][jobtimestamp][uid]
expiry_timestr = datetime.utcfromtimestamp(int(jobtimestamp))\
.strftime('%Y-%m-%d %H:%M:%S (UTC)')
embed.add_field(name=f"Reminder for {expiry_timestr}",
value=f"Added on: {job_details['added']}, "
f"Text: {job_details['text']}",
inline=False)
expiry_timestr = datetime.utcfromtimestamp(int(jobtimestamp)).strftime(
"%Y-%m-%d %H:%M:%S (UTC)"
)
embed.add_field(
name=f"Reminder for {expiry_timestr}",
value=f"Added on: {job_details['added']}, "
f"Text: {job_details['text']}",
inline=False,
)
await ctx.send(embed=embed)
@commands.cooldown(1, 60, type=commands.BucketType.user)
@commands.command(aliases=["remindme"])
async def remind(self, ctx, when: str, *, text: str = "something"):
"""Reminds you about something."""
ref_message = None
if ctx.message.reference is not None:
ref_message = await ctx.channel.fetch_message(
ctx.message.reference.message_id
)
if ctx.guild:
await ctx.message.delete()
current_timestamp = time.time()
expiry_timestamp = self.bot.parse_time(when)
if current_timestamp + 5 > expiry_timestamp:
msg = await ctx.send(f"{ctx.author.mention}: Minimum "
"remind interval is 5 seconds.")
msg = await ctx.send(
f"{ctx.author.mention}: Minimum remind interval is 5 seconds."
)
await asyncio.sleep(5)
await msg.delete()
return
expiry_datetime = datetime.utcfromtimestamp(expiry_timestamp)
duration_text = self.bot.get_relative_timestamp(time_to=expiry_datetime,
include_to=True,
humanized=True)
duration_text = self.bot.get_relative_timestamp(
time_to=expiry_datetime, include_to=True, humanized=True
)
safe_text = await commands.clean_content().convert(ctx, str(text))
if ref_message is not None:
safe_text += f"\nMessage reference: {ref_message.jump_url}"
added_on = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S (UTC)")
add_job("remind",
ctx.author.id,
{"text": safe_text, "added": added_on},
expiry_timestamp)
add_job(
self.bot,
"remind",
ctx.author.id,
{"text": safe_text, "added": added_on},
expiry_timestamp,
)
msg = await ctx.send(f"{ctx.author.mention}: I'll remind you in "
f"DMs about `{safe_text}` in {duration_text}.")
await asyncio.sleep(5)
await msg.delete()
await ctx.send(
f"{ctx.author.mention}: I'll remind you in "
f"DMs about `{safe_text}` in {duration_text}."
)
def setup(bot):
bot.add_cog(Remind(bot))
async def setup(bot):
await bot.add_cog(Remind(bot))

View file

@ -0,0 +1,179 @@
import time
import traceback
import discord
from discord.ext import commands, tasks
from discord.ext.commands import Cog
from robocop_ng.helpers.checks import check_if_staff
from robocop_ng.helpers.restrictions import remove_restriction
from robocop_ng.helpers.robocronp import get_crontab, delete_job
class Robocronp(Cog):
def __init__(self, bot):
self.bot = bot
self.minutely.start()
self.hourly.start()
self.daily.start()
def cog_unload(self):
self.minutely.cancel()
self.hourly.cancel()
self.daily.cancel()
async def send_data(self):
await self.bot.wait_until_ready()
data_files = [discord.File(fpath) for fpath in self.bot.wanted_jsons]
log_channel = await self.bot.get_channel_safe(self.bot.config.botlog_channel)
await log_channel.send("Hourly data backups:", files=data_files)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command()
async def listjobs(self, ctx):
"""Lists timed robocronp jobs, staff only."""
ctab = get_crontab(self.bot)
embed = discord.Embed(title=f"Active robocronp jobs")
for jobtype in ctab:
for jobtimestamp in ctab[jobtype]:
for job_name in ctab[jobtype][jobtimestamp]:
job_details = repr(ctab[jobtype][jobtimestamp][job_name])
embed.add_field(
name=f"{jobtype} for {job_name}",
value=f"Timestamp: {jobtimestamp}, Details: {job_details}",
inline=False,
)
await ctx.send(embed=embed)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command(aliases=["removejob"])
async def deletejob(self, ctx, timestamp: str, job_type: str, job_name: str):
"""Removes a timed robocronp job, staff only.
You'll need to supply:
- timestamp (like 1545981602)
- job type (like "unban")
- job name (userid, like 420332322307571713)
You can get all 3 from listjobs command."""
delete_job(self.bot, timestamp, job_type, job_name)
await ctx.send(f"{ctx.author.mention}: Deleted!")
async def do_jobs(self, ctab, jobtype, timestamp):
await self.bot.wait_until_ready()
log_channel = await self.bot.get_channel_safe(self.bot.config.botlog_channel)
for job_name in ctab[jobtype][timestamp]:
try:
job_details = ctab[jobtype][timestamp][job_name]
if jobtype == "unban":
target_user = await self.bot.fetch_user(job_name)
target_guild = self.bot.get_guild(job_details["guild"])
delete_job(self.bot, timestamp, jobtype, job_name)
await target_guild.unban(
target_user, reason="Robocronp: Timed ban expired."
)
elif jobtype == "unmute":
remove_restriction(self.bot, job_name, self.bot.config.mute_role)
target_guild = self.bot.get_guild(job_details["guild"])
target_member = target_guild.get_member(int(job_name))
target_role = target_guild.get_role(self.bot.config.mute_role)
await target_member.remove_roles(
target_role, reason="Robocronp: Timed mute expired."
)
delete_job(self.bot, timestamp, jobtype, job_name)
elif jobtype == "remind":
text = job_details["text"]
added_on = job_details["added"]
target = await self.bot.fetch_user(int(job_name))
if target:
await target.send(
f"You asked to be reminded about `{text}` on {added_on}."
)
delete_job(self.bot, timestamp, jobtype, job_name)
except:
# Don't kill cronjobs if something goes wrong.
delete_job(self.bot, timestamp, jobtype, job_name)
await log_channel.send(
"Crondo has errored, job deleted: ```"
f"{traceback.format_exc()}```"
)
async def clean_channel(self, channel_id):
await self.bot.wait_until_ready()
log_channel = await self.bot.get_channel_safe(self.bot.config.botlog_channel)
channel = await self.bot.get_channel_safe(channel_id)
try:
done_cleaning = False
count = 0
while not done_cleaning:
purge_res = await channel.purge(limit=100)
count += len(purge_res)
if len(purge_res) != 100:
done_cleaning = True
await log_channel.send(
f"Wiped {count} messages from <#{channel.id}> automatically."
)
except:
# Don't kill cronjobs if something goes wrong.
await log_channel.send(
f"Cronclean has errored: ```{traceback.format_exc()}```"
)
@tasks.loop(minutes=1)
async def minutely(self):
await self.bot.wait_until_ready()
log_channel = await self.bot.get_channel_safe(self.bot.config.botlog_channel)
try:
ctab = get_crontab(self.bot)
timestamp = time.time()
for jobtype in ctab:
for jobtimestamp in ctab[jobtype]:
if timestamp > int(jobtimestamp):
await self.do_jobs(ctab, jobtype, jobtimestamp)
# Handle clean channels
for clean_channel in self.bot.config.minutely_clean_channels:
await self.clean_channel(clean_channel)
except:
# Don't kill cronjobs if something goes wrong.
await log_channel.send(
f"Cron-minutely has errored: ```{traceback.format_exc()}```"
)
@tasks.loop(hours=1)
async def hourly(self):
await self.bot.wait_until_ready()
log_channel = await self.bot.get_channel_safe(self.bot.config.botlog_channel)
try:
await self.send_data()
# Handle clean channels
for clean_channel in self.bot.config.hourly_clean_channels:
await self.clean_channel(clean_channel)
except:
# Don't kill cronjobs if something goes wrong.
await log_channel.send(
f"Cron-hourly has errored: ```{traceback.format_exc()}```"
)
@tasks.loop(hours=24)
async def daily(self):
await self.bot.wait_until_ready()
log_channel = await self.bot.get_channel_safe(self.bot.config.botlog_channel)
try:
# Reset verification and algorithm
if "cogs.verification" in self.bot.config.initial_cogs:
verif_channel = await self.bot.get_channel_safe(
self.bot.config.welcome_channel
)
await self.bot.do_resetalgo(verif_channel, "daily robocronp")
except:
# Don't kill cronjobs if something goes wrong.
await log_channel.send(
f"Cron-daily has errored: ```{traceback.format_exc()}```"
)
async def setup(bot):
await bot.add_cog(Robocronp(bot))

View file

@ -0,0 +1,42 @@
from discord import RawMemberRemoveEvent, Member
from discord.ext.commands import Cog
from robocop_ng.helpers.roles import add_user_roles, get_user_roles
class RolePersistence(Cog):
def __init__(self, bot):
self.bot = bot
@Cog.listener()
async def on_raw_member_remove(self, payload: RawMemberRemoveEvent):
save_roles = []
for role in payload.user.roles:
if (
role.is_assignable()
and not role.is_default()
and not role.is_premium_subscriber()
and not role.is_bot_managed()
and not role.is_integration()
):
save_roles.append(role.id)
if len(save_roles) > 0:
add_user_roles(self.bot, payload.user.id, save_roles)
@Cog.listener()
async def on_member_join(self, member: Member):
user_roles = get_user_roles(self.bot, member.id)
if len(user_roles) > 0:
user_roles = [
member.guild.get_role(int(role))
for role in user_roles
if member.guild.get_role(int(role)) is not None
]
await member.add_roles(
*user_roles, reason="Restoring old roles from `role_persistence`."
)
async def setup(bot):
await bot.add_cog(RolePersistence(bot))

View file

@ -0,0 +1,266 @@
import collections
import json
import os
import discord
from discord.ext import commands
from discord.ext.commands import Cog
from robocop_ng.helpers.checks import check_if_staff
class RyujinxReactionRoles(Cog):
def __init__(self, bot):
self.bot = bot
self.channel_id = (
self.bot.config.reaction_roles_channel_id
) # The channel to send the reaction role message. (self-roles channel)
self.file = os.path.join(
self.bot.state_dir, "data/reactionroles.json"
) # the file to store the required reaction role data. (message id of the RR message.)
self.msg_id = None
self.m = None # the msg object
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command()
async def register_reaction_role(self, ctx, target_role_id: int, emoji_name: str):
"""Register a reaction role, staff only."""
await self.bot.wait_until_ready()
if emoji_name[0] == "<":
emoji_name = emoji_name[1:-1]
if target_role_id in self.bot.config.staff_role_ids:
return await ctx.send("Error: Dangerous role found!")
target_role = ctx.guild.get_role(target_role_id)
if target_role is None:
return await ctx.send("Error: Role not found!")
target_role_name = target_role.name
for key in self.reaction_config["reaction_roles_emoji_map"]:
value = self.reaction_config["reaction_roles_emoji_map"][key]
if type(value) is str and target_role_name == value:
return await ctx.send(f"Error: {target_role_name}: already registered.")
self.reaction_config["reaction_roles_emoji_map"][emoji_name] = target_role_name
self.save_reaction_config(self.reaction_config)
await self.reload_reaction_message(False)
await ctx.send(f"{target_role_name}: registered.")
def get_emoji_full_name(self, emoji):
emoji_name = emoji.name
if emoji_name is not None and emoji.id is not None:
emoji_name = f":{emoji_name}:{emoji.id}"
return emoji_name
def get_role(self, key):
return discord.utils.get(
self.bot.guilds[0].roles,
name=self.get_role_from_emoji(key),
)
def get_role_from_emoji(self, key):
value = self.emoji_map.get(key)
if value is not None and type(value) is not str:
return value.get("role")
return value
async def generate_embed(self):
last_descrption = []
description = [
"React to this message with the emojis given below to get your 'Looking for LDN game' roles. \n"
]
for x in self.emoji_map:
value = self.emoji_map[x]
emoji = x
if len(emoji.split(":")) == 3:
emoji = f"<{emoji}>"
if type(value) is str:
description.append(
f"{emoji} for __{self.emoji_map.get(x).split('(')[1].split(')')[0]}__"
)
else:
role_name = value["role"]
line_fmt = value["fmt"]
if value.get("should_be_last", False):
last_descrption.append(line_fmt.format(emoji, role_name))
else:
description.append(line_fmt.format(emoji, role_name))
embed = discord.Embed(
title="**Select your roles**",
description="\n".join(description) + "\n" + "\n".join(last_descrption),
color=420420,
)
embed.set_footer(
text="To remove a role, simply remove the corresponding reaction."
)
return embed
async def handle_offline_reaction_add(self):
await self.bot.wait_until_ready()
for reaction in self.m.reactions:
reactions_users = []
async for user in reaction.users():
reactions_users.append(user)
for user in reactions_users:
emoji_name = str(reaction.emoji)
if emoji_name[0] == "<":
emoji_name = emoji_name[1:-1]
if self.get_role_from_emoji(emoji_name) is not None:
role = self.get_role(emoji_name)
if (
not user in role.members
and not user.bot
and type(user) is discord.Member
):
await user.add_roles(role)
else:
await self.m.clear_reaction(reaction.emoji)
async def handle_offline_reaction_remove(self):
await self.bot.wait_until_ready()
for emoji in self.emoji_map:
for reaction in self.m.reactions:
emoji_name = str(reaction.emoji)
if emoji_name[0] == "<":
emoji_name = emoji_name[1:-1]
reactions_users = []
async for user in reaction.users():
reactions_users.append(user)
role = self.get_role(emoji_name)
for user in role.members:
if user not in reactions_users:
member = self.m.guild.get_member(user.id)
if member is not None:
await member.remove_roles(role)
def load_reaction_config(self):
if not os.path.exists(self.file):
with open(self.file, "w") as f:
json.dump({}, f)
with open(self.file, "r") as f:
return json.load(f)
def save_reaction_config(self, value):
with open(self.file, "w") as f:
json.dump(value, f)
async def reload_reaction_message(self, should_handle_offline=True):
await self.bot.wait_until_ready()
self.emoji_map = collections.OrderedDict(
sorted(
self.reaction_config["reaction_roles_emoji_map"].items(),
key=lambda x: str(x[1]),
)
)
guild = self.bot.guilds[0] # The ryu guild in which the bot is.
channel = guild.get_channel(self.channel_id)
if channel is None:
channel = await guild.fetch_channel(self.channel_id)
history = []
async for msg in channel.history():
history.append(msg)
m = discord.utils.get(history, id=self.reaction_config["id"])
if m is None:
self.reaction_config["id"] = None
embed = await self.generate_embed()
self.m = await channel.send(embed=embed)
self.msg_id = self.m.id
for x in self.emoji_map:
await self.m.add_reaction(x)
self.reaction_config["id"] = self.m.id
self.save_reaction_config(self.reaction_config)
await self.handle_offline_reaction_remove()
else:
self.m = m
self.msg_id = self.m.id
await self.m.edit(embed=await self.generate_embed())
for x in self.emoji_map:
if not x in self.m.reactions:
await self.m.add_reaction(x)
if should_handle_offline:
await self.handle_offline_reaction_add()
await self.handle_offline_reaction_remove()
@Cog.listener()
async def on_ready(self):
await self.bot.wait_until_ready()
self.reaction_config = self.load_reaction_config()
await self.reload_reaction_message()
@Cog.listener()
async def on_raw_reaction_add(self, payload):
await self.bot.wait_until_ready()
if payload.member.bot:
pass
else:
if payload.message_id == self.msg_id:
emoji_name = self.get_emoji_full_name(payload.emoji)
if self.get_role_from_emoji(emoji_name) is not None:
target_role = self.get_role(emoji_name)
if target_role is not None:
await payload.member.add_roles(target_role)
else:
self.bot.log.error(
f"Role {self.emoji_map[emoji_name]} not found."
)
await self.m.clear_reaction(payload.emoji)
else:
await self.m.clear_reaction(payload.emoji)
@Cog.listener()
async def on_raw_reaction_remove(self, payload):
await self.bot.wait_until_ready()
if payload.message_id == self.msg_id:
emoji_name = self.get_emoji_full_name(payload.emoji)
if self.get_role_from_emoji(emoji_name) is not None:
guild = discord.utils.find(
lambda guild: guild.id == payload.guild_id, self.bot.guilds
)
target_role = self.get_role(emoji_name)
if target_role is not None:
await guild.get_member(payload.user_id).remove_roles(
self.get_role(emoji_name)
) # payload.member.remove_roles will throw error
async def setup(bot):
await bot.add_cog(RyujinxReactionRoles(bot))

View file

@ -0,0 +1,100 @@
import discord
from discord.ext import commands
from discord.ext.commands import Cog
from robocop_ng.helpers.checks import check_if_staff
class RyujinxVerification(Cog):
def __init__(self, bot):
self.bot = bot
# Export reset channel functions
self.bot.do_reset = self.do_reset
self.bot.do_resetalgo = self.do_resetalgo
@Cog.listener()
async def on_member_join(self, member):
await self.bot.wait_until_ready()
if member.guild.id not in self.bot.config.guild_whitelist:
return
join_channel = self.bot.get_channel(self.bot.config.welcome_channel)
if join_channel is not None:
await join_channel.send(
"Hello {0.mention}! Welcome to Ryujinx! Please read the <#411271165429022730>, and then type the verifying command here to gain access to the rest of the channels.\n\nIf you need help with basic common questions, visit the <#585288848704143371> channel after joining.\n\nIf you need help with Animal Crossing visit the <#692104087889641472> channel for common issues and solutions. If you need help that is not Animal Crossing related, please visit the <#410208610455519243> channel after verifying.".format(
member
)
)
async def process_message(self, message):
"""Process the verification process"""
if message.channel.id == self.bot.config.welcome_channel:
# Assign common stuff into variables to make stuff less of a mess
mcl = message.content.lower()
# Get the role we will give in case of success
success_role = message.guild.get_role(self.bot.config.participant_role)
if self.bot.config.verification_string == mcl:
await message.author.add_roles(success_role)
await message.delete()
@Cog.listener()
async def on_message(self, message):
if message.author.bot:
return
try:
await self.process_message(message)
except discord.errors.Forbidden:
chan = self.bot.get_channel(message.channel)
await chan.send("💢 I don't have permission to do this.")
@Cog.listener()
async def on_message_edit(self, before, after):
if after.author.bot:
return
try:
await self.process_message(after)
except discord.errors.Forbidden:
chan = self.bot.get_channel(after.channel)
await chan.send("💢 I don't have permission to do this.")
@Cog.listener()
async def on_member_join(self, member):
await self.bot.wait_until_ready()
if member.guild.id not in self.bot.config.guild_whitelist:
return
join_channel = self.bot.get_channel(self.bot.config.welcome_channel)
if join_channel is not None:
await join_channel.send(self.bot.config.join_message.format(member))
@commands.check(check_if_staff)
@commands.command()
async def reset(self, ctx, limit: int = 100, force: bool = False):
"""Wipes messages and pastes the welcome message again. Staff only."""
if ctx.message.channel.id != self.bot.config.welcome_channel and not force:
await ctx.send(
f"This command is limited to"
f" <#{self.bot.config.welcome_channel}>, unless forced."
)
return
await self.do_reset(ctx.channel, ctx.author.mention, limit)
async def do_reset(self, channel, author, limit: int = 100):
await channel.purge(limit=limit)
async def do_resetalgo(self, channel, author, limit: int = 100):
# We only auto clear the channel daily
await self.do_reset(channel, author)
async def setup(bot):
await bot.add_cog(RyujinxVerification(bot))

48
robocop_ng/cogs/sar.py Normal file
View file

@ -0,0 +1,48 @@
from discord.ext import commands
from discord.ext.commands import Cog
from robocop_ng.helpers.checks import check_if_staff_or_ot
class SAR(Cog):
def __init__(self, bot):
self.bot = bot
@commands.guild_only()
@commands.command()
@commands.check(check_if_staff_or_ot)
async def sar(self, ctx):
"""Lists self assignable roles."""
return await ctx.send(
"Self assignable roles in this guild: "
+ ",".join(self.bot.config.self_assignable_roles)
+ f"\n\nRun `{self.bot.config.prefixes[0]}iam role_name_goes_here` to get or remove one."
)
@commands.cooldown(1, 30, type=commands.BucketType.user)
@commands.guild_only()
@commands.command(aliases=["iamnot"])
@commands.check(check_if_staff_or_ot)
async def iam(self, ctx, role: str):
"""Gets you a self assignable role."""
if role not in self.bot.config.self_assignable_roles:
return await ctx.send(
"There's no self assignable role with that name. Run .sar to see what you can self assign."
)
target_role = ctx.guild.get_role(self.bot.config.self_assignable_roles[role])
if target_role in ctx.author.roles:
await ctx.author.remove_roles(target_role, reason=str(ctx.author))
await ctx.send(
f"{ctx.author.mention}: Successfully removed your `{role}` role. Run the command again if you want to add it again."
)
else:
await ctx.author.add_roles(target_role, reason=str(ctx.author))
await ctx.send(
f"{ctx.author.mention}: Successfully gave you the `{role}` role. Run the command again if you want to remove it."
)
async def setup(bot):
await bot.add_cog(SAR(bot))

View file

@ -0,0 +1,40 @@
from discord import Guild
from discord.errors import Forbidden
from discord.ext import tasks
from discord.ext.commands import Cog
class VanityUrl(Cog):
def __init__(self, bot):
self.bot = bot
self.vanity_codes: dict[int, str] = self.bot.config.vanity_codes
self.check_changed_vanity_codes.start()
def cog_unload(self):
self.check_changed_vanity_codes.cancel()
async def update_vanity_code(self, guild: Guild, code: str):
if "VANITY_URL" in guild.features and guild.vanity_url_code != code:
try:
await guild.edit(
reason="Configured vanity code was different", vanity_code=code
)
except Forbidden:
self.bot.log.exception(f"Not allowed to edit vanity url for: {guild}")
self.cog_unload()
await self.bot.unload_extension("robocop_ng.cogs.vanity_url")
@Cog.listener()
async def on_guild_update(self, before: Guild, after: Guild):
if after.id in self.vanity_codes:
await self.update_vanity_code(after, self.vanity_codes[after.id])
@tasks.loop(hours=12)
async def check_changed_vanity_codes(self):
await self.bot.wait_until_ready()
for guild, vanity_code in self.vanity_codes.items():
await self.update_vanity_code(self.bot.get_guild(guild), vanity_code)
async def setup(bot):
await bot.add_cog(VanityUrl(bot))

View file

@ -0,0 +1,227 @@
import asyncio
import hashlib
import itertools
import random
from inspect import cleandoc
import discord
from discord.ext import commands
from discord.ext.commands import Cog
from robocop_ng.helpers.checks import check_if_staff
class Verification(Cog):
def __init__(self, bot):
self.bot = bot
self.hash_choice = random.choice(self.bot.config.welcome_hashes)
# Export reset channel functions
self.bot.do_reset = self.do_reset
self.bot.do_resetalgo = self.do_resetalgo
async def do_reset(self, channel, author, limit: int = 100):
await channel.purge(limit=limit)
await channel.send(self.bot.config.welcome_header)
rules = [
"**{}**. {}".format(i, cleandoc(r))
for i, r in enumerate(self.bot.config.welcome_rules, 1)
]
rule_choice = random.randint(2, len(rules))
hash_choice_str = self.hash_choice.upper()
if hash_choice_str == "BLAKE2B":
hash_choice_str += "-512"
elif hash_choice_str == "BLAKE2S":
hash_choice_str += "-256"
rules[rule_choice - 1] += "\n" + self.bot.config.hidden_term_line.format(
hash_choice_str
)
msg = (
f"🗑 **Reset**: {author} cleared {limit} messages " f" in {channel.mention}"
)
msg += f"\n💬 __Current challenge location__: under rule {rule_choice}"
log_channel = self.bot.get_channel(self.bot.config.log_channel)
await log_channel.send(msg)
# find rule that puts us over 2,000 characters, if any
total = 0
messages = []
current_message = ""
for item in rules:
total += len(item) + 2 # \n\n
if total < 2000:
current_message += item + "\n\n"
else:
# we've hit the limit; split!
messages += [current_message]
current_message = "\n\u200B\n" + item + "\n\u200B\n"
total = 0
messages += [current_message]
for item in messages:
await channel.send(item)
await asyncio.sleep(1)
for x in self.bot.config.welcome_footer:
await channel.send(cleandoc(x))
await asyncio.sleep(1)
async def do_resetalgo(self, channel, author, limit: int = 100):
# randomize hash_choice on reset
self.hash_choice = random.choice(tuple(self.bot.config.welcome_hashes))
msg = (
f"📘 **Reset Algorithm**: {author} reset " f"algorithm in {channel.mention}"
)
msg += f"\n💬 __Current algorithm__: {self.hash_choice.upper()}"
log_channel = self.bot.get_channel(self.bot.config.log_channel)
await log_channel.send(msg)
await self.do_reset(channel, author)
@commands.check(check_if_staff)
@commands.command()
async def reset(self, ctx, limit: int = 100, force: bool = False):
"""Wipes messages and pastes the welcome message again. Staff only."""
if ctx.message.channel.id != self.bot.config.welcome_channel and not force:
await ctx.send(
f"This command is limited to"
f" <#{self.bot.config.welcome_channel}>, unless forced."
)
return
await self.do_reset(ctx.channel, ctx.author.mention, limit)
@commands.check(check_if_staff)
@commands.command()
async def resetalgo(self, ctx, limit: int = 100, force: bool = False):
"""Resets the verification algorithm and does what reset does. Staff only."""
if ctx.message.channel.id != self.bot.config.welcome_channel and not force:
await ctx.send(
f"This command is limited to"
f" <#{self.bot.config.welcome_channel}>, unless forced."
)
return
await self.do_resetalgo(ctx.channel, ctx.author.mention, limit)
async def process_message(self, message):
"""Big code that makes me want to shoot myself
Not really a rewrite but more of a port
Git blame tells me that I should blame/credit Robin Lambertz"""
if message.channel.id == self.bot.config.welcome_channel:
# Assign common stuff into variables to make stuff less of a mess
member = message.author
full_name = str(member)
discrim = str(member.discriminator)
guild = message.guild
chan = message.channel
mcl = message.content.lower()
# Reply to users that insult the bot
oof = [
"bad",
"broken",
"buggy",
"bugged",
"stupid",
"dumb",
"silly",
"fuck",
"heck",
"h*ck",
]
if "bot" in mcl and any(insult in mcl for insult in oof):
snark = random.choice(["bad human", "no u", "no u, rtfm", "pebkac"])
return await chan.send(snark)
# Get the role we will give in case of success
success_role = guild.get_role(self.bot.config.named_roles["participant"])
# Get a list of stuff we'll allow and will consider close
allowed_names = [f"@{full_name}", full_name, str(member.id)]
close_names = [f"@{member.name}", member.name, discrim, f"#{discrim}"]
# Now add the same things but with newlines at the end of them
allowed_names += [(an + "\n") for an in allowed_names]
close_names += [(cn + "\n") for cn in close_names]
allowed_names += [(an + "\r\n") for an in allowed_names]
close_names += [(cn + "\r\n") for cn in close_names]
# [ ͡° ͜ᔦ ͡°] 𝐖𝐞𝐥𝐜𝐨𝐦𝐞 𝐭𝐨 𝐌𝐚𝐜 𝐎𝐒 𝟗.
allowed_names += [(an + "\r") for an in allowed_names]
close_names += [(cn + "\r") for cn in close_names]
# Finally, hash the stuff so that we can access them later :)
hash_allow = [
hashlib.new(self.hash_choice, name.encode("utf-8")).hexdigest()
for name in allowed_names
]
# I'm not even going to attempt to break those into lines jfc
if any(allow in mcl for allow in hash_allow):
await member.add_roles(success_role)
return await chan.purge(
limit=100,
check=lambda m: m.author == message.author
or (
m.author == self.bot.user
and message.author.mention in m.content
),
)
# Detect if the user uses the wrong hash algorithm
wrong_hash_algos = list(
set(self.bot.config.welcome_hashes) - {self.hash_choice}
)
for algo in wrong_hash_algos:
for name in itertools.chain(allowed_names, close_names):
if hashlib.new(algo, name.encode("utf-8")).hexdigest() in mcl:
log_channel = self.bot.get_channel(self.bot.config.log_channel)
await log_channel.send(
f"User {message.author.mention} tried verification with algo {algo} instead of {self.hash_choice}."
)
return await chan.send(
f"{message.author.mention} :no_entry: Close, but not quite. Go back and re-read!"
)
if (
full_name in message.content
or str(member.id) in message.content
or member.name in message.content
or discrim in message.content
):
no_text = ":no_entry: Incorrect. You need to do something *specific* with your name and discriminator instead of just posting it. Please re-read the rules carefully and look up any terms you are not familiar with."
rand_num = random.randint(1, 100)
if rand_num == 42:
no_text = "you're doing it wrong"
elif rand_num == 43:
no_text = "ugh, wrong, read the rules."
elif rand_num == 44:
no_text = '"The definition of insanity is doing the same thing over and over again, but expecting different results."\n-Albert Einstein'
await chan.send(f"{message.author.mention} {no_text}")
@Cog.listener()
async def on_message(self, message):
if message.author.bot:
return
try:
await self.process_message(message)
except discord.errors.Forbidden:
chan = self.bot.get_channel(message.channel)
await chan.send("💢 I don't have permission to do this.")
@Cog.listener()
async def on_message_edit(self, before, after):
if after.author.bot:
return
try:
await self.process_message(after)
except discord.errors.Forbidden:
chan = self.bot.get_channel(after.channel)
await chan.send("💢 I don't have permission to do this.")
async def setup(bot):
await bot.add_cog(Verification(bot))

View file

@ -0,0 +1,153 @@
import asyncio
import base64
import hmac
import re
import secrets
from discord.ext.commands import Cog
class YubicoOTP(Cog):
def __init__(self, bot):
self.bot = bot
self.otp_re = re.compile("((cc|vv)[cbdefghijklnrtuv]{42})$")
self.api_servers = [
"https://api.yubico.com",
"https://api2.yubico.com",
"https://api3.yubico.com",
"https://api4.yubico.com",
"https://api5.yubico.com",
]
self.reuse_responses = ["BAD_OTP", "REPLAYED_OTP"]
self.bad_responses = [
"MISSING_PARAMETER",
"NO_SUCH_CLIENT",
"OPERATION_NOT_ALLOWED",
]
self.modhex_to_hex_conversion_map = {
"c": "0",
"b": "1",
"d": "2",
"e": "3",
"f": "4",
"g": "5",
"h": "6",
"i": "7",
"j": "8",
"k": "9",
"l": "a",
"n": "b",
"r": "c",
"t": "d",
"u": "e",
"v": "f",
}
def get_serial(self, otp):
"""Get OTP from serial, based on code by linuxgemini"""
if otp[:2] != "cc":
return False
hexconv = []
for modhexletter in otp[0:12]:
hexconv.append(self.modhex_to_hex_conversion_map[modhexletter])
return int("".join(hexconv), 16)
def calc_signature(self, text):
key = base64.b64decode(self.bot.config.yubico_otp_secret)
signature_bytes = hmac.digest(key, text.encode(), "SHA1")
return base64.b64encode(signature_bytes).decode()
def validate_response_signature(self, response_dict):
yubico_signature = response_dict["h"]
to_sign = ""
for key in sorted(response_dict.keys()):
if key == "h":
continue
to_sign += f"{key}={response_dict[key]}&"
our_signature = self.calc_signature(to_sign.strip("&"))
return our_signature == yubico_signature
async def validate_yubico_otp(self, otp):
nonce = secrets.token_hex(15) # Random number in the valid range
params = f"id={self.bot.config.yubico_otp_client_id}&nonce={nonce}&otp={otp}"
# If secret is supplied, sign our request
if self.bot.config.yubico_otp_secret:
params += "&h=" + self.calc_signature(params)
for api_server in self.api_servers:
url = f"{api_server}/wsapi/2.0/verify?{params}"
try:
resp = await self.bot.aiosession.get(url)
assert resp.status == 200
except Exception as ex:
self.bot.log.warning(f"Got {repr(ex)} on {api_server} with otp {otp}.")
continue
resptext = await resp.text()
# Turn the fields to a python dict for easier parsing
datafields = resptext.strip().split("\r\n")
datafields = {
line[: line.index("=")]: line[line.index("=") + 1 :]
for line in datafields
}
# Verify nonce
assert datafields["nonce"] == nonce
# Verify signature if secret is present
if self.bot.config.yubico_otp_secret:
assert self.validate_response_signature(datafields)
# If we got a success, then return True
if datafields["status"] == "OK":
return True
elif datafields["status"] in self.reuse_responses:
return False
# If status isn't an expected one, log it
self.bot.log.warning(
f"Got {repr(datafields)} on {api_server} with otp {otp} and nonce {nonce}"
)
# If we fucked up in a way we can't recover from, just return None
if datafields["status"] in self.bad_responses:
return None
# Return None if we fail to get responses from any server
return None
@Cog.listener()
async def on_message(self, message):
await self.bot.wait_until_ready()
otps = self.otp_re.findall(message.content.strip())
if otps:
otp = otps[0][0]
# Validate OTP
validation_result = await self.validate_yubico_otp(otp)
if validation_result is not True:
return
# Derive serial and a string to use it
serial = self.get_serial(otp)
serial_str = f" (serial: `{serial}`)" if serial else ""
# If the message content is _just_ the OTP code, delete it toos
if message.content.strip() == otp:
await message.delete()
# If OTP is valid, tell user that it was revoked
msg = await message.channel.send(
f"{message.author.mention}: Ate Yubico OTP `{otp}`{serial_str}"
". This message will self destruct in 5 seconds."
)
# and delete message after 5s to help SNR
await asyncio.sleep(5)
await msg.delete()
async def setup(bot):
await bot.add_cog(YubicoOTP(bot))

View file

@ -0,0 +1,343 @@
import datetime
import hashlib
# Basic bot config, insert your token here, update description if you want
prefixes = [".", "!"]
client_id = 0
token = "token-goes-here"
bot_description = "Robocop-NG, the moderation bot of ReSwitched."
# If you forked robocop-ng, put your repo here
source_url = "https://github.com/reswitched/robocop-ng"
rules_url = "https://reswitched.github.io/discord/#rules"
# The bot description to be used in .robocop embed
embed_desc = (
"Robocop-NG is developed by [Ave](https://github.com/aveao)"
" and [tomGER](https://github.com/tumGER), and is a rewrite "
"of Robocop.\nRobocop is based on Kurisu by 916253 and ihaveamac."
)
# The cogs the bot will load on startup.
initial_cogs = [
"cogs.common",
"cogs.admin",
"cogs.verification",
"cogs.mod",
"cogs.mod_note",
"cogs.mod_reacts",
"cogs.mod_userlog",
"cogs.mod_timed",
"cogs.mod_watch",
"cogs.basic",
"cogs.logs",
"cogs.err",
"cogs.lockdown",
"cogs.legacy",
"cogs.links",
"cogs.remind",
"cogs.robocronp",
"cogs.meme",
"cogs.invites",
"cogs.yubicootp",
]
# The following cogs are also available but aren't loaded by default:
# cogs.imagemanip - Adds a meme command called .cox.
# Requires Pillow to be installed with pip.
# cogs.lists - Allows managing list channels (rules, FAQ) easily through the bot
# PR'd in at: https://github.com/reswitched/robocop-ng/pull/65
# cogs.pin - Lets users pin important messages
# and sends pins above limit to a github gist
# The string that users need to say to get past verification
verification_string = "go read the rules, not the code"
# Minimum account age required to join the guild
# If user's account creation is shorter than the time delta given here
# then user will be kicked and informed
min_age = datetime.timedelta(minutes=15)
# The bot will only work in these guilds
guild_whitelist = [269333940928512010] # ReSwitched discord
# Custom invite URL codes
vanity_codes = {269333940928512010: "reswitched"}
# Named roles to be used with .approve and .revoke
# Example: .approve User hacker
named_roles = {
"community": 420010997877833731,
"hacker": 364508795038072833,
"participant": 434353085926866946,
"pirate": 0,
}
# The bot manager and staff roles
# Bot manager can run eval, exit and other destructive commands
# Staff can run administrative commands
bot_manager_role_id = 466447265863696394 # Bot management role in ReSwitched
staff_role_ids = [
364647829248933888, # Team role in ReSwitched
360138431524765707, # Mod role in ReSwitched
466447265863696394, # Bot management role in ReSwitched
360138163156549632, # Admin role in ReSwitched
287289529986187266, # Wizard role in ReSwitched
]
# Various log channels used to log bot and guild's activity
# You can use same channel for multiple log types
# Spylog channel logs suspicious messages or messages by members under watch
# Invites created with .invite will direct to the welcome channel.
log_channel = 290958160414375946 # server-logs in ReSwitched
botlog_channel = 529070282409771048 # bot-logs channel in ReSwitched
modlog_channel = 542114169244221452 # mod-logs channel in ReSwitched
spylog_channel = 548304839294189579 # spy channel in ReSwitched
welcome_channel = 326416669058662401 # newcomers channel in ReSwitched
# These channel entries are used to determine which roles will be given
# access when we unmute on them
general_channels = [
420029476634886144,
414949821003202562,
383368936466546698,
343244421044633602,
491316901692178432,
539212260350885908,
] # Channels everyone can access
community_channels = [
269333940928512010,
438839875970662400,
404722395845361668,
435687501068501002,
286612533757083648,
] # Channels requiring community role
# Controls which roles are blocked during lockdown
lockdown_configs = {
# Used as a default value for channels without a config
"default": {"channels": general_channels, "roles": [named_roles["participant"]]},
"community": {
"channels": community_channels,
"roles": [named_roles["community"], named_roles["hacker"]],
},
}
# Mute role is applied to users when they're muted
# As we no longer have mute role on ReSwitched, I set it to 0 here
mute_role = 0 # Mute role in ReSwitched
# Channels that will be cleaned every minute/hour.
# This feature isn't very good rn.
# See https://github.com/reswitched/robocop-ng/issues/23
minutely_clean_channels = []
hourly_clean_channels = []
# Edited and deletes messages in these channels will be logged
spy_channels = general_channels
# All lower case, no spaces, nothing non-alphanumeric
suspect_words = [
"deepsea", # piracy-enabling cfw
"sx", # piracy-enabling cfw
"tx", # piracy-enabling cfw
"reinx", # piracy-enabling cfw
"gomanx", # piracy-enabling cfw
"neutos", # piracy-enabling cfw
"underpack", # piracy-enabling cfw
"underos", # piracy-enabling cfw
"tinfoil", # title manager
"dz", # title manager
"goldleaf", # potential title manager
"lithium", # title manager
"cracked", # older term for pirated games
"xci", # "backup" format
"xcz", # "backup" format
"nsz", # "backup" format
"hbg", # piracy source
"jits", # piracy source
]
# List of words that will be ignored if they match one of the
# suspect_words (This is used to remove false positives)
suspect_ignored_words = [
"excit",
"s/x",
"3dsx",
"psx",
"txt",
"s(x",
"txd",
"t=x",
"osx",
"rtx",
"shift-x",
"users/x",
"tx1",
"tx2",
"tcptx",
"udptx",
"ctx",
"jit's",
]
# == For cogs.links ==
links_guide_text = """**Generic starter guides:**
Nintendo Homebrew's Guide: <https://nh-server.github.io/switch-guide/>
**Specific guides:**
Manually Updating/Downgrading (with HOS): <https://switch.homebrew.guide/usingcfw/manualupgrade>
Manually Repairing/Downgrading (without HOS): <https://switch.homebrew.guide/usingcfw/manualchoiupgrade>
How to set up a Homebrew development environment: <https://devkitpro.org/wiki/Getting_Started>
Getting full RAM in homebrew without NSPs: As of Atmosphere 0.8.6, hold R while opening any game.
Check if a switch is vulnerable to RCM through serial: <https://akdm.github.io/ssnc/checker/>
"""
# == For cogs.verification ==
# ReSwitched verification system is rather unique.
# You might want to reimplement it.
# If you do, use a different name for easier upstream merge.
# https://docs.python.org/3.7/library/hashlib.html#shake-variable-length-digests
_welcome_blacklisted_hashes = {"shake_128", "shake_256"}
# List of hashes that are to be used during verification
welcome_hashes = tuple(hashlib.algorithms_guaranteed - _welcome_blacklisted_hashes)
# Header before rules in #newcomers - https://elixi.re/i/opviq90y.png
welcome_header = """
<:ReSwitched:326421448543567872> __**Welcome to ReSwitched!**__
__**Be sure you read the following rules and information before participating. If you came here to ask about "backups", this is NOT the place.**__
__**Got questions about Nintendo Switch hacking? Before asking in the server, please see our FAQ at <https://reswitched.github.io/faq/> to see if your question has already been answered.**__
__**This is a server for technical discussion and development support. If you are looking for end-user support, the Nintendo Homebrew discord server may be a better fit: <https://discord.gg/C29hYvh>.**__
:bookmark_tabs:__Rules:__
"""
# Rules in #newcomers - https://elixi.re/i/dp3enq5i.png
welcome_rules = (
# 1
"""
Read all the rules before participating in chat. Not reading the rules is *not* an excuse for breaking them.
It's suggested that you read channel topics and pins before asking questions as well, as some questions may have already been answered in those.
""",
# 2
"""
Be nice to each other. It's fine to disagree, it's not fine to insult or attack other people.
You may disagree with anyone or anything you like, but you should try to keep it to opinions, and not people. Avoid vitriol.
Constant antagonistic behavior is considered uncivil and appropriate action will be taken.
The use of derogatory slurs -- sexist, racist, homophobic, transphobic, or otherwise -- is unacceptable and may be grounds for an immediate ban.
""",
# 3
'If you have concerns about another user, please take up your concerns with a staff member (someone with the "mod" role in the sidebar) in private. Don\'t publicly call other users out.',
# 4
"""
From time to time, we may mention everyone in the server. We do this when we feel something important is going on that requires attention. Complaining about these pings may result in a ban.
To disable notifications for these pings, suppress them in "ReSwitched → Notification Settings".
""",
# 5
"""
Don't spam.
For excessively long text, use a service like <https://0bin.net/>.
""",
# 6
"Don't brigade, raid, or otherwise attack other people or communities. Don't discuss participation in these attacks. This may warrant an immediate permanent ban.",
# 7
"Off-topic content goes to #off-topic. Keep low-quality content like memes out.",
# 8
"Trying to evade, look for loopholes, or stay borderline within the rules will be treated as breaking them.",
# 9
"""
Absolutely no piracy or related discussion. This includes:
"Backups", even if you legally own a copy of the game.
"Installable" NSPs, XCIs, and NCAs; this **includes** installable homebrew (i.e. on the Home Menu instead of within nx-hbmenu).
Signature and ES patches, also known as "sigpatches"
Usage of piracy-focused groups' (Team Xecuter, etc.) hardware and software, such as SX OS.
This is a zero-tolerance, non-negotiable policy that is enforced strictly and swiftly, up to and including instant bans without warning.
""",
# 10
"The first character of your server nickname should be alphanumeric if you wish to talk in chat.",
# 11
"""
Do not boost the server.
ReSwitched neither wants nor needs your server boosts, and your money is better off elsewhere. Consider the EFF (or a charity of your choice).
Boosting the server is liable to get you kicked (to remove the nitro boost role), and/or warned. Roles you possessed prior to the kick may not be restored in a timely fashion.
""",
)
# Footer after rules in #newcomers - https://elixi.re/i/uhfiecib.png
welcome_footer = (
"""
:hash: __Channel Breakdown:__
#news - Used exclusively for updates on ReSwitched progress and community information. Most major announcements are passed through this channel and whenever something is posted there it's usually something you'll want to look at.
#switch-hacking-meta - For "meta-discussion" related to hacking the switch. This is where we talk *about* the switch hacking that's going on, and where you can get clarification about the hacks that exist and the work that's being done.
#user-support - End-user focused support, mainly between users. Ask your questions about using switch homebrew here.
#tool-support - Developer focused support. Ask your questions about using PegaSwitch, libtransistor, Mephisto, and other tools here.
#hack-n-all - General hacking, hardware and software development channel for hacking on things *other* than the switch. This is a great place to ask about hacking other systems-- and for the community to have technical discussions.
""",
"""
#switch-hacking-general - Channel for everyone working on hacking the switch-- both in an exploit and a low-level hardware sense. This is where a lot of our in-the-open development goes on. Note that this isn't the place for developing homebrew-- we have #homebrew-development for that!
#homebrew-development - Discussion about the development of homebrew goes there. Feel free to show off your latest creation here.
#off-topic - Channel for discussion of anything that doesn't belong in #general. Anything goes, so long as you make sure to follow the rules and be on your best behavior.
#toolchain-development - Discussion about the development of libtransistor itself goes there.
#cfw-development - Development discussion regarding custom firmware (CFW) projects, such as Atmosphère. This channel is meant for the discussion accompanying active development.
#bot-cmds - Channel for excessive/random use of Robocop's various commands.
**If you are still not sure how to get access to the other channels, please read the rules again.**
**If you have questions about the rules, feel free to ask here!**
**Note: This channel is completely automated (aside from responding to questions about the rules). If your message didn't give you access to the other channels, you failed the test. Feel free to try again.**
""",
)
# Line to be hidden in rules
hidden_term_line = ' • When you have finished reading all of the rules, send a message in this channel that includes the {0} hex digest of your discord "name#discriminator", and bot will automatically grant you access to the other channels. You can find your "name#discriminator" (your username followed by a # and four numbers) under the discord channel list.'
# == Only if you want to use cogs.pin ==
# Used for the pinboard. Leave empty if you don't wish for a gist pinboard.
github_oauth_token = ""
# Channels and roles where users can pin messages
allowed_pin_channels = []
allowed_pin_roles = []
# Channel to upload text files while editing list items. (They are cleaned up.)
list_files_channel = 0
# == Only if you want to use cogs.lists ==
# Channels that are lists that are controlled by the lists cog.
list_channels = []
# == Only if you want to use cogs.sar ==
self_assignable_roles = {
"streamnotifs": 715158689060880384,
}
# == Only if you want to use cogs.mod_reswitched ==
pingmods_allow = [named_roles["community"]] + staff_role_ids
pingmods_role = 360138431524765707
modtoggle_role = 360138431524765707
# == Only if you want to use cogs.yubicootp ==
# Optiona: Get your own from https://upgrade.yubico.com/getapikey/
yubico_otp_client_id = 1
# Note: You can keep client ID on 1, it will function.
yubico_otp_secret = ""
# Optional: If you provide a secret, requests will be signed
# and responses will be verified.

View file

@ -16,17 +16,25 @@ def check_if_bot_manager(ctx):
def check_if_staff_or_ot(ctx):
if not ctx.guild:
return True
is_ot = (ctx.channel.name == "off-topic")
is_bot_cmds = (ctx.channel.name == "bot-cmds")
is_ot = ctx.channel.name == "off-topic"
is_bot_cmds = ctx.channel.name == "bot-cmds"
is_staff = any(r.id in config.staff_role_ids for r in ctx.author.roles)
return (is_ot or is_staff or is_bot_cmds)
return is_ot or is_staff or is_bot_cmds
def check_if_staff_or_dm(ctx):
if not ctx.guild:
return True
return any(r.id in config.staff_role_ids for r in ctx.author.roles)
def check_if_collaborator(ctx):
if not ctx.guild:
return False
return any(r.id in config.staff_role_ids + config.allowed_pin_roles
for r in ctx.author.roles)
return any(
r.id in config.staff_role_ids + config.allowed_pin_roles
for r in ctx.author.roles
)
def check_if_pin_channel(ctx):

View file

@ -0,0 +1,21 @@
import json
import os
from robocop_ng.helpers.notifications import report_critical_error
def read_json(bot, filepath: str) -> dict:
if os.path.isfile(filepath) and os.path.getsize(filepath) > 0:
with open(filepath, "r") as f:
try:
return json.load(f)
except json.JSONDecodeError as e:
content = f.read()
report_critical_error(
bot,
e,
additional_info={
"file": {"length": len(content), "content": content}
},
)
return {}

View file

@ -0,0 +1,189 @@
import json
import os
from typing import Union
from robocop_ng.helpers.data_loader import read_json
def get_disabled_ids_path(bot) -> str:
return os.path.join(bot.state_dir, "data/disabled_ids.json")
def is_app_id_valid(app_id: str) -> bool:
return len(app_id) == 16 and app_id.isalnum()
def is_build_id_valid(build_id: str) -> bool:
return 32 <= len(build_id) <= 64 and build_id.isalnum()
def is_ro_section_valid(ro_section: dict[str, str]) -> bool:
return "module" in ro_section.keys() and "sdk_libraries" in ro_section.keys()
def get_disabled_ids(bot) -> dict[str, dict[str, Union[str, dict[str, str]]]]:
disabled_ids = read_json(bot, get_disabled_ids_path(bot))
if len(disabled_ids) > 0:
# Migration code
if "app_id" in disabled_ids.keys():
old_disabled_ids = disabled_ids.copy()
disabled_ids = {}
for key in old_disabled_ids["app_id"].values():
disabled_ids[key.lower()] = {
"app_id": "",
"build_id": "",
"ro_section": {},
}
for id_type in ["app_id", "build_id"]:
for value, key in old_disabled_ids[id_type].items():
disabled_ids[key.lower()][id_type] = value
for key, value in old_disabled_ids["ro_section"].items():
disabled_ids[key.lower()]["ro_section"] = value
set_disabled_ids(bot, disabled_ids)
return disabled_ids
def set_disabled_ids(bot, contents: dict[str, dict[str, Union[str, dict[str, str]]]]):
with open(get_disabled_ids_path(bot), "w") as f:
json.dump(contents, f)
def add_disable_id_if_necessary(
disable_id: str, disabled_ids: dict[str, dict[str, Union[str, dict[str, str]]]]
):
if disable_id not in disabled_ids.keys():
disabled_ids[disable_id] = {"app_id": "", "build_id": "", "ro_section": {}}
def is_app_id_disabled(bot, app_id: str) -> bool:
disabled_app_ids = [
entry["app_id"]
for entry in get_disabled_ids(bot).values()
if len(entry["app_id"]) > 0
]
app_id = app_id.lower()
return app_id in disabled_app_ids
def is_build_id_disabled(bot, build_id: str) -> bool:
disabled_build_ids = [
entry["build_id"]
for entry in get_disabled_ids(bot).values()
if len(entry["build_id"]) > 0
]
build_id = build_id.lower()
if len(build_id) < 64:
build_id += "0" * (64 - len(build_id))
return build_id in disabled_build_ids
def is_ro_section_disabled(bot, ro_section: dict[str, Union[str, list[str]]]) -> bool:
disabled_ro_sections = [
entry["ro_section"]
for entry in get_disabled_ids(bot).values()
if len(entry["ro_section"]) > 0
]
matches = []
for disabled_ro_section in disabled_ro_sections:
for key, content in disabled_ro_section.items():
if key == "module":
matches.append(ro_section[key].lower() == content.lower())
else:
matches.append(ro_section[key] == content)
if all(matches) and len(matches) > 0:
return True
else:
matches = []
return False
def remove_disable_id(bot, disable_id: str) -> bool:
disabled_ids = get_disabled_ids(bot)
if disable_id in disabled_ids.keys():
del disabled_ids[disable_id]
set_disabled_ids(bot, disabled_ids)
return True
return False
def add_disabled_app_id(bot, disable_id: str, app_id: str) -> bool:
disabled_ids = get_disabled_ids(bot)
disable_id = disable_id.lower()
app_id = app_id.lower()
if not is_app_id_disabled(bot, app_id):
add_disable_id_if_necessary(disable_id, disabled_ids)
disabled_ids[disable_id]["app_id"] = app_id
set_disabled_ids(bot, disabled_ids)
return True
return False
def add_disabled_build_id(bot, disable_id: str, build_id: str) -> bool:
disabled_ids = get_disabled_ids(bot)
disable_id = disable_id.lower()
build_id = build_id.lower()
if len(build_id) < 64:
build_id += "0" * (64 - len(build_id))
if not is_build_id_disabled(bot, build_id):
add_disable_id_if_necessary(disable_id, disabled_ids)
disabled_ids[disable_id]["build_id"] = build_id
set_disabled_ids(bot, disabled_ids)
return True
return False
def remove_disabled_app_id(bot, disable_id: str) -> bool:
disabled_ids = get_disabled_ids(bot)
disable_id = disable_id.lower()
if (
disable_id in disabled_ids.keys()
and len(disabled_ids[disable_id]["app_id"]) > 0
):
disabled_ids[disable_id]["app_id"] = ""
set_disabled_ids(bot, disabled_ids)
return True
return False
def remove_disabled_build_id(bot, disable_id: str) -> bool:
disabled_ids = get_disabled_ids(bot)
disable_id = disable_id.lower()
if (
disable_id in disabled_ids.keys()
and len(disabled_ids[disable_id]["build_id"]) > 0
):
disabled_ids[disable_id]["build_id"] = ""
set_disabled_ids(bot, disabled_ids)
return True
return False
def add_disabled_ro_section(
bot, disable_id: str, ro_section: dict[str, Union[str, list[str]]]
) -> bool:
disabled_ids = get_disabled_ids(bot)
disable_id = disable_id.lower()
add_disable_id_if_necessary(disable_id, disabled_ids)
if len(ro_section) > len(disabled_ids[disable_id]["ro_section"]):
for key, content in ro_section.items():
if key == "module":
disabled_ids[disable_id]["ro_section"][key] = content.lower()
else:
disabled_ids[disable_id]["ro_section"][key] = content
set_disabled_ids(bot, disabled_ids)
return True
return False
def remove_disabled_ro_section(bot, disable_id: str) -> bool:
disabled_ids = get_disabled_ids(bot)
disable_id = disable_id.lower()
if (
disable_id in disabled_ids.keys()
and len(disabled_ids[disable_id]["ro_section"]) > 0
):
disabled_ids[disable_id]["ro_section"] = {}
set_disabled_ids(bot, disabled_ids)
return True
return False

View file

@ -0,0 +1,47 @@
import json
import os
from robocop_ng.helpers.data_loader import read_json
def get_disabled_paths_path(bot) -> str:
return os.path.join(bot.state_dir, "data/disabled_paths.json")
def get_disabled_paths(bot) -> list[str]:
disabled_paths = read_json(bot, get_disabled_paths_path(bot))
if "paths" not in disabled_paths.keys():
return []
return disabled_paths["paths"]
def set_disabled_paths(bot, contents: list[str]):
with open(get_disabled_paths_path(bot), "w") as f:
json.dump({"paths": contents}, f)
def is_path_disabled(bot, path: str) -> bool:
for disabled_path in get_disabled_paths(bot):
if disabled_path in path.strip().lower():
return True
return False
def add_disabled_path(bot, disabled_path: str) -> bool:
disabled_path = disabled_path.strip().lower()
disabled_paths = get_disabled_paths(bot)
if disabled_path not in disabled_paths:
disabled_paths.append(disabled_path)
set_disabled_paths(bot, disabled_paths)
return True
return False
def remove_disabled_path(bot, disabled_path: str) -> bool:
disabled_path = disabled_path.strip().lower()
disabled_paths = get_disabled_paths(bot)
if disabled_path in disabled_paths:
disabled_paths.remove(disabled_path)
set_disabled_paths(bot, disabled_paths)
return True
return False

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
import json
import os
from typing import Union
from robocop_ng.helpers.data_loader import read_json
def get_invites_path(bot):
return os.path.join(bot.state_dir, "data/invites.json")
def get_invites(bot) -> dict[str, dict[str, Union[str, int]]]:
return read_json(bot, get_invites_path(bot))
def add_invite(bot, invite_id: str, url: str, max_uses: int, code: str):
invites = get_invites(bot)
invites[invite_id] = {
"uses": 0,
"url": url,
"max_uses": max_uses,
code: code,
}
set_invites(bot, invites)
def set_invites(bot, contents: dict[str, dict[str, Union[str, int]]]):
with open(get_invites_path(bot), "w") as f:
json.dump(contents, f)

View file

@ -0,0 +1,142 @@
import json
import os
from typing import Optional, Union
from robocop_ng.helpers.data_loader import read_json
def get_macros_path(bot):
return os.path.join(bot.state_dir, "data/macros.json")
def get_macros_dict(bot) -> dict[str, dict[str, Union[list[str], str]]]:
macros = read_json(bot, get_macros_path(bot))
if len(macros) > 0:
# Migration code
if "aliases" not in macros.keys():
new_macros = {"macros": macros, "aliases": {}}
unique_macros = set(new_macros["macros"].values())
for macro_text in unique_macros:
first_macro_key = ""
duplicate_num = 0
for key, macro in new_macros["macros"].copy().items():
if macro == macro_text and duplicate_num == 0:
first_macro_key = key
duplicate_num += 1
continue
elif macro == macro_text:
if first_macro_key not in new_macros["aliases"].keys():
new_macros["aliases"][first_macro_key] = []
new_macros["aliases"][first_macro_key].append(key)
del new_macros["macros"][key]
duplicate_num += 1
set_macros(bot, new_macros)
return new_macros
return macros
return {"macros": {}, "aliases": {}}
def is_macro_key_available(
bot, key: str, macros: dict[str, dict[str, Union[list[str], str]]] = None
) -> bool:
if macros is None:
macros = get_macros_dict(bot)
if key in macros["macros"].keys():
return False
for aliases in macros["aliases"].values():
if key in aliases:
return False
return True
def set_macros(bot, contents: dict[str, dict[str, Union[list[str], str]]]):
with open(get_macros_path(bot), "w") as f:
json.dump(contents, f)
def get_macro(bot, key: str) -> Optional[str]:
macros = get_macros_dict(bot)
key = key.lower()
if key in macros["macros"].keys():
return macros["macros"][key]
for main_key, aliases in macros["aliases"].items():
if key in aliases:
return macros["macros"][main_key]
return None
def add_macro(bot, key: str, message: str) -> bool:
macros = get_macros_dict(bot)
key = key.lower()
if is_macro_key_available(bot, key, macros):
macros["macros"][key] = message
set_macros(bot, macros)
return True
return False
def add_aliases(bot, key: str, aliases: list[str]) -> bool:
macros = get_macros_dict(bot)
key = key.lower()
success = False
if key in macros["macros"].keys():
for alias in aliases:
alias = alias.lower()
if is_macro_key_available(bot, alias, macros):
if key not in macros["aliases"].keys():
macros["aliases"][key] = []
macros["aliases"][key].append(alias)
success = True
if success:
set_macros(bot, macros)
return success
def edit_macro(bot, key: str, message: str) -> bool:
macros = get_macros_dict(bot)
key = key.lower()
if key in macros["macros"].keys():
macros["macros"][key] = message
set_macros(bot, macros)
return True
return False
def remove_aliases(bot, key: str, aliases: list[str]) -> bool:
macros = get_macros_dict(bot)
key = key.lower()
success = False
if key not in macros["aliases"].keys():
return False
for alias in aliases:
alias = alias.lower()
if alias in macros["aliases"][key]:
macros["aliases"][key].remove(alias)
if len(macros["aliases"][key]) == 0:
del macros["aliases"][key]
success = True
if success:
set_macros(bot, macros)
return success
def remove_macro(bot, key: str) -> bool:
macros = get_macros_dict(bot)
key = key.lower()
if key in macros["macros"].keys():
del macros["macros"][key]
set_macros(bot, macros)
return True
return False
def clear_aliases(bot, key: str) -> bool:
macros = get_macros_dict(bot)
key = key.lower()
if key in macros["macros"].keys() and key in macros["aliases"].keys():
del macros["aliases"][key]
set_macros(bot, macros)
return True
return False

View file

@ -0,0 +1,53 @@
import json
from typing import Optional, Union
from discord import Message, MessageReference, PartialMessage
MessageReferenceTypes = Union[Message, MessageReference, PartialMessage]
async def notify_management(
bot, message: str, reference_message: Optional[MessageReferenceTypes] = None
):
log_channel = await bot.get_channel_safe(bot.config.botlog_channel)
bot_manager_role = log_channel.guild.get_role(bot.config.bot_manager_role_id)
notification_message = f"{bot_manager_role.mention}:\n"
if reference_message is not None and reference_message.channel != log_channel:
notification_message += f"Message reference: {reference_message.jump_url}\n"
notification_message += message
return await log_channel.send(notification_message)
else:
notification_message += message
return await log_channel.send(
notification_message,
reference=reference_message,
mention_author=False,
)
async def report_critical_error(
bot,
error: BaseException,
reference_message: Optional[MessageReferenceTypes] = None,
additional_info: Optional[dict] = None,
):
message = "⛔ A critical error occurred!"
if additional_info is not None:
message += f"""
```json
{json.dumps(additional_info)}
```"""
message += f"""
Exception:
```
{error}
```"""
return await notify_management(bot, message, reference_message)

View file

@ -0,0 +1,47 @@
import json
import os
from robocop_ng.helpers.data_loader import read_json
def get_restrictions_path(bot):
return os.path.join(bot.state_dir, "data/restrictions.json")
def get_restrictions(bot):
return read_json(bot, get_restrictions_path(bot))
def set_restrictions(bot, contents):
with open(get_restrictions_path(bot), "w") as f:
f.write(contents)
def get_user_restrictions(bot, uid):
uid = str(uid)
rsts = get_restrictions(bot)
if uid in rsts:
return rsts[uid]
return []
def add_restriction(bot, uid, rst):
# mostly from kurisu source, credits go to ihaveamac
uid = str(uid)
rsts = get_restrictions(bot)
if uid not in rsts:
rsts[uid] = []
if rst not in rsts[uid]:
rsts[uid].append(rst)
set_restrictions(bot, json.dumps(rsts))
def remove_restriction(bot, uid, rst):
# mostly from kurisu source, credits go to ihaveamac
uid = str(uid)
rsts = get_restrictions(bot)
if uid not in rsts:
rsts[uid] = []
if rst in rsts[uid]:
rsts[uid].remove(rst)
set_restrictions(bot, json.dumps(rsts))

View file

@ -0,0 +1,43 @@
import json
import math
import os
from robocop_ng.helpers.data_loader import read_json
def get_crontab_path(bot):
return os.path.join(bot.state_dir, "data/robocronptab.json")
def get_crontab(bot):
return read_json(bot, get_crontab_path(bot))
def set_crontab(bot, contents):
with open(get_crontab_path(bot), "w") as f:
f.write(contents)
def add_job(bot, job_type, job_name, job_details, timestamp):
timestamp = str(math.floor(timestamp))
job_name = str(job_name)
ctab = get_crontab(bot)
if job_type not in ctab:
ctab[job_type] = {}
if timestamp not in ctab[job_type]:
ctab[job_type][timestamp] = {}
ctab[job_type][timestamp][job_name] = job_details
set_crontab(bot, json.dumps(ctab))
def delete_job(bot, timestamp, job_type, job_name):
timestamp = str(timestamp)
job_name = str(job_name)
ctab = get_crontab(bot)
del ctab[job_type][timestamp][job_name]
set_crontab(bot, json.dumps(ctab))

View file

@ -0,0 +1,33 @@
import json
import os.path
import os
from robocop_ng.helpers.data_loader import read_json
def get_persistent_roles_path(bot):
return os.path.join(bot.state_dir, "data/persistent_roles.json")
def get_persistent_roles(bot) -> dict[str, list[str]]:
return read_json(bot, get_persistent_roles_path(bot))
def set_persistent_roles(bot, contents: dict[str, list[str]]):
with open(get_persistent_roles_path(bot), "w") as f:
json.dump(contents, f)
def add_user_roles(bot, uid: int, roles: list[int]):
uid = str(uid)
roles = [str(x) for x in roles]
persistent_roles = get_persistent_roles(bot)
persistent_roles[uid] = roles
set_persistent_roles(bot, persistent_roles)
def get_user_roles(bot, uid: int) -> list[str]:
uid = str(uid)
persistent_roles = get_persistent_roles(bot)
return persistent_roles[uid] if uid in persistent_roles else []

View file

@ -0,0 +1,745 @@
import re
from enum import IntEnum, auto
from typing import Optional, Union
from robocop_ng.helpers.disabled_ids import is_build_id_valid
from robocop_ng.helpers.size import Size
class CommonError(IntEnum):
SHADER_CACHE_COLLISION = auto()
DUMP_HASH = auto()
SHADER_CACHE_CORRUPTION = auto()
UPDATE_KEYS = auto()
FILE_PERMISSIONS = auto()
FILE_NOT_FOUND = auto()
MISSING_SERVICES = auto()
VULKAN_OUT_OF_MEMORY = auto()
class RyujinxVersion(IntEnum):
MASTER = auto()
OLD_MASTER = auto()
LDN = auto()
MAC = auto()
PR = auto()
CUSTOM = auto()
class LogAnalyser:
_log_text: str
_log_errors: list[list[str]]
_hardware_info: dict[str, Optional[str]]
_emu_info: dict[str, Optional[str]]
_game_info: dict[str, Optional[str]]
_settings: dict[str, Optional[str]]
_notes: Union[set[str], list[str]]
@staticmethod
def is_homebrew(log_file: str) -> bool:
return (
re.search("Load.*Application: Loading as [Hh]omebrew", log_file) is not None
)
@staticmethod
def get_filepaths(log_file: str) -> set[str]:
return set(
x.rstrip("\u0000")
for x in re.findall(r"(?:[A-Za-z]:)?(?:[\\/]+[^\\/:\"\r\n]+)+", log_file)
)
@staticmethod
def get_main_ro_section(log_file: str) -> Optional[dict[str, str]]:
ro_section_matches = re.findall(
r"PrintRoSectionInfo: main:[\r\n]((?:\s+.*[\r\n])*)", log_file
)
if ro_section_matches and len(ro_section_matches) > 0:
ro_section_match: str = ro_section_matches[-1]
ro_section = {"module": "", "sdk_libraries": []}
if ro_section_match is None or len(ro_section_match) == 0:
return None
for line in ro_section_match.splitlines():
line = line.strip()
if line.startswith("Module:"):
ro_section["module"] = line[8:]
elif line.startswith("SDK Libraries:"):
ro_section["sdk_libraries"].append(line[19:])
elif line.startswith("SDK "):
ro_section["sdk_libraries"].append(line[4:])
else:
break
return ro_section
return None
@staticmethod
def get_app_info(
log_file: str,
) -> Optional[tuple[str, str, str, list[str], dict[str, str]]]:
game_name_match = re.findall(
r"Loader [A-Za-z]*: Application Loaded:\s([^;\n\r]*)",
log_file,
re.MULTILINE,
)
if game_name_match:
game_name = game_name_match[-1].rstrip()
app_id_match = re.match(r".* \[([a-zA-Z0-9]*)\]", game_name)
if app_id_match:
app_id = app_id_match.group(1).strip().upper()
else:
app_id = ""
bids_match_all = re.findall(
r"Build ids found for (?:title|application) ([a-zA-Z0-9]*):[\n\r]*((?:\s+.*[\n\r]+)+)",
log_file,
)
if bids_match_all and len(bids_match_all) > 0:
bids_match: tuple[str] = bids_match_all[-1]
app_id_from_bids = None
build_ids = None
if bids_match[0] is not None:
app_id_from_bids = bids_match[0].strip().upper()
if bids_match[1] is not None:
build_ids = [
bid.strip().upper()
for bid in bids_match[1].splitlines()
if is_build_id_valid(bid.strip())
]
return (
game_name,
app_id,
app_id_from_bids,
build_ids,
LogAnalyser.get_main_ro_section(log_file),
)
return None
@staticmethod
def contains_errors(search_terms, errors):
for term in search_terms:
for error_lines in errors:
line = "\n".join(error_lines)
if term in line:
return True
return False
def __init__(self, log_text: Union[str, list[str]]):
self.__init_members()
if isinstance(log_text, str):
self._log_text = log_text.replace("\r\n", "\n")
elif isinstance(log_text, list):
self._log_text = "\n".join(log_text)
else:
raise TypeError(log_text)
# Large files show a header value when not downloaded completely
# this regex makes sure that the log text to read starts from the first timestamp, ignoring headers
log_file_header_regex = re.compile(r"\d{2}:\d{2}:\d{2}\.\d{3}.*", re.DOTALL)
log_file_match = re.search(log_file_header_regex, self._log_text)
if log_file_match and log_file_match.group(0) is not None:
self._log_text = log_file_match.group(0)
else:
raise ValueError("No log entries found.")
self.__get_errors()
self.__get_hardware_info()
self.__get_settings_info()
self.__get_ryujinx_info()
self.__get_app_name()
self.__get_mods()
self.__get_cheats()
self.__get_notes()
def __init_members(self):
self._hardware_info = {
"cpu": "Unknown",
"gpu": "Unknown",
"ram": "Unknown",
"os": "Unknown",
}
self._emu_info = {
"ryu_version": "Unknown",
"ryu_firmware": "Unknown",
"logs_enabled": None,
}
self._game_info = {
"game_name": "Unknown",
"errors": "No errors found in log",
"mods": "No mods found",
"cheats": "No cheats found",
}
self._settings = {
"audio_backend": "Unknown",
"backend_threading": "Unknown",
"docked": "Unknown",
"expand_ram": "Unknown",
"fs_integrity": "Unknown",
"graphics_backend": "Unknown",
"ignore_missing_services": "Unknown",
"memory_manager": "Unknown",
"pptc": "Unknown",
"shader_cache": "Unknown",
"vsync": "Unknown",
"hypervisor": "Unknown",
"resolution_scale": "Unknown",
"anisotropic_filtering": "Unknown",
"aspect_ratio": "Unknown",
"texture_recompression": "Unknown",
}
self._notes = set()
self._log_errors = []
def __get_errors(self):
errors = []
curr_error_lines = []
error_line = False
for line in self._log_text.splitlines():
if len(line.strip()) == 0:
continue
if "|E|" in line:
curr_error_lines = [line]
errors.append(curr_error_lines)
error_line = True
elif error_line and line[0] == " ":
curr_error_lines.append(line)
if len(curr_error_lines) > 0:
errors.append(curr_error_lines)
self._log_errors = errors
def __get_hardware_info(self):
for setting in self._hardware_info.keys():
match setting:
case "cpu":
cpu_match = re.search(
r"CPU:\s([^;\n\r]*)", self._log_text, re.MULTILINE
)
if cpu_match is not None and cpu_match.group(1) is not None:
self._hardware_info[setting] = cpu_match.group(1).rstrip()
case "ram":
sizes = "|".join(Size.names())
ram_match = re.search(
rf"RAM: Total ([\d.]+) ({sizes}) ; Available ([\d.]+) ({sizes})",
self._log_text,
re.MULTILINE,
)
if ram_match is not None:
try:
dest_unit = Size.MiB
ram_available = float(ram_match.group(3))
ram_available = Size.from_name(ram_match.group(4)).convert(
ram_available, dest_unit
)
ram_total = float(ram_match.group(1))
ram_total = Size.from_name(ram_match.group(2)).convert(
ram_total, dest_unit
)
self._hardware_info[setting] = (
f"{ram_available:.0f}/{ram_total:.0f} {dest_unit.name}"
)
except ValueError:
# ram_match.group(1) or ram_match.group(3) couldn't be parsed as a float.
self._hardware_info[setting] = "Error"
case "os":
os_match = re.search(
r"Operating System:\s([^;\n\r]*)",
self._log_text,
re.MULTILINE,
)
if os_match is not None and os_match.group(1) is not None:
self._hardware_info[setting] = os_match.group(1).rstrip()
case "gpu":
gpu_match = re.search(
r"PrintGpuInformation:\s([^;\n\r]*)",
self._log_text,
re.MULTILINE,
)
if gpu_match is not None and gpu_match.group(1) is not None:
self._hardware_info[setting] = gpu_match.group(1).rstrip()
case _:
raise NotImplementedError(setting)
def __get_ryujinx_info(self):
for setting in self._emu_info.keys():
match setting:
case "ryu_version":
for line in self._log_text.splitlines():
if "Ryujinx Version:" in line:
self._emu_info[setting] = line.split()[-1].strip()
break
case "logs_enabled":
logs_match = re.search(
r"Logs Enabled:\s([^;\n\r]*)", self._log_text, re.MULTILINE
)
if logs_match is not None and logs_match.group(1) is not None:
self._emu_info[setting] = logs_match.group(1).rstrip()
case "ryu_firmware":
for line in self._log_text.splitlines():
if "Firmware Version:" in line:
self._emu_info[setting] = line.split()[-1].strip()
break
case _:
raise NotImplementedError(setting)
def __get_setting_value(self, name, key):
values = [
line.split()[-1]
for line in self._log_text.splitlines()
if re.search(rf"LogValueChange: ({key})\s", line)
]
if len(values) > 0:
value = values[-1]
else:
return None
match name:
case "docked":
return "Docked" if value == "True" else "Handheld"
case "resolution_scale":
resolution_map = {
"-1": "Custom",
"1": "Native (720p/1080p)",
"2": "2x (1440p/2160p)",
"3": "3x (2160p/3240p)",
"4": "4x (2880p/4320p)",
}
if value in resolution_map.keys():
return resolution_map[value]
else:
return "Custom"
case "anisotropic_filtering":
anisotropic_map = {
"-1": "Auto",
"2": "2x",
"4": "4x",
"8": "8x",
"16": "16x",
}
if value in anisotropic_map.keys():
return anisotropic_map[value]
else:
return "Auto"
case "aspect_ratio":
aspect_map = {
"Fixed4x3": "4:3",
"Fixed16x9": "16:9",
"Fixed16x10": "16:10",
"Fixed21x9": "21:9",
"Fixed32x9": "32:9",
"Stretched": "Stretch to Fit Window",
}
if value in aspect_map.keys():
return aspect_map[value]
else:
return "Unknown"
case "pptc" | "shader_cache" | "texture_recompression" | "vsync":
return "Enabled" if value == "True" else "Disabled"
case "hypervisor":
if "mac" in self._hardware_info["os"]:
return "Enabled" if value == "True" else "Disabled"
else:
return "N/A"
case _:
return value
def __get_settings_info(self):
settings_map = {
"anisotropic_filtering": "MaxAnisotropy",
"aspect_ratio": "AspectRatio",
"audio_backend": "AudioBackend",
"backend_threading": "BackendThreading",
"docked": "EnableDockedMode",
"expand_ram": "ExpandRam",
"fs_integrity": "EnableFsIntegrityChecks",
"graphics_backend": "GraphicsBackend",
"ignore_missing_services": "IgnoreMissingServices",
"memory_manager": "MemoryManagerMode",
"pptc": "EnablePtc",
"resolution_scale": "ResScale",
"shader_cache": "EnableShaderCache",
"texture_recompression": "EnableTextureRecompression",
"vsync": "EnableVsync",
"hypervisor": "UseHypervisor",
}
for key in self._settings.keys():
if key in settings_map:
self._settings[key] = self.__get_setting_value(key, settings_map[key])
else:
raise NotImplementedError(key)
def __get_mods(self):
mods_regex = re.compile(
r"Found\s(enabled|disabled)?\s?mod\s\'(.+?)\'\s(\[.+?\])"
)
matches = re.findall(mods_regex, self._log_text)
if matches:
mods = [
{"mod": match[1], "status": match[0], "type": match[2]}
for match in matches
]
mods_status = [
f" {i['mod']} ({'ExeFS' if i['type'] == '[E]' else 'RomFS'})"
for i in mods
if i["status"] == "" or i["status"] == "enabled"
]
# Remove duplicated mods from output
mods_status = list(dict.fromkeys(mods_status))
self._game_info["mods"] = "\n".join(mods_status)
def __get_cheats(self):
# Make sure to skip cheats which fail to compile
cheat_regex = re.compile(
r"Installing cheat\s'(.+)'(?!\s\d{2}:\d{2}:\d{2}\.\d{3}\s\|E\|\sTamperMachine\sCompile)"
)
matches = re.findall(cheat_regex, self._log_text)
if matches:
cheats = [f" {match}" for match in matches]
self._game_info["cheats"] = "\n".join(cheats)
def __get_app_name(self):
app_match = re.findall(
r"Loader [A-Za-z]*: Application Loaded:\s([^;\n\r]*)",
self._log_text,
re.MULTILINE,
)
if app_match:
self._game_info["game_name"] = app_match[-1].rstrip()
def __get_controller_notes(self):
controllers_regex = re.compile(r"Hid Configure: ([^\r\n]+)")
controllers = re.findall(controllers_regex, self._log_text)
if controllers:
input_status = [f" {match}" for match in controllers]
# Hid Configure lines can appear multiple times, so converting to dict keys removes duplicate entries,
# also maintains the list order
input_status = list(dict.fromkeys(input_status))
self._notes.add("\n".join(input_status))
# If emulator crashes on startup without game load, there is no need to show controller notification at all
elif self._game_info["game_name"] != "Unknown":
self._notes.add("⚠️ No controller information found")
def __get_os_notes(self):
if (
"Windows" in self._hardware_info["os"]
and self._settings["graphics_backend"] != "Vulkan"
):
if "Intel" in self._hardware_info["gpu"]:
self._notes.add(
"**⚠️ Intel iGPU users should consider using Vulkan graphics backend**"
)
if "AMD" in self._hardware_info["gpu"]:
self._notes.add(
"**⚠️ AMD GPU users should consider using Vulkan graphics backend**"
)
def __get_cpu_notes(self):
if "VirtualApple" in self._hardware_info["cpu"]:
self._notes.add("🔴 **Rosetta should be disabled**")
def __get_log_notes(self):
default_logs = ["Info", "Warning", "Error", "Guest"]
user_logs = []
if self._emu_info["logs_enabled"] is not None:
user_logs = (
self._emu_info["logs_enabled"].rstrip().replace(" ", "").split(",")
)
if "Debug" in user_logs:
self._notes.add(
"⚠️ **Debug logs enabled will have a negative impact on performance**"
)
disabled_logs = set(default_logs).difference(set(user_logs))
if disabled_logs:
logs_status = [f"⚠️ {log} log is not enabled" for log in disabled_logs]
log_string = "\n".join(logs_status)
else:
log_string = "✅ Default logs enabled"
self._notes.add(log_string)
def __get_settings_notes(self):
if self._settings["audio_backend"] == "Dummy":
self._notes.add(
"⚠️ Dummy audio backend, consider changing to SDL2 or OpenAL"
)
if self._settings["pptc"] == "Disabled":
self._notes.add("🔴 **PPTC cache should be enabled**")
if self._settings["shader_cache"] == "Disabled":
self._notes.add("🔴 **Shader cache should be enabled**")
if self._settings["expand_ram"] == "True":
self._notes.add(
"⚠️ `Use alternative memory layout` should only be enabled for 4K mods"
)
if self._settings["memory_manager"] == "SoftwarePageTable":
self._notes.add(
"🔴 **`Software` setting in Memory Manager Mode will give slower performance than the default setting of `Host unchecked`**"
)
if self._settings["ignore_missing_services"] == "True":
self._notes.add(
"⚠️ `Ignore Missing Services` being enabled can cause instability"
)
if self._settings["vsync"] == "Disabled":
self._notes.add(
"⚠️ V-Sync disabled can cause instability like games running faster than intended or longer load times"
)
if self._settings["fs_integrity"] == "Disabled":
self._notes.add(
"⚠️ Disabling file integrity checks may cause corrupted dumps to not be detected"
)
if self._settings["backend_threading"] == "Off":
self._notes.add(
"🔴 **Graphics Backend Multithreading should be set to `Auto`**"
)
def __sort_notes(self):
def severity(log_note_string):
symbols = ["", "🔴", "⚠️", "", ""]
return next(
i for i, symbol in enumerate(symbols) if symbol in log_note_string
)
game_notes = [note for note in self._notes]
# Warnings split on the string after the warning symbol for alphabetical ordering
# Severity key then orders alphabetically sorted warnings to show most severe first
return sorted(sorted(game_notes, key=lambda x: x.split()[1]), key=severity)
def __get_notes(self):
for common_error in self.get_common_errors():
match common_error:
case CommonError.SHADER_CACHE_COLLISION:
self._notes.add(
"⚠️ Cache collision detected. Investigate possible shader cache issues"
)
case CommonError.SHADER_CACHE_CORRUPTION:
self._notes.add(
"⚠️ Cache corruption detected. Investigate possible shader cache issues"
)
case CommonError.DUMP_HASH:
self._notes.add(
"⚠️ Dump error detected. Investigate possible bad game/firmware dump issues"
)
case CommonError.UPDATE_KEYS:
self._notes.add(
"⚠️ Keys or firmware out of date, consider updating them"
)
case CommonError.FILE_PERMISSIONS:
self._notes.add(
"⚠️ File permission error. Consider deleting save directory and allowing Ryujinx to make a new one"
)
case CommonError.FILE_NOT_FOUND:
self._notes.add(
"⚠️ Save not found error. Consider starting game without a save file or using a new save file"
)
case CommonError.MISSING_SERVICES:
if self._settings["ignore_missing_services"] == "False":
self._notes.add(
"⚠️ Consider enabling `Ignore Missing Services` in Ryujinx settings"
)
case CommonError.VULKAN_OUT_OF_MEMORY:
if self._settings["texture_recompression"] == "Disabled":
self._notes.add(
"⚠️ Consider enabling `Texture Recompression` in Ryujinx settings"
)
case _:
raise NotImplementedError(common_error)
timestamp_regex = re.compile(r"(\d{2}:\d{2}:\d{2}\.\d{3})\s+?\|")
latest_timestamp = re.findall(timestamp_regex, self._log_text)[-1]
if latest_timestamp:
timestamp_message = f" Time elapsed: `{latest_timestamp}`"
self._notes.add(timestamp_message)
if self.is_default_user_profile():
self._notes.add(
"⚠️ Default user profile in use, consider creating a custom one."
)
self.__get_controller_notes()
self.__get_os_notes()
self.__get_cpu_notes()
if (
self._emu_info["ryu_firmware"] == "Unknown"
and self._game_info["game_name"] != "Unknown"
):
firmware_warning = f"**❌ Nintendo Switch firmware not found**"
self._notes.add(firmware_warning)
self.__get_settings_notes()
if self.get_ryujinx_version() == RyujinxVersion.CUSTOM:
self._notes.add("**⚠️ Custom builds are not officially supported**")
def get_ryujinx_version(self):
mainline_version = re.compile(r"^\d\.\d\.\d+$")
old_mainline_version = re.compile(r"^\d\.\d\.(\d){4}$")
pr_version = re.compile(r"^\d\.\d\.\d\+([a-f]|\d){7}$")
ldn_version = re.compile(r"^\d\.\d\.\d-ldn\d+\.\d+(?:\.\d+|$)")
mac_version = re.compile(r"^\d\.\d\.\d-macos\d+(?:\.\d+(?:\.\d+|$)|$)")
if re.match(mainline_version, self._emu_info["ryu_version"]):
return RyujinxVersion.MASTER
elif re.match(old_mainline_version, self._emu_info["ryu_version"]):
return RyujinxVersion.OLD_MASTER
elif re.match(mac_version, self._emu_info["ryu_version"]):
return RyujinxVersion.MAC
elif re.match(ldn_version, self._emu_info["ryu_version"]):
return RyujinxVersion.LDN
elif re.match(pr_version, self._emu_info["ryu_version"]):
return RyujinxVersion.PR
else:
return RyujinxVersion.CUSTOM
def is_default_user_profile(self) -> bool:
return (
re.search(r"UserId: 00000000000000010000000000000000", self._log_text)
is not None
)
def get_last_error(self) -> Optional[list[str]]:
return self._log_errors[-1] if len(self._log_errors) > 0 else None
def get_common_errors(self) -> list[CommonError]:
errors = []
if self.contains_errors(["Cache collision found"], self._log_errors):
errors.append(CommonError.SHADER_CACHE_COLLISION)
if self.contains_errors(
[
"ResultFsInvalidIvfcHash",
"ResultFsNonRealDataVerificationFailed",
],
self._log_errors,
):
errors.append(CommonError.DUMP_HASH)
if self.contains_errors(
[
"Ryujinx.Graphics.Gpu.Shader.ShaderCache.Initialize()",
"System.IO.InvalidDataException: End of Central Directory record could not be found",
"ICSharpCode.SharpZipLib.Zip.ZipException: Cannot find central directory",
],
self._log_errors,
):
errors.append(CommonError.SHADER_CACHE_CORRUPTION)
if self.contains_errors(["MissingKeyException"], self._log_errors):
errors.append(CommonError.UPDATE_KEYS)
if self.contains_errors(["ResultFsPermissionDenied"], self._log_errors):
errors.append(CommonError.FILE_PERMISSIONS)
if self.contains_errors(["ResultFsTargetNotFound"], self._log_errors):
errors.append(CommonError.FILE_NOT_FOUND)
if self.contains_errors(["ServiceNotImplementedException"], self._log_errors):
errors.append(CommonError.MISSING_SERVICES)
if self.contains_errors(["ErrorOutOfDeviceMemory"], self._log_errors):
errors.append(CommonError.VULKAN_OUT_OF_MEMORY)
return errors
def analyse_discord(
self, is_channel_allowed: bool, pr_channel: int
) -> dict[str, dict[str, str]]:
last_error = self.get_last_error()
if last_error is not None:
last_error = "\n".join(last_error[:2])
self._game_info["errors"] = f"```\n{last_error}\n```"
else:
self._game_info["errors"] = "No errors found in log"
# Limit mods and cheats to 5 entries
mods = self._game_info["mods"].splitlines()
cheats = self._game_info["cheats"].splitlines()
if len(mods) > 5:
limit_mods = mods[:5]
limit_mods.append(f"✂️ {len(mods) - 5} other mods")
self._game_info["mods"] = "\n".join(limit_mods)
if len(cheats) > 5:
limit_cheats = cheats[:5]
limit_cheats.append(f"✂️ {len(cheats) - 5} other cheats")
self._game_info["cheats"] = "\n".join(limit_cheats)
if is_channel_allowed and self.get_ryujinx_version() == RyujinxVersion.PR:
self._notes.add(
f"**⚠️ PR build logs should be posted in <#{pr_channel}> if reporting bugs or tests**"
)
self._notes = self.__sort_notes()
full_game_info = self._game_info
full_game_info["notes"] = (
"\n".join(self._notes) if len(self._notes) > 0 else "Nothing to note"
)
return {
"hardware_info": self._hardware_info,
"emu_info": self._emu_info,
"game_info": full_game_info,
"settings": self._settings,
}
def analyse(self) -> dict[str, Union[dict[str, str], list[str], list[list[str]]]]:
self._notes = list(self.__sort_notes())
last_error = self.get_last_error()
if last_error is not None:
last_error = "\n".join(last_error[:2])
self._game_info["errors"] = f"```\n{last_error}\n```"
else:
self._game_info["errors"] = "No errors found in log"
return {
"hardware_info": self._hardware_info,
"emu_info": self._emu_info,
"game_info": self._game_info,
"notes": self._notes,
"errors": self._log_errors,
"settings": self._settings,
"app_info": self.get_app_info(self._log_text),
"paths": list(self.get_filepaths(self._log_text)),
}
if __name__ == "__main__":
import argparse
import json
import os
parser = argparse.ArgumentParser()
parser.add_argument("log_file", type=str)
args = parser.parse_args()
if not os.path.isfile(args.log_file):
print(f"Couldn't find log file: {args.log_file}")
exit(1)
with open(args.log_file, "r") as file:
text = file.read()
analyser = LogAnalyser(text)
result = analyser.analyse()
print(json.dumps(result, indent=2))

View file

@ -0,0 +1,53 @@
from enum import IntEnum, auto
from typing import Self
class Size(IntEnum):
KB = auto()
KiB = auto()
MB = auto()
MiB = auto()
GB = auto()
GiB = auto()
@classmethod
def names(cls) -> list[str]:
return [size.name for size in cls]
@classmethod
def from_name(cls, name: str) -> Self:
for size in cls:
if size.name.lower() == name.lower():
return size
raise ValueError(f"No matching member found for: {name}")
@property
def _is_si_unit(self) -> bool:
return self.value % 2 != 0
@property
def _unit_value(self) -> int:
return self.value // 2 + (1 if self._is_si_unit else 0)
@property
def _base_factor(self) -> int:
return 10**3 if self._is_si_unit else 2**10
@property
def _byte_factor(self) -> int:
return (
10 ** (3 * self._unit_value)
if self._is_si_unit
else 2 ** (10 * self._unit_value)
)
def convert(self, value: float, fmt: Self) -> float:
if self == fmt:
return value
if self._is_si_unit == fmt._is_si_unit:
if self < fmt:
return value / self._base_factor ** (fmt._unit_value - self._unit_value)
else:
return value * self._base_factor ** (self._unit_value - fmt._unit_value)
else:
return value * (self._byte_factor / fmt._byte_factor)

View file

@ -0,0 +1,70 @@
import json
import os
import time
from robocop_ng.helpers.data_loader import read_json
userlog_event_types = {
"warns": "Warn",
"bans": "Ban",
"kicks": "Kick",
"mutes": "Mute",
"notes": "Note",
}
def get_userlog_path(bot):
return os.path.join(bot.state_dir, "data/userlog.json")
def get_userlog(bot):
return read_json(bot, get_userlog_path(bot))
def set_userlog(bot, contents):
with open(get_userlog_path(bot), "w") as f:
f.write(contents)
def fill_userlog(bot, userid, uname):
userlogs = get_userlog(bot)
uid = str(userid)
if uid not in userlogs:
userlogs[uid] = {
"warns": [],
"mutes": [],
"kicks": [],
"bans": [],
"notes": [],
"watch": False,
"name": "n/a",
}
if uname:
userlogs[uid]["name"] = uname
return userlogs, uid
def userlog(bot, uid, issuer, reason, event_type, uname: str = ""):
userlogs, uid = fill_userlog(bot, uid, uname)
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
log_data = {
"issuer_id": issuer.id,
"issuer_name": f"{issuer}",
"reason": reason,
"timestamp": timestamp,
}
if event_type not in userlogs[uid]:
userlogs[uid][event_type] = []
userlogs[uid][event_type].append(log_data)
set_userlog(bot, json.dumps(userlogs))
return len(userlogs[uid][event_type])
def setwatch(bot, uid, issuer, watch_state, uname: str = ""):
userlogs, uid = fill_userlog(bot, uid, uname)
userlogs[uid]["watch"] = watch_state
set_userlog(bot, json.dumps(userlogs))
return

4
shell.nix Normal file
View file

@ -0,0 +1,4 @@
(import (fetchTarball
"https://github.com/edolstra/flake-compat/archive/master.tar.gz") {
src = builtins.fetchGit ./.;
}).shellNix