Compare commits

..

148 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
63 changed files with 4630 additions and 1724 deletions

View file

@ -1 +1,8 @@
config.py
**/__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

View file

@ -1,12 +1,12 @@
FROM python:alpine
FROM python:3.10-alpine
WORKDIR /usr/src/app
COPY requirements.txt ./
RUN apk add --no-cache gcc musl-dev python3-dev libffi-dev openssl-dev cargo && pip install --no-cache-dir -r requirements.txt && apk del gcc musl-dev python3-dev libffi-dev openssl-dev cargo
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/robocop_ng
WORKDIR /usr/src/app
CMD [ "python", "./__init__.py" ]
CMD [ "python", "-m", "robocop_ng", "/state" ]

View file

@ -1,22 +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/a/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 `robocop_ng/config_template.py` to `robocop_ng/config.py`, configure all necessary parts to your server.
- Enable the `Server Members` privileged intent ([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 Robocop-NG is not designed to run at >1 guild at once, let alone >100.
- (obviously) Add the bot to your guild. Many resources about this online.
- 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.
- Install python3.6+.
- Install python dependencies (`pip3 install -Ur requirements.txt`, you might need to put `sudo -H` before that). You can also install with [poetry](https://python-poetry.org/) with just `poetry install`.
- If you're moving from Kurisu or Robocop: Follow `Tips for people moving from Kurisu/Robocop` below.
- Run `robocop_ng/__init__.py` (`cd robocop_ng;python3 __init__.py`). Alternatively, if you did `poetry install`, run `python3 -m robocop_ng` in the same directory as your config files.
- 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.
@ -26,7 +47,7 @@ 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 `'`).
@ -34,7 +55,7 @@ If you're moving from Kurisu/Robocop, and want to preserve your data, you'll wan
## Contributing
Contributions are welcome. If you're unsure if your PR would be merged or not, either open an issue, ask on ReSwitched off-topic pinging ave or DM ave.
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.
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 .`.
@ -42,13 +63,14 @@ You're expected to use [black](https://github.com/psf/black) for code formatting
## Credits
Robocop-NG was initially developed by @aveao and @tumGER. It is currently maintained by @aveao. Similarly, the official robocop-ng on reswitched discord guild is hosted by @aveao too.
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).
I (ave) would like to thank the following, in no particular order:
[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.
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
- 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 in any way (reporting a bug, sending a PR, forking and hosting their own at their own guild, etc).
- 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).

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;
});
}

882
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "robocop_ng"
version = "1.0.0"
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"
@ -8,16 +8,16 @@ readme = "README.md"
repository = "https://github.com/reswitched/robocop-ng"
[tool.poetry.dependencies]
python = "^3.8"
"discord.py" = "^1.7.3"
python = "^3.11"
python-dateutil = "^2.8.2"
humanize = "^3.10.0"
humanize = "^4.8.0"
parsedatetime = "^2.6"
aiohttp = "^3.7.4"
gidgethub = "^5.0.1"
aiohttp = "^3.9.3"
gidgethub = "^5.3.0"
"discord.py" = "^2.3.2"
[tool.poetry.dev-dependencies]
[build-system]
requires = ["poetry-core>=1.0.0"]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View file

@ -1,216 +0,0 @@
aiohttp==3.7.4.post0; python_version >= "3.6" \
--hash=sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5 \
--hash=sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8 \
--hash=sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95 \
--hash=sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290 \
--hash=sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f \
--hash=sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809 \
--hash=sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe \
--hash=sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287 \
--hash=sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc \
--hash=sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87 \
--hash=sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0 \
--hash=sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970 \
--hash=sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f \
--hash=sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde \
--hash=sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c \
--hash=sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8 \
--hash=sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f \
--hash=sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5 \
--hash=sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf \
--hash=sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df \
--hash=sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213 \
--hash=sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4 \
--hash=sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009 \
--hash=sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5 \
--hash=sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013 \
--hash=sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16 \
--hash=sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5 \
--hash=sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b \
--hash=sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd \
--hash=sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439 \
--hash=sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22 \
--hash=sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a \
--hash=sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb \
--hash=sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb \
--hash=sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9 \
--hash=sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe \
--hash=sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf
async-timeout==3.0.1; python_version >= "3.6" and python_full_version >= "3.5.3" \
--hash=sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f \
--hash=sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3
attrs==21.2.0; python_version >= "3.6" and python_full_version >= "3.5.3" \
--hash=sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1 \
--hash=sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb
cffi==1.14.6; python_version >= "3.6" \
--hash=sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c \
--hash=sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99 \
--hash=sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819 \
--hash=sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20 \
--hash=sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224 \
--hash=sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7 \
--hash=sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33 \
--hash=sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534 \
--hash=sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a \
--hash=sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5 \
--hash=sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca \
--hash=sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218 \
--hash=sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f \
--hash=sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872 \
--hash=sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195 \
--hash=sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d \
--hash=sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b \
--hash=sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb \
--hash=sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a \
--hash=sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e \
--hash=sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5 \
--hash=sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf \
--hash=sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69 \
--hash=sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56 \
--hash=sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c \
--hash=sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762 \
--hash=sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771 \
--hash=sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a \
--hash=sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0 \
--hash=sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e \
--hash=sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346 \
--hash=sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc \
--hash=sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd \
--hash=sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc \
--hash=sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548 \
--hash=sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156 \
--hash=sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d \
--hash=sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e \
--hash=sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c \
--hash=sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202 \
--hash=sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f \
--hash=sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87 \
--hash=sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728 \
--hash=sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2 \
--hash=sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd
chardet==4.0.0; python_version >= "3.6" and python_full_version >= "3.5.3" \
--hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5 \
--hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa
cryptography==3.4.7; python_version >= "3.6" \
--hash=sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1 \
--hash=sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250 \
--hash=sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2 \
--hash=sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6 \
--hash=sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959 \
--hash=sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d \
--hash=sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca \
--hash=sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873 \
--hash=sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d \
--hash=sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177 \
--hash=sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9 \
--hash=sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713
discord.py==1.7.3; python_full_version >= "3.5.3" \
--hash=sha256:c6f64db136de0e18e090f6752ea68bdd4ab0a61b82dfe7acecefa22d6477bb0c \
--hash=sha256:462cd0fe307aef8b29cbfa8dd613e548ae4b2cb581d46da9ac0d46fb6ea19408
gidgethub==5.0.1; python_version >= "3.6" \
--hash=sha256:67245e93eb0918b37df038148af675df43b62e832c529d7f859f6b90d9f3e70d \
--hash=sha256:3efbd6998600254ec7a2869318bd3ffde38edc3a0d37be0c14bc46b45947b682
humanize==3.10.0; python_version >= "3.6" \
--hash=sha256:aab7625d62dd5e0a054c8413a47d1fa257f3bdd8e9a2442c2fe36061bdd1d9bf \
--hash=sha256:b2413730ce6684f85e0439a5b80b8f402e09f03e16ab8023d1da758c6ff41148
idna==3.2; python_version >= "3.6" and python_full_version >= "3.5.3" \
--hash=sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a \
--hash=sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3
multidict==5.1.0; python_version >= "3.6" and python_full_version >= "3.5.3" \
--hash=sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f \
--hash=sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf \
--hash=sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281 \
--hash=sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d \
--hash=sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d \
--hash=sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da \
--hash=sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224 \
--hash=sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26 \
--hash=sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6 \
--hash=sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76 \
--hash=sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a \
--hash=sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f \
--hash=sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348 \
--hash=sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93 \
--hash=sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9 \
--hash=sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37 \
--hash=sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5 \
--hash=sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632 \
--hash=sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952 \
--hash=sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79 \
--hash=sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456 \
--hash=sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7 \
--hash=sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635 \
--hash=sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a \
--hash=sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea \
--hash=sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656 \
--hash=sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3 \
--hash=sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93 \
--hash=sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647 \
--hash=sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d \
--hash=sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8 \
--hash=sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1 \
--hash=sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841 \
--hash=sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda \
--hash=sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80 \
--hash=sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359 \
--hash=sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5
parsedatetime==2.6 \
--hash=sha256:cb96edd7016872f58479e35879294258c71437195760746faffedb692aef000b \
--hash=sha256:4cb368fbb18a0b7231f4d76119165451c8d2e35951455dfee97c62a87b04d455
pycparser==2.20; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0" \
--hash=sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705 \
--hash=sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0
pyjwt==2.1.0; python_version >= "3.6" \
--hash=sha256:934d73fbba91b0483d3857d1aff50e96b2a892384ee2c17417ed3203f173fca1 \
--hash=sha256:fba44e7898bbca160a2b2b501f492824fc8382485d3a6f11ba5d0c1937ce6130
python-dateutil==2.8.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") \
--hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \
--hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9
six==1.16.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" \
--hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 \
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926
typing-extensions==3.10.0.0; python_version >= "3.6" and python_full_version >= "3.5.3" \
--hash=sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497 \
--hash=sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84 \
--hash=sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342
uritemplate==3.0.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" \
--hash=sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f \
--hash=sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae
yarl==1.6.3; python_version >= "3.6" and python_full_version >= "3.5.3" \
--hash=sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434 \
--hash=sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478 \
--hash=sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6 \
--hash=sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e \
--hash=sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406 \
--hash=sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76 \
--hash=sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366 \
--hash=sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721 \
--hash=sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643 \
--hash=sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e \
--hash=sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3 \
--hash=sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8 \
--hash=sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a \
--hash=sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c \
--hash=sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f \
--hash=sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970 \
--hash=sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e \
--hash=sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50 \
--hash=sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2 \
--hash=sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec \
--hash=sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71 \
--hash=sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc \
--hash=sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959 \
--hash=sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2 \
--hash=sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2 \
--hash=sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896 \
--hash=sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a \
--hash=sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e \
--hash=sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724 \
--hash=sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c \
--hash=sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25 \
--hash=sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96 \
--hash=sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0 \
--hash=sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4 \
--hash=sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424 \
--hash=sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6 \
--hash=sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10

View file

@ -1,15 +1,23 @@
import asyncio
import logging.handlers
import os
import sys
import logging
import logging.handlers
import traceback
import aiohttp
import config
import aiohttp
import discord
from discord.ext import commands
from discord.ext.commands import CommandError, Context
# TODO: check __name__ for __main__ nerd
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]
@ -46,11 +54,24 @@ wanted_jsons = [
"data/robocronptab.json",
"data/userlog.json",
"data/invites.json",
"data/macros.json",
"data/persistent_roles.json",
"data/disabled_ids.json",
]
intents = discord.Intents.default()
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
intents.members = True
bot = commands.Bot(
command_prefix=get_prefix, description=config.bot_description, intents=intents
@ -60,15 +81,19 @@ 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
if __name__ == "__main__":
for cog in config.initial_cogs:
try:
bot.load_extension(cog)
except:
log.error(f"Failed to load cog {cog}.")
log.error(traceback.print_exc())
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
@ -76,7 +101,7 @@ 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)
bot.botlog_channel = await bot.get_channel_safe(config.botlog_channel)
log.info(
f"\nLogged in as: {bot.user.name} - "
@ -115,12 +140,31 @@ async def on_command(ctx):
@bot.event
async def on_error(event_method, *args, **kwargs):
log.error(f"Error on {event_method}: {sys.exc_info()}")
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, error):
async def on_command_error(ctx: Context, error: CommandError):
error_text = str(error)
err_msg = (
@ -129,7 +173,7 @@ async def on_command_error(ctx, error):
f"of type {type(error)}: {error_text}"
)
log.error(err_msg)
log.exception(err_msg)
if not isinstance(error, commands.CommandNotFound):
err_msg = bot.escape_message(err_msg)
@ -187,6 +231,7 @@ async def on_command_error(ctx, error):
# 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):
@ -226,12 +271,26 @@ async def on_message(message):
await bot.invoke(ctx)
if not os.path.exists("data"):
os.makedirs("data")
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)
for wanted_json in wanted_jsons:
if not os.path.exists(wanted_json):
with open(wanted_json, "w") as f:
f.write("{}")
log.info(f"\nInvite URL: {invite_url}\n")
bot.run(config.token, bot=True, reconnect=True)
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())

View file

@ -1,11 +1,12 @@
import inspect
import re
import traceback
import discord
from discord.ext import commands
from discord.ext.commands import Cog
import traceback
import inspect
import re
import config
from helpers.checks import check_if_bot_manager
from robocop_ng.helpers.checks import check_if_bot_manager
class Admin(Cog):
@ -20,7 +21,7 @@ class Admin(Cog):
async def _exit(self, ctx):
"""Shuts down the bot, bot manager only."""
await ctx.send(":wave: Goodbye!")
await self.bot.logout()
await self.bot.close()
@commands.guild_only()
@commands.check(check_if_bot_manager)
@ -92,7 +93,7 @@ class Admin(Cog):
async def cog_load_actions(self, cog_name):
if cog_name == "verification":
verif_channel = self.bot.get_channel(config.welcome_channel)
verif_channel = self.bot.get_channel(self.bot.config.welcome_channel)
await self.bot.do_resetalgo(verif_channel, "cog load")
@commands.guild_only()
@ -106,13 +107,13 @@ class Admin(Cog):
if auto:
cogs_to_reload = re.findall(r"cogs/([a-z_]*).py[ ]*\|", git_output)
for cog in cogs_to_reload:
cog_name = "cogs." + cog
if cog_name not in config.initial_cogs:
cog_name = "robocop_ng.cogs." + cog
if cog_name not in self.bot.config.initial_cogs:
continue
try:
self.bot.unload_extension(cog_name)
self.bot.load_extension(cog_name)
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)
@ -129,7 +130,7 @@ class Admin(Cog):
async def load(self, ctx, ext: str):
"""Loads a cog, bot manager only."""
try:
self.bot.load_extension("cogs." + ext)
await self.bot.load_extension("robocop_ng.cogs." + ext)
await self.cog_load_actions(ext)
except:
await ctx.send(
@ -145,7 +146,7 @@ class Admin(Cog):
@commands.command()
async def unload(self, ctx, ext: str):
"""Unloads a cog, bot manager only."""
self.bot.unload_extension("cogs." + ext)
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.")
@ -159,8 +160,8 @@ class Admin(Cog):
self.lastreload = ext
try:
self.bot.unload_extension("cogs." + ext)
self.bot.load_extension("cogs." + ext)
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(
@ -172,5 +173,5 @@ class Admin(Cog):
await ctx.send(f":white_check_mark: `{ext}` successfully reloaded.")
def setup(bot):
bot.add_cog(Admin(bot))
async def setup(bot):
await bot.add_cog(Admin(bot))

View file

@ -1,5 +1,5 @@
import time
import config
import discord
from discord.ext import commands
from discord.ext.commands import Cog
@ -24,7 +24,7 @@ class Basic(Cog):
@commands.cooldown(1, 10, type=commands.BucketType.user)
@commands.command(name="dec")
async def _dec(self, ctx, num):
"""Converts base 10 to 16"""
"""Converts base 16 to 10"""
await ctx.send(f"{ctx.author.mention}: {int(num, 16)}")
@commands.guild_only()
@ -37,10 +37,12 @@ class Basic(Cog):
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
title="Robocop-NG",
url=self.bot.config.source_url,
description=self.bot.config.embed_desc,
)
embed.set_thumbnail(url=self.bot.user.avatar_url)
embed.set_thumbnail(url=str(self.bot.user.display_avatar))
await ctx.send(embed=embed)
@ -56,12 +58,10 @@ class Basic(Cog):
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`"
)
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)
def setup(bot):
bot.add_cog(Basic(bot))
async def setup(bot):
await bot.add_cog(Basic(bot))

View file

@ -1,4 +1,3 @@
import config
from discord.ext import commands
from discord.ext.commands import Cog
@ -11,7 +10,7 @@ class BasicReswitched(Cog):
@commands.command()
async def communitycount(self, ctx):
"""Prints the community member count of the server."""
community = ctx.guild.get_role(config.named_roles["community"])
community = ctx.guild.get_role(self.bot.config.named_roles["community"])
await ctx.send(
f"{ctx.guild.name} has {len(community.members)} community members!"
)
@ -20,11 +19,11 @@ class BasicReswitched(Cog):
@commands.command()
async def hackercount(self, ctx):
"""Prints the hacker member count of the server."""
h4x0r = ctx.guild.get_role(config.named_roles["hacker"])
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!"
)
def setup(bot):
bot.add_cog(BasicReswitched(bot))
async def setup(bot):
await bot.add_cog(BasicReswitched(bot))

View file

@ -191,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,9 +1,10 @@
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):
@ -66,7 +67,7 @@ class Err(Cog):
@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]
@ -94,10 +95,9 @@ class Err(Cog):
@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)
@ -142,7 +142,7 @@ class Err(Cog):
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")
@ -168,7 +168,7 @@ class Err(Cog):
@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])
@ -182,7 +182,7 @@ class Err(Cog):
@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)
@ -194,5 +194,5 @@ class Err(Cog):
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

@ -1,87 +0,0 @@
import discord
from discord.ext import commands
from discord.ext.commands import Cog
from helpers.checks import check_if_staff_or_ot
import textwrap
import PIL.Image
import PIL.ImageFilter
import PIL.ImageOps
import PIL.ImageFont
import PIL.ImageDraw
class ImageManip(Cog):
def __init__(self, bot):
self.bot = bot
@commands.cooldown(1, 60 * 60 * 3, type=commands.BucketType.user)
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True)
async def cox(self, ctx, *, headline: str):
"""Gives a cox headline"""
mention = ctx.author.mention
headline = await commands.clean_content(fix_channel_mentions=True).convert(
ctx, headline
)
in_vice = "assets/motherboardlogo.png"
in_byjcox = "assets/byjcox.png"
font_path = "assets/neue-haas-grotesk-display-bold-regular.otf"
# Settings for image generation, don't touch anything
horipos = 18
vertpos = 75
line_spacing = 10
font_size = 50
image_width = 800
font_wrap_count = 30
sig_height = 15
# Wrap into lines
lines = textwrap.wrap(headline, width=font_wrap_count)
# not great, 4am be like
image_height = (len(lines) + 2) * (vertpos + line_spacing)
# Load font
f = PIL.ImageFont.truetype(font_path, font_size)
# Create image base, paste mobo logo
im = PIL.Image.new("RGB", (image_width, image_height), color="#FFFFFF")
moboim = PIL.Image.open(in_vice)
im.paste(moboim, (horipos, 17))
# Go through all the wrapped text lines
for line in lines:
# Get size of the text by font, create a new image of that size
size = f.getsize(line)
txt = PIL.Image.new("L", size)
# Draw the text
d = PIL.ImageDraw.Draw(txt)
d.text((0, 0), line, font=f, fill=255)
# Paste the text into the base image
w = txt.rotate(0, expand=1)
im.paste(
PIL.ImageOps.colorize(w, (0, 0, 0), (0, 0, 0)), (horipos, vertpos), w
)
# Calculate position on next line
vertpos += size[1] + line_spacing
# Add jcox signature
jcoxim = PIL.Image.open(in_byjcox)
im.paste(jcoxim, (horipos, vertpos + sig_height))
# Crop the image to the actual resulting size
im = im.crop((0, 0, image_width, vertpos + (sig_height * 3)))
# Save image
out_filename = f"/tmp/{ctx.message.id}-out.png"
im.save(out_filename, quality=100, optimize=True)
await ctx.send(content=f"{mention}: Enjoy.", file=discord.File(out_filename))
def setup(bot):
bot.add_cog(ImageManip(bot))

View file

@ -1,8 +1,12 @@
import json
import os
import discord
from discord.ext import commands
from discord.ext.commands import Cog
from helpers.checks import check_if_collaborator
import config
import json
from robocop_ng.helpers.checks import check_if_collaborator
from robocop_ng.helpers.invites import add_invite
class Invites(Cog):
@ -13,25 +17,14 @@ class Invites(Cog):
@commands.guild_only()
@commands.check(check_if_collaborator)
async def invite(self, ctx):
welcome_channel = self.bot.get_channel(config.welcome_channel)
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
)
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))
add_invite(self.bot, invite.id, invite.url, 1, invite.code)
await ctx.message.add_reaction("🆗")
try:
@ -43,5 +36,5 @@ class Invites(Cog):
)
def setup(bot):
bot.add_cog(Invites(bot))
async def setup(bot):
await bot.add_cog(Invites(bot))

View file

@ -33,5 +33,5 @@ class Legacy(Cog):
)
def setup(bot):
bot.add_cog(Legacy(bot))
async def setup(bot):
await bot.add_cog(Legacy(bot))

View file

@ -1,5 +1,4 @@
import discord
import config
from discord.ext import commands
from discord.ext.commands import Cog
@ -37,13 +36,13 @@ class Links(Cog):
@commands.command(hidden=True, aliases=["guides", "link"])
async def guide(self, ctx):
"""Link to the guides"""
await ctx.send(config.links_guide_text)
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 {config.source_url}. "
f"You can find my source at {self.bot.config.source_url}. "
"Serious PRs and issues welcome!"
)
@ -54,7 +53,7 @@ class Links(Cog):
targetuser = ctx.author
await ctx.send(
f"{targetuser.mention}: A link to the rules "
f"can be found here: {config.rules_url}"
f"can be found here: {self.bot.config.rules_url}"
)
@commands.command()
@ -76,5 +75,5 @@ class Links(Cog):
)
def setup(bot):
bot.add_cog(Links(bot))
async def setup(bot):
await bot.add_cog(Links(bot))

View file

@ -1,8 +1,7 @@
import config
import discord
import io
import urllib.parse
import os.path
import discord
from discord.ext import commands
from discord.ext.commands import Cog
@ -18,7 +17,7 @@ class Lists(Cog):
# Helpers
def check_if_target_is_staff(self, target):
return any(r.id in config.staff_role_ids for r in target.roles)
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] == "📝"
@ -83,7 +82,7 @@ class Lists(Cog):
fields = embeds[0].fields
for field in fields:
if field.name == "Message ID":
files_channel = self.bot.get_channel(config.list_files_channel)
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()
@ -132,7 +131,7 @@ class Lists(Cog):
await ctx.send(f"Number must be greater than 0.")
return
if channel.id not in config.list_channels:
if channel.id not in self.bot.config.list_channels:
await ctx.send(f"{channel.mention} is not a list channel.")
return
@ -159,7 +158,7 @@ class Lists(Cog):
await self.bot.wait_until_ready()
# We only care about reactions in Rules, and Support FAQ
if payload.channel_id not in config.list_channels:
if payload.channel_id not in self.bot.config.list_channels:
return
channel = self.bot.get_channel(payload.channel_id)
@ -200,8 +199,8 @@ class Lists(Cog):
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 config.list_files_channel != 0:
files_channel = self.bot.get_channel(config.list_files_channel)
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",
@ -220,7 +219,7 @@ class Lists(Cog):
await self.bot.wait_until_ready()
# We only care about reactions in Rules, and Support FAQ
if payload.channel_id not in config.list_channels:
if payload.channel_id not in self.bot.config.list_channels:
return
channel = self.bot.get_channel(payload.channel_id)
@ -231,7 +230,7 @@ class Lists(Cog):
return
# We want to remove the embed we added.
if self.is_edit(payload.emoji) and config.list_files_channel != 0:
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()
@ -239,7 +238,7 @@ class Lists(Cog):
await self.bot.wait_until_ready()
# We only care about messages in Rules, and Support FAQ
if message.channel.id not in config.list_channels:
if message.channel.id not in self.bot.config.list_channels:
return
# We don"t care about messages from bots.
@ -251,7 +250,7 @@ class Lists(Cog):
await message.delete()
return
log_channel = self.bot.get_channel(config.log_channel)
log_channel = self.bot.get_channel(self.bot.config.log_channel)
channel = message.channel
content = message.content
user = message.author
@ -299,7 +298,7 @@ class Lists(Cog):
targeted_message = targeted_reaction.message
if self.is_edit(targeted_reaction):
if config.list_files_channel != 0:
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)
@ -389,5 +388,5 @@ class Lists(Cog):
)
def setup(bot):
bot.add_cog(Lists(bot))
async def setup(bot):
await bot.add_cog(Lists(bot))

View file

@ -1,8 +1,8 @@
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):
@ -23,7 +23,7 @@ class Lockdown(Cog):
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()
@ -35,15 +35,15 @@ class Lockdown(Cog):
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)
roles = None
for key, lockdown_conf in config.lockdown_configs.items():
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)
@ -75,15 +75,15 @@ 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)
roles = None
for key, lockdown_conf in config.lockdown_configs.items():
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)
@ -101,5 +101,5 @@ class Lockdown(Cog):
await log_channel.send(msg)
def setup(bot):
bot.add_cog(Lockdown(bot))
async def setup(bot):
await bot.add_cog(Lockdown(bot))

File diff suppressed because it is too large Load diff

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):
@ -21,7 +25,7 @@ class Logs(Cog):
self.clean_re = re.compile(r"[^a-zA-Z0-9_ ]+", re.UNICODE)
# All lower case, no spaces, nothing non-alphanumeric
susp_hellgex = "|".join(
[r"\W*".join(list(word)) for word in config.suspect_words]
[r"\W*".join(list(word)) for word in self.bot.config.suspect_words]
)
self.susp_hellgex = re.compile(susp_hellgex, re.IGNORECASE)
@ -29,16 +33,15 @@ class Logs(Cog):
async def on_member_join(self, member):
await self.bot.wait_until_ready()
if member.guild.id not in config.guild_whitelist:
if member.guild.id not in self.bot.config.guild_whitelist:
return
log_channel = self.bot.get_channel(config.log_channel)
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()
@ -72,8 +75,7 @@ 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:
@ -86,7 +88,7 @@ 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 "
@ -124,13 +126,12 @@ class Logs(Cog):
# 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)
@ -138,7 +139,7 @@ class Logs(Cog):
embed = discord.Embed(
color=discord.Color.dark_red(), title=f"Warns for {escaped_name}"
)
embed.set_thumbnail(url=member.avatar_url)
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']}",
@ -168,16 +169,17 @@ class Logs(Cog):
msg += f"\n- Has invite: https://{invite[0]}"
alert = True
for susp_word in config.suspect_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 config.suspect_ignored_words
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("_", "")
@ -188,7 +190,8 @@ class Logs(Cog):
# Show a message embed
embed = discord.Embed(description=regd)
embed.set_author(
name=message.author.display_name, icon_url=message.author.avatar_url
name=message.author.display_name,
icon_url=str(message.author.display_avatar),
)
await spy_channel.send(msg, embed=embed)
@ -202,13 +205,13 @@ class Logs(Cog):
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)
@ -216,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
@ -230,7 +233,7 @@ class Logs(Cog):
before_content = before.clean_content.replace("`", "`\u200d")
after_content = after.clean_content.replace("`", "`\u200d")
log_channel = self.bot.get_channel(config.log_channel)
log_channel = self.bot.get_channel(self.bot.config.log_channel)
msg = (
"📝 **Message edit**: \n"
@ -249,10 +252,10 @@ 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)
log_channel = self.bot.get_channel(self.bot.config.log_channel)
msg = (
"🗑️ **Message delete**: \n"
f"from {self.bot.escape_message(message.author.name)} "
@ -271,10 +274,10 @@ class Logs(Cog):
async def on_member_remove(self, member):
await self.bot.wait_until_ready()
if member.guild.id not in config.guild_whitelist:
if member.guild.id not in self.bot.config.guild_whitelist:
return
log_channel = self.bot.get_channel(config.log_channel)
log_channel = self.bot.get_channel(self.bot.config.log_channel)
msg = (
f"⬅️ **Leave**: {member.mention} | "
f"{self.bot.escape_message(member)}\n"
@ -286,10 +289,10 @@ class Logs(Cog):
async def on_member_ban(self, guild, member):
await self.bot.wait_until_ready()
if guild.id not in config.guild_whitelist:
if guild.id not in self.bot.config.guild_whitelist:
return
log_channel = self.bot.get_channel(config.modlog_channel)
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
msg = (
f"⛔ **Ban**: {member.mention} | "
f"{self.bot.escape_message(member)}\n"
@ -301,10 +304,10 @@ class Logs(Cog):
async def on_member_unban(self, guild, user):
await self.bot.wait_until_ready()
if guild.id not in config.guild_whitelist:
if guild.id not in self.bot.config.guild_whitelist:
return
log_channel = self.bot.get_channel(config.modlog_channel)
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
msg = (
f"⚠️ **Unban**: {user.mention} | "
f"{self.bot.escape_message(user)}\n"
@ -325,11 +328,11 @@ class Logs(Cog):
async def on_member_update(self, member_before, member_after):
await self.bot.wait_until_ready()
if member_after.guild.id not in config.guild_whitelist:
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 = []
@ -381,5 +384,5 @@ class Logs(Cog):
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))

View file

@ -1,11 +1,14 @@
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
import math
import platform
from helpers.checks import check_if_staff_or_ot
import datetime
from robocop_ng.helpers.checks import check_if_staff_or_ot
class Meme(Cog):
@ -26,42 +29,100 @@ class Meme(Cog):
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True, name="warm")
async def warm_member(self, ctx, user: discord.Member):
async def warm_member(self, ctx, user: Optional[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)."
)
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: discord.Member):
async def chill_member(self, ctx, user: Optional[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)."
)
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: discord.Member):
async def gild(self, ctx, user: Optional[discord.Member]):
"""Gives a star to a user"""
await ctx.send(f"{user.mention} gets a :star:, yay!")
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: discord.Member):
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}!",
@ -129,23 +190,30 @@ class Meme(Cog):
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True, name="bam")
async def bam_member(self, ctx, target: discord.Member):
async def bam_member(self, ctx, target: Optional[discord.Member]):
"""Bams a user owo"""
if target == ctx.author:
if target.id == 181627658520625152:
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(
"https://cdn.discordapp.com/attachments/286612533757083648/403080855402315796/rehedge.PNG"
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
)
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̕&̡.̷ 👍̡")
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):
@ -174,5 +242,5 @@ class Meme(Cog):
)
def setup(bot):
bot.add_cog(Meme(bot))
async def setup(bot):
await bot.add_cog(Meme(bot))

View file

@ -1,11 +1,13 @@
import io
from typing import Optional
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
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):
@ -13,7 +15,7 @@ class Mod(Cog):
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)
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)
@ -24,7 +26,7 @@ class Mod(Cog):
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_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}>"
@ -36,8 +38,19 @@ class Mod(Cog):
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command()
async def mute(self, ctx, target: discord.Member, *, reason: str = ""):
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.")
@ -50,7 +63,7 @@ class Mod(Cog):
"I can't mute this user as they're a member of staff."
)
userlog(target.id, ctx.author, reason, "mutes", target.name)
userlog(self.bot, target.id, ctx.author, reason, "mutes", target.name)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
@ -67,7 +80,7 @@ class Mod(Cog):
# or has DMs disabled
pass
mute_role = ctx.guild.get_role(config.mute_role)
mute_role = ctx.guild.get_role(self.bot.config.mute_role)
await target.add_roles(mute_role, reason=str(ctx.author))
@ -87,10 +100,10 @@ class Mod(Cog):
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(config.modlog_channel)
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(target.id, config.mute_role)
add_restriction(self.bot, target.id, self.bot.config.mute_role)
@commands.guild_only()
@commands.check(check_if_staff)
@ -101,7 +114,7 @@ class Mod(Cog):
ctx, str(target)
)
mute_role = ctx.guild.get_role(config.mute_role)
mute_role = ctx.guild.get_role(self.bot.config.mute_role)
await target.remove_roles(mute_role, reason=str(ctx.author))
chan_message = (
@ -112,17 +125,28 @@ class Mod(Cog):
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(config.modlog_channel)
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(target.id, config.mute_role)
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: discord.Member, *, reason: str = ""):
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.")
@ -135,7 +159,7 @@ class Mod(Cog):
"I can't kick this user as they're a member of staff."
)
userlog(target.id, ctx.author, reason, "kicks", target.name)
userlog(self.bot, target.id, ctx.author, reason, "kicks", target.name)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
@ -174,7 +198,7 @@ class Mod(Cog):
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(config.modlog_channel)
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
await log_channel.send(chan_message)
await ctx.send(f"👢 {safe_name}, 👍.")
@ -182,8 +206,19 @@ class Mod(Cog):
@commands.bot_has_permissions(ban_members=True)
@commands.check(check_if_staff)
@commands.command(aliases=["yeet"])
async def ban(self, ctx, target: discord.Member, *, reason: str = ""):
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:
@ -198,7 +233,7 @@ class Mod(Cog):
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)
userlog(self.bot, target.id, ctx.author, reason, "bans", target.name)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
@ -235,17 +270,29 @@ class Mod(Cog):
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(config.modlog_channel)
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: discord.Member, *, reason: str = ""
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:
@ -265,7 +312,7 @@ class Mod(Cog):
"Message delete day count needs to be between 0 and 7 days."
)
userlog(target.id, ctx.author, reason, "bans", target.name)
userlog(self.bot, target.id, ctx.author, reason, "bans", target.name)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
@ -303,7 +350,7 @@ class Mod(Cog):
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(config.modlog_channel)
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. 👍"
@ -327,7 +374,7 @@ class Mod(Cog):
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)
userlog(self.bot, target, ctx.author, reason, "bans", target_user.name)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
@ -352,7 +399,7 @@ class Mod(Cog):
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(config.modlog_channel)
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&. 👍")
@ -381,7 +428,7 @@ class Mod(Cog):
)
continue
userlog(target, ctx.author, f"massban", "bans", target_user.name)
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)
@ -401,7 +448,7 @@ class Mod(Cog):
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(config.modlog_channel)
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&. 👍")
@ -434,7 +481,7 @@ class Mod(Cog):
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(config.modlog_channel)
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&.")
@ -454,7 +501,7 @@ class Mod(Cog):
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)
userlog(self.bot, target.id, ctx.author, reason, "bans", target.name)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
@ -479,21 +526,34 @@ class Mod(Cog):
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(config.modlog_channel)
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: discord.Member, role: str = "community"):
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 config.named_roles:
if role not in self.bot.config.named_roles:
return await ctx.send(
"No such role! Available roles: " + ",".join(config.named_roles)
"No such role! Available roles: "
+ ",".join(self.bot.config.named_roles)
)
log_channel = self.bot.get_channel(config.modlog_channel)
target_role = ctx.guild.get_role(config.named_roles[role])
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.")
@ -511,15 +571,28 @@ class Mod(Cog):
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command(aliases=["unapprove"])
async def revoke(self, ctx, target: discord.Member, role: str = "community"):
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 config.named_roles:
if role not in self.bot.config.named_roles:
return await ctx.send(
"No such role! Available roles: " + ",".join(config.named_roles)
"No such role! Available roles: "
+ ",".join(self.bot.config.named_roles)
)
log_channel = self.bot.get_channel(config.modlog_channel)
target_role = ctx.guild.get_role(config.named_roles[role])
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.")
@ -539,21 +612,47 @@ class Mod(Cog):
@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)
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
await channel.purge(limit=limit)
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 log_channel.send(msg)
await modlog_channel.send(msg)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command()
async def warn(self, ctx, target: discord.Member, *, reason: str = ""):
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.")
@ -566,8 +665,10 @@ class Mod(Cog):
"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)
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)
@ -582,7 +683,7 @@ class Mod(Cog):
if reason:
msg += " The given reason is: " + reason
msg += (
f"\n\nPlease read the rules in {config.rules_url}. "
f"\n\nPlease read the rules in {self.bot.config.rules_url}. "
f"This is warn #{warn_count}."
)
if warn_count == 2:
@ -625,13 +726,87 @@ class Mod(Cog):
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: discord.Member, *, nick: str = ""):
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:
@ -689,6 +864,70 @@ class Mod(Cog):
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.
def setup(bot):
bot.add_cog(Mod(bot))
!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,7 +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()
@ -22,9 +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")
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):
@ -22,16 +23,16 @@ class ModReact(Cog):
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)
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 "
@ -47,7 +48,7 @@ class ModReact(Cog):
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
@ -118,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

@ -1,7 +1,7 @@
import config
from discord.ext import commands
from discord.ext.commands import Cog
from helpers.checks import check_if_staff
from robocop_ng.helpers.checks import check_if_staff
class ModReswitched(Cog):
@ -12,10 +12,10 @@ class ModReswitched(Cog):
@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 config.pingmods_allow for r in ctx.author.roles)
can_ping = any(r.id in self.bot.config.pingmods_allow for r in ctx.author.roles)
if can_ping:
await ctx.send(
f"<@&{config.pingmods_role}>: {ctx.author.mention} needs assistance."
f"<@&{self.bot.config.pingmods_role}>: {ctx.author.mention} needs assistance."
)
else:
await ctx.send(
@ -27,7 +27,7 @@ class ModReswitched(Cog):
@commands.command(aliases=["togglemod"])
async def modtoggle(self, ctx):
"""Toggles your mod role, staff only."""
target_role = ctx.guild.get_role(config.modtoggle_role)
target_role = ctx.guild.get_role(self.bot.config.modtoggle_role)
if target_role in ctx.author.roles:
await ctx.author.remove_roles(
@ -41,5 +41,5 @@ class ModReswitched(Cog):
await ctx.send(f"{ctx.author.mention}: Gave you mod role.")
def setup(bot):
bot.add_cog(ModReswitched(bot))
async def setup(bot):
await bot.add_cog(ModReswitched(bot))

View file

@ -1,12 +1,14 @@
import discord
import config
from datetime import datetime
from typing import Optional
import discord
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
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):
@ -14,16 +16,27 @@ class ModTimed(Cog):
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)
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: discord.Member, duration: str, *, reason: str = ""
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.")
@ -37,6 +50,7 @@ class ModTimed(Cog):
)
userlog(
self.bot,
target.id,
ctx.author,
f"{reason} (Timed, until " f"{duration_text})",
@ -77,9 +91,9 @@ class ModTimed(Cog):
" as the reason is automatically sent to the user."
)
add_job("unban", target.id, {"guild": ctx.guild.id}, expiry_timestamp)
add_job(self.bot, "unban", target.id, {"guild": ctx.guild.id}, expiry_timestamp)
log_channel = self.bot.get_channel(config.log_channel)
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}. 👍")
@ -87,9 +101,20 @@ class ModTimed(Cog):
@commands.check(check_if_staff)
@commands.command()
async def timemute(
self, ctx, target: discord.Member, duration: str, *, reason: str = ""
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.")
@ -105,6 +130,7 @@ class ModTimed(Cog):
)
userlog(
self.bot,
target.id,
ctx.author,
f"{reason} (Timed, until " f"{duration_text})",
@ -128,7 +154,7 @@ class ModTimed(Cog):
# or has DMs disabled
pass
mute_role = ctx.guild.get_role(config.mute_role)
mute_role = ctx.guild.get_role(self.bot.config.mute_role)
await target.add_roles(mute_role, reason=str(ctx.author))
@ -146,15 +172,17 @@ class ModTimed(Cog):
" as the reason is automatically sent to the user."
)
add_job("unmute", target.id, {"guild": ctx.guild.id}, expiry_timestamp)
add_job(
self.bot, "unmute", target.id, {"guild": ctx.guild.id}, expiry_timestamp
)
log_channel = self.bot.get_channel(config.log_channel)
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(target.id, config.mute_role)
add_restriction(self.bot, target.id, self.bot.config.mute_role)
def setup(bot):
bot.add_cog(ModTimed(bot))
async def setup(bot):
await bot.add_cog(ModTimed(bot))

View file

@ -1,10 +1,11 @@
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):
@ -20,7 +21,7 @@ class ModUserlog(Cog):
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)"
@ -53,18 +54,18 @@ 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])
@ -83,7 +84,7 @@ class ModUserlog(Cog):
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()
@ -135,7 +136,7 @@ class ModUserlog(Cog):
@commands.command(aliases=["clearwarns"])
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(escape_markdown=True).convert(
ctx, str(target)
@ -154,7 +155,7 @@ 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 = (
@ -169,7 +170,7 @@ class ModUserlog(Cog):
@commands.command(aliases=["delwarn"])
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.
@ -193,7 +194,7 @@ 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.
@ -233,7 +234,7 @@ class ModUserlog(Cog):
await ctx.send(
f"user = {user_name}\n"
f"id = {user.id}\n"
f"avatar = {user.avatar_url}\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"
@ -244,5 +245,5 @@ class ModUserlog(Cog):
)
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,12 @@
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):
@ -59,13 +59,13 @@ class Pin(Cog):
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
session, "RoboCop-NG", oauth_token=self.bot.config.github_oauth_token
)
(id, content) = await self.get_pinboard(gh, channel)
content += "- " + data + "\n"
@ -102,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)
@ -111,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)
@ -157,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,7 +17,7 @@ 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"]:
@ -37,8 +39,15 @@ class Remind(Cog):
@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)
@ -56,22 +65,24 @@ class Remind(Cog):
)
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(
self.bot,
"remind",
ctx.author.id,
{"text": safe_text, "added": added_on},
expiry_timestamp,
)
msg = await ctx.send(
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()
def setup(bot):
bot.add_cog(Remind(bot))
async def setup(bot):
await bot.add_cog(Remind(bot))

View file

@ -1,25 +1,31 @@
import asyncio
import config
import time
import discord
import traceback
from discord.ext import commands
import discord
from discord.ext import commands, tasks
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
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
bot.loop.create_task(self.minutely())
bot.loop.create_task(self.hourly())
bot.loop.create_task(self.daily())
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 = self.bot.get_channel(config.botlog_channel)
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()
@ -27,7 +33,7 @@ class Robocronp(Cog):
@commands.command()
async def listjobs(self, ctx):
"""Lists timed robocronp jobs, staff only."""
ctab = get_crontab()
ctab = get_crontab(self.bot)
embed = discord.Embed(title=f"Active robocronp jobs")
for jobtype in ctab:
for jobtimestamp in ctab[jobtype]:
@ -52,30 +58,31 @@ class Robocronp(Cog):
- job name (userid, like 420332322307571713)
You can get all 3 from listjobs command."""
delete_job(timestamp, job_type, job_name)
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):
log_channel = self.bot.get_channel(config.botlog_channel)
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(timestamp, jobtype, job_name)
delete_job(self.bot, 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)
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(config.mute_role)
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(timestamp, jobtype, job_name)
delete_job(self.bot, timestamp, jobtype, job_name)
elif jobtype == "remind":
text = job_details["text"]
added_on = job_details["added"]
@ -84,18 +91,19 @@ class Robocronp(Cog):
await target.send(
f"You asked to be reminded about `{text}` on {added_on}."
)
delete_job(timestamp, jobtype, job_name)
delete_job(self.bot, timestamp, jobtype, job_name)
except:
# Don't kill cronjobs if something goes wrong.
delete_job(timestamp, jobtype, job_name)
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):
log_channel = self.bot.get_channel(config.botlog_channel)
channel = self.bot.get_channel(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
@ -113,69 +121,59 @@ class Robocronp(Cog):
f"Cronclean has errored: ```{traceback.format_exc()}```"
)
@tasks.loop(minutes=1)
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)
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 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()}```"
)
await asyncio.sleep(60)
# 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 = 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(
f"Cron-hourly has errored: ```{traceback.format_exc()}```"
)
# Your stuff that should run an hour after boot
# and after that every hour goes here
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 = self.bot.get_channel(config.botlog_channel)
while not self.bot.is_closed():
# Your stuff that should run at boot
# and after that every day goes here
try:
# Reset verification and algorithm
if "cogs.verification" in config.initial_cogs:
verif_channel = self.bot.get_channel(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()}```"
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 asyncio.sleep(86400)
# Your stuff that should run a day after boot
# and after that every day goes here
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()}```"
)
def setup(bot):
bot.add_cog(Robocronp(bot))
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

@ -1,9 +1,8 @@
import discord
from discord.ext import commands
from discord.ext.commands import Cog
import config
import random
from helpers.checks import check_if_staff
from robocop_ng.helpers.checks import check_if_staff
class RyujinxVerification(Cog):
@ -18,24 +17,28 @@ class RyujinxVerification(Cog):
async def on_member_join(self, member):
await self.bot.wait_until_ready()
if (member.guild.id not in config.guild_whitelist):
if member.guild.id not in self.bot.config.guild_whitelist:
return
join_channel = self.bot.get_channel(config.welcome_channel)
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))
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 == config.welcome_channel:
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(config.participant_role)
success_role = message.guild.get_role(self.bot.config.participant_role)
if config.verification_string == mcl:
if self.bot.config.verification_string == mcl:
await message.author.add_roles(success_role)
await message.delete()
@ -65,21 +68,23 @@ class RyujinxVerification(Cog):
async def on_member_join(self, member):
await self.bot.wait_until_ready()
if (member.guild.id not in config.guild_whitelist):
if member.guild.id not in self.bot.config.guild_whitelist:
return
join_channel = self.bot.get_channel(config.welcome_channel)
join_channel = self.bot.get_channel(self.bot.config.welcome_channel)
if join_channel is not None:
await join_channel.send(config.join_message.format(member))
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 != config.welcome_channel and not force:
await ctx.send(f"This command is limited to"
f" <#{config.welcome_channel}>, unless forced.")
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)
@ -90,5 +95,6 @@ class RyujinxVerification(Cog):
# We only auto clear the channel daily
await self.do_reset(channel, author)
def setup(bot):
bot.add_cog(RyujinxVerification(bot))
async def setup(bot):
await bot.add_cog(RyujinxVerification(bot))

View file

@ -1,7 +1,7 @@
import config
from discord.ext import commands
from discord.ext.commands import Cog
from helpers.checks import check_if_staff_or_ot
from robocop_ng.helpers.checks import check_if_staff_or_ot
class SAR(Cog):
@ -15,8 +15,8 @@ class SAR(Cog):
"""Lists self assignable roles."""
return await ctx.send(
"Self assignable roles in this guild: "
+ ",".join(config.self_assignable_roles)
+ f"\n\nRun `{config.prefixes[0]}iam role_name_goes_here` to get or remove one."
+ ",".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)
@ -25,12 +25,12 @@ class SAR(Cog):
@commands.check(check_if_staff_or_ot)
async def iam(self, ctx, role: str):
"""Gets you a self assignable role."""
if role not in config.self_assignable_roles:
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(config.self_assignable_roles[role])
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))
@ -44,5 +44,5 @@ class SAR(Cog):
)
def setup(bot):
bot.add_cog(SAR(bot))
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

@ -1,19 +1,20 @@
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
import asyncio
import config
import random
from inspect import cleandoc
import hashlib
import itertools
from helpers.checks import check_if_staff
from robocop_ng.helpers.checks import check_if_staff
class Verification(Cog):
def __init__(self, bot):
self.bot = bot
self.hash_choice = random.choice(config.welcome_hashes)
self.hash_choice = random.choice(self.bot.config.welcome_hashes)
# Export reset channel functions
self.bot.do_reset = self.do_reset
@ -22,10 +23,10 @@ class Verification(Cog):
async def do_reset(self, channel, author, limit: int = 100):
await channel.purge(limit=limit)
await channel.send(config.welcome_header)
await channel.send(self.bot.config.welcome_header)
rules = [
"**{}**. {}".format(i, cleandoc(r))
for i, r in enumerate(config.welcome_rules, 1)
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()
@ -33,12 +34,14 @@ class Verification(Cog):
hash_choice_str += "-512"
elif hash_choice_str == "BLAKE2S":
hash_choice_str += "-256"
rules[rule_choice - 1] += "\n" + config.hidden_term_line.format(hash_choice_str)
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(config.log_channel)
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
@ -60,19 +63,19 @@ class Verification(Cog):
await channel.send(item)
await asyncio.sleep(1)
for x in config.welcome_footer:
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(config.welcome_hashes))
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(config.log_channel)
log_channel = self.bot.get_channel(self.bot.config.log_channel)
await log_channel.send(msg)
await self.do_reset(channel, author)
@ -81,10 +84,10 @@ class Verification(Cog):
@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:
if ctx.message.channel.id != self.bot.config.welcome_channel and not force:
await ctx.send(
f"This command is limited to"
f" <#{config.welcome_channel}>, unless forced."
f" <#{self.bot.config.welcome_channel}>, unless forced."
)
return
await self.do_reset(ctx.channel, ctx.author.mention, limit)
@ -93,10 +96,10 @@ class Verification(Cog):
@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 != config.welcome_channel and not force:
if ctx.message.channel.id != self.bot.config.welcome_channel and not force:
await ctx.send(
f"This command is limited to"
f" <#{config.welcome_channel}>, unless forced."
f" <#{self.bot.config.welcome_channel}>, unless forced."
)
return
@ -107,7 +110,7 @@ class Verification(Cog):
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:
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)
@ -134,7 +137,7 @@ class Verification(Cog):
return await chan.send(snark)
# Get the role we will give in case of success
success_role = guild.get_role(config.named_roles["participant"])
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)]
@ -167,11 +170,13 @@ class Verification(Cog):
)
# Detect if the user uses the wrong hash algorithm
wrong_hash_algos = list(set(config.welcome_hashes) - {self.hash_choice})
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(config.log_channel)
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}."
)
@ -218,5 +223,5 @@ class Verification(Cog):
await chan.send("💢 I don't have permission to do this.")
def setup(bot):
bot.add_cog(Verification(bot))
async def setup(bot):
await bot.add_cog(Verification(bot))

View file

@ -1,10 +1,10 @@
from discord.ext.commands import Cog
import re
import config
import secrets
import asyncio
import base64
import hmac
import re
import secrets
from discord.ext.commands import Cog
class YubicoOTP(Cog):
@ -56,7 +56,7 @@ class YubicoOTP(Cog):
return int("".join(hexconv), 16)
def calc_signature(self, text):
key = base64.b64decode(config.yubico_otp_secret)
key = base64.b64decode(self.bot.config.yubico_otp_secret)
signature_bytes = hmac.digest(key, text.encode(), "SHA1")
return base64.b64encode(signature_bytes).decode()
@ -72,10 +72,10 @@ class YubicoOTP(Cog):
async def validate_yubico_otp(self, otp):
nonce = secrets.token_hex(15) # Random number in the valid range
params = f"id={config.yubico_otp_client_id}&nonce={nonce}&otp={otp}"
params = f"id={self.bot.config.yubico_otp_client_id}&nonce={nonce}&otp={otp}"
# If secret is supplied, sign our request
if config.yubico_otp_secret:
if self.bot.config.yubico_otp_secret:
params += "&h=" + self.calc_signature(params)
for api_server in self.api_servers:
@ -84,21 +84,22 @@ class YubicoOTP(Cog):
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}."
)
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}
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 config.yubico_otp_secret:
if self.bot.config.yubico_otp_secret:
assert self.validate_response_signature(datafields)
# If we got a success, then return True
@ -148,5 +149,5 @@ class YubicoOTP(Cog):
await msg.delete()
def setup(bot):
bot.add_cog(YubicoOTP(bot))
async def setup(bot):
await bot.add_cog(YubicoOTP(bot))

View file

@ -1,8 +1,9 @@
import hashlib
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."
@ -39,7 +40,7 @@ initial_cogs = [
"cogs.robocronp",
"cogs.meme",
"cogs.invites",
"cogs.yubicootp"
"cogs.yubicootp",
]
# The following cogs are also available but aren't loaded by default:
@ -63,12 +64,16 @@ 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

View file

@ -22,6 +22,12 @@ def check_if_staff_or_ot(ctx):
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

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

View file

@ -11,17 +11,26 @@ switch_modules = {
9: "Loader ",
10: "CMIF (IPC command interface) ",
11: "HIPC (IPC) ",
12: "TMA ",
15: "PM ",
16: "NS ",
17: "Sockets ",
17: "BSDSockets ",
18: "HTC ",
19: "TSC ",
20: "NCM Content ",
21: "SM ",
22: "RO userland ",
23: "Gc ",
24: "SDMMC ",
25: "OVLN ",
26: "SPL ",
27: "Socket ",
29: "HTCLOW ",
30: "Bus",
31: "HTCFS ",
32: "Async ",
33: "Util ",
35: "TIPC ",
100: "ETHC ",
101: "I2C ",
102: "GPIO ",
@ -31,6 +40,7 @@ switch_modules = {
108: "XCD ",
110: "NIFM ",
111: "Hwopus ",
112: "LSM6DS3 ",
113: "Bluetooth ",
114: "VI ",
115: "NFP ",
@ -48,6 +58,7 @@ switch_modules = {
128: "AM ",
129: "Play Report ",
130: "AHID ",
131: "Applet ",
132: "Home Menu (Qlaunch) ",
133: "PCV ",
134: "OMM ",
@ -68,37 +79,74 @@ switch_modules = {
149: "CEC",
150: "Profiler ",
151: "Error Upload ",
152: "LIDBE ",
153: "Audio ",
154: "NPNS ",
155: "NPNS HTTP Stream ",
156: "IDLE ",
157: "ARP ",
158: "Updater ",
159: "SWKBD ",
160: "Network Diagnostics ",
161: "NFC Mifare ",
162: "Userland assert ",
163: "Fatal ",
164: "NIM Shop ",
165: "SPSM ",
166: "AOC ",
167: "BGTC ",
168: "Userland crash ",
169: "SASBUS ",
170: "PL ",
173: "LBL ",
175: "JIT ",
176: "HDCP ",
177: "OMM ",
178: "PDM",
179: "OLSC ",
180: "SREPO ",
181: "Dauth ",
191: "RTC",
192: "Regulator",
197: "Clkrst",
182: "STDFU ",
183: "Debug ",
187: "SPI ",
189: "PWM ",
191: "RTC ",
192: "Regulator ",
193: "LED ",
197: "Clkrst ",
198: "Powctl ",
202: "HID ",
203: "LDN ",
204: "CS ",
205: "Irsensor ",
206: "Capture ",
208: "Manu ",
209: "ATK ",
210: "Web ",
211: " ",
211: "LCS ",
212: "GRC ",
213: "Repair ",
214: "Album ",
215: "RID ",
216: "Migration ",
217: "Migration Idc Server ",
218: "HIDBUS ",
219: "ENS ",
223: "Websocket ",
227: "CAPMTP ",
228: "PGL ",
229: "Notification ",
230: "INS ",
231: "LP2P ",
232: "RCD ",
235: "PRC ",
238: "ECTX ",
239: "MNPP ",
244: "DP2HDMI ",
246: "Sprofile ",
250: "NDRM ",
499: "TSPM ",
500: "Devmenu ",
# Libnx
345: "libnx ",
346: "Homebrew ABI ",
@ -106,6 +154,7 @@ switch_modules = {
348: "libnx Nvidia",
349: "libnx Binder",
# Support Errors
520: "Nverpt",
800: "General web-applet",
809: "WifiWebAuthApplet",
810: "Whitelisted-applet",
@ -125,6 +174,7 @@ switch_known_errcodes = {
0xCE01: "Resource exhaustion ",
0xD001: "Memory exhaustion ",
0xD201: "Handle-table exhaustion ",
0xD401: "Invalid memory state / Invalid memory permissions. ",
0xD801: "Invalid memory permissions. ",
0xDC01: "Invalid memory range ",
0xE001: "Invalid thread priority. ",
@ -291,6 +341,28 @@ switch_known_errcodes = {
0x1D60A: "Invalid in object count. ",
0x1D80A: "Invalid out object count. ",
0x25A0A: "Out of domain entries. ",
0x1423: "Invalid command ID received by tipc processor. ",
0x1E23: "Invalid message format received by tipc processor. ",
0xC823: "Tipc request was deferred for future processing. ",
0xCA23: "Tipc object was closed. ",
0x4AF: "Bad version returned from calling the (nnjitpluginGetVersion) symbol. ",
0xCAAF: "Input NRO/NRR are too large for the storage buffer. ",
0x4B0AF: "Symbol funcptr used by this cmd is not initialized (Control/GenerateCode). ",
0x4B2AF: "DllPlugin Not initialized, or plugin NRO has already been loaded. ",
0x4B4AF: "An error was returned from calling the symbol funcptr with the Control cmd. ",
0x104E7: "Nullptr passed to the LocalCommunicationId control.nacp validation func. ",
0x140E7: "GroupInfo field out of range. ",
0x142E7: "SupportedPlatform not appropriate for this operation. ",
0x146E7: "Invalid ServiceName. ",
0x148E7: "Must provide PresharedBinaryKey. ",
0x1C0E7: "Requested Priority value not allowed. ",
0x1C2E7: "Matching LocalCommunicationId not found in the user-process control.nacp. ",
0x200E7: "Invalid flag. ",
0x204E7: "Invalid SupportedPlatform. ",
0x208E7: "Invalid StaticAesKeyIndex. ",
0x20AE7: "MemberCountMax cannot be greater than 8. ",
0x210E7: "GroupInfo+8F must be 0x20. ",
0xA83: "Unrecognized applet ID ",
0x20B: "Unsupported operation ",
0xCC0B: "Out of server session memory ",
0x11A0B: "Went past maximum during marshalling. ",
@ -441,6 +513,26 @@ switch_known_errcodes = {
0x9F469: "Null Amiibo ECQV BLS key buffer ",
0x9F669: "Null Amiibo ECQV BLS certificate buffer ",
0x9F869: "Null Amiibo ECQV BLS root certificate buffer ",
# erpt error codes
0x293: "Not Initialized ",
0x493: "Already Initialized ",
0x693: "Out of Array Space ",
0x893: "Out of Field Space ",
0xA93: "Out of Memory ",
0xC93: "Not Supported ",
0xE93: "Invalid Argument ",
0x1093: "Not Found ",
0x1293: "Field Category Mismatch ",
0x1493: "Field Type Mismatch ",
0x1693: "Already Exists ",
0x1893: "Corrupt Journal ",
0x1A93: "Category Not Found ",
0x1C93: "Required Context Missing ",
0x1E93: "Required Field Missing ",
0x2093: "Formatter Error ",
0x2293: "Invalid Power State ",
0x2493: "Array Field Too Large ",
0x2693: "Already Owned ",
0x272: "Generic error ",
0xCC74: "Time not set ",
0x287C: "Argument is NULL ",
@ -538,6 +630,9 @@ switch_known_errcodes = {
0x31B002: "Operation not supported in nn::fssystem::ConcatenationFile",
0x327202: "Writable file not closed when committing",
0x35F202: "Mount name not found in table.",
0x28CB: "The [6.0.0+] Authentication challenge failed. ",
0xE2CB: "The specified LocalCommunicationVersion is less than the AccessPoint LocalCommunicationVersion. ",
0xE4CB: "The specified LocalCommunicationVersion is larger than the AccessPoint LocalCommunicationVersion. ",
0x21A: "SMC is not implemented",
0x61A: "SMC is currently in progress/secmon is busy",
0x81A: "Secmon not currently performing async operation",
@ -557,6 +652,14 @@ switch_known_errcodes = {
0xA27A: "Data verification failed",
0xB47A: "Invalid API call",
0xC47A: "Invalid operation",
0x4DA: "StatusManager entry IsValid flag not set, or controller-update currently in-progress. ",
0x6DA: "Controller-update failed via the LibraryApplet. ",
0x8DA: "Invalid BusHandle. ",
0xADA: "StatusManager entry flag +0x0 not set, or device not connected. ",
0xEDA: "PollingReceivedData not available. ",
0x10DA: "StatusManager entry DeviceEnabled flag not set, or flag +0x0 not set. ",
0x12DA: "ExternalDeviceId mismatch. ",
0x14DA: "BusHandle already initialized. ",
0x290: "Exited Abnormally ([[Applet_Manager_services#LibraryAppletExitReason|ExitReason]] == Abormal)",
0x690: "Canceled ([[Applet_Manager_services#LibraryAppletExitReason|ExitReason]] == Canceled)",
0x890: "Rejected", # me_irl
@ -566,14 +669,51 @@ switch_known_errcodes = {
0x198CD: "IR camera invalid handle value.",
# FS Codes
0xD401: "Error: Passed buffer is not usable for fs library. ",
0x7802: "Error: Specified mount name already exists. ",
0x7D202: "Error: Specified partition is not found. ",
0x7D402: "Error: Specified target is not found. ",
0xFA002: "Error: Failed to access SD card. ",
0x136802: "Error: Failed to access game card. ",
0x177202: "Error: Specified operation is not implemented. ",
0x177A02: "Error: Specified value is out of range. ",
0x2F5C02: "Error: Invalid size was specified.",
0x190002: "Error: Failed to allocate memory. ",
0x1B5802: "Error: Failed to access MMC. ",
0x1F4202: "Error: ROM is corrupted. ",
0x219A02: "Error: Save data is corrupted. ",
0x232A02: "Error: NCA is corrupted. ",
0x23F202: "Error: Integrity verification failed. ",
0x244202: "Error: Partition FS is corrupted. ",
0x246A02: "Error: Built-in-storage is corrupted. ",
0x249202: "Error: FAT FS is corrupted. ",
0x24BA02: "Error: HOST FS is corrupted. ",
0x1F4002: "Error: Data is corrupted. ",
0x271002: "Error: Unexpected failure occurred. ",
0x2F5C02: "Error: Invalid size was specified. ",
0x2F5E02: "Error: Null pointer argument was specified. ",
0x2EE002: "Error: Precondition violation. ",
0x2EE202: "Error: Invalid argument was specified. ",
0x2EE402: "Error: Invalid path was specified. ",
0x2EE602: "Error: Too long path was specified. ",
0x2EE802: "Error: Invalid path character was specified. ",
0x2EEA02: "Error: Invalid path format was specified. ",
0x2F5A02: "Error: Invalid offset was specified. ",
0x2F5C02: "Error: Invalid size was specified. ",
0x2F5E02: "Error: Null pointer argument was specified. ",
0x2F6202: "Error: Invalid mount name was specified. ",
0x2F6402: "Error: Extension size exceeds max value set in nmeta file. ",
0x2F6602: "Error: Extension size is not a multiple of nn::fs::SaveDataExtensionUnitSize. ",
0x307202: "Error: OpenMode_AllowAppend is required for implicit extension of file size by WriteFile(). ",
0x307002: "Error: Invalid operation for the open mode. ",
0x313802: "Error: Unsupported operation. ",
0x320002: "Error: Permission denied. ",
0x327202: "Error: Close files opened in write mode before committing. ",
0x328202: "Error: Specified user doesn't exist. ",
0x346402: "Error: Enough journal space is not left. ",
0x346A02: "Error: The open count of files and directories reached the limitation. ",
0x353602: "Error: Save data extension count reached the limitation. ",
0x35F202: "Error: Specified mount name is not found. ",
# Fatal
0x2A2: "An internal assert occured within the application, application aborted.",
0x4A2: "Can be triggered by running svcBreak. The svcBreak params have no affect on the value of the thrown error-code.",
0xA8: "Userland ARM undefined instruction exception",
0x2A8: "Userland ARM prefetch-abort due to PC set to non-executable region",

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

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

View file

@ -1,21 +1,27 @@
import json
import math
import os
from robocop_ng.helpers.data_loader import read_json
def get_crontab():
with open("data/robocronptab.json", "r") as f:
return json.load(f)
def get_crontab_path(bot):
return os.path.join(bot.state_dir, "data/robocronptab.json")
def set_crontab(contents):
with open("data/robocronptab.json", "w") as f:
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(job_type, job_name, job_details, timestamp):
def add_job(bot, job_type, job_name, job_details, timestamp):
timestamp = str(math.floor(timestamp))
job_name = str(job_name)
ctab = get_crontab()
ctab = get_crontab(bot)
if job_type not in ctab:
ctab[job_type] = {}
@ -24,14 +30,14 @@ def add_job(job_type, job_name, job_details, timestamp):
ctab[job_type][timestamp] = {}
ctab[job_type][timestamp][job_name] = job_details
set_crontab(json.dumps(ctab))
set_crontab(bot, json.dumps(ctab))
def delete_job(timestamp, job_type, job_name):
def delete_job(bot, timestamp, job_type, job_name):
timestamp = str(timestamp)
job_name = str(job_name)
ctab = get_crontab()
ctab = get_crontab(bot)
del ctab[job_type][timestamp][job_name]
set_crontab(json.dumps(ctab))
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

@ -1,6 +1,9 @@
import json
import os
import time
from robocop_ng.helpers.data_loader import read_json
userlog_event_types = {
"warns": "Warn",
"bans": "Ban",
@ -10,18 +13,21 @@ userlog_event_types = {
}
def get_userlog():
with open("data/userlog.json", "r") as f:
return json.load(f)
def get_userlog_path(bot):
return os.path.join(bot.state_dir, "data/userlog.json")
def set_userlog(contents):
with open("data/userlog.json", "w") as f:
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(userid, uname):
userlogs = get_userlog()
def fill_userlog(bot, userid, uname):
userlogs = get_userlog(bot)
uid = str(userid)
if uid not in userlogs:
userlogs[uid] = {
@ -39,8 +45,8 @@ def fill_userlog(userid, uname):
return userlogs, uid
def userlog(uid, issuer, reason, event_type, uname: str = ""):
userlogs, uid = fill_userlog(uid, uname)
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 = {
@ -52,13 +58,13 @@ def userlog(uid, issuer, reason, event_type, uname: str = ""):
if event_type not in userlogs[uid]:
userlogs[uid][event_type] = []
userlogs[uid][event_type].append(log_data)
set_userlog(json.dumps(userlogs))
set_userlog(bot, json.dumps(userlogs))
return len(userlogs[uid][event_type])
def setwatch(uid, issuer, watch_state, uname: str = ""):
userlogs, uid = fill_userlog(uid, uname)
def setwatch(bot, uid, issuer, watch_state, uname: str = ""):
userlogs, uid = fill_userlog(bot, uid, uname)
userlogs[uid]["watch"] = watch_state
set_userlog(json.dumps(userlogs))
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