Compare commits

..

5 commits

Author SHA1 Message Date
Mark Araujo
da9ea20a9d 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
2021-07-31 23:34:39 +02:00
Mark Araujo
d1c536e792 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
2021-07-31 23:34:39 +02:00
Mary
d562e2ab88 Fix logfilereader logic 2021-07-31 23:34:39 +02:00
Mark Araujo
8e05468cb7 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
2021-07-31 23:34:37 +02:00
Ave Ozkal
86eb6a7ad2 Rewrite the verification code to the one required by Ryujinx Guild 2021-07-31 23:34:21 +02:00
63 changed files with 1741 additions and 4647 deletions

View file

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

View file

@ -1,18 +0,0 @@
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

View file

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

View file

@ -1,59 +0,0 @@
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

View file

@ -1,41 +0,0 @@
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:3.10-alpine
FROM python:alpine
WORKDIR /usr/src/app
COPY poetry.lock pyproject.toml ./
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
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
WORKDIR /usr/src/app/robocop_ng
CMD [ "python", "-m", "robocop_ng", "/state" ]
CMD [ "python", "./__init__.py" ]

View file

@ -1,43 +1,22 @@
# ryuko-ng
# robocop-ng
Discord bot for handling Ryujinx moderation tasks and such, (n)ext-(g)en rewrite of Robocop
Next-gen rewrite of Kurisu/Robocop bot used on ReSwitched bot with discord.py rewrite, designed to be relatively clean, consistent and un-bloated.
Code is based on https://github.com/reswitched/robocop-ng.
Code is based on https://gitlab.com/a/dpybotbase and https://github.com/916253/Kurisu-Reswitched.
---
## 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` 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.
- 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.
- If you haven't already done this already, **move the bot's role above the roles it'll need to manage, or else it won't function properly**, this is especially important for verification as it doesn't work otherwise.
- If you're moving from Kurisu or Robocop: Follow [Tips for people moving from Kurisu/Robocop](https://github.com/Ryujinx/ryuko-ng#tips-for-people-moving-from-kurisurobocop) below.
### Running with docker
- `docker build . -t robocopng`
- Assuming your robocop-ng repo is on `~/docker/`: `docker run --restart=always -v ~/docker/robocop-ng:/usr/src/app --name robocop_ng robocopng:latest`
For updates, run `git pull;docker rm -f robocop_ng` then run the two commands above again.
### Running manually
- Install python3.8+.
- Install dependencies with [poetry](https://python-poetry.org/) using `poetry install`.
- Run `robocop_ng/__main__.py` (`cd robocop_ng;python3 __main__.py`).
- 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.
To keep the bot running, you might want to use pm2 or a systemd service.
@ -47,7 +26,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 into the `robocop_ng` folder.
- Copy your `data` folder over.
- 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 `'`).
@ -55,7 +34,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, ask in the [Ryujinx discord guild](https://discord.gg/ryujinx) pinging Berry.
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.
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 .`.
@ -63,14 +42,13 @@ You're expected to use [black](https://github.com/psf/black) for code formatting
## Credits
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).
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.
[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:
I (ave) 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/ryuko-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 in any way (reporting a bug, sending a PR, forking and hosting their own at their own guild, etc).

View file

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

View file

@ -1,175 +0,0 @@
{
"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
View file

@ -1,133 +0,0 @@
{
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.1"
version = "1.0.0"
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.11"
python = "^3.8"
"discord.py" = "^1.7.3"
python-dateutil = "^2.8.2"
humanize = "^4.8.0"
humanize = "^3.10.0"
parsedatetime = "^2.6"
aiohttp = "^3.9.3"
gidgethub = "^5.3.0"
"discord.py" = "^2.3.2"
aiohttp = "^3.7.4"
gidgethub = "^5.0.1"
[tool.poetry.dev-dependencies]
[build-system]
requires = ["poetry-core"]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

216
requirements.txt Normal file
View file

@ -0,0 +1,216 @@
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,23 +1,15 @@
import asyncio
import logging.handlers
import os
import sys
import logging
import logging.handlers
import traceback
import aiohttp
import config
import discord
from discord.ext import commands
from discord.ext.commands import CommandError, Context
from robocop_ng.helpers.notifications import report_critical_error
if len(sys.argv[1:]) != 1:
sys.stderr.write("usage: <state_dir>")
sys.exit(1)
state_dir = os.path.abspath(sys.argv[1])
sys.path.append(state_dir)
import config
# TODO: check __name__ for __main__ nerd
script_name = os.path.basename(__file__).split(".")[0]
@ -54,24 +46,11 @@ wanted_jsons = [
"data/robocronptab.json",
"data/userlog.json",
"data/invites.json",
"data/macros.json",
"data/persistent_roles.json",
"data/disabled_ids.json",
]
if not os.path.exists(os.path.join(state_dir, "data")):
os.makedirs(os.path.join(state_dir, "data"))
for wanted_json_idx in range(len(wanted_jsons)):
wanted_jsons[wanted_json_idx] = os.path.join(
state_dir, wanted_jsons[wanted_json_idx]
)
if not os.path.isfile(wanted_jsons[wanted_json_idx]):
with open(wanted_jsons[wanted_json_idx], "w") as file:
file.write("{}")
intents = discord.Intents.all()
intents = discord.Intents.default()
intents.typing = False
intents.members = True
bot = commands.Bot(
command_prefix=get_prefix, description=config.bot_description, intents=intents
@ -81,19 +60,15 @@ bot.help_command = commands.DefaultHelpCommand(dm_help=True)
bot.log = log
bot.config = config
bot.script_name = script_name
bot.state_dir = state_dir
bot.wanted_jsons = wanted_jsons
async def get_channel_safe(self, channel_id: int):
res = self.get_channel(channel_id)
if res is None:
res = await self.fetch_channel(channel_id)
return res
commands.Bot.get_channel_safe = get_channel_safe
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())
@bot.event
@ -101,7 +76,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 = await bot.get_channel_safe(config.botlog_channel)
bot.botlog_channel = bot.get_channel(config.botlog_channel)
log.info(
f"\nLogged in as: {bot.user.name} - "
@ -140,31 +115,12 @@ async def on_command(ctx):
@bot.event
async def on_error(event: str, *args, **kwargs):
log.exception(f"Error on {event}:")
exception = sys.exc_info()[1]
is_report_allowed = any(
[
not isinstance(exception, x)
for x in [
discord.RateLimited,
discord.GatewayNotFound,
discord.InteractionResponded,
discord.LoginFailure,
]
]
)
if exception is not None and is_report_allowed:
await report_critical_error(
bot,
exception,
additional_info={"Event": event, "args": args, "kwargs": kwargs},
)
async def on_error(event_method, *args, **kwargs):
log.error(f"Error on {event_method}: {sys.exc_info()}")
@bot.event
async def on_command_error(ctx: Context, error: CommandError):
async def on_command_error(ctx, error):
error_text = str(error)
err_msg = (
@ -173,7 +129,7 @@ async def on_command_error(ctx: Context, error: CommandError):
f"of type {type(error)}: {error_text}"
)
log.exception(err_msg)
log.error(err_msg)
if not isinstance(error, commands.CommandNotFound):
err_msg = bot.escape_message(err_msg)
@ -231,7 +187,6 @@ async def on_command_error(ctx: Context, error: CommandError):
# 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):
@ -271,26 +226,12 @@ async def on_message(message):
await bot.invoke(ctx)
async def main():
async with bot:
if len(config.guild_whitelist) == 1:
invite_url = discord.utils.oauth_url(
config.client_id,
guild=discord.Object(config.guild_whitelist[0]),
disable_guild_select=True,
)
else:
invite_url = discord.utils.oauth_url(config.client_id)
if not os.path.exists("data"):
os.makedirs("data")
log.info(f"\nInvite URL: {invite_url}\n")
for wanted_json in wanted_jsons:
if not os.path.exists(wanted_json):
with open(wanted_json, "w") as f:
f.write("{}")
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())
bot.run(config.token, bot=True, reconnect=True)

View file

@ -1,12 +1,11 @@
import inspect
import re
import traceback
import discord
from discord.ext import commands
from discord.ext.commands import Cog
from robocop_ng.helpers.checks import check_if_bot_manager
import traceback
import inspect
import re
import config
from helpers.checks import check_if_bot_manager
class Admin(Cog):
@ -21,7 +20,7 @@ class Admin(Cog):
async def _exit(self, ctx):
"""Shuts down the bot, bot manager only."""
await ctx.send(":wave: Goodbye!")
await self.bot.close()
await self.bot.logout()
@commands.guild_only()
@commands.check(check_if_bot_manager)
@ -93,7 +92,7 @@ class Admin(Cog):
async def cog_load_actions(self, cog_name):
if cog_name == "verification":
verif_channel = self.bot.get_channel(self.bot.config.welcome_channel)
verif_channel = self.bot.get_channel(config.welcome_channel)
await self.bot.do_resetalgo(verif_channel, "cog load")
@commands.guild_only()
@ -107,13 +106,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 = "robocop_ng.cogs." + cog
if cog_name not in self.bot.config.initial_cogs:
cog_name = "cogs." + cog
if cog_name not in config.initial_cogs:
continue
try:
await self.bot.unload_extension(cog_name)
await self.bot.load_extension(cog_name)
self.bot.unload_extension(cog_name)
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)
@ -130,7 +129,7 @@ class Admin(Cog):
async def load(self, ctx, ext: str):
"""Loads a cog, bot manager only."""
try:
await self.bot.load_extension("robocop_ng.cogs." + ext)
self.bot.load_extension("cogs." + ext)
await self.cog_load_actions(ext)
except:
await ctx.send(
@ -146,7 +145,7 @@ class Admin(Cog):
@commands.command()
async def unload(self, ctx, ext: str):
"""Unloads a cog, bot manager only."""
await self.bot.unload_extension("robocop_ng.cogs." + ext)
self.bot.unload_extension("cogs." + ext)
self.bot.log.info(f"Unloaded ext {ext}")
await ctx.send(f":white_check_mark: `{ext}` successfully unloaded.")
@ -160,8 +159,8 @@ class Admin(Cog):
self.lastreload = ext
try:
await self.bot.unload_extension("robocop_ng.cogs." + ext)
await self.bot.load_extension("robocop_ng.cogs." + ext)
self.bot.unload_extension("cogs." + ext)
self.bot.load_extension("cogs." + ext)
await self.cog_load_actions(ext)
except:
await ctx.send(
@ -173,5 +172,5 @@ class Admin(Cog):
await ctx.send(f":white_check_mark: `{ext}` successfully reloaded.")
async def setup(bot):
await bot.add_cog(Admin(bot))
def setup(bot):
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 16 to 10"""
"""Converts base 10 to 16"""
await ctx.send(f"{ctx.author.mention}: {int(num, 16)}")
@commands.guild_only()
@ -37,12 +37,10 @@ class Basic(Cog):
async def robocop(self, ctx):
"""Shows a quick embed with bot info."""
embed = discord.Embed(
title="Robocop-NG",
url=self.bot.config.source_url,
description=self.bot.config.embed_desc,
title="Robocop-NG", url=config.source_url, description=config.embed_desc
)
embed.set_thumbnail(url=str(self.bot.user.display_avatar))
embed.set_thumbnail(url=self.bot.user.avatar_url)
await ctx.send(embed=embed)
@ -58,10 +56,12 @@ 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)
async def setup(bot):
await bot.add_cog(Basic(bot))
def setup(bot):
bot.add_cog(Basic(bot))

View file

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

View file

@ -191,5 +191,5 @@ class Common(Cog):
return "No output."
async def setup(bot):
await bot.add_cog(Common(bot))
def setup(bot):
bot.add_cog(Common(bot))

View file

@ -1,10 +1,9 @@
import re
import discord
from discord.ext import commands
from discord.ext.commands import Cog
from robocop_ng.helpers.errcodes import *
from helpers.errcodes import *
class Err(Cog):
@ -98,6 +97,7 @@ class Err(Cog):
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(text="F to you | Console: Switch")
embed.set_footer("F to you | Console: Switch")
else:
embed.set_footer(text="Console: Switch")
@ -194,5 +194,5 @@ class Err(Cog):
await ctx.send("This doesn't look like typical hex!")
async def setup(bot):
await bot.add_cog(Err(bot))
def setup(bot):
bot.add_cog(Err(bot))

View file

@ -0,0 +1,87 @@
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,12 +1,8 @@
import json
import os
import discord
from discord.ext import commands
from discord.ext.commands import Cog
from robocop_ng.helpers.checks import check_if_collaborator
from robocop_ng.helpers.invites import add_invite
from helpers.checks import check_if_collaborator
import config
import json
class Invites(Cog):
@ -17,14 +13,25 @@ class Invites(Cog):
@commands.guild_only()
@commands.check(check_if_collaborator)
async def invite(self, ctx):
welcome_channel = self.bot.get_channel(self.bot.config.welcome_channel)
welcome_channel = self.bot.get_channel(config.welcome_channel)
author = ctx.message.author
reason = f"Created by {str(author)} ({author.id})"
invite = await welcome_channel.create_invite(
max_age=0, max_uses=1, temporary=True, unique=True, reason=reason
)
add_invite(self.bot, invite.id, invite.url, 1, invite.code)
with open("data/invites.json", "r") as f:
invites = json.load(f)
invites[invite.id] = {
"uses": 0,
"url": invite.url,
"max_uses": 1,
"code": invite.code,
}
with open("data/invites.json", "w") as f:
f.write(json.dumps(invites))
await ctx.message.add_reaction("🆗")
try:
@ -36,5 +43,5 @@ class Invites(Cog):
)
async def setup(bot):
await bot.add_cog(Invites(bot))
def setup(bot):
bot.add_cog(Invites(bot))

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
import discord
from discord.ext import commands
from discord.ext.commands import Cog
from robocop_ng.helpers.checks import check_if_staff
import config
import discord
from 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 self.bot.config.staff_role_ids:
for role in 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(self.bot.config.modlog_channel)
log_channel = self.bot.get_channel(config.modlog_channel)
roles = None
for key, lockdown_conf in self.bot.config.lockdown_configs.items():
for key, lockdown_conf in config.lockdown_configs.items():
if channel.id in lockdown_conf["channels"]:
roles = lockdown_conf["roles"]
if roles is None:
roles = self.bot.config.lockdown_configs["default"]["roles"]
roles = 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(self.bot.config.modlog_channel)
log_channel = self.bot.get_channel(config.modlog_channel)
roles = None
for key, lockdown_conf in self.bot.config.lockdown_configs.items():
for key, lockdown_conf in config.lockdown_configs.items():
if channel.id in lockdown_conf["channels"]:
roles = lockdown_conf["roles"]
if roles is None:
roles = self.bot.config.lockdown_configs["default"]["roles"]
roles = 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)
async def setup(bot):
await bot.add_cog(Lockdown(bot))
def setup(bot):
bot.add_cog(Lockdown(bot))

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,10 @@
import json
import os
import re
import discord
from discord.ext.commands import Cog
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
import json
import re
import config
from helpers.restrictions import get_user_restrictions
from helpers.checks import check_if_staff
class Logs(Cog):
@ -25,7 +21,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 self.bot.config.suspect_words]
[r"\W*".join(list(word)) for word in config.suspect_words]
)
self.susp_hellgex = re.compile(susp_hellgex, re.IGNORECASE)
@ -33,15 +29,16 @@ class Logs(Cog):
async def on_member_join(self, member):
await self.bot.wait_until_ready()
if member.guild.id not in self.bot.config.guild_whitelist:
if member.guild.id not in config.guild_whitelist:
return
log_channel = self.bot.get_channel(self.bot.config.log_channel)
log_channel = self.bot.get_channel(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
invites = get_invites(self.bot)
with open("data/invites.json", "r") as f:
invites = json.load(f)
real_invites = await member.guild.invites()
@ -75,7 +72,8 @@ class Logs(Cog):
del invites[id]
# Save invites data.
set_invites(self.bot, invites)
with open("data/invites.json", "w") as f:
f.write(json.dumps(invites))
# Prepare the invite correlation message
if len(probable_invites_used) == 1:
@ -88,7 +86,7 @@ class Logs(Cog):
# Check if user account is older than 15 minutes
age = member.joined_at - member.created_at
if age < self.bot.config.min_age:
if age < config.min_age:
try:
await member.send(
f"Your account is too new to "
@ -126,12 +124,13 @@ class Logs(Cog):
# Handles user restrictions
# Basically, gives back muted role to users that leave with it.
rsts = get_user_restrictions(self.bot, member.id)
rsts = get_user_restrictions(member.id)
roles = [discord.utils.get(member.guild.roles, id=rst) for rst in rsts]
await member.add_roles(*roles)
# Real hell zone.
warns = get_userlog(self.bot)
with open("data/userlog.json", "r") as f:
warns = json.load(f)
try:
if len(warns[str(member.id)]["warns"]) == 0:
await log_channel.send(msg)
@ -139,7 +138,7 @@ class Logs(Cog):
embed = discord.Embed(
color=discord.Color.dark_red(), title=f"Warns for {escaped_name}"
)
embed.set_thumbnail(url=str(member.display_avatar))
embed.set_thumbnail(url=member.avatar_url)
for idx, warn in enumerate(warns[str(member.id)]["warns"]):
embed.add_field(
name=f"{idx + 1}: {warn['timestamp']}",
@ -169,17 +168,16 @@ class Logs(Cog):
msg += f"\n- Has invite: https://{invite[0]}"
alert = True
for susp_word in self.bot.config.suspect_words:
for susp_word in config.suspect_words:
if susp_word in cleancont and not any(
ok_word in cleancont
for ok_word in self.bot.config.suspect_ignored_words
ok_word in cleancont for ok_word in 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(self.bot.config.spylog_channel)
spy_channel = self.bot.get_channel(config.spylog_channel)
# Bad Code :tm:, blame retr0id
message_clean = message.content.replace("*", "").replace("_", "")
@ -190,8 +188,7 @@ class Logs(Cog):
# Show a message embed
embed = discord.Embed(description=regd)
embed.set_author(
name=message.author.display_name,
icon_url=str(message.author.display_avatar),
name=message.author.display_name, icon_url=message.author.avatar_url
)
await spy_channel.send(msg, embed=embed)
@ -205,13 +202,13 @@ class Logs(Cog):
f"R11 violating name by {message.author.mention} " f"({message.author.id})."
)
spy_channel = self.bot.get_channel(self.bot.config.spylog_channel)
spy_channel = self.bot.get_channel(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 self.bot.config.spy_channels:
if message.channel.id not in config.spy_channels:
return
await self.do_spy(message)
@ -219,7 +216,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 self.bot.config.spy_channels or after.author.bot:
if after.channel.id not in config.spy_channels or after.author.bot:
return
# If content is the same, just skip over it
@ -233,7 +230,7 @@ class Logs(Cog):
before_content = before.clean_content.replace("`", "`\u200d")
after_content = after.clean_content.replace("`", "`\u200d")
log_channel = self.bot.get_channel(self.bot.config.log_channel)
log_channel = self.bot.get_channel(config.log_channel)
msg = (
"📝 **Message edit**: \n"
@ -252,10 +249,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 self.bot.config.spy_channels or message.author.bot:
if message.channel.id not in config.spy_channels or message.author.bot:
return
log_channel = self.bot.get_channel(self.bot.config.log_channel)
log_channel = self.bot.get_channel(config.log_channel)
msg = (
"🗑️ **Message delete**: \n"
f"from {self.bot.escape_message(message.author.name)} "
@ -274,10 +271,10 @@ class Logs(Cog):
async def on_member_remove(self, member):
await self.bot.wait_until_ready()
if member.guild.id not in self.bot.config.guild_whitelist:
if member.guild.id not in config.guild_whitelist:
return
log_channel = self.bot.get_channel(self.bot.config.log_channel)
log_channel = self.bot.get_channel(config.log_channel)
msg = (
f"⬅️ **Leave**: {member.mention} | "
f"{self.bot.escape_message(member)}\n"
@ -289,10 +286,10 @@ class Logs(Cog):
async def on_member_ban(self, guild, member):
await self.bot.wait_until_ready()
if guild.id not in self.bot.config.guild_whitelist:
if guild.id not in config.guild_whitelist:
return
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
log_channel = self.bot.get_channel(config.modlog_channel)
msg = (
f"⛔ **Ban**: {member.mention} | "
f"{self.bot.escape_message(member)}\n"
@ -304,10 +301,10 @@ class Logs(Cog):
async def on_member_unban(self, guild, user):
await self.bot.wait_until_ready()
if guild.id not in self.bot.config.guild_whitelist:
if guild.id not in config.guild_whitelist:
return
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
log_channel = self.bot.get_channel(config.modlog_channel)
msg = (
f"⚠️ **Unban**: {user.mention} | "
f"{self.bot.escape_message(user)}\n"
@ -328,11 +325,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 self.bot.config.guild_whitelist:
if member_after.guild.id not in config.guild_whitelist:
return
msg = ""
log_channel = self.bot.get_channel(self.bot.config.log_channel)
log_channel = self.bot.get_channel(config.log_channel)
if member_before.roles != member_after.roles:
# role removal
role_removal = []
@ -384,5 +381,5 @@ class Logs(Cog):
await log_channel.send(msg)
async def setup(bot):
await bot.add_cog(Logs(bot))
def setup(bot):
bot.add_cog(Logs(bot))

View file

@ -1,179 +0,0 @@
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,14 +1,11 @@
import datetime
import math
import platform
import random
from typing import Optional
import discord
from discord.ext import commands
from discord.ext.commands import Cog
from robocop_ng.helpers.checks import check_if_staff_or_ot
import math
import platform
from helpers.checks import check_if_staff_or_ot
import datetime
class Meme(Cog):
@ -29,24 +26,8 @@ class Meme(Cog):
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True, name="warm")
async def warm_member(self, ctx, user: Optional[discord.Member]):
async def warm_member(self, ctx, user: discord.Member):
"""Warms a user :3"""
if user is None and ctx.message.reference is None:
celsius = random.randint(15, 20)
fahrenheit = self.c_to_f(celsius)
kelvin = self.c_to_k(celsius)
await ctx.send(
f"{ctx.author.mention} tries to warm themself."
f" User is now {celsius}°C "
f"({fahrenheit}°F, {kelvin}K).\n"
"You might have more success warming someone else :3"
)
else:
if user is None:
user = (
await ctx.channel.fetch_message(ctx.message.reference.message_id)
).author
celsius = random.randint(15, 100)
fahrenheit = self.c_to_f(celsius)
kelvin = self.c_to_k(celsius)
@ -56,38 +37,10 @@ class Meme(Cog):
f"({fahrenheit}°F, {kelvin}K)."
)
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True)
async def lick(self, ctx, user: Optional[discord.Member]):
"""licks a user :?"""
if user is None and ctx.message.reference is None:
await ctx.send(f"{ctx.author.mention} licks their lips! 👅")
else:
if user is None:
user = (
await ctx.channel.fetch_message(ctx.message.reference.message_id)
).author
await ctx.send(f"{user.mention} has been licked! 👅")
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True, name="chill", aliases=["cold"])
async def chill_member(self, ctx, user: Optional[discord.Member]):
async def chill_member(self, ctx, user: discord.Member):
"""Chills a user >:3"""
if user is None and ctx.message.reference is None:
celsius = random.randint(-75, 10)
fahrenheit = self.c_to_f(celsius)
kelvin = self.c_to_k(celsius)
await ctx.send(
f"{ctx.author.mention} chills themself."
f" User is now {celsius}°C "
f"({fahrenheit}°F, {kelvin}K).\n"
"🧊 Don't be so hard on yourself. 😔"
)
else:
if user is None:
user = (
await ctx.channel.fetch_message(ctx.message.reference.message_id)
).author
celsius = random.randint(-50, 15)
fahrenheit = self.c_to_f(celsius)
kelvin = self.c_to_k(celsius)
@ -99,30 +52,16 @@ class Meme(Cog):
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True, aliases=["thank", "reswitchedgold"])
async def gild(self, ctx, user: Optional[discord.Member]):
async def gild(self, ctx, user: discord.Member):
"""Gives a star to a user"""
if user is None and ctx.message.reference is None:
await ctx.send(f"No stars for you, {ctx.author.mention}!")
else:
if user is None:
user = (
await ctx.channel.fetch_message(ctx.message.reference.message_id)
).author
await ctx.send(f"{user.mention} gets a :star:, yay!")
@commands.check(check_if_staff_or_ot)
@commands.command(
hidden=True, aliases=["reswitchedsilver", "silv3r", "reswitchedsilv3r"]
)
async def silver(self, ctx, user: Optional[discord.Member]):
async def silver(self, ctx, user: 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}!",
@ -190,15 +129,8 @@ class Meme(Cog):
@commands.check(check_if_staff_or_ot)
@commands.command(hidden=True, name="bam")
async def bam_member(self, ctx, target: Optional[discord.Member]):
async def bam_member(self, ctx, target: discord.Member):
"""Bams a user owo"""
if target is None and ctx.message.reference is None:
await ctx.reply("https://tenor.com/view/bonk-gif-26414884")
else:
if ctx.message.reference is not None:
target = (
await ctx.channel.fetch_message(ctx.message.reference.message_id)
).author
if target == ctx.author:
if target.id == 181627658520625152:
return await ctx.send(
@ -242,5 +174,5 @@ class Meme(Cog):
)
async def setup(bot):
await bot.add_cog(Meme(bot))
def setup(bot):
bot.add_cog(Meme(bot))

View file

@ -1,13 +1,11 @@
import io
from typing import Optional
import discord
from discord.ext import commands
from discord.ext.commands import Cog, Context
from robocop_ng.helpers.checks import check_if_staff, check_if_bot_manager
from robocop_ng.helpers.restrictions import add_restriction, remove_restriction
from robocop_ng.helpers.userlogs import userlog
from discord.ext.commands import Cog
import config
from helpers.checks import check_if_staff, check_if_bot_manager
from helpers.userlogs import userlog
from helpers.restrictions import add_restriction, remove_restriction
import io
class Mod(Cog):
@ -15,7 +13,7 @@ class Mod(Cog):
self.bot = bot
def check_if_target_is_staff(self, target):
return any(r.id in self.bot.config.staff_role_ids for r in target.roles)
return any(r.id in config.staff_role_ids for r in target.roles)
@commands.guild_only()
@commands.check(check_if_bot_manager)
@ -26,7 +24,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(self.bot.config.modlog_channel)
log_channel = self.bot.get_channel(config.modlog_channel)
log_msg = (
f"✏️ **Guild Icon Update**: {ctx.author} changed the guild icon."
f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
@ -38,19 +36,8 @@ class Mod(Cog):
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command()
async def mute(self, ctx, target: Optional[discord.Member], *, reason: str = ""):
async def mute(self, ctx, target: 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.")
@ -63,7 +50,7 @@ class Mod(Cog):
"I can't mute this user as they're a member of staff."
)
userlog(self.bot, target.id, ctx.author, reason, "mutes", target.name)
userlog(target.id, ctx.author, reason, "mutes", target.name)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
@ -80,7 +67,7 @@ class Mod(Cog):
# or has DMs disabled
pass
mute_role = ctx.guild.get_role(self.bot.config.mute_role)
mute_role = ctx.guild.get_role(config.mute_role)
await target.add_roles(mute_role, reason=str(ctx.author))
@ -100,10 +87,10 @@ class Mod(Cog):
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
log_channel = self.bot.get_channel(config.modlog_channel)
await log_channel.send(chan_message)
await ctx.send(f"{target.mention} can no longer speak.")
add_restriction(self.bot, target.id, self.bot.config.mute_role)
add_restriction(target.id, config.mute_role)
@commands.guild_only()
@commands.check(check_if_staff)
@ -114,7 +101,7 @@ class Mod(Cog):
ctx, str(target)
)
mute_role = ctx.guild.get_role(self.bot.config.mute_role)
mute_role = ctx.guild.get_role(config.mute_role)
await target.remove_roles(mute_role, reason=str(ctx.author))
chan_message = (
@ -125,28 +112,17 @@ class Mod(Cog):
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
log_channel = self.bot.get_channel(config.modlog_channel)
await log_channel.send(chan_message)
await ctx.send(f"{target.mention} can now speak again.")
remove_restriction(self.bot, target.id, self.bot.config.mute_role)
remove_restriction(target.id, config.mute_role)
@commands.guild_only()
@commands.bot_has_permissions(kick_members=True)
@commands.check(check_if_staff)
@commands.command()
async def kick(self, ctx, target: Optional[discord.Member], *, reason: str = ""):
async def kick(self, ctx, target: 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.")
@ -159,7 +135,7 @@ class Mod(Cog):
"I can't kick this user as they're a member of staff."
)
userlog(self.bot, target.id, ctx.author, reason, "kicks", target.name)
userlog(target.id, ctx.author, reason, "kicks", target.name)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
@ -198,7 +174,7 @@ class Mod(Cog):
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
log_channel = self.bot.get_channel(config.modlog_channel)
await log_channel.send(chan_message)
await ctx.send(f"👢 {safe_name}, 👍.")
@ -206,19 +182,8 @@ 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: Optional[discord.Member], *, reason: str = ""):
async def ban(self, ctx, target: 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:
@ -233,7 +198,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(self.bot, target.id, ctx.author, reason, "bans", target.name)
userlog(target.id, ctx.author, reason, "bans", target.name)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
@ -270,29 +235,17 @@ class Mod(Cog):
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
log_channel = self.bot.get_channel(config.modlog_channel)
await log_channel.send(chan_message)
await ctx.send(f"{safe_name} is now b&. 👍")
@commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@commands.check(check_if_staff)
@commands.command()
async def bandel(
self, ctx, day_count: int, target: Optional[discord.Member], *, reason: str = ""
self, ctx, day_count: int, target: 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:
@ -312,7 +265,7 @@ class Mod(Cog):
"Message delete day count needs to be between 0 and 7 days."
)
userlog(self.bot, target.id, ctx.author, reason, "bans", target.name)
userlog(target.id, ctx.author, reason, "bans", target.name)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
@ -350,7 +303,7 @@ class Mod(Cog):
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
log_channel = self.bot.get_channel(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. 👍"
@ -374,7 +327,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(self.bot, target, ctx.author, reason, "bans", target_user.name)
userlog(target, ctx.author, reason, "bans", target_user.name)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
@ -399,7 +352,7 @@ class Mod(Cog):
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
log_channel = self.bot.get_channel(config.modlog_channel)
await log_channel.send(chan_message)
await ctx.send(f"{safe_name} is now b&. 👍")
@ -428,7 +381,7 @@ class Mod(Cog):
)
continue
userlog(self.bot, target, ctx.author, f"massban", "bans", target_user.name)
userlog(target, ctx.author, f"massban", "bans", target_user.name)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
@ -448,7 +401,7 @@ class Mod(Cog):
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
log_channel = self.bot.get_channel(config.modlog_channel)
await log_channel.send(chan_message)
await ctx.send(f"All {len(targets_int)} users are now b&. 👍")
@ -481,7 +434,7 @@ class Mod(Cog):
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
log_channel = self.bot.get_channel(config.modlog_channel)
await log_channel.send(chan_message)
await ctx.send(f"{safe_name} is now unb&.")
@ -501,7 +454,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(self.bot, target.id, ctx.author, reason, "bans", target.name)
userlog(target.id, ctx.author, reason, "bans", target.name)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
@ -526,34 +479,21 @@ class Mod(Cog):
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
log_channel = self.bot.get_channel(config.modlog_channel)
await log_channel.send(chan_message)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command()
async def approve(
self, ctx, target: Optional[discord.Member], role: str = "community"
):
async def approve(self, ctx, target: discord.Member, role: str = "community"):
"""Add a role to a user (default: community), staff only."""
if role not in self.bot.config.named_roles:
if role not in config.named_roles:
return await ctx.send(
"No such role! Available roles: "
+ ",".join(self.bot.config.named_roles)
"No such role! Available roles: " + ",".join(config.named_roles)
)
if target is None and ctx.message.reference is None:
return await ctx.send(
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
)
else:
if ctx.message.reference is not None:
target = (
await ctx.channel.fetch_message(ctx.message.reference.message_id)
).author
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
target_role = ctx.guild.get_role(self.bot.config.named_roles[role])
log_channel = self.bot.get_channel(config.modlog_channel)
target_role = ctx.guild.get_role(config.named_roles[role])
if target_role in target.roles:
return await ctx.send("Target already has this role.")
@ -571,28 +511,15 @@ class Mod(Cog):
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command(aliases=["unapprove"])
async def revoke(
self, ctx, target: Optional[discord.Member], role: str = "community"
):
async def revoke(self, ctx, target: discord.Member, role: str = "community"):
"""Remove a role from a user (default: community), staff only."""
if role not in self.bot.config.named_roles:
if role not in config.named_roles:
return await ctx.send(
"No such role! Available roles: "
+ ",".join(self.bot.config.named_roles)
"No such role! Available roles: " + ",".join(config.named_roles)
)
if target is None and ctx.message.reference is None:
return await ctx.send(
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
)
else:
if ctx.message.reference is not None:
target = (
await ctx.channel.fetch_message(ctx.message.reference.message_id)
).author
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
target_role = ctx.guild.get_role(self.bot.config.named_roles[role])
log_channel = self.bot.get_channel(config.modlog_channel)
target_role = ctx.guild.get_role(config.named_roles[role])
if target_role not in target.roles:
return await ctx.send("Target doesn't have this role.")
@ -612,47 +539,21 @@ 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."""
modlog_channel = self.bot.get_channel(self.bot.config.modlog_channel)
log_channel = self.bot.get_channel(self.bot.config.log_channel)
log_channel = self.bot.get_channel(config.modlog_channel)
if not channel:
channel = ctx.channel
purged_log_jump_url = ""
for deleted_message in await channel.purge(limit=limit):
msg = (
"🗑️ **Message purged**: \n"
f"from {self.bot.escape_message(deleted_message.author.name)} "
f"({deleted_message.author.id}), in {deleted_message.channel.mention}:\n"
f"`{deleted_message.clean_content}`"
)
if len(purged_log_jump_url) == 0:
purged_log_jump_url = (await log_channel.send(msg)).jump_url
else:
await log_channel.send(msg)
await channel.purge(limit=limit)
msg = (
f"🗑 **Purged**: {str(ctx.author)} purged {limit} "
f"messages in {channel.mention}."
f"\n🔗 __Jump__: <{purged_log_jump_url}>"
)
await modlog_channel.send(msg)
await log_channel.send(msg)
@commands.guild_only()
@commands.check(check_if_staff)
@commands.command()
async def warn(self, ctx, target: Optional[discord.Member], *, reason: str = ""):
async def warn(self, ctx, target: 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.")
@ -665,10 +566,8 @@ class Mod(Cog):
"I can't warn this user as they're a member of staff."
)
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
warn_count = userlog(
self.bot, target.id, ctx.author, reason, "warns", target.name
)
log_channel = self.bot.get_channel(config.modlog_channel)
warn_count = userlog(target.id, ctx.author, reason, "warns", target.name)
safe_name = await commands.clean_content(escape_markdown=True).convert(
ctx, str(target)
@ -683,7 +582,7 @@ class Mod(Cog):
if reason:
msg += " The given reason is: " + reason
msg += (
f"\n\nPlease read the rules in {self.bot.config.rules_url}. "
f"\n\nPlease read the rules in {config.rules_url}. "
f"This is warn #{warn_count}."
)
if warn_count == 2:
@ -726,87 +625,13 @@ 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: Optional[discord.Member], *, nick: str = ""):
async def nickname(self, ctx, target: discord.Member, *, nick: str = ""):
"""Sets a user's nickname, staff only.
Just send .nickname <user> to wipe the nickname."""
if 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:
@ -864,70 +689,6 @@ 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.
!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))
def setup(bot):
bot.add_cog(Mod(bot))

View file

@ -1,9 +1,8 @@
import discord
from discord.ext import commands
from discord.ext.commands import Cog
from robocop_ng.helpers.checks import check_if_staff
from robocop_ng.helpers.userlogs import userlog
from helpers.checks import check_if_staff
from helpers.userlogs import userlog
class ModNote(Cog):
@ -15,7 +14,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(self.bot, target.id, ctx.author, note, "notes", target.name)
userlog(target.id, ctx.author, note, "notes", target.name)
await ctx.send(f"{ctx.author.mention}: noted!")
@commands.guild_only()
@ -23,9 +22,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(self.bot, target, ctx.author, note, "notes")
userlog(target, ctx.author, note, "notes")
await ctx.send(f"{ctx.author.mention}: noted!")
async def setup(bot):
await bot.add_cog(ModNote(bot))
def setup(bot):
bot.add_cog(ModNote(bot))

View file

@ -1,10 +1,9 @@
import asyncio
import discord
from discord.ext import commands
from discord.ext.commands import Cog
from robocop_ng.helpers.checks import check_if_staff
import config
from helpers.checks import check_if_staff
class ModReact(Cog):
@ -23,16 +22,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(self.bot.config.modlog_channel)
log_channel = self.bot.get_channel(config.modlog_channel)
if not channel:
channel = ctx.channel
count = 0
async for msg in channel.history(limit=limit):
for react in msg.reactions:
async for react_user in react.users():
if react_user == user:
if await react.users().find(lambda u: u == user):
count += 1
await react.remove(user)
async for u in react.users():
await msg.remove_reaction(react, u)
msg = (
f"✏️ **Cleared reacts**: {ctx.author.mention} cleared "
f"{user.mention}'s reacts from the last {limit} messages "
@ -48,7 +47,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(self.bot.config.modlog_channel)
log_channel = self.bot.get_channel(config.modlog_channel)
if not channel:
channel = ctx.channel
count = 0
@ -119,5 +118,5 @@ class ModReact(Cog):
await msg.edit(content=f"{msg_text} Done!")
async def setup(bot):
await bot.add_cog(ModReact(bot))
def setup(bot):
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 robocop_ng.helpers.checks import check_if_staff
from 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 self.bot.config.pingmods_allow for r in ctx.author.roles)
can_ping = any(r.id in config.pingmods_allow for r in ctx.author.roles)
if can_ping:
await ctx.send(
f"<@&{self.bot.config.pingmods_role}>: {ctx.author.mention} needs assistance."
f"<@&{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(self.bot.config.modtoggle_role)
target_role = ctx.guild.get_role(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.")
async def setup(bot):
await bot.add_cog(ModReswitched(bot))
def setup(bot):
bot.add_cog(ModReswitched(bot))

View file

@ -1,14 +1,12 @@
from datetime import datetime
from typing import Optional
import discord
import config
from datetime import datetime
from discord.ext import commands
from discord.ext.commands import Cog
from robocop_ng.helpers.checks import check_if_staff
from robocop_ng.helpers.restrictions import add_restriction
from robocop_ng.helpers.robocronp import add_job
from robocop_ng.helpers.userlogs import userlog
from helpers.checks import check_if_staff
from helpers.robocronp import add_job
from helpers.userlogs import userlog
from helpers.restrictions import add_restriction
class ModTimed(Cog):
@ -16,27 +14,16 @@ class ModTimed(Cog):
self.bot = bot
def check_if_target_is_staff(self, target):
return any(r.id in self.bot.config.staff_role_ids for r in target.roles)
return any(r.id in config.staff_role_ids for r in target.roles)
@commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@commands.check(check_if_staff)
@commands.command()
async def timeban(
self, ctx, target: Optional[discord.Member], duration: str, *, reason: str = ""
self, ctx, target: 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.")
@ -50,7 +37,6 @@ class ModTimed(Cog):
)
userlog(
self.bot,
target.id,
ctx.author,
f"{reason} (Timed, until " f"{duration_text})",
@ -91,9 +77,9 @@ class ModTimed(Cog):
" as the reason is automatically sent to the user."
)
add_job(self.bot, "unban", target.id, {"guild": ctx.guild.id}, expiry_timestamp)
add_job("unban", target.id, {"guild": ctx.guild.id}, expiry_timestamp)
log_channel = self.bot.get_channel(self.bot.config.log_channel)
log_channel = self.bot.get_channel(config.log_channel)
await log_channel.send(chan_message)
await ctx.send(f"{safe_name} is now b&. " f"It will expire {duration_text}. 👍")
@ -101,20 +87,9 @@ class ModTimed(Cog):
@commands.check(check_if_staff)
@commands.command()
async def timemute(
self, ctx, target: Optional[discord.Member], duration: str, *, reason: str = ""
self, ctx, target: 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.")
@ -130,7 +105,6 @@ class ModTimed(Cog):
)
userlog(
self.bot,
target.id,
ctx.author,
f"{reason} (Timed, until " f"{duration_text})",
@ -154,7 +128,7 @@ class ModTimed(Cog):
# or has DMs disabled
pass
mute_role = ctx.guild.get_role(self.bot.config.mute_role)
mute_role = ctx.guild.get_role(config.mute_role)
await target.add_roles(mute_role, reason=str(ctx.author))
@ -172,17 +146,15 @@ class ModTimed(Cog):
" as the reason is automatically sent to the user."
)
add_job(
self.bot, "unmute", target.id, {"guild": ctx.guild.id}, expiry_timestamp
)
add_job("unmute", target.id, {"guild": ctx.guild.id}, expiry_timestamp)
log_channel = self.bot.get_channel(self.bot.config.log_channel)
log_channel = self.bot.get_channel(config.log_channel)
await log_channel.send(chan_message)
await ctx.send(
f"{target.mention} can no longer speak. " f"It will expire {duration_text}."
)
add_restriction(self.bot, target.id, self.bot.config.mute_role)
add_restriction(target.id, config.mute_role)
async def setup(bot):
await bot.add_cog(ModTimed(bot))
def setup(bot):
bot.add_cog(ModTimed(bot))

View file

@ -1,11 +1,10 @@
import json
import discord
from discord.ext import commands
from discord.ext.commands import Cog
from robocop_ng.helpers.checks import check_if_staff
from robocop_ng.helpers.userlogs import get_userlog, set_userlog, userlog_event_types
import config
import json
from helpers.checks import check_if_staff
from helpers.userlogs import get_userlog, set_userlog, userlog_event_types
class ModUserlog(Cog):
@ -21,7 +20,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(self.bot)
userlog = get_userlog()
if uid not in userlog:
embed.description = f"There are none!{own_note} (no entry)"
@ -54,18 +53,18 @@ class ModUserlog(Cog):
return embed
def clear_event_from_id(self, uid: str, event_type):
userlog = get_userlog(self.bot)
userlog = get_userlog()
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(self.bot, json.dumps(userlog))
set_userlog(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(self.bot)
userlog = get_userlog()
if uid not in userlog:
return f"<@{uid}> has no {event_type}!"
event_count = len(userlog[uid][event_type])
@ -84,7 +83,7 @@ class ModUserlog(Cog):
f"Reason: {event['reason']}",
)
del userlog[uid][event_type][idx - 1]
set_userlog(self.bot, json.dumps(userlog))
set_userlog(json.dumps(userlog))
return embed
@commands.guild_only()
@ -136,7 +135,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(self.bot.config.modlog_channel)
log_channel = self.bot.get_channel(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)
@ -155,7 +154,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(self.bot.config.modlog_channel)
log_channel = self.bot.get_channel(config.modlog_channel)
msg = self.clear_event_from_id(str(target), event)
await ctx.send(msg)
msg = (
@ -170,7 +169,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(self.bot.config.modlog_channel)
log_channel = self.bot.get_channel(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.
@ -194,7 +193,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(self.bot.config.modlog_channel)
log_channel = self.bot.get_channel(config.modlog_channel)
del_event = self.delete_event_from_id(str(target), idx, event)
event_name = userlog_event_types[event].lower()
# This is hell.
@ -234,7 +233,7 @@ class ModUserlog(Cog):
await ctx.send(
f"user = {user_name}\n"
f"id = {user.id}\n"
f"avatar = {user.display_avatar}\n"
f"avatar = {user.avatar_url}\n"
f"bot = {user.bot}\n"
f"created_at = {user.created_at}\n"
f"display_name = {display_name}\n"
@ -245,5 +244,5 @@ class ModUserlog(Cog):
)
async def setup(bot):
await bot.add_cog(ModUserlog(bot))
def setup(bot):
bot.add_cog(ModUserlog(bot))

View file

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

View file

@ -1,12 +1,12 @@
import aiohttp
import gidgethub.aiohttp
from discord import Embed
from discord.enums import MessageType
import config
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
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
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 self.bot.config.github_oauth_token == "":
if 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=self.bot.config.github_oauth_token
session, "RoboCop-NG", oauth_token=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 self.bot.config.allowed_pin_channels:
if payload.channel_id not in 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 self.bot.config.staff_role_ids + self.bot.config.allowed_pin_roles:
for role in config.staff_role_ids + 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
async def setup(bot):
await bot.add_cog(Pin(bot))
def setup(bot):
bot.add_cog(Pin(bot))

View file

@ -1,12 +1,10 @@
import discord
import asyncio
import time
from datetime import datetime
import discord
from discord.ext import commands
from discord.ext.commands import Cog
from robocop_ng.helpers.robocronp import add_job, get_crontab
from helpers.robocronp import add_job, get_crontab
class Remind(Cog):
@ -17,7 +15,7 @@ class Remind(Cog):
@commands.command()
async def remindlist(self, ctx):
"""Lists your reminders."""
ctab = get_crontab(self.bot)
ctab = get_crontab()
uid = str(ctx.author.id)
embed = discord.Embed(title=f"Active robocronp jobs")
for jobtimestamp in ctab["remind"]:
@ -39,15 +37,8 @@ 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)
@ -65,24 +56,22 @@ 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,
)
await ctx.send(
msg = await ctx.send(
f"{ctx.author.mention}: I'll remind you in "
f"DMs about `{safe_text}` in {duration_text}."
)
await asyncio.sleep(5)
await msg.delete()
async def setup(bot):
await bot.add_cog(Remind(bot))
def setup(bot):
bot.add_cog(Remind(bot))

View file

@ -1,31 +1,25 @@
import asyncio
import config
import time
import traceback
import discord
from discord.ext import commands, tasks
import traceback
from discord.ext import commands
from discord.ext.commands import Cog
from robocop_ng.helpers.checks import check_if_staff
from robocop_ng.helpers.restrictions import remove_restriction
from robocop_ng.helpers.robocronp import get_crontab, delete_job
from helpers.robocronp import get_crontab, delete_job
from helpers.restrictions import remove_restriction
from helpers.checks import check_if_staff
class Robocronp(Cog):
def __init__(self, bot):
self.bot = bot
self.minutely.start()
self.hourly.start()
self.daily.start()
def cog_unload(self):
self.minutely.cancel()
self.hourly.cancel()
self.daily.cancel()
bot.loop.create_task(self.minutely())
bot.loop.create_task(self.hourly())
bot.loop.create_task(self.daily())
async def send_data(self):
await self.bot.wait_until_ready()
data_files = [discord.File(fpath) for fpath in self.bot.wanted_jsons]
log_channel = await self.bot.get_channel_safe(self.bot.config.botlog_channel)
log_channel = self.bot.get_channel(config.botlog_channel)
await log_channel.send("Hourly data backups:", files=data_files)
@commands.guild_only()
@ -33,7 +27,7 @@ class Robocronp(Cog):
@commands.command()
async def listjobs(self, ctx):
"""Lists timed robocronp jobs, staff only."""
ctab = get_crontab(self.bot)
ctab = get_crontab()
embed = discord.Embed(title=f"Active robocronp jobs")
for jobtype in ctab:
for jobtimestamp in ctab[jobtype]:
@ -58,31 +52,30 @@ class Robocronp(Cog):
- job name (userid, like 420332322307571713)
You can get all 3 from listjobs command."""
delete_job(self.bot, timestamp, job_type, job_name)
delete_job(timestamp, job_type, job_name)
await ctx.send(f"{ctx.author.mention}: Deleted!")
async def do_jobs(self, ctab, jobtype, timestamp):
await self.bot.wait_until_ready()
log_channel = await self.bot.get_channel_safe(self.bot.config.botlog_channel)
log_channel = self.bot.get_channel(config.botlog_channel)
for job_name in ctab[jobtype][timestamp]:
try:
job_details = ctab[jobtype][timestamp][job_name]
if jobtype == "unban":
target_user = await self.bot.fetch_user(job_name)
target_guild = self.bot.get_guild(job_details["guild"])
delete_job(self.bot, timestamp, jobtype, job_name)
delete_job(timestamp, jobtype, job_name)
await target_guild.unban(
target_user, reason="Robocronp: Timed ban expired."
)
elif jobtype == "unmute":
remove_restriction(self.bot, job_name, self.bot.config.mute_role)
remove_restriction(job_name, config.mute_role)
target_guild = self.bot.get_guild(job_details["guild"])
target_member = target_guild.get_member(int(job_name))
target_role = target_guild.get_role(self.bot.config.mute_role)
target_role = target_guild.get_role(config.mute_role)
await target_member.remove_roles(
target_role, reason="Robocronp: Timed mute expired."
)
delete_job(self.bot, timestamp, jobtype, job_name)
delete_job(timestamp, jobtype, job_name)
elif jobtype == "remind":
text = job_details["text"]
added_on = job_details["added"]
@ -91,19 +84,18 @@ class Robocronp(Cog):
await target.send(
f"You asked to be reminded about `{text}` on {added_on}."
)
delete_job(self.bot, timestamp, jobtype, job_name)
delete_job(timestamp, jobtype, job_name)
except:
# Don't kill cronjobs if something goes wrong.
delete_job(self.bot, timestamp, jobtype, job_name)
delete_job(timestamp, jobtype, job_name)
await log_channel.send(
"Crondo has errored, job deleted: ```"
f"{traceback.format_exc()}```"
)
async def clean_channel(self, channel_id):
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)
log_channel = self.bot.get_channel(config.botlog_channel)
channel = self.bot.get_channel(channel_id)
try:
done_cleaning = False
count = 0
@ -121,12 +113,12 @@ 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 = await self.bot.get_channel_safe(self.bot.config.botlog_channel)
log_channel = self.bot.get_channel(config.botlog_channel)
while not self.bot.is_closed():
try:
ctab = get_crontab(self.bot)
ctab = get_crontab()
timestamp = time.time()
for jobtype in ctab:
for jobtimestamp in ctab[jobtype]:
@ -134,46 +126,56 @@ class Robocronp(Cog):
await self.do_jobs(ctab, jobtype, jobtimestamp)
# Handle clean channels
for clean_channel in self.bot.config.minutely_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)
@tasks.loop(hours=1)
async def hourly(self):
await self.bot.wait_until_ready()
log_channel = await self.bot.get_channel_safe(self.bot.config.botlog_channel)
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 self.bot.config.hourly_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
@tasks.loop(hours=24)
async def daily(self):
await self.bot.wait_until_ready()
log_channel = await self.bot.get_channel_safe(self.bot.config.botlog_channel)
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 self.bot.config.initial_cogs:
verif_channel = await self.bot.get_channel_safe(
self.bot.config.welcome_channel
)
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()}```"
)
await asyncio.sleep(86400)
# Your stuff that should run a day after boot
# and after that every day goes here
async def setup(bot):
await bot.add_cog(Robocronp(bot))
def setup(bot):
bot.add_cog(Robocronp(bot))

View file

@ -1,42 +0,0 @@
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

@ -1,266 +0,0 @@
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,8 +1,9 @@
import discord
from discord.ext import commands
from discord.ext.commands import Cog
from robocop_ng.helpers.checks import check_if_staff
import config
import random
from helpers.checks import check_if_staff
class RyujinxVerification(Cog):
@ -17,28 +18,24 @@ class RyujinxVerification(Cog):
async def on_member_join(self, member):
await self.bot.wait_until_ready()
if member.guild.id not in self.bot.config.guild_whitelist:
if (member.guild.id not in config.guild_whitelist):
return
join_channel = self.bot.get_channel(self.bot.config.welcome_channel)
join_channel = self.bot.get_channel(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 == self.bot.config.welcome_channel:
if message.channel.id == config.welcome_channel:
# Assign common stuff into variables to make stuff less of a mess
mcl = message.content.lower()
# Get the role we will give in case of success
success_role = message.guild.get_role(self.bot.config.participant_role)
success_role = message.guild.get_role(config.participant_role)
if self.bot.config.verification_string == mcl:
if config.verification_string == mcl:
await message.author.add_roles(success_role)
await message.delete()
@ -68,23 +65,21 @@ class RyujinxVerification(Cog):
async def on_member_join(self, member):
await self.bot.wait_until_ready()
if member.guild.id not in self.bot.config.guild_whitelist:
if (member.guild.id not in config.guild_whitelist):
return
join_channel = self.bot.get_channel(self.bot.config.welcome_channel)
join_channel = self.bot.get_channel(config.welcome_channel)
if join_channel is not None:
await join_channel.send(self.bot.config.join_message.format(member))
await join_channel.send(config.join_message.format(member))
@commands.check(check_if_staff)
@commands.command()
async def reset(self, ctx, limit: int = 100, force: bool = False):
"""Wipes messages and pastes the welcome message again. Staff only."""
if ctx.message.channel.id != self.bot.config.welcome_channel and not force:
await ctx.send(
f"This command is limited to"
f" <#{self.bot.config.welcome_channel}>, unless forced."
)
if ctx.message.channel.id != config.welcome_channel and not force:
await ctx.send(f"This command is limited to"
f" <#{config.welcome_channel}>, unless forced.")
return
await self.do_reset(ctx.channel, ctx.author.mention, limit)
@ -95,6 +90,5 @@ class RyujinxVerification(Cog):
# We only auto clear the channel daily
await self.do_reset(channel, author)
async def setup(bot):
await bot.add_cog(RyujinxVerification(bot))
def setup(bot):
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 robocop_ng.helpers.checks import check_if_staff_or_ot
from 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(self.bot.config.self_assignable_roles)
+ f"\n\nRun `{self.bot.config.prefixes[0]}iam role_name_goes_here` to get or remove one."
+ ",".join(config.self_assignable_roles)
+ f"\n\nRun `{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 self.bot.config.self_assignable_roles:
if role not in config.self_assignable_roles:
return await ctx.send(
"There's no self assignable role with that name. Run .sar to see what you can self assign."
)
target_role = ctx.guild.get_role(self.bot.config.self_assignable_roles[role])
target_role = ctx.guild.get_role(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):
)
async def setup(bot):
await bot.add_cog(SAR(bot))
def setup(bot):
bot.add_cog(SAR(bot))

View file

@ -1,40 +0,0 @@
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,20 +1,19 @@
import asyncio
import hashlib
import itertools
import random
from inspect import cleandoc
import discord
from discord.ext import commands
from discord.ext.commands import Cog
from robocop_ng.helpers.checks import check_if_staff
import asyncio
import config
import random
from inspect import cleandoc
import hashlib
import itertools
from helpers.checks import check_if_staff
class Verification(Cog):
def __init__(self, bot):
self.bot = bot
self.hash_choice = random.choice(self.bot.config.welcome_hashes)
self.hash_choice = random.choice(config.welcome_hashes)
# Export reset channel functions
self.bot.do_reset = self.do_reset
@ -23,10 +22,10 @@ class Verification(Cog):
async def do_reset(self, channel, author, limit: int = 100):
await channel.purge(limit=limit)
await channel.send(self.bot.config.welcome_header)
await channel.send(config.welcome_header)
rules = [
"**{}**. {}".format(i, cleandoc(r))
for i, r in enumerate(self.bot.config.welcome_rules, 1)
for i, r in enumerate(config.welcome_rules, 1)
]
rule_choice = random.randint(2, len(rules))
hash_choice_str = self.hash_choice.upper()
@ -34,14 +33,12 @@ class Verification(Cog):
hash_choice_str += "-512"
elif hash_choice_str == "BLAKE2S":
hash_choice_str += "-256"
rules[rule_choice - 1] += "\n" + self.bot.config.hidden_term_line.format(
hash_choice_str
)
rules[rule_choice - 1] += "\n" + config.hidden_term_line.format(hash_choice_str)
msg = (
f"🗑 **Reset**: {author} cleared {limit} messages " f" in {channel.mention}"
)
msg += f"\n💬 __Current challenge location__: under rule {rule_choice}"
log_channel = self.bot.get_channel(self.bot.config.log_channel)
log_channel = self.bot.get_channel(config.log_channel)
await log_channel.send(msg)
# find rule that puts us over 2,000 characters, if any
@ -63,19 +60,19 @@ class Verification(Cog):
await channel.send(item)
await asyncio.sleep(1)
for x in self.bot.config.welcome_footer:
for x in config.welcome_footer:
await channel.send(cleandoc(x))
await asyncio.sleep(1)
async def do_resetalgo(self, channel, author, limit: int = 100):
# randomize hash_choice on reset
self.hash_choice = random.choice(tuple(self.bot.config.welcome_hashes))
self.hash_choice = random.choice(tuple(config.welcome_hashes))
msg = (
f"📘 **Reset Algorithm**: {author} reset " f"algorithm in {channel.mention}"
)
msg += f"\n💬 __Current algorithm__: {self.hash_choice.upper()}"
log_channel = self.bot.get_channel(self.bot.config.log_channel)
log_channel = self.bot.get_channel(config.log_channel)
await log_channel.send(msg)
await self.do_reset(channel, author)
@ -84,10 +81,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 != self.bot.config.welcome_channel and not force:
if ctx.message.channel.id != config.welcome_channel and not force:
await ctx.send(
f"This command is limited to"
f" <#{self.bot.config.welcome_channel}>, unless forced."
f" <#{config.welcome_channel}>, unless forced."
)
return
await self.do_reset(ctx.channel, ctx.author.mention, limit)
@ -96,10 +93,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 != self.bot.config.welcome_channel and not force:
if ctx.message.channel.id != config.welcome_channel and not force:
await ctx.send(
f"This command is limited to"
f" <#{self.bot.config.welcome_channel}>, unless forced."
f" <#{config.welcome_channel}>, unless forced."
)
return
@ -110,7 +107,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 == self.bot.config.welcome_channel:
if message.channel.id == config.welcome_channel:
# Assign common stuff into variables to make stuff less of a mess
member = message.author
full_name = str(member)
@ -137,7 +134,7 @@ class Verification(Cog):
return await chan.send(snark)
# Get the role we will give in case of success
success_role = guild.get_role(self.bot.config.named_roles["participant"])
success_role = guild.get_role(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)]
@ -170,13 +167,11 @@ class Verification(Cog):
)
# Detect if the user uses the wrong hash algorithm
wrong_hash_algos = list(
set(self.bot.config.welcome_hashes) - {self.hash_choice}
)
wrong_hash_algos = list(set(config.welcome_hashes) - {self.hash_choice})
for algo in wrong_hash_algos:
for name in itertools.chain(allowed_names, close_names):
if hashlib.new(algo, name.encode("utf-8")).hexdigest() in mcl:
log_channel = self.bot.get_channel(self.bot.config.log_channel)
log_channel = self.bot.get_channel(config.log_channel)
await log_channel.send(
f"User {message.author.mention} tried verification with algo {algo} instead of {self.hash_choice}."
)
@ -223,5 +218,5 @@ class Verification(Cog):
await chan.send("💢 I don't have permission to do this.")
async def setup(bot):
await bot.add_cog(Verification(bot))
def setup(bot):
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(self.bot.config.yubico_otp_secret)
key = base64.b64decode(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={self.bot.config.yubico_otp_client_id}&nonce={nonce}&otp={otp}"
params = f"id={config.yubico_otp_client_id}&nonce={nonce}&otp={otp}"
# If secret is supplied, sign our request
if self.bot.config.yubico_otp_secret:
if config.yubico_otp_secret:
params += "&h=" + self.calc_signature(params)
for api_server in self.api_servers:
@ -84,22 +84,21 @@ 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 self.bot.config.yubico_otp_secret:
if config.yubico_otp_secret:
assert self.validate_response_signature(datafields)
# If we got a success, then return True
@ -149,5 +148,5 @@ class YubicoOTP(Cog):
await msg.delete()
async def setup(bot):
await bot.add_cog(YubicoOTP(bot))
def setup(bot):
bot.add_cog(YubicoOTP(bot))

View file

@ -1,9 +1,8 @@
import datetime
import hashlib
import datetime
# 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."
@ -40,7 +39,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:
@ -64,16 +63,12 @@ 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,12 +22,6 @@ 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

@ -1,21 +0,0 @@
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

@ -1,189 +0,0 @@
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

@ -1,47 +0,0 @@
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,26 +11,17 @@ switch_modules = {
9: "Loader ",
10: "CMIF (IPC command interface) ",
11: "HIPC (IPC) ",
12: "TMA ",
15: "PM ",
16: "NS ",
17: "BSDSockets ",
17: "Sockets ",
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 ",
@ -40,7 +31,6 @@ switch_modules = {
108: "XCD ",
110: "NIFM ",
111: "Hwopus ",
112: "LSM6DS3 ",
113: "Bluetooth ",
114: "VI ",
115: "NFP ",
@ -58,7 +48,6 @@ switch_modules = {
128: "AM ",
129: "Play Report ",
130: "AHID ",
131: "Applet ",
132: "Home Menu (Qlaunch) ",
133: "PCV ",
134: "OMM ",
@ -79,74 +68,37 @@ 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 ",
182: "STDFU ",
183: "Debug ",
187: "SPI ",
189: "PWM ",
191: "RTC ",
192: "Regulator ",
193: "LED ",
197: "Clkrst ",
198: "Powctl ",
191: "RTC",
192: "Regulator",
197: "Clkrst",
202: "HID ",
203: "LDN ",
204: "CS ",
205: "Irsensor ",
206: "Capture ",
208: "Manu ",
209: "ATK ",
210: "Web ",
211: "LCS ",
211: " ",
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 ",
@ -154,7 +106,6 @@ switch_modules = {
348: "libnx Nvidia",
349: "libnx Binder",
# Support Errors
520: "Nverpt",
800: "General web-applet",
809: "WifiWebAuthApplet",
810: "Whitelisted-applet",
@ -174,7 +125,6 @@ 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. ",
@ -341,28 +291,6 @@ 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. ",
@ -513,26 +441,6 @@ 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 ",
@ -630,9 +538,6 @@ 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",
@ -652,14 +557,6 @@ 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
@ -669,51 +566,14 @@ 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. ",
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. ",
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

@ -1,29 +0,0 @@
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

@ -1,142 +0,0 @@
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

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

View file

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

View file

@ -1,33 +0,0 @@
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

@ -1,745 +0,0 @@
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

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

View file

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