Compare commits
2 commits
master
...
random_has
Author | SHA1 | Date | |
---|---|---|---|
|
0a8dae2c41 | ||
|
6ec56d56ed |
81 changed files with 2867 additions and 8600 deletions
|
@ -1,8 +0,0 @@
|
||||||
**/__pycache__/
|
|
||||||
.git/
|
|
||||||
.github/
|
|
||||||
.gitignore
|
|
||||||
.dockerignore
|
|
||||||
|
|
||||||
**/config.py
|
|
||||||
**/data/
|
|
18
.github/dependabot.yml
vendored
18
.github/dependabot.yml
vendored
|
@ -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
|
|
2
.github/reviewers.yml
vendored
2
.github/reviewers.yml
vendored
|
@ -1,2 +0,0 @@
|
||||||
default:
|
|
||||||
- TSRBerry
|
|
59
.github/workflows/formatting.yml
vendored
59
.github/workflows/formatting.yml
vendored
|
@ -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
|
|
||||||
|
|
41
.github/workflows/mako.yml
vendored
41
.github/workflows/mako.yml
vendored
|
@ -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
2
.gitignore
vendored
|
@ -98,7 +98,7 @@ files/*
|
||||||
*.ttf
|
*.ttf
|
||||||
|
|
||||||
priv-*
|
priv-*
|
||||||
|
config.py
|
||||||
|
|
||||||
# Prevent data files from being committed
|
# Prevent data files from being committed
|
||||||
data/
|
data/
|
||||||
robocop_ng/config.py
|
|
||||||
|
|
12
Dockerfile
12
Dockerfile
|
@ -1,12 +0,0 @@
|
||||||
FROM python:3.10-alpine
|
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
COPY poetry.lock pyproject.toml ./
|
|
||||||
|
|
||||||
RUN apk add --no-cache gcc musl-dev python3-dev libffi-dev openssl-dev cargo && pip install --no-cache-dir poetry && poetry config virtualenvs.create false && poetry install --no-root --no-interaction --no-ansi -vvv && apk del gcc musl-dev python3-dev libffi-dev openssl-dev cargo
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
CMD [ "python", "-m", "robocop_ng", "/state" ]
|
|
145
README.md
145
README.md
|
@ -1,43 +1,19 @@
|
||||||
# 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/ao/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
|
## How to run
|
||||||
|
|
||||||
- Copy `robocop_ng/config_template.py` to `robocop_ng/config.py` and **configure all necessary parts for your server**.
|
- Copy `config.py.template` to `config.py`, configure all necessary parts to 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.
|
- Install python3.6+.
|
||||||
- Add the bot to your guild. There are many resources about this online.
|
- Install python dependencies (`pip3 install -Ur requirements.txt`, you might need to put `sudo -H` before that)
|
||||||
- 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` below.
|
||||||
- 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.
|
- Run `Robocop.py` (`python3 Robocop.py`)
|
||||||
|
|
||||||
### Running with docker
|
|
||||||
|
|
||||||
- `docker build . -t robocopng`
|
|
||||||
- Assuming your robocop-ng repo is on `~/docker/`: `docker run --restart=always -v ~/docker/robocop-ng:/usr/src/app --name robocop_ng robocopng:latest`
|
|
||||||
|
|
||||||
For updates, run `git pull;docker rm -f robocop_ng` then run the two commands above again.
|
|
||||||
|
|
||||||
### Running manually
|
|
||||||
|
|
||||||
- Install python3.8+.
|
|
||||||
- Install dependencies with [poetry](https://python-poetry.org/) using `poetry install`.
|
|
||||||
- Run `robocop_ng/__main__.py` (`cd robocop_ng;python3 __main__.py`).
|
|
||||||
|
|
||||||
To keep the bot running, you might want to use pm2 or a systemd service.
|
To keep the bot running, you might want to use pm2 or a systemd service.
|
||||||
|
|
||||||
|
@ -47,30 +23,115 @@ 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:
|
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`.
|
- 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 `'`).
|
- 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 `'`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Contributing
|
## TODO
|
||||||
|
|
||||||
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.
|
All Robocop features are now supported.
|
||||||
|
|
||||||
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 .`.
|
<details>
|
||||||
|
<summary>List of added Kurisu/Robocop features</summary>
|
||||||
|
<p>
|
||||||
|
|
||||||
|
- [x] .py configs
|
||||||
|
- [x] membercount command
|
||||||
|
- [x] Meme commands and pegaswitch (honestly the easiest part)
|
||||||
|
- [x] source command
|
||||||
|
- [x] robocop command
|
||||||
|
- [x] Verification: Actual verification system
|
||||||
|
- [x] Verification: Reset command
|
||||||
|
- [x] Logging: joins
|
||||||
|
- [x] Logging: leaves
|
||||||
|
- [x] Logging: role changes
|
||||||
|
- [x] Logging: bans
|
||||||
|
- [x] Logging: kicks
|
||||||
|
- [x] Moderation: speak
|
||||||
|
- [x] Moderation: ban
|
||||||
|
- [x] Moderation: silentban
|
||||||
|
- [x] Moderation: kick
|
||||||
|
- [x] Moderation: userinfo
|
||||||
|
- [x] Moderation: approve-revoke (community)
|
||||||
|
- [x] Moderation: addhacker-removehacker (hacker)
|
||||||
|
- [x] Moderation: probate-unprobate (participant)
|
||||||
|
- [x] Moderation: lock-softlock-unlock (channel lockdown)
|
||||||
|
- [x] Moderation: mute-unmute
|
||||||
|
- [x] Moderation: playing
|
||||||
|
- [x] Moderation: botnickname
|
||||||
|
- [x] Moderation: nickname
|
||||||
|
- [x] Moderation: clear/purge
|
||||||
|
- [x] Moderation: restrictions (people who leave with muted role will get muted role on join)
|
||||||
|
- [x] Warns: warn
|
||||||
|
- [x] Warns: listwarns-listwarnsid
|
||||||
|
- [x] Warns: clearwarns-clearwarnsid
|
||||||
|
- [x] Warns: delwarnid-delwarn
|
||||||
|
- [x] .serr and .err (thanks tomger!)
|
||||||
|
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Credits
|
The main goal of this project, to get Robocop functionality done, is complete.
|
||||||
|
|
||||||
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).
|
Secondary goal is adding new features:
|
||||||
|
|
||||||
[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.
|
- [ ] Purge: On purge, send logs in form of txt file to server logs
|
||||||
|
- [ ] New verification feature: Using log module from akbbot for logging attempts and removing old attempts
|
||||||
|
- [ ] New feature: Modmail
|
||||||
|
- [ ] New feature: Submiterr (relies on modmail)
|
||||||
|
- [ ] New feature: Highlights (problematic words automatically get posted to modmail channel, relies on modmail)
|
||||||
|
- [ ] Feature creep: Shortlink completion (gl/ao/etc)
|
||||||
|
- [ ] New moderation feature: timelock (channel lockdown with time, relies on robocronp)
|
||||||
|
|
||||||
I would like to thank the following, in no particular order:
|
<details>
|
||||||
|
<summary>Completed features</summary>
|
||||||
|
<p>
|
||||||
|
|
||||||
|
- [x] Better security, better checks and better guild whitelisting
|
||||||
|
- [x] Feature creep: Reminds
|
||||||
|
- [x] A system for running jobs in background with an interval (will be called robocronp)
|
||||||
|
- [x] Commands to list said jobs and remove them
|
||||||
|
- [x] New moderation feature: timemute (mute with time, relies on robocronp)
|
||||||
|
- [x] New moderation feature: timeban (ban with expiry, relies on robocronp)
|
||||||
|
- [x] Improvements to lockdown to ensure that staff can talk
|
||||||
|
- [x] New moderation feature: Display of mutes, bans and kicks on listwarns (.userlog now)
|
||||||
|
- [x] New moderation feature: User notes
|
||||||
|
- [x] New moderation feature: Reaction removing features (thanks misson20000!)
|
||||||
|
- [x] New moderation feature: User nickname change
|
||||||
|
- [x] New moderation feature: watch-unwatch
|
||||||
|
- [x] New moderation feature: tracking suspicious keywords
|
||||||
|
- [x] New moderation feature: tracking invites posted
|
||||||
|
- [x] New self-moderation feature: .mywarns
|
||||||
|
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>TODO for robocronp</summary>
|
||||||
|
<p>
|
||||||
|
|
||||||
|
- [ ] Reduce code repetition on mod_timed.py
|
||||||
|
- [x] Allow non-hour values on timed bans
|
||||||
|
|
||||||
|
the following require me to rethink some of the lockdown code, which I don't feel like
|
||||||
|
|
||||||
|
- [ ] lockdown in helper
|
||||||
|
- [ ] timelock command
|
||||||
|
- [ ] working cronjob for unlock
|
||||||
|
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Thanks to
|
||||||
|
|
||||||
- ReSwitched community, for being amazing
|
- ReSwitched community, for being amazing
|
||||||
- ihaveamac/ihaveahax and f916253 for the original kurisu/robocop
|
- ihaveamac/ihaveahax and f916253 for the original kurisu/robocop
|
||||||
|
- tomGER for working hard on rewriting the .err/.serr commands, those were a nightmare
|
||||||
- misson20000 for adding in reaction removal feature and putting up with my many BS requests on PR reviews
|
- 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).
|
|
||||||
|
|
209
Robocop.py
Executable file
209
Robocop.py
Executable file
|
@ -0,0 +1,209 @@
|
||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
import logging.handlers
|
||||||
|
import traceback
|
||||||
|
import aiohttp
|
||||||
|
import config
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
script_name = os.path.basename(__file__).split('.')[0]
|
||||||
|
|
||||||
|
log_file_name = f"{script_name}.log"
|
||||||
|
|
||||||
|
# Limit of discord (non-nitro) is 8MB (not MiB)
|
||||||
|
max_file_size = 1000 * 1000 * 8
|
||||||
|
backup_count = 3
|
||||||
|
file_handler = logging.handlers.RotatingFileHandler(
|
||||||
|
filename=log_file_name, maxBytes=max_file_size, backupCount=backup_count)
|
||||||
|
stdout_handler = logging.StreamHandler(sys.stdout)
|
||||||
|
|
||||||
|
log_format = logging.Formatter(
|
||||||
|
'[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s')
|
||||||
|
file_handler.setFormatter(log_format)
|
||||||
|
stdout_handler.setFormatter(log_format)
|
||||||
|
|
||||||
|
log = logging.getLogger('discord')
|
||||||
|
log.setLevel(logging.INFO)
|
||||||
|
log.addHandler(file_handler)
|
||||||
|
log.addHandler(stdout_handler)
|
||||||
|
|
||||||
|
|
||||||
|
def get_prefix(bot, message):
|
||||||
|
prefixes = config.prefixes
|
||||||
|
|
||||||
|
return commands.when_mentioned_or(*prefixes)(bot, message)
|
||||||
|
|
||||||
|
|
||||||
|
wanted_jsons = ["data/restrictions.json",
|
||||||
|
"data/robocronptab.json",
|
||||||
|
"data/userlog.json",
|
||||||
|
"data/invites.json"]
|
||||||
|
|
||||||
|
initial_extensions = ['cogs.common',
|
||||||
|
'cogs.admin',
|
||||||
|
'cogs.verification',
|
||||||
|
'cogs.mod',
|
||||||
|
'cogs.mod_note',
|
||||||
|
'cogs.mod_reacts',
|
||||||
|
'cogs.mod_userlog',
|
||||||
|
'cogs.mod_timed',
|
||||||
|
'cogs.mod_watch',
|
||||||
|
'cogs.basic',
|
||||||
|
'cogs.logs',
|
||||||
|
'cogs.err',
|
||||||
|
'cogs.lockdown',
|
||||||
|
'cogs.legacy',
|
||||||
|
'cogs.links',
|
||||||
|
'cogs.remind',
|
||||||
|
'cogs.robocronp',
|
||||||
|
'cogs.meme',
|
||||||
|
'cogs.pin',
|
||||||
|
'cogs.invites']
|
||||||
|
|
||||||
|
bot = commands.Bot(command_prefix=get_prefix,
|
||||||
|
description=config.bot_description, pm_help=True)
|
||||||
|
|
||||||
|
bot.log = log
|
||||||
|
bot.loop = asyncio.get_event_loop()
|
||||||
|
bot.config = config
|
||||||
|
bot.script_name = script_name
|
||||||
|
bot.wanted_jsons = wanted_jsons
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
for extension in initial_extensions:
|
||||||
|
try:
|
||||||
|
bot.load_extension(extension)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f'Failed to load extension {extension}.')
|
||||||
|
log.error(traceback.print_exc())
|
||||||
|
|
||||||
|
|
||||||
|
@bot.event
|
||||||
|
async def on_ready():
|
||||||
|
aioh = {"User-Agent": f"{script_name}/1.0'"}
|
||||||
|
bot.aiosession = aiohttp.ClientSession(headers=aioh)
|
||||||
|
bot.app_info = await bot.application_info()
|
||||||
|
bot.botlog_channel = bot.get_channel(config.botlog_channel)
|
||||||
|
|
||||||
|
log.info(f'\nLogged in as: {bot.user.name} - '
|
||||||
|
f'{bot.user.id}\ndpy version: {discord.__version__}\n')
|
||||||
|
game_name = f"{config.prefixes[0]}help"
|
||||||
|
|
||||||
|
# Send "Robocop has started! x has y members!"
|
||||||
|
guild = bot.botlog_channel.guild
|
||||||
|
msg = f"{bot.user.name} has started! "\
|
||||||
|
f"{guild.name} has {guild.member_count} members!"
|
||||||
|
|
||||||
|
data_files = [discord.File(fpath) for fpath in wanted_jsons]
|
||||||
|
await bot.botlog_channel.send(msg, files=data_files)
|
||||||
|
|
||||||
|
activity = discord.Activity(name=game_name,
|
||||||
|
type=discord.ActivityType.listening)
|
||||||
|
|
||||||
|
await bot.change_presence(activity=activity)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.event
|
||||||
|
async def on_command(ctx):
|
||||||
|
log_text = f"{ctx.message.author} ({ctx.message.author.id}): "\
|
||||||
|
f"\"{ctx.message.content}\" "
|
||||||
|
if ctx.guild: # was too long for tertiary if
|
||||||
|
log_text += f"on \"{ctx.channel.name}\" ({ctx.channel.id}) "\
|
||||||
|
f"at \"{ctx.guild.name}\" ({ctx.guild.id})"
|
||||||
|
else:
|
||||||
|
log_text += f"on DMs ({ctx.channel.id})"
|
||||||
|
log.info(log_text)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.event
|
||||||
|
async def on_error(event_method, *args, **kwargs):
|
||||||
|
log.error(f"Error on {event_method}: {sys.exc_info()}")
|
||||||
|
|
||||||
|
|
||||||
|
@bot.event
|
||||||
|
async def on_command_error(ctx, error):
|
||||||
|
error_text = str(error)
|
||||||
|
|
||||||
|
err_msg = f"Error with \"{ctx.message.content}\" from "\
|
||||||
|
f"\"{ctx.message.author} ({ctx.message.author.id}) "\
|
||||||
|
f"of type {type(error)}: {error_text}"
|
||||||
|
|
||||||
|
log.error(err_msg)
|
||||||
|
|
||||||
|
if not isinstance(error, commands.CommandNotFound):
|
||||||
|
err_msg = bot.escape_message(err_msg)
|
||||||
|
await bot.botlog_channel.send(err_msg)
|
||||||
|
|
||||||
|
if isinstance(error, commands.NoPrivateMessage):
|
||||||
|
return await ctx.send("This command doesn't work on DMs.")
|
||||||
|
elif isinstance(error, commands.MissingPermissions):
|
||||||
|
roles_needed = '\n- '.join(error.missing_perms)
|
||||||
|
return await ctx.send(f"{ctx.author.mention}: You don't have the right"
|
||||||
|
" permissions to run this command. You need: "
|
||||||
|
f"```- {roles_needed}```")
|
||||||
|
elif isinstance(error, commands.BotMissingPermissions):
|
||||||
|
roles_needed = '\n-'.join(error.missing_perms)
|
||||||
|
return await ctx.send(f"{ctx.author.mention}: Bot doesn't have "
|
||||||
|
"the right permissions to run this command. "
|
||||||
|
"Please add the following roles: "
|
||||||
|
f"```- {roles_needed}```")
|
||||||
|
elif isinstance(error, commands.CommandOnCooldown):
|
||||||
|
return await ctx.send(f"{ctx.author.mention}: You're being "
|
||||||
|
"ratelimited. Try in "
|
||||||
|
f"{error.retry_after:.1f} seconds.")
|
||||||
|
elif isinstance(error, commands.CheckFailure):
|
||||||
|
return await ctx.send(f"{ctx.author.mention}: Check failed. "
|
||||||
|
"You might not have the right permissions "
|
||||||
|
"to run this command, or you may not be able "
|
||||||
|
"to run this command in the current channel.")
|
||||||
|
elif isinstance(error, commands.CommandInvokeError) and\
|
||||||
|
("Cannot send messages to this user" in error_text):
|
||||||
|
return await ctx.send(f"{ctx.author.mention}: I can't DM you.\n"
|
||||||
|
"You might have me blocked or have DMs "
|
||||||
|
f"blocked globally or for {ctx.guild.name}.\n"
|
||||||
|
"Please resolve that, then "
|
||||||
|
"run the command again.")
|
||||||
|
elif isinstance(error, commands.CommandNotFound):
|
||||||
|
# Nothing to do when command is not found.
|
||||||
|
return
|
||||||
|
|
||||||
|
help_text = f"Usage of this command is: ```{ctx.prefix}"\
|
||||||
|
f"{ctx.command.signature}```\nPlease see `{ctx.prefix}help "\
|
||||||
|
f"{ctx.command.name}` for more info about this command."
|
||||||
|
if isinstance(error, commands.BadArgument):
|
||||||
|
return await ctx.send(f"{ctx.author.mention}: You gave incorrect "
|
||||||
|
f"arguments. {help_text}")
|
||||||
|
elif isinstance(error, commands.MissingRequiredArgument):
|
||||||
|
return await ctx.send(f"{ctx.author.mention}: You gave incomplete "
|
||||||
|
f"arguments. {help_text}")
|
||||||
|
|
||||||
|
|
||||||
|
@bot.event
|
||||||
|
async def on_message(message):
|
||||||
|
if message.author.bot:
|
||||||
|
return
|
||||||
|
|
||||||
|
if (message.guild) and (message.guild.id not in config.guild_whitelist):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ignore messages in newcomers channel, unless it's potentially reset
|
||||||
|
if message.channel.id == config.welcome_channel and\
|
||||||
|
"reset" not in message.content:
|
||||||
|
return
|
||||||
|
|
||||||
|
ctx = await bot.get_context(message)
|
||||||
|
await bot.invoke(ctx)
|
||||||
|
|
||||||
|
if not os.path.exists("data"):
|
||||||
|
os.makedirs("data")
|
||||||
|
|
||||||
|
for wanted_json in wanted_jsons:
|
||||||
|
if not os.path.exists(wanted_json):
|
||||||
|
with open(wanted_json, "w") as f:
|
||||||
|
f.write("{}")
|
||||||
|
|
||||||
|
bot.run(config.token, bot=True, reconnect=True, loop=bot.loop)
|
22
SECURITY.md
22
SECURITY.md
|
@ -1,22 +0,0 @@
|
||||||
# Security Policy
|
|
||||||
|
|
||||||
PRs to this file to improve wording are welcome.
|
|
||||||
|
|
||||||
Please do not try to exploit public instances if it's going to cause harm, instead, set up your own instance of robocop-ng.
|
|
||||||
|
|
||||||
Breaking "database" files, running arbitrary code, using an unprivileged user to do something user can't normally do (editing channels or guild, deleting others' messages, making bot do an @e or @h mention, reading channels that user can't read, writing to channels that user can't write to, etc.) are all considered harmful.
|
|
||||||
|
|
||||||
## Supported Versions
|
|
||||||
|
|
||||||
Use this section to tell people about which versions of your project are
|
|
||||||
currently being supported with security updates.
|
|
||||||
|
|
||||||
| Version | Supported |
|
|
||||||
| ------------ | ------------------ |
|
|
||||||
| Latest git | :white_check_mark: |
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
If the vulnerability fits into the "harmful" category specified above, then please email arcab [at] ave [dot] zone with details, as creating a public issue may cause it to be abused on public instances.
|
|
||||||
|
|
||||||
If not, please open an issue.
|
|
160
cogs/admin.py
Normal file
160
cogs/admin.py
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
from discord.ext.commands import Cog
|
||||||
|
import traceback
|
||||||
|
import inspect
|
||||||
|
import re
|
||||||
|
from helpers.checks import check_if_bot_manager
|
||||||
|
|
||||||
|
|
||||||
|
class Admin(Cog):
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.last_eval_result = None
|
||||||
|
self.previous_eval_code = None
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.check(check_if_bot_manager)
|
||||||
|
@commands.command(name='exit', aliases=["quit", "bye"])
|
||||||
|
async def _exit(self, ctx):
|
||||||
|
"""Shuts down the bot, bot manager only."""
|
||||||
|
await ctx.send(":wave: Goodbye!")
|
||||||
|
await self.bot.logout()
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.check(check_if_bot_manager)
|
||||||
|
@commands.command()
|
||||||
|
async def load(self, ctx, ext: str):
|
||||||
|
"""Loads a cog, bot manager only."""
|
||||||
|
try:
|
||||||
|
self.bot.load_extension("cogs." + ext)
|
||||||
|
except:
|
||||||
|
await ctx.send(f':x: Cog loading failed, traceback: '
|
||||||
|
f'```\n{traceback.format_exc()}\n```')
|
||||||
|
return
|
||||||
|
self.bot.log.info(f'Loaded ext {ext}')
|
||||||
|
await ctx.send(f':white_check_mark: `{ext}` successfully loaded.')
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.check(check_if_bot_manager)
|
||||||
|
@commands.command()
|
||||||
|
async def fetchlog(self, ctx):
|
||||||
|
"""Returns log"""
|
||||||
|
await ctx.send("Here's the current log file:",
|
||||||
|
file=discord.File(f"{self.bot.script_name}.log"))
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.check(check_if_bot_manager)
|
||||||
|
@commands.command()
|
||||||
|
async def fetchdata(self, ctx):
|
||||||
|
"""Returns data files"""
|
||||||
|
data_files = [discord.File(fpath) for fpath in self.bot.wanted_jsons]
|
||||||
|
await ctx.send("Here you go:", files=data_files)
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.check(check_if_bot_manager)
|
||||||
|
@commands.command(name='eval')
|
||||||
|
async def _eval(self, ctx, *, code: str):
|
||||||
|
"""Evaluates some code, bot manager only."""
|
||||||
|
try:
|
||||||
|
code = code.strip('` ')
|
||||||
|
|
||||||
|
env = {
|
||||||
|
'bot': self.bot,
|
||||||
|
'ctx': ctx,
|
||||||
|
'message': ctx.message,
|
||||||
|
'server': ctx.guild,
|
||||||
|
'guild': ctx.guild,
|
||||||
|
'channel': ctx.message.channel,
|
||||||
|
'author': ctx.message.author,
|
||||||
|
|
||||||
|
# modules
|
||||||
|
'discord': discord,
|
||||||
|
'commands': commands,
|
||||||
|
|
||||||
|
# utilities
|
||||||
|
'_get': discord.utils.get,
|
||||||
|
'_find': discord.utils.find,
|
||||||
|
|
||||||
|
# last result
|
||||||
|
'_': self.last_eval_result,
|
||||||
|
'_p': self.previous_eval_code,
|
||||||
|
}
|
||||||
|
env.update(globals())
|
||||||
|
|
||||||
|
self.bot.log.info(f"Evaling {repr(code)}:")
|
||||||
|
result = eval(code, env)
|
||||||
|
if inspect.isawaitable(result):
|
||||||
|
result = await result
|
||||||
|
|
||||||
|
if result is not None:
|
||||||
|
self.last_eval_result = result
|
||||||
|
|
||||||
|
self.previous_eval_code = code
|
||||||
|
|
||||||
|
sliced_message = await self.bot.slice_message(repr(result),
|
||||||
|
prefix="```",
|
||||||
|
suffix="```")
|
||||||
|
for msg in sliced_message:
|
||||||
|
await ctx.send(msg)
|
||||||
|
except:
|
||||||
|
sliced_message = \
|
||||||
|
await self.bot.slice_message(traceback.format_exc(),
|
||||||
|
prefix="```",
|
||||||
|
suffix="```")
|
||||||
|
for msg in sliced_message:
|
||||||
|
await ctx.send(msg)
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.check(check_if_bot_manager)
|
||||||
|
@commands.command()
|
||||||
|
async def pull(self, ctx, auto=False):
|
||||||
|
"""Does a git pull, bot manager only."""
|
||||||
|
tmp = await ctx.send('Pulling...')
|
||||||
|
git_output = await self.bot.async_call_shell("git pull")
|
||||||
|
await tmp.edit(content=f"Pull complete. Output: ```{git_output}```")
|
||||||
|
if auto:
|
||||||
|
cogs_to_reload = re.findall(r'cogs/([a-z_]*).py[ ]*\|', git_output)
|
||||||
|
for cog in cogs_to_reload:
|
||||||
|
try:
|
||||||
|
self.bot.unload_extension("cogs." + cog)
|
||||||
|
self.bot.load_extension("cogs." + cog)
|
||||||
|
self.bot.log.info(f'Reloaded ext {cog}')
|
||||||
|
await ctx.send(f':white_check_mark: `{cog}` '
|
||||||
|
'successfully reloaded.')
|
||||||
|
except:
|
||||||
|
await ctx.send(f':x: Cog reloading failed, traceback: '
|
||||||
|
f'```\n{traceback.format_exc()}\n```')
|
||||||
|
return
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.check(check_if_bot_manager)
|
||||||
|
@commands.command()
|
||||||
|
async def unload(self, ctx, ext: str):
|
||||||
|
"""Unloads a cog, bot manager only."""
|
||||||
|
self.bot.unload_extension("cogs." + ext)
|
||||||
|
self.bot.log.info(f'Unloaded ext {ext}')
|
||||||
|
await ctx.send(f':white_check_mark: `{ext}` successfully unloaded.')
|
||||||
|
|
||||||
|
@commands.check(check_if_bot_manager)
|
||||||
|
@commands.command()
|
||||||
|
async def reload(self, ctx, ext="_"):
|
||||||
|
"""Reloads a cog, bot manager only."""
|
||||||
|
if ext == "_":
|
||||||
|
ext = self.lastreload
|
||||||
|
else:
|
||||||
|
self.lastreload = ext
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.bot.unload_extension("cogs." + ext)
|
||||||
|
self.bot.load_extension("cogs." + ext)
|
||||||
|
except:
|
||||||
|
await ctx.send(f':x: Cog reloading failed, traceback: '
|
||||||
|
f'```\n{traceback.format_exc()}\n```')
|
||||||
|
return
|
||||||
|
self.bot.log.info(f'Reloaded ext {ext}')
|
||||||
|
await ctx.send(f':white_check_mark: `{ext}` successfully reloaded.')
|
||||||
|
|
||||||
|
|
||||||
|
def setup(bot):
|
||||||
|
bot.add_cog(Admin(bot))
|
62
cogs/basic.py
Normal file
62
cogs/basic.py
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import time
|
||||||
|
import config
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
from discord.ext.commands import Cog
|
||||||
|
|
||||||
|
class Basic(Cog):
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def hello(self, ctx):
|
||||||
|
"""Says hello. Duh."""
|
||||||
|
await ctx.send(f"Hello {ctx.author.mention}!")
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.command()
|
||||||
|
async def communitycount(self, ctx):
|
||||||
|
"""Prints the community member count of the server."""
|
||||||
|
community = ctx.guild.get_role(config.named_roles["community"])
|
||||||
|
await ctx.send(f"{ctx.guild.name} has "
|
||||||
|
f"{len(community.members)} community members!")
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.command()
|
||||||
|
async def membercount(self, ctx):
|
||||||
|
"""Prints the member count of the server."""
|
||||||
|
await ctx.send(f"{ctx.guild.name} has "
|
||||||
|
f"{ctx.guild.member_count} members!")
|
||||||
|
|
||||||
|
@commands.command(aliases=["robocopng", "robocop-ng"])
|
||||||
|
async def robocop(self, ctx):
|
||||||
|
"""Shows a quick embed with bot info."""
|
||||||
|
embed = discord.Embed(title="Robocop-NG",
|
||||||
|
url=config.source_url,
|
||||||
|
description=config.embed_desc)
|
||||||
|
|
||||||
|
embed.set_thumbnail(url=self.bot.user.avatar_url)
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@commands.command(aliases=['p'])
|
||||||
|
async def ping(self, ctx):
|
||||||
|
"""Shows ping values to discord.
|
||||||
|
|
||||||
|
RTT = Round-trip time, time taken to send a message to discord
|
||||||
|
GW = Gateway Ping"""
|
||||||
|
before = time.monotonic()
|
||||||
|
tmp = await ctx.send('Calculating ping...')
|
||||||
|
after = time.monotonic()
|
||||||
|
rtt_ms = (after - before) * 1000
|
||||||
|
gw_ms = self.bot.latency * 1000
|
||||||
|
|
||||||
|
message_text = f":ping_pong:\n"\
|
||||||
|
f"rtt: `{rtt_ms:.1f}ms`\n"\
|
||||||
|
f"gw: `{gw_ms:.1f}ms`"
|
||||||
|
self.bot.log.info(message_text)
|
||||||
|
await tmp.edit(content=message_text)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(bot):
|
||||||
|
bot.add_cog(Basic(bot))
|
|
@ -7,7 +7,6 @@ import math
|
||||||
import parsedatetime
|
import parsedatetime
|
||||||
from discord.ext.commands import Cog
|
from discord.ext.commands import Cog
|
||||||
|
|
||||||
|
|
||||||
class Common(Cog):
|
class Common(Cog):
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
@ -31,14 +30,9 @@ class Common(Cog):
|
||||||
res_timestamp = math.floor(time.mktime(time_struct))
|
res_timestamp = math.floor(time.mktime(time_struct))
|
||||||
return res_timestamp
|
return res_timestamp
|
||||||
|
|
||||||
def get_relative_timestamp(
|
def get_relative_timestamp(self, time_from=None, time_to=None,
|
||||||
self,
|
humanized=False, include_from=False,
|
||||||
time_from=None,
|
include_to=False):
|
||||||
time_to=None,
|
|
||||||
humanized=False,
|
|
||||||
include_from=False,
|
|
||||||
include_to=False,
|
|
||||||
):
|
|
||||||
# Setting default value to utcnow() makes it show time from cog load
|
# Setting default value to utcnow() makes it show time from cog load
|
||||||
# which is not what we want
|
# which is not what we want
|
||||||
if not time_from:
|
if not time_from:
|
||||||
|
@ -48,19 +42,17 @@ class Common(Cog):
|
||||||
if humanized:
|
if humanized:
|
||||||
humanized_string = humanize.naturaltime(time_from - time_to)
|
humanized_string = humanize.naturaltime(time_from - time_to)
|
||||||
if include_from and include_to:
|
if include_from and include_to:
|
||||||
str_with_from_and_to = (
|
str_with_from_and_to = f"{humanized_string} "\
|
||||||
f"{humanized_string} "
|
f"({str(time_from).split('.')[0]} "\
|
||||||
f"({str(time_from).split('.')[0]} "
|
|
||||||
f"- {str(time_to).split('.')[0]})"
|
f"- {str(time_to).split('.')[0]})"
|
||||||
)
|
|
||||||
return str_with_from_and_to
|
return str_with_from_and_to
|
||||||
elif include_from:
|
elif include_from:
|
||||||
str_with_from = (
|
str_with_from = f"{humanized_string} "\
|
||||||
f"{humanized_string} " f"({str(time_from).split('.')[0]})"
|
f"({str(time_from).split('.')[0]})"
|
||||||
)
|
|
||||||
return str_with_from
|
return str_with_from
|
||||||
elif include_to:
|
elif include_to:
|
||||||
str_with_to = f"{humanized_string} " f"({str(time_to).split('.')[0]})"
|
str_with_to = f"{humanized_string} "\
|
||||||
|
f"({str(time_to).split('.')[0]})"
|
||||||
return str_with_to
|
return str_with_to
|
||||||
return humanized_string
|
return humanized_string
|
||||||
else:
|
else:
|
||||||
|
@ -68,7 +60,8 @@ class Common(Cog):
|
||||||
epoch_from = (time_from - epoch).total_seconds()
|
epoch_from = (time_from - epoch).total_seconds()
|
||||||
epoch_to = (time_to - epoch).total_seconds()
|
epoch_to = (time_to - epoch).total_seconds()
|
||||||
second_diff = epoch_to - epoch_from
|
second_diff = epoch_to - epoch_from
|
||||||
result_string = str(datetime.timedelta(seconds=second_diff)).split(".")[0]
|
result_string = str(datetime.timedelta(
|
||||||
|
seconds=second_diff)).split('.')[0]
|
||||||
return result_string
|
return result_string
|
||||||
|
|
||||||
async def aioget(self, url):
|
async def aioget(self, url):
|
||||||
|
@ -79,12 +72,11 @@ class Common(Cog):
|
||||||
self.bot.log.info(f"Data from {url}: {text_data}")
|
self.bot.log.info(f"Data from {url}: {text_data}")
|
||||||
return text_data
|
return text_data
|
||||||
else:
|
else:
|
||||||
self.bot.log.error(f"HTTP Error {data.status} while getting {url}")
|
self.bot.log.error(f"HTTP Error {data.status} "
|
||||||
|
"while getting {url}")
|
||||||
except:
|
except:
|
||||||
self.bot.log.error(
|
self.bot.log.error(f"Error while getting {url} "
|
||||||
f"Error while getting {url} "
|
f"on aiogetbytes: {traceback.format_exc()}")
|
||||||
f"on aiogetbytes: {traceback.format_exc()}"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def aiogetbytes(self, url):
|
async def aiogetbytes(self, url):
|
||||||
try:
|
try:
|
||||||
|
@ -94,12 +86,11 @@ class Common(Cog):
|
||||||
self.bot.log.debug(f"Data from {url}: {byte_data}")
|
self.bot.log.debug(f"Data from {url}: {byte_data}")
|
||||||
return byte_data
|
return byte_data
|
||||||
else:
|
else:
|
||||||
self.bot.log.error(f"HTTP Error {data.status} while getting {url}")
|
self.bot.log.error(f"HTTP Error {data.status} "
|
||||||
|
"while getting {url}")
|
||||||
except:
|
except:
|
||||||
self.bot.log.error(
|
self.bot.log.error(f"Error while getting {url} "
|
||||||
f"Error while getting {url} "
|
f"on aiogetbytes: {traceback.format_exc()}")
|
||||||
f"on aiogetbytes: {traceback.format_exc()}"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def aiojson(self, url):
|
async def aiojson(self, url):
|
||||||
try:
|
try:
|
||||||
|
@ -107,19 +98,18 @@ class Common(Cog):
|
||||||
if data.status == 200:
|
if data.status == 200:
|
||||||
text_data = await data.text()
|
text_data = await data.text()
|
||||||
self.bot.log.info(f"Data from {url}: {text_data}")
|
self.bot.log.info(f"Data from {url}: {text_data}")
|
||||||
content_type = data.headers["Content-Type"]
|
content_type = data.headers['Content-Type']
|
||||||
return await data.json(content_type=content_type)
|
return await data.json(content_type=content_type)
|
||||||
else:
|
else:
|
||||||
self.bot.log.error(f"HTTP Error {data.status} while getting {url}")
|
self.bot.log.error(f"HTTP Error {data.status} "
|
||||||
|
"while getting {url}")
|
||||||
except:
|
except:
|
||||||
self.bot.log.error(
|
self.bot.log.error(f"Error while getting {url} "
|
||||||
f"Error while getting {url} "
|
f"on aiogetbytes: {traceback.format_exc()}")
|
||||||
f"on aiogetbytes: {traceback.format_exc()}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def hex_to_int(self, color_hex: str):
|
def hex_to_int(self, color_hex: str):
|
||||||
"""Turns a given hex color into an integer"""
|
"""Turns a given hex color into an integer"""
|
||||||
return int("0x" + color_hex.strip("#"), 16)
|
return int("0x" + color_hex.strip('#'), 16)
|
||||||
|
|
||||||
def escape_message(self, text: str):
|
def escape_message(self, text: str):
|
||||||
"""Escapes unfun stuff from messages"""
|
"""Escapes unfun stuff from messages"""
|
||||||
|
@ -139,12 +129,10 @@ class Common(Cog):
|
||||||
"""Slices a message into multiple messages"""
|
"""Slices a message into multiple messages"""
|
||||||
if len(text) > size * self.max_split_length:
|
if len(text) > size * self.max_split_length:
|
||||||
haste_url = await self.haste(text)
|
haste_url = await self.haste(text)
|
||||||
return [
|
return [f"Message is too long ({len(text)} > "
|
||||||
f"Message is too long ({len(text)} > "
|
|
||||||
f"{size * self.max_split_length} "
|
f"{size * self.max_split_length} "
|
||||||
f"({size} * {self.max_split_length}))"
|
f"({size} * {self.max_split_length}))"
|
||||||
f", go to haste: <{haste_url}>"
|
f", go to haste: <{haste_url}>"]
|
||||||
]
|
|
||||||
reply_list = []
|
reply_list = []
|
||||||
size_wo_fix = size - len(prefix) - len(suffix)
|
size_wo_fix = size - len(prefix) - len(suffix)
|
||||||
while len(text) > size_wo_fix:
|
while len(text) > size_wo_fix:
|
||||||
|
@ -153,28 +141,28 @@ class Common(Cog):
|
||||||
reply_list.append(f"{prefix}{text}{suffix}")
|
reply_list.append(f"{prefix}{text}{suffix}")
|
||||||
return reply_list
|
return reply_list
|
||||||
|
|
||||||
async def haste(self, text, instance="https://mystb.in/"):
|
async def haste(self, text, instance='https://mystb.in/'):
|
||||||
response = await self.bot.aiosession.post(f"{instance}documents", data=text)
|
response = await self.bot.aiosession.post(f"{instance}documents",
|
||||||
|
data=text)
|
||||||
if response.status == 200:
|
if response.status == 200:
|
||||||
result_json = await response.json()
|
result_json = await response.json()
|
||||||
return f"{instance}{result_json['key']}"
|
return f"{instance}{result_json['key']}"
|
||||||
else:
|
else:
|
||||||
return f"Error {response.status}: {response.text}"
|
return f"Error {response.status}: {response.text}"
|
||||||
|
|
||||||
async def async_call_shell(
|
async def async_call_shell(self, shell_command: str,
|
||||||
self, shell_command: str, inc_stdout=True, inc_stderr=True
|
inc_stdout=True, inc_stderr=True):
|
||||||
):
|
|
||||||
pipe = asyncio.subprocess.PIPE
|
pipe = asyncio.subprocess.PIPE
|
||||||
proc = await asyncio.create_subprocess_shell(
|
proc = await asyncio.create_subprocess_shell(str(shell_command),
|
||||||
str(shell_command), stdout=pipe, stderr=pipe
|
stdout=pipe,
|
||||||
)
|
stderr=pipe)
|
||||||
|
|
||||||
if not (inc_stdout or inc_stderr):
|
if not (inc_stdout or inc_stderr):
|
||||||
return "??? you set both stdout and stderr to False????"
|
return "??? you set both stdout and stderr to False????"
|
||||||
|
|
||||||
proc_result = await proc.communicate()
|
proc_result = await proc.communicate()
|
||||||
stdout_str = proc_result[0].decode("utf-8").strip()
|
stdout_str = proc_result[0].decode('utf-8').strip()
|
||||||
stderr_str = proc_result[1].decode("utf-8").strip()
|
stderr_str = proc_result[1].decode('utf-8').strip()
|
||||||
|
|
||||||
if inc_stdout and not inc_stderr:
|
if inc_stdout and not inc_stderr:
|
||||||
return stdout_str
|
return stdout_str
|
||||||
|
@ -182,7 +170,8 @@ class Common(Cog):
|
||||||
return stderr_str
|
return stderr_str
|
||||||
|
|
||||||
if stdout_str and stderr_str:
|
if stdout_str and stderr_str:
|
||||||
return f"stdout:\n\n{stdout_str}\n\n" f"======\n\nstderr:\n\n{stderr_str}"
|
return f"stdout:\n\n{stdout_str}\n\n"\
|
||||||
|
f"======\n\nstderr:\n\n{stderr_str}"
|
||||||
elif stdout_str:
|
elif stdout_str:
|
||||||
return f"stdout:\n\n{stdout_str}"
|
return f"stdout:\n\n{stdout_str}"
|
||||||
elif stderr_str:
|
elif stderr_str:
|
||||||
|
@ -191,5 +180,5 @@ class Common(Cog):
|
||||||
return "No output."
|
return "No output."
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
def setup(bot):
|
||||||
await bot.add_cog(Common(bot))
|
bot.add_cog(Common(bot))
|
|
@ -1,26 +1,23 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from discord.ext.commands import Cog
|
from discord.ext.commands import Cog
|
||||||
|
from helpers.errcodes import *
|
||||||
from robocop_ng.helpers.errcodes import *
|
|
||||||
|
|
||||||
|
|
||||||
class Err(Cog):
|
class Err(Cog):
|
||||||
"""Everything related to Nintendo 3DS, Wii U and Switch error codes"""
|
"""Everything related to Nintendo 3DS, Wii U and Switch error codes"""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.dds_re = re.compile(r"0\d{2}\-\d{4}")
|
self.dds_re = re.compile(r'0\d{2}\-\d{4}')
|
||||||
self.wiiu_re = re.compile(r"1\d{2}\-\d{4}")
|
self.wiiu_re = re.compile(r'1\d{2}\-\d{4}')
|
||||||
self.switch_re = re.compile(r"2\d{3}\-\d{4}")
|
self.switch_re = re.compile(r'2\d{3}\-\d{4}')
|
||||||
self.no_err_desc = (
|
self.no_err_desc = "It seems like your error code is unknown. "\
|
||||||
"It seems like your error code is unknown. "
|
"You should report relevant details to "\
|
||||||
"You can check on Switchbrew for your error code at "
|
"<@141532589725974528> (tomGER#7462) "\
|
||||||
"<https://switchbrew.org/wiki/Error_codes>"
|
"so it can be added to the bot."
|
||||||
)
|
self.rickroll = "https://www.youtube.com/watch?v=4uj896lr3-E"
|
||||||
self.rickroll = "https://www.youtube.com/watch?v=z3ZiVn5L9vM"
|
|
||||||
|
|
||||||
@commands.command(aliases=["3dserr", "3err", "dserr"])
|
@commands.command(aliases=["3dserr", "3err", "dserr"])
|
||||||
async def dderr(self, ctx, err: str):
|
async def dderr(self, ctx, err: str):
|
||||||
|
@ -32,9 +29,9 @@ class Err(Cog):
|
||||||
else:
|
else:
|
||||||
err_description = self.no_err_desc
|
err_description = self.no_err_desc
|
||||||
# Make a nice Embed out of it
|
# Make a nice Embed out of it
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(title=err,
|
||||||
title=err, url=self.rickroll, description=err_description
|
url=self.rickroll,
|
||||||
)
|
description=err_description)
|
||||||
embed.set_footer(text="Console: 3DS")
|
embed.set_footer(text="Console: 3DS")
|
||||||
|
|
||||||
# Send message, crazy
|
# Send message, crazy
|
||||||
|
@ -51,7 +48,8 @@ class Err(Cog):
|
||||||
level = (rc >> 27) & 0x1F
|
level = (rc >> 27) & 0x1F
|
||||||
embed = discord.Embed(title=f"0x{rc:X}")
|
embed = discord.Embed(title=f"0x{rc:X}")
|
||||||
embed.add_field(name="Module", value=dds_modules.get(mod, mod))
|
embed.add_field(name="Module", value=dds_modules.get(mod, mod))
|
||||||
embed.add_field(name="Description", value=dds_descriptions.get(desc, desc))
|
embed.add_field(name="Description",
|
||||||
|
value=dds_descriptions.get(desc, desc))
|
||||||
embed.add_field(name="Summary", value=dds_summaries.get(summ, summ))
|
embed.add_field(name="Summary", value=dds_summaries.get(summ, summ))
|
||||||
embed.add_field(name="Level", value=dds_levels.get(level, level))
|
embed.add_field(name="Level", value=dds_levels.get(level, level))
|
||||||
embed.set_footer(text="Console: 3DS")
|
embed.set_footer(text="Console: 3DS")
|
||||||
|
@ -59,10 +57,8 @@ class Err(Cog):
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
await ctx.send("Unknown Format - This is either "
|
||||||
"Unknown Format - This is either "
|
"no error code or you made some mistake!")
|
||||||
"no error code or you made some mistake!"
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.command(aliases=["wiiuserr", "uerr", "wuerr", "mochaerr"])
|
@commands.command(aliases=["wiiuserr", "uerr", "wuerr", "mochaerr"])
|
||||||
async def wiiuerr(self, ctx, err: str):
|
async def wiiuerr(self, ctx, err: str):
|
||||||
|
@ -77,9 +73,9 @@ class Err(Cog):
|
||||||
err_description = self.no_err_desc
|
err_description = self.no_err_desc
|
||||||
|
|
||||||
# Make a nice Embed out of it
|
# Make a nice Embed out of it
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(title=err,
|
||||||
title=err, url=self.rickroll, description=err_description
|
url=self.rickroll,
|
||||||
)
|
description=err_description)
|
||||||
embed.set_footer(text="Console: Wii U")
|
embed.set_footer(text="Console: Wii U")
|
||||||
embed.add_field(name="Module", value=module, inline=True)
|
embed.add_field(name="Module", value=module, inline=True)
|
||||||
embed.add_field(name="Description", value=desc, inline=True)
|
embed.add_field(name="Description", value=desc, inline=True)
|
||||||
|
@ -87,10 +83,8 @@ class Err(Cog):
|
||||||
# Send message, crazy
|
# Send message, crazy
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
await ctx.send("Unknown Format - This is either "
|
||||||
"Unknown Format - This is either "
|
"no error code or you made some mistake!")
|
||||||
"no error code or you made some mistake!"
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.command(aliases=["nxerr", "serr"])
|
@commands.command(aliases=["nxerr", "serr"])
|
||||||
async def err(self, ctx, err: str):
|
async def err(self, ctx, err: str):
|
||||||
|
@ -98,6 +92,7 @@ class Err(Cog):
|
||||||
Usage: .serr/.nxerr/.err <Error Code>"""
|
Usage: .serr/.nxerr/.err <Error Code>"""
|
||||||
|
|
||||||
if self.switch_re.match(err) or err.startswith("0x"): # Switch
|
if self.switch_re.match(err) or err.startswith("0x"): # Switch
|
||||||
|
|
||||||
if err.startswith("0x"):
|
if err.startswith("0x"):
|
||||||
err = err[2:]
|
err = err[2:]
|
||||||
errcode = int(err, 16)
|
errcode = int(err, 16)
|
||||||
|
@ -108,7 +103,7 @@ class Err(Cog):
|
||||||
desc = int(err[5:9])
|
desc = int(err[5:9])
|
||||||
errcode = (desc << 9) + module
|
errcode = (desc << 9) + module
|
||||||
|
|
||||||
str_errcode = f"{(module + 2000):04}-{desc:04}"
|
str_errcode = f'{(module + 2000):04}-{desc:04}'
|
||||||
|
|
||||||
# Searching for Modules in list
|
# Searching for Modules in list
|
||||||
if module in switch_modules:
|
if module in switch_modules:
|
||||||
|
@ -131,18 +126,16 @@ class Err(Cog):
|
||||||
err_description = errcode_range[2]
|
err_description = errcode_range[2]
|
||||||
|
|
||||||
# Make a nice Embed out of it
|
# Make a nice Embed out of it
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(title=f"{str_errcode} / {hex(errcode)}",
|
||||||
title=f"{str_errcode} / {hex(errcode)}",
|
|
||||||
url=self.rickroll,
|
url=self.rickroll,
|
||||||
description=err_description,
|
description=err_description)
|
||||||
)
|
embed.add_field(name="Module",
|
||||||
embed.add_field(
|
value=f"{err_module} ({module})",
|
||||||
name="Module", value=f"{err_module} ({module})", inline=True
|
inline=True)
|
||||||
)
|
|
||||||
embed.add_field(name="Description", value=desc, inline=True)
|
embed.add_field(name="Description", value=desc, inline=True)
|
||||||
|
|
||||||
if "ban" in err_description:
|
if "ban" in err_description:
|
||||||
embed.set_footer(text="F to you | Console: Switch")
|
embed.set_footer("F to you | Console: Switch")
|
||||||
else:
|
else:
|
||||||
embed.set_footer(text="Console: Switch")
|
embed.set_footer(text="Console: Switch")
|
||||||
|
|
||||||
|
@ -153,17 +146,17 @@ class Err(Cog):
|
||||||
elif err in switch_game_err:
|
elif err in switch_game_err:
|
||||||
game, desc = switch_game_err[err].split(":")
|
game, desc = switch_game_err[err].split(":")
|
||||||
|
|
||||||
embed = discord.Embed(title=err, url=self.rickroll, description=desc)
|
embed = discord.Embed(title=err,
|
||||||
|
url=self.rickroll,
|
||||||
|
description=desc)
|
||||||
embed.set_footer(text="Console: Switch")
|
embed.set_footer(text="Console: Switch")
|
||||||
embed.add_field(name="Game", value=game, inline=True)
|
embed.add_field(name="Game", value=game, inline=True)
|
||||||
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
await ctx.send("Unknown Format - This is either "
|
||||||
"Unknown Format - This is either "
|
"no error code or you made some mistake!")
|
||||||
"no error code or you made some mistake!"
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.command(aliases=["e2h"])
|
@commands.command(aliases=["e2h"])
|
||||||
async def err2hex(self, ctx, err: str):
|
async def err2hex(self, ctx, err: str):
|
||||||
|
@ -175,9 +168,8 @@ class Err(Cog):
|
||||||
errcode = (desc << 9) + module
|
errcode = (desc << 9) + module
|
||||||
await ctx.send(hex(errcode))
|
await ctx.send(hex(errcode))
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
await ctx.send("This doesn't follow the typical"
|
||||||
"This doesn't follow the typical Nintendo Switch 2XXX-XXXX format!"
|
" Nintendo Switch 2XXX-XXXX format!")
|
||||||
)
|
|
||||||
|
|
||||||
@commands.command(aliases=["h2e"])
|
@commands.command(aliases=["h2e"])
|
||||||
async def hex2err(self, ctx, err: str):
|
async def hex2err(self, ctx, err: str):
|
||||||
|
@ -188,11 +180,11 @@ class Err(Cog):
|
||||||
err = int(err, 16)
|
err = int(err, 16)
|
||||||
module = err & 0x1FF
|
module = err & 0x1FF
|
||||||
desc = (err >> 9) & 0x3FFF
|
desc = (err >> 9) & 0x3FFF
|
||||||
errcode = f"{(module + 2000):04}-{desc:04}"
|
errcode = f'{(module + 2000):04}-{desc:04}'
|
||||||
await ctx.send(errcode)
|
await ctx.send(errcode)
|
||||||
else:
|
else:
|
||||||
await ctx.send("This doesn't look like typical hex!")
|
await ctx.send("This doesn't look like typical hex!")
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
def setup(bot):
|
||||||
await bot.add_cog(Err(bot))
|
bot.add_cog(Err(bot))
|
43
cogs/invites.py
Normal file
43
cogs/invites.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
from discord.ext import commands
|
||||||
|
from discord.ext.commands import Cog
|
||||||
|
from helpers.checks import check_if_collaborator
|
||||||
|
import config
|
||||||
|
import json
|
||||||
|
|
||||||
|
class Invites(Cog):
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.check(check_if_collaborator)
|
||||||
|
async def invite(self, ctx):
|
||||||
|
welcome_channel = self.bot.get_channel(config.welcome_channel)
|
||||||
|
author = ctx.message.author
|
||||||
|
reason = f"Created by {str(author)} ({author.id})"
|
||||||
|
invite = await welcome_channel.create_invite(max_age = 0,
|
||||||
|
max_uses = 1, temporary = True, unique = True, reason = reason)
|
||||||
|
|
||||||
|
with open("data/invites.json", "r") as f:
|
||||||
|
invites = json.load(f)
|
||||||
|
|
||||||
|
invites[invite.id] = {
|
||||||
|
"uses": 0,
|
||||||
|
"url": invite.url,
|
||||||
|
"max_uses": 1,
|
||||||
|
"code": invite.code
|
||||||
|
}
|
||||||
|
|
||||||
|
with open("data/invites.json", "w") as f:
|
||||||
|
f.write(json.dumps(invites))
|
||||||
|
|
||||||
|
await ctx.message.add_reaction("🆗")
|
||||||
|
try:
|
||||||
|
await ctx.author.send(f"Created single-use invite {invite.url}")
|
||||||
|
except discord.errors.Forbidden:
|
||||||
|
await ctx.send(f"{ctx.author.mention} I could not send you the \
|
||||||
|
invite. Send me a DM so I can reply to you.")
|
||||||
|
|
||||||
|
|
||||||
|
def setup(bot):
|
||||||
|
bot.add_cog(Invites(bot))
|
30
cogs/legacy.py
Normal file
30
cogs/legacy.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
from discord.ext import commands
|
||||||
|
from discord.ext.commands import Cog
|
||||||
|
|
||||||
|
class Legacy(Cog):
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
@commands.command(hidden=True, aliases=["removehacker"])
|
||||||
|
async def probate(self, ctx):
|
||||||
|
"""Use .revoke <user> <role>"""
|
||||||
|
await ctx.send("This command was replaced with `.revoke <user> <role>`"
|
||||||
|
" on Robocop-NG, please use that instead.")
|
||||||
|
|
||||||
|
@commands.command(hidden=True)
|
||||||
|
async def softlock(self, ctx):
|
||||||
|
"""Use .lock True"""
|
||||||
|
await ctx.send("This command was replaced with `.lock True`"
|
||||||
|
" on Robocop-NG, please use that instead.\n"
|
||||||
|
"Also... good luck, and sorry for taking your time. "
|
||||||
|
"Lockdown rarely means anything good.")
|
||||||
|
|
||||||
|
@commands.command(hidden=True, aliases=["addhacker"])
|
||||||
|
async def unprobate(self, ctx):
|
||||||
|
"""Use .approve <user> <role>"""
|
||||||
|
await ctx.send("This command was replaced with `.approve <user> <role>`"
|
||||||
|
" on Robocop-NG, please use that instead.")
|
||||||
|
|
||||||
|
|
||||||
|
def setup(bot):
|
||||||
|
bot.add_cog(Legacy(bot))
|
87
cogs/links.py
Normal file
87
cogs/links.py
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import discord
|
||||||
|
import config
|
||||||
|
from discord.ext import commands
|
||||||
|
from discord.ext.commands import Cog
|
||||||
|
|
||||||
|
class Links(Cog):
|
||||||
|
"""
|
||||||
|
Commands for easily linking to projects.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
@commands.command(hidden=True)
|
||||||
|
async def pegaswitch(self, ctx):
|
||||||
|
"""Link to the Pegaswitch repo"""
|
||||||
|
await ctx.send("https://github.com/reswitched/pegaswitch")
|
||||||
|
|
||||||
|
@commands.command(hidden=True, aliases=["atmos"])
|
||||||
|
async def atmosphere(self, ctx):
|
||||||
|
"""Link to the Atmosphere repo"""
|
||||||
|
await ctx.send("https://github.com/atmosphere-nx/atmosphere")
|
||||||
|
|
||||||
|
@commands.command(hidden=True, aliases=["xyproblem"])
|
||||||
|
async def xy(self, ctx):
|
||||||
|
"""Link to the "What is the XY problem?" post from SE"""
|
||||||
|
await ctx.send("<https://meta.stackexchange.com/q/66377/285481>\n\n"
|
||||||
|
"TL;DR: It's asking about your attempted solution "
|
||||||
|
"rather than your actual problem.\n"
|
||||||
|
"It's perfectly okay to want to learn about a "
|
||||||
|
"solution, but please be clear about your intentions "
|
||||||
|
"if you're not actually trying to solve a problem.")
|
||||||
|
|
||||||
|
@commands.command(hidden=True, aliases=["guides", "link"])
|
||||||
|
async def guide(self, ctx):
|
||||||
|
"""Link to the guide(s)"""
|
||||||
|
await ctx.send("**Generic starter guides:**\n"
|
||||||
|
"Nintendo Homebrew's Guide: "
|
||||||
|
"<https://nh-server.github.io/switch-guide/>\n"
|
||||||
|
"AtlasNX's Guide: "
|
||||||
|
"<https://guide.teamatlasnx.com>\n"
|
||||||
|
"Pegaswitch Guide: <https://switch.hacks.guide/> "
|
||||||
|
"(outdated for anything but Pegaswitch/3.0.0)\n\n"
|
||||||
|
"**Specific guides:**\n"
|
||||||
|
"Manually Updating/Downgrading (with HOS): "
|
||||||
|
"<https://guide.sdsetup.com/usingcfw/manualupgrade>\n"
|
||||||
|
"Manually Repairing/Downgrading (without HOS): "
|
||||||
|
"<https://guide.sdsetup.com/usingcfw/manualchoiupgrade>\n"
|
||||||
|
"How to get started developing Homebrew: "
|
||||||
|
"<https://gbatemp.net/threads/"
|
||||||
|
"tutorial-switch-homebrew-development.507284/>\n"
|
||||||
|
"Getting full RAM in homebrew without NSPs: "
|
||||||
|
"as of Atmosphere 0.8.6, hold R while opening any game.")
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def source(self, ctx):
|
||||||
|
"""Gives link to source code."""
|
||||||
|
await ctx.send(f"You can find my source at {config.source_url}. "
|
||||||
|
"Serious PRs and issues welcome!")
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def rules(self, ctx, *, targetuser: discord.Member = None):
|
||||||
|
"""Post a link to the Rules"""
|
||||||
|
if not targetuser:
|
||||||
|
targetuser = ctx.author
|
||||||
|
await ctx.send(f"{targetuser.mention}: A link to the rules "
|
||||||
|
f"can be found here: {config.rules_url}")
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def community(self, ctx, *, targetuser: discord.Member = None):
|
||||||
|
"""Post a link to the community section of the rules"""
|
||||||
|
if not targetuser:
|
||||||
|
targetuser = ctx.author
|
||||||
|
await ctx.send(f"{targetuser.mention}: "
|
||||||
|
"https://reswitched.team/discord/#member-roles-breakdown"
|
||||||
|
"\n\n"
|
||||||
|
"Community role allows access to the set of channels "
|
||||||
|
"on the community category (#off-topic, "
|
||||||
|
"#homebrew-development, #switch-hacking-general etc)."
|
||||||
|
"\n\n"
|
||||||
|
"What you need to get the role is to be around, "
|
||||||
|
"be helpful and nice to people and "
|
||||||
|
"show an understanding of rules.")
|
||||||
|
|
||||||
|
|
||||||
|
def setup(bot):
|
||||||
|
bot.add_cog(Links(bot))
|
|
@ -1,49 +1,47 @@
|
||||||
import discord
|
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from discord.ext.commands import Cog
|
from discord.ext.commands import Cog
|
||||||
|
import config
|
||||||
from robocop_ng.helpers.checks import check_if_staff
|
import discord
|
||||||
|
from helpers.checks import check_if_staff
|
||||||
|
|
||||||
class Lockdown(Cog):
|
class Lockdown(Cog):
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
async def set_sendmessage(
|
async def set_sendmessage(self, channel: discord.TextChannel,
|
||||||
self, channel: discord.TextChannel, role, allow_send, issuer
|
role, allow_send, issuer):
|
||||||
):
|
|
||||||
try:
|
try:
|
||||||
roleobj = channel.guild.get_role(role)
|
roleobj = channel.guild.get_role(role)
|
||||||
overrides = channel.overwrites_for(roleobj)
|
overrides = channel.overwrites_for(roleobj)
|
||||||
overrides.send_messages = allow_send
|
overrides.send_messages = allow_send
|
||||||
await channel.set_permissions(
|
await channel.set_permissions(roleobj,
|
||||||
roleobj, overwrite=overrides, reason=str(issuer)
|
overwrite=overrides,
|
||||||
)
|
reason=str(issuer))
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def unlock_for_staff(self, channel: discord.TextChannel, issuer):
|
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)
|
await self.set_sendmessage(channel, role, True, issuer)
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.check(check_if_staff)
|
@commands.check(check_if_staff)
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def lock(self, ctx, channel: discord.TextChannel = None, soft: bool = False):
|
async def lock(self, ctx, channel: discord.TextChannel = None,
|
||||||
|
soft: bool = False):
|
||||||
"""Prevents people from speaking in a channel, staff only.
|
"""Prevents people from speaking in a channel, staff only.
|
||||||
|
|
||||||
Defaults to current channel."""
|
Defaults to current channel."""
|
||||||
if not channel:
|
if not channel:
|
||||||
channel = ctx.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 config.lockdown_configs.items():
|
||||||
for key, lockdown_conf in self.bot.config.lockdown_configs.items():
|
|
||||||
if channel.id in lockdown_conf["channels"]:
|
if channel.id in lockdown_conf["channels"]:
|
||||||
roles = lockdown_conf["roles"]
|
roles = lockdown_conf["roles"]
|
||||||
|
|
||||||
if roles is None:
|
if roles is None:
|
||||||
roles = self.bot.config.lockdown_configs["default"]["roles"]
|
roles = config.lockdown_configs["default"]["roles"]
|
||||||
|
|
||||||
for role in roles:
|
for role in roles:
|
||||||
await self.set_sendmessage(channel, role, False, ctx.author)
|
await self.set_sendmessage(channel, role, False, ctx.author)
|
||||||
|
@ -52,20 +50,14 @@ class Lockdown(Cog):
|
||||||
|
|
||||||
public_msg = "🔒 Channel locked down. "
|
public_msg = "🔒 Channel locked down. "
|
||||||
if not soft:
|
if not soft:
|
||||||
public_msg += (
|
public_msg += "Only staff members may speak. "\
|
||||||
"Only staff members may speak. "
|
"Do not bring the topic to other channels or risk "\
|
||||||
"Do not bring the topic to other channels or risk "
|
|
||||||
"disciplinary actions."
|
"disciplinary actions."
|
||||||
)
|
|
||||||
|
|
||||||
await ctx.send(public_msg)
|
await ctx.send(public_msg)
|
||||||
safe_name = await commands.clean_content(escape_markdown=True).convert(
|
safe_name = await commands.clean_content().convert(ctx, str(ctx.author))
|
||||||
ctx, str(ctx.author)
|
msg = f"🔒 **Lockdown**: {ctx.channel.mention} by {ctx.author.mention} "\
|
||||||
)
|
|
||||||
msg = (
|
|
||||||
f"🔒 **Lockdown**: {ctx.channel.mention} by {ctx.author.mention} "
|
|
||||||
f"| {safe_name}"
|
f"| {safe_name}"
|
||||||
)
|
|
||||||
await log_channel.send(msg)
|
await log_channel.send(msg)
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
|
@ -75,31 +67,26 @@ class Lockdown(Cog):
|
||||||
"""Unlocks speaking in current channel, staff only."""
|
"""Unlocks speaking in current channel, staff only."""
|
||||||
if not channel:
|
if not channel:
|
||||||
channel = ctx.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 config.lockdown_configs.items():
|
||||||
for key, lockdown_conf in self.bot.config.lockdown_configs.items():
|
|
||||||
if channel.id in lockdown_conf["channels"]:
|
if channel.id in lockdown_conf["channels"]:
|
||||||
roles = lockdown_conf["roles"]
|
roles = lockdown_conf["roles"]
|
||||||
|
|
||||||
if roles is None:
|
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)
|
await self.unlock_for_staff(channel, ctx.author)
|
||||||
|
|
||||||
for role in roles:
|
for role in roles:
|
||||||
await self.set_sendmessage(channel, role, True, ctx.author)
|
await self.set_sendmessage(channel, role, True, ctx.author)
|
||||||
|
|
||||||
safe_name = await commands.clean_content(escape_markdown=True).convert(
|
safe_name = await commands.clean_content().convert(ctx, str(ctx.author))
|
||||||
ctx, str(ctx.author)
|
|
||||||
)
|
|
||||||
await ctx.send("🔓 Channel unlocked.")
|
await ctx.send("🔓 Channel unlocked.")
|
||||||
msg = (
|
msg = f"🔓 **Unlock**: {ctx.channel.mention} by {ctx.author.mention} "\
|
||||||
f"🔓 **Unlock**: {ctx.channel.mention} by {ctx.author.mention} "
|
|
||||||
f"| {safe_name}"
|
f"| {safe_name}"
|
||||||
)
|
|
||||||
await log_channel.send(msg)
|
await log_channel.send(msg)
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
def setup(bot):
|
||||||
await bot.add_cog(Lockdown(bot))
|
bot.add_cog(Lockdown(bot))
|
|
@ -1,14 +1,10 @@
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext.commands import Cog
|
from discord.ext.commands import Cog
|
||||||
|
import json
|
||||||
from robocop_ng.helpers.checks import check_if_staff
|
import re
|
||||||
from robocop_ng.helpers.invites import get_invites, set_invites
|
import config
|
||||||
from robocop_ng.helpers.restrictions import get_user_restrictions
|
from helpers.restrictions import get_user_restrictions
|
||||||
from robocop_ng.helpers.userlogs import get_userlog
|
from helpers.checks import check_if_staff
|
||||||
|
|
||||||
|
|
||||||
class Logs(Cog):
|
class Logs(Cog):
|
||||||
|
@ -18,30 +14,32 @@ class Logs(Cog):
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.invite_re = re.compile(
|
self.invite_re = re.compile(r"((discord\.gg|discordapp\.com/"
|
||||||
r"((discord\.gg|discordapp\.com/" r"+invite)/+[a-zA-Z0-9-]+)", re.IGNORECASE
|
r"+invite)/+[a-zA-Z0-9-]+)",
|
||||||
)
|
re.IGNORECASE)
|
||||||
self.name_re = re.compile(r"[a-zA-Z0-9].*")
|
self.name_re = re.compile(r"[a-zA-Z0-9].*")
|
||||||
self.clean_re = re.compile(r"[^a-zA-Z0-9_ ]+", re.UNICODE)
|
self.clean_re = re.compile(r'[^a-zA-Z0-9_ ]+', re.UNICODE)
|
||||||
# All lower case, no spaces, nothing non-alphanumeric
|
# All lower case, no spaces, nothing non-alphanumeric
|
||||||
susp_hellgex = "|".join(
|
self.susp_words = ["sx", "tx", "reinx", # piracy-enabling cfws
|
||||||
[r"\W*".join(list(word)) for word in self.bot.config.suspect_words]
|
"tinfoil", "dz", # title managers
|
||||||
)
|
"goldleaf", "lithium", # title managers
|
||||||
|
"xci"] # "backup" format
|
||||||
|
susp_hellgex = "|".join([r"\W*".join(list(word)) for
|
||||||
|
word in self.susp_words])
|
||||||
self.susp_hellgex = re.compile(susp_hellgex, re.IGNORECASE)
|
self.susp_hellgex = re.compile(susp_hellgex, re.IGNORECASE)
|
||||||
|
|
||||||
|
self.ok_words = []
|
||||||
|
|
||||||
@Cog.listener()
|
@Cog.listener()
|
||||||
async def on_member_join(self, member):
|
async def on_member_join(self, member):
|
||||||
await self.bot.wait_until_ready()
|
await self.bot.wait_until_ready()
|
||||||
|
log_channel = self.bot.get_channel(config.log_channel)
|
||||||
if member.guild.id not in self.bot.config.guild_whitelist:
|
|
||||||
return
|
|
||||||
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.log_channel)
|
|
||||||
# We use this a lot, might as well get it once
|
# We use this a lot, might as well get it once
|
||||||
escaped_name = self.bot.escape_message(member)
|
escaped_name = self.bot.escape_message(member)
|
||||||
|
|
||||||
# Attempt to correlate the user joining with an invite
|
# 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()
|
real_invites = await member.guild.invites()
|
||||||
|
|
||||||
|
@ -52,7 +50,7 @@ class Logs(Cog):
|
||||||
"uses": 0,
|
"uses": 0,
|
||||||
"url": invite.url,
|
"url": invite.url,
|
||||||
"max_uses": invite.max_uses,
|
"max_uses": invite.max_uses,
|
||||||
"code": invite.code,
|
"code": invite.code
|
||||||
}
|
}
|
||||||
|
|
||||||
probable_invites_used = []
|
probable_invites_used = []
|
||||||
|
@ -75,11 +73,12 @@ class Logs(Cog):
|
||||||
del invites[id]
|
del invites[id]
|
||||||
|
|
||||||
# Save invites data.
|
# 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
|
# Prepare the invite correlation message
|
||||||
if len(probable_invites_used) == 1:
|
if len(probable_invites_used) == 1:
|
||||||
invite_used = probable_invites_used[0]["code"]
|
invite_used = probable_invites_used[0]["url"]
|
||||||
elif len(probable_invites_used) == 0:
|
elif len(probable_invites_used) == 0:
|
||||||
invite_used = "Unknown"
|
invite_used = "Unknown"
|
||||||
else:
|
else:
|
||||||
|
@ -88,64 +87,54 @@ class Logs(Cog):
|
||||||
|
|
||||||
# Check if user account is older than 15 minutes
|
# Check if user account is older than 15 minutes
|
||||||
age = member.joined_at - member.created_at
|
age = member.joined_at - member.created_at
|
||||||
if age < self.bot.config.min_age:
|
if age < config.min_age:
|
||||||
try:
|
try:
|
||||||
await member.send(
|
await member.send(f"Your account is too new to "
|
||||||
f"Your account is too new to "
|
|
||||||
f"join {member.guild.name}."
|
f"join {member.guild.name}."
|
||||||
" Please try again later."
|
" Please try again later.")
|
||||||
)
|
|
||||||
sent = True
|
sent = True
|
||||||
except discord.errors.Forbidden:
|
except discord.errors.Forbidden:
|
||||||
sent = False
|
sent = False
|
||||||
await member.kick(reason="Too new")
|
await member.kick(reason="Too new")
|
||||||
|
|
||||||
msg = (
|
msg = f"🚨 **Account too new**: {member.mention} | "\
|
||||||
f"🚨 **Account too new**: {member.mention} | "
|
f"{escaped_name}\n"\
|
||||||
f"{escaped_name}\n"
|
f"🗓 __Creation__: {member.created_at}\n"\
|
||||||
f"🗓 __Creation__: {member.created_at}\n"
|
f"🕓 Account age: {age}\n"\
|
||||||
f"🕓 Account age: {age}\n"
|
f"✉ Joined with: {invite_used}\n"\
|
||||||
f"✉ Joined with: {invite_used}\n"
|
|
||||||
f"🏷 __User ID__: {member.id}"
|
f"🏷 __User ID__: {member.id}"
|
||||||
)
|
|
||||||
if not sent:
|
if not sent:
|
||||||
msg += (
|
msg += "\nThe user has disabled direct messages,"\
|
||||||
"\nThe user has disabled direct messages, "
|
|
||||||
" so the reason was not sent."
|
" so the reason was not sent."
|
||||||
)
|
|
||||||
await log_channel.send(msg)
|
await log_channel.send(msg)
|
||||||
return
|
return
|
||||||
msg = (
|
msg = f"✅ **Join**: {member.mention} | "\
|
||||||
f"✅ **Join**: {member.mention} | "
|
f"{escaped_name}\n"\
|
||||||
f"{escaped_name}\n"
|
f"🗓 __Creation__: {member.created_at}\n"\
|
||||||
f"🗓 __Creation__: {member.created_at}\n"
|
f"🕓 Account age: {age}\n"\
|
||||||
f"🕓 Account age: {age}\n"
|
f"✉ Joined with: {invite_used}\n"\
|
||||||
f"✉ Joined with: {invite_used}\n"
|
|
||||||
f"🏷 __User ID__: {member.id}"
|
f"🏷 __User ID__: {member.id}"
|
||||||
)
|
|
||||||
|
|
||||||
# Handles user restrictions
|
# Handles user restrictions
|
||||||
# Basically, gives back muted role to users that leave with it.
|
# 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]
|
roles = [discord.utils.get(member.guild.roles, id=rst) for rst in rsts]
|
||||||
await member.add_roles(*roles)
|
await member.add_roles(*roles)
|
||||||
|
|
||||||
# Real hell zone.
|
# Real hell zone.
|
||||||
warns = get_userlog(self.bot)
|
with open("data/userlog.json", "r") as f:
|
||||||
|
warns = json.load(f)
|
||||||
try:
|
try:
|
||||||
if len(warns[str(member.id)]["warns"]) == 0:
|
if len(warns[str(member.id)]["warns"]) == 0:
|
||||||
await log_channel.send(msg)
|
await log_channel.send(msg)
|
||||||
else:
|
else:
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(color=discord.Color.dark_red(),
|
||||||
color=discord.Color.dark_red(), title=f"Warns for {escaped_name}"
|
title=f"Warns for {escaped_name}")
|
||||||
)
|
embed.set_thumbnail(url=member.avatar_url)
|
||||||
embed.set_thumbnail(url=str(member.display_avatar))
|
|
||||||
for idx, warn in enumerate(warns[str(member.id)]["warns"]):
|
for idx, warn in enumerate(warns[str(member.id)]["warns"]):
|
||||||
embed.add_field(
|
embed.add_field(name=f"{idx + 1}: {warn['timestamp']}",
|
||||||
name=f"{idx + 1}: {warn['timestamp']}",
|
|
||||||
value=f"Issuer: {warn['issuer_name']}"
|
value=f"Issuer: {warn['issuer_name']}"
|
||||||
f"\nReason: {warn['reason']}",
|
f"\nReason: {warn['reason']}")
|
||||||
)
|
|
||||||
await log_channel.send(msg, embed=embed)
|
await log_channel.send(msg, embed=embed)
|
||||||
except KeyError: # if the user is not in the file
|
except KeyError: # if the user is not in the file
|
||||||
await log_channel.send(msg)
|
await log_channel.send(msg)
|
||||||
|
@ -158,41 +147,34 @@ class Logs(Cog):
|
||||||
return
|
return
|
||||||
|
|
||||||
alert = False
|
alert = False
|
||||||
cleancont = self.clean_re.sub("", message.content).lower()
|
cleancont = self.clean_re.sub('', message.content).lower()
|
||||||
msg = (
|
msg = f"🚨 Suspicious message by {message.author.mention} "\
|
||||||
f"🚨 Suspicious message by {message.author.mention} "
|
|
||||||
f"({message.author.id}):"
|
f"({message.author.id}):"
|
||||||
)
|
|
||||||
|
|
||||||
invites = self.invite_re.findall(message.content)
|
invites = self.invite_re.findall(message.content)
|
||||||
for invite in invites:
|
for invite in invites:
|
||||||
msg += f"\n- Has invite: https://{invite[0]}"
|
msg += f"\n- Has invite: https://{invite[0]}"
|
||||||
alert = True
|
alert = True
|
||||||
|
|
||||||
for susp_word in self.bot.config.suspect_words:
|
for susp_word in self.susp_words:
|
||||||
if susp_word in cleancont and not any(
|
if susp_word in cleancont and\
|
||||||
ok_word in cleancont
|
not any(ok_word in cleancont for ok_word in self.ok_words):
|
||||||
for ok_word in self.bot.config.suspect_ignored_words
|
|
||||||
):
|
|
||||||
msg += f"\n- Contains suspicious word: `{susp_word}`"
|
msg += f"\n- Contains suspicious word: `{susp_word}`"
|
||||||
alert = True
|
alert = True
|
||||||
|
|
||||||
if alert:
|
if alert:
|
||||||
msg += f"\n\nJump: <{message.jump_url}>"
|
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
|
# Bad Code :tm:, blame retr0id
|
||||||
message_clean = message.content.replace("*", "").replace("_", "")
|
message_clean = message.content.replace("*", "").replace("_", "")
|
||||||
regd = self.susp_hellgex.sub(
|
regd = self.susp_hellgex.sub(lambda w: "**{}**".format(w.group(0)),
|
||||||
lambda w: "**{}**".format(w.group(0)), message_clean
|
message_clean)
|
||||||
)
|
|
||||||
|
|
||||||
# Show a message embed
|
# Show a message embed
|
||||||
embed = discord.Embed(description=regd)
|
embed = discord.Embed(description=regd)
|
||||||
embed.set_author(
|
embed.set_author(name=message.author.display_name,
|
||||||
name=message.author.display_name,
|
icon_url=message.author.avatar_url)
|
||||||
icon_url=str(message.author.display_avatar),
|
|
||||||
)
|
|
||||||
|
|
||||||
await spy_channel.send(msg, embed=embed)
|
await spy_channel.send(msg, embed=embed)
|
||||||
|
|
||||||
|
@ -201,17 +183,16 @@ class Logs(Cog):
|
||||||
if compliant:
|
if compliant:
|
||||||
return
|
return
|
||||||
|
|
||||||
msg = (
|
msg = f"R11 violating name by {message.author.mention} "\
|
||||||
f"R11 violating name by {message.author.mention} " f"({message.author.id})."
|
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)
|
await spy_channel.send(msg)
|
||||||
|
|
||||||
@Cog.listener()
|
@Cog.listener()
|
||||||
async def on_message(self, message):
|
async def on_message(self, message):
|
||||||
await self.bot.wait_until_ready()
|
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
|
return
|
||||||
|
|
||||||
await self.do_spy(message)
|
await self.do_spy(message)
|
||||||
|
@ -219,7 +200,7 @@ class Logs(Cog):
|
||||||
@Cog.listener()
|
@Cog.listener()
|
||||||
async def on_message_edit(self, before, after):
|
async def on_message_edit(self, before, after):
|
||||||
await self.bot.wait_until_ready()
|
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
|
return
|
||||||
|
|
||||||
# If content is the same, just skip over it
|
# If content is the same, just skip over it
|
||||||
|
@ -229,18 +210,11 @@ class Logs(Cog):
|
||||||
|
|
||||||
await self.do_spy(after)
|
await self.do_spy(after)
|
||||||
|
|
||||||
# U+200D is a Zero Width Joiner stopping backticks from breaking the formatting
|
log_channel = self.bot.get_channel(config.log_channel)
|
||||||
before_content = before.clean_content.replace("`", "`\u200d")
|
msg = "📝 **Message edit**: \n"\
|
||||||
after_content = after.clean_content.replace("`", "`\u200d")
|
f"from {self.bot.escape_message(after.author.name)} "\
|
||||||
|
f"({after.author.id}), in {after.channel.mention}:\n"\
|
||||||
log_channel = self.bot.get_channel(self.bot.config.log_channel)
|
f"`{before.clean_content}` → `{after.clean_content}`"
|
||||||
|
|
||||||
msg = (
|
|
||||||
"📝 **Message edit**: \n"
|
|
||||||
f"from {self.bot.escape_message(after.author.name)} "
|
|
||||||
f"({after.author.id}), in {after.channel.mention}:\n"
|
|
||||||
f"```{before_content}``` → ```{after_content}```"
|
|
||||||
)
|
|
||||||
|
|
||||||
# If resulting message is too long, upload to hastebin
|
# If resulting message is too long, upload to hastebin
|
||||||
if len(msg) > 2000:
|
if len(msg) > 2000:
|
||||||
|
@ -252,16 +226,14 @@ class Logs(Cog):
|
||||||
@Cog.listener()
|
@Cog.listener()
|
||||||
async def on_message_delete(self, message):
|
async def on_message_delete(self, message):
|
||||||
await self.bot.wait_until_ready()
|
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
|
return
|
||||||
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.log_channel)
|
log_channel = self.bot.get_channel(config.log_channel)
|
||||||
msg = (
|
msg = "🗑️ **Message delete**: \n"\
|
||||||
"🗑️ **Message delete**: \n"
|
f"from {self.bot.escape_message(message.author.name)} "\
|
||||||
f"from {self.bot.escape_message(message.author.name)} "
|
f"({message.author.id}), in {message.channel.mention}:\n"\
|
||||||
f"({message.author.id}), in {message.channel.mention}:\n"
|
|
||||||
f"`{message.clean_content}`"
|
f"`{message.clean_content}`"
|
||||||
)
|
|
||||||
|
|
||||||
# If resulting message is too long, upload to hastebin
|
# If resulting message is too long, upload to hastebin
|
||||||
if len(msg) > 2000:
|
if len(msg) > 2000:
|
||||||
|
@ -273,46 +245,28 @@ class Logs(Cog):
|
||||||
@Cog.listener()
|
@Cog.listener()
|
||||||
async def on_member_remove(self, member):
|
async def on_member_remove(self, member):
|
||||||
await self.bot.wait_until_ready()
|
await self.bot.wait_until_ready()
|
||||||
|
log_channel = self.bot.get_channel(config.log_channel)
|
||||||
if member.guild.id not in self.bot.config.guild_whitelist:
|
msg = f"⬅️ **Leave**: {member.mention} | "\
|
||||||
return
|
f"{self.bot.escape_message(member)}\n"\
|
||||||
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.log_channel)
|
|
||||||
msg = (
|
|
||||||
f"⬅️ **Leave**: {member.mention} | "
|
|
||||||
f"{self.bot.escape_message(member)}\n"
|
|
||||||
f"🏷 __User ID__: {member.id}"
|
f"🏷 __User ID__: {member.id}"
|
||||||
)
|
|
||||||
await log_channel.send(msg)
|
await log_channel.send(msg)
|
||||||
|
|
||||||
@Cog.listener()
|
@Cog.listener()
|
||||||
async def on_member_ban(self, guild, member):
|
async def on_member_ban(self, guild, member):
|
||||||
await self.bot.wait_until_ready()
|
await self.bot.wait_until_ready()
|
||||||
|
log_channel = self.bot.get_channel(config.modlog_channel)
|
||||||
if guild.id not in self.bot.config.guild_whitelist:
|
msg = f"⛔ **Ban**: {member.mention} | "\
|
||||||
return
|
f"{self.bot.escape_message(member)}\n"\
|
||||||
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
|
|
||||||
msg = (
|
|
||||||
f"⛔ **Ban**: {member.mention} | "
|
|
||||||
f"{self.bot.escape_message(member)}\n"
|
|
||||||
f"🏷 __User ID__: {member.id}"
|
f"🏷 __User ID__: {member.id}"
|
||||||
)
|
|
||||||
await log_channel.send(msg)
|
await log_channel.send(msg)
|
||||||
|
|
||||||
@Cog.listener()
|
@Cog.listener()
|
||||||
async def on_member_unban(self, guild, user):
|
async def on_member_unban(self, guild, user):
|
||||||
await self.bot.wait_until_ready()
|
await self.bot.wait_until_ready()
|
||||||
|
log_channel = self.bot.get_channel(config.modlog_channel)
|
||||||
if guild.id not in self.bot.config.guild_whitelist:
|
msg = f"⚠️ **Unban**: {user.mention} | "\
|
||||||
return
|
f"{self.bot.escape_message(user)}\n"\
|
||||||
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
|
|
||||||
msg = (
|
|
||||||
f"⚠️ **Unban**: {user.mention} | "
|
|
||||||
f"{self.bot.escape_message(user)}\n"
|
|
||||||
f"🏷 __User ID__: {user.id}"
|
f"🏷 __User ID__: {user.id}"
|
||||||
)
|
|
||||||
# if user.id in self.bot.timebans:
|
# if user.id in self.bot.timebans:
|
||||||
# msg += "\nTimeban removed."
|
# msg += "\nTimeban removed."
|
||||||
# self.bot.timebans.pop(user.id)
|
# self.bot.timebans.pop(user.id)
|
||||||
|
@ -327,12 +281,8 @@ class Logs(Cog):
|
||||||
@Cog.listener()
|
@Cog.listener()
|
||||||
async def on_member_update(self, member_before, member_after):
|
async def on_member_update(self, member_before, member_after):
|
||||||
await self.bot.wait_until_ready()
|
await self.bot.wait_until_ready()
|
||||||
|
|
||||||
if member_after.guild.id not in self.bot.config.guild_whitelist:
|
|
||||||
return
|
|
||||||
|
|
||||||
msg = ""
|
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:
|
if member_before.roles != member_after.roles:
|
||||||
# role removal
|
# role removal
|
||||||
role_removal = []
|
role_removal = []
|
||||||
|
@ -360,11 +310,9 @@ class Logs(Cog):
|
||||||
msg += ", ".join(roles)
|
msg += ", ".join(roles)
|
||||||
|
|
||||||
if member_before.name != member_after.name:
|
if member_before.name != member_after.name:
|
||||||
msg += (
|
msg += "\n📝 __Username change__: "\
|
||||||
"\n📝 __Username change__: "
|
f"{self.bot.escape_message(member_before)} → "\
|
||||||
f"{self.bot.escape_message(member_before)} → "
|
|
||||||
f"{self.bot.escape_message(member_after)}"
|
f"{self.bot.escape_message(member_after)}"
|
||||||
)
|
|
||||||
if member_before.nick != member_after.nick:
|
if member_before.nick != member_after.nick:
|
||||||
if not member_before.nick:
|
if not member_before.nick:
|
||||||
msg += "\n🏷 __Nickname addition__"
|
msg += "\n🏷 __Nickname addition__"
|
||||||
|
@ -372,17 +320,13 @@ class Logs(Cog):
|
||||||
msg += "\n🏷 __Nickname removal__"
|
msg += "\n🏷 __Nickname removal__"
|
||||||
else:
|
else:
|
||||||
msg += "\n🏷 __Nickname change__"
|
msg += "\n🏷 __Nickname change__"
|
||||||
msg += (
|
msg += f": {self.bot.escape_message(member_before.nick)} → "\
|
||||||
f": {self.bot.escape_message(member_before.nick)} → "
|
|
||||||
f"{self.bot.escape_message(member_after.nick)}"
|
f"{self.bot.escape_message(member_after.nick)}"
|
||||||
)
|
|
||||||
if msg:
|
if msg:
|
||||||
msg = (
|
msg = f"ℹ️ **Member update**: {member_after.mention} | "\
|
||||||
f"ℹ️ **Member update**: {member_after.mention} | "
|
|
||||||
f"{self.bot.escape_message(member_after)}{msg}"
|
f"{self.bot.escape_message(member_after)}{msg}"
|
||||||
)
|
|
||||||
await log_channel.send(msg)
|
await log_channel.send(msg)
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
def setup(bot):
|
||||||
await bot.add_cog(Logs(bot))
|
bot.add_cog(Logs(bot))
|
132
cogs/meme.py
Normal file
132
cogs/meme.py
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
import random
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
from discord.ext.commands import Cog
|
||||||
|
import math
|
||||||
|
import platform
|
||||||
|
from helpers.checks import check_if_staff_or_ot
|
||||||
|
|
||||||
|
|
||||||
|
class Meme(Cog):
|
||||||
|
"""
|
||||||
|
Meme commands.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
def c_to_f(self, c):
|
||||||
|
"""this is where we take memes too far"""
|
||||||
|
return math.floor(9.0 / 5.0 * c + 32)
|
||||||
|
|
||||||
|
def c_to_k(self, c):
|
||||||
|
"""this is where we take memes REALLY far"""
|
||||||
|
return math.floor(c + 273.15)
|
||||||
|
|
||||||
|
@commands.check(check_if_staff_or_ot)
|
||||||
|
@commands.command(hidden=True, name="warm")
|
||||||
|
async def warm_member(self, ctx, user: discord.Member):
|
||||||
|
"""Warms a user :3"""
|
||||||
|
celsius = random.randint(15, 100)
|
||||||
|
fahrenheit = self.c_to_f(celsius)
|
||||||
|
kelvin = self.c_to_k(celsius)
|
||||||
|
await ctx.send(f"{user.mention} warmed."
|
||||||
|
f" User is now {celsius}°C "
|
||||||
|
f"({fahrenheit}°F, {kelvin}K).")
|
||||||
|
|
||||||
|
@commands.check(check_if_staff_or_ot)
|
||||||
|
@commands.command(hidden=True, name="chill", aliases=["cold"])
|
||||||
|
async def chill_member(self, ctx, user: discord.Member):
|
||||||
|
"""Chills a user >:3"""
|
||||||
|
celsius = random.randint(-50, 15)
|
||||||
|
fahrenheit = self.c_to_f(celsius)
|
||||||
|
kelvin = self.c_to_k(celsius)
|
||||||
|
await ctx.send(f"{user.mention} chilled."
|
||||||
|
f" User is now {celsius}°C "
|
||||||
|
f"({fahrenheit}°F, {kelvin}K).")
|
||||||
|
|
||||||
|
@commands.check(check_if_staff_or_ot)
|
||||||
|
@commands.command(hidden=True, aliases=["thank", "reswitchedgold"])
|
||||||
|
async def gild(self, ctx, user: discord.Member):
|
||||||
|
"""Gives a star to a user"""
|
||||||
|
await ctx.send(f"{user.mention} gets a :star:, yay!")
|
||||||
|
|
||||||
|
@commands.check(check_if_staff_or_ot)
|
||||||
|
@commands.command(hidden=True, aliases=["reswitchedsilver", "silv3r",
|
||||||
|
"reswitchedsilv3r"])
|
||||||
|
async def silver(self, ctx, user: discord.Member):
|
||||||
|
"""Gives a user ReSwitched Silver™"""
|
||||||
|
embed = discord.Embed(title="ReSwitched Silver™!",
|
||||||
|
description=f"Here's your ReSwitched Silver™,"
|
||||||
|
f"{user.mention}!")
|
||||||
|
embed.set_image(url="https://cdn.discordapp.com/emojis/"
|
||||||
|
"548623626916724747.png?v=1")
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@commands.check(check_if_staff_or_ot)
|
||||||
|
@commands.command(hidden=True)
|
||||||
|
async def btwiuse(self, ctx):
|
||||||
|
"""btw i use arch"""
|
||||||
|
uname = platform.uname()
|
||||||
|
await ctx.send(f"BTW I use {platform.python_implementation()} "
|
||||||
|
f"{platform.python_version()} on {uname.system} "
|
||||||
|
f"{uname.release}")
|
||||||
|
|
||||||
|
@commands.check(check_if_staff_or_ot)
|
||||||
|
@commands.command(hidden=True)
|
||||||
|
async def yahaha(self, ctx):
|
||||||
|
"""secret command"""
|
||||||
|
await ctx.send(f"🍂 you found me 🍂")
|
||||||
|
|
||||||
|
@commands.check(check_if_staff_or_ot)
|
||||||
|
@commands.command(hidden=True)
|
||||||
|
async def peng(self, ctx):
|
||||||
|
"""heck tomger"""
|
||||||
|
await ctx.send(f"🐧")
|
||||||
|
|
||||||
|
@commands.check(check_if_staff_or_ot)
|
||||||
|
@commands.command(hidden=True, aliases=["outstanding"])
|
||||||
|
async def outstandingmove(self, ctx):
|
||||||
|
"""Posts the outstanding move meme"""
|
||||||
|
await ctx.send("https://cdn.discordapp.com/attachments"
|
||||||
|
"/371047036348268545/528413677007929344"
|
||||||
|
"/image0-5.jpg")
|
||||||
|
|
||||||
|
@commands.check(check_if_staff_or_ot)
|
||||||
|
@commands.command(hidden=True)
|
||||||
|
async def bones(self, ctx):
|
||||||
|
await ctx.send("https://cdn.discordapp.com/emojis/"
|
||||||
|
"443501365843591169.png?v=1")
|
||||||
|
|
||||||
|
@commands.check(check_if_staff_or_ot)
|
||||||
|
@commands.command(hidden=True)
|
||||||
|
async def headpat(self, ctx):
|
||||||
|
await ctx.send("https://cdn.discordapp.com/emojis/"
|
||||||
|
"465650811909701642.png?v=1")
|
||||||
|
|
||||||
|
@commands.check(check_if_staff_or_ot)
|
||||||
|
@commands.command(hidden=True, aliases=["when", "etawhen",
|
||||||
|
"emunand", "thermosphere"])
|
||||||
|
async def eta(self, ctx):
|
||||||
|
await ctx.send("June 15.")
|
||||||
|
|
||||||
|
@commands.check(check_if_staff_or_ot)
|
||||||
|
@commands.command(hidden=True, name="bam")
|
||||||
|
async def bam_member(self, ctx, target: discord.Member):
|
||||||
|
"""Bams a user owo"""
|
||||||
|
safe_name = await commands.clean_content().convert(ctx, str(target))
|
||||||
|
await ctx.send(f"{safe_name} is ̶n͢ow b̕&̡.̷ 👍̡")
|
||||||
|
|
||||||
|
@commands.command(hidden=True)
|
||||||
|
async def memebercount(self, ctx):
|
||||||
|
"""Checks memeber count, as requested by dvdfreitag"""
|
||||||
|
await ctx.send("There's like, uhhhhh a bunch")
|
||||||
|
|
||||||
|
@commands.command(hidden=True)
|
||||||
|
async def frolics(self, ctx):
|
||||||
|
"""test"""
|
||||||
|
await ctx.send("https://www.youtube.com/watch?v=VmarNEsjpDI")
|
||||||
|
|
||||||
|
|
||||||
|
def setup(bot):
|
||||||
|
bot.add_cog(Meme(bot))
|
440
cogs/mod.py
Normal file
440
cogs/mod.py
Normal file
|
@ -0,0 +1,440 @@
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
from discord.ext.commands import Cog
|
||||||
|
import config
|
||||||
|
from helpers.checks import check_if_staff, check_if_bot_manager
|
||||||
|
from helpers.userlogs import userlog
|
||||||
|
from helpers.restrictions import add_restriction, remove_restriction
|
||||||
|
import io
|
||||||
|
|
||||||
|
|
||||||
|
class Mod(Cog):
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
def check_if_target_is_staff(self, target):
|
||||||
|
return any(r.id in config.staff_role_ids for r in target.roles)
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.check(check_if_bot_manager)
|
||||||
|
@commands.command()
|
||||||
|
async def setguildicon(self, ctx, url):
|
||||||
|
"""Changes guild icon, bot manager only."""
|
||||||
|
img_bytes = await self.bot.aiogetbytes(url)
|
||||||
|
await ctx.guild.edit(icon=img_bytes, reason=str(ctx.author))
|
||||||
|
await ctx.send(f"Done!")
|
||||||
|
|
||||||
|
log_channel = self.bot.get_channel(config.modlog_channel)
|
||||||
|
log_msg = f"✏️ **Guild Icon Update**: {ctx.author} "\
|
||||||
|
"changed the guild icon."
|
||||||
|
img_filename = url.split("/")[-1].split("#")[0] # hacky
|
||||||
|
img_file = discord.File(io.BytesIO(img_bytes),
|
||||||
|
filename=img_filename)
|
||||||
|
await log_channel.send(log_msg, file=img_file)
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.check(check_if_staff)
|
||||||
|
@commands.command()
|
||||||
|
async def mute(self, ctx, target: discord.Member, *, reason: str = ""):
|
||||||
|
"""Mutes a user, staff only."""
|
||||||
|
# Hedge-proofing the code
|
||||||
|
if target == ctx.author:
|
||||||
|
return await ctx.send("You can't do mod actions on yourself.")
|
||||||
|
elif self.check_if_target_is_staff(target):
|
||||||
|
return await ctx.send("I can't mute this user as "
|
||||||
|
"they're a member of staff.")
|
||||||
|
|
||||||
|
userlog(target.id, ctx.author, reason, "mutes", target.name)
|
||||||
|
|
||||||
|
safe_name = await commands.clean_content().convert(ctx, str(target))
|
||||||
|
|
||||||
|
dm_message = f"You were muted!"
|
||||||
|
if reason:
|
||||||
|
dm_message += f" The given reason is: \"{reason}\"."
|
||||||
|
|
||||||
|
try:
|
||||||
|
await target.send(dm_message)
|
||||||
|
except discord.errors.Forbidden:
|
||||||
|
# Prevents kick issues in cases where user blocked bot
|
||||||
|
# or has DMs disabled
|
||||||
|
pass
|
||||||
|
|
||||||
|
mute_role = ctx.guild.get_role(config.mute_role)
|
||||||
|
|
||||||
|
await target.add_roles(mute_role, reason=str(ctx.author))
|
||||||
|
|
||||||
|
chan_message = f"🔇 **Muted**: {ctx.author.mention} muted "\
|
||||||
|
f"{target.mention} | {safe_name}\n"\
|
||||||
|
f"🏷 __User ID__: {target.id}\n"
|
||||||
|
if reason:
|
||||||
|
chan_message += f"✏️ __Reason__: \"{reason}\""
|
||||||
|
else:
|
||||||
|
chan_message += "Please add an explanation below. In the future, "\
|
||||||
|
"it is recommended to use `.mute <user> [reason]`"\
|
||||||
|
" as the reason is automatically sent to the user."
|
||||||
|
|
||||||
|
log_channel = self.bot.get_channel(config.modlog_channel)
|
||||||
|
await log_channel.send(chan_message)
|
||||||
|
await ctx.send(f"{target.mention} can no longer speak.")
|
||||||
|
add_restriction(target.id, config.mute_role)
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.check(check_if_staff)
|
||||||
|
@commands.command()
|
||||||
|
async def unmute(self, ctx, target: discord.Member):
|
||||||
|
"""Unmutes a user, staff only."""
|
||||||
|
safe_name = await commands.clean_content().convert(ctx, str(target))
|
||||||
|
|
||||||
|
mute_role = ctx.guild.get_role(config.mute_role)
|
||||||
|
await target.remove_roles(mute_role, reason=str(ctx.author))
|
||||||
|
|
||||||
|
chan_message = f"🔈 **Unmuted**: {ctx.author.mention} unmuted "\
|
||||||
|
f"{target.mention} | {safe_name}\n"\
|
||||||
|
f"🏷 __User ID__: {target.id}\n"
|
||||||
|
|
||||||
|
log_channel = self.bot.get_channel(config.modlog_channel)
|
||||||
|
await log_channel.send(chan_message)
|
||||||
|
await ctx.send(f"{target.mention} can now speak again.")
|
||||||
|
remove_restriction(target.id, config.mute_role)
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.bot_has_permissions(kick_members=True)
|
||||||
|
@commands.check(check_if_staff)
|
||||||
|
@commands.command()
|
||||||
|
async def kick(self, ctx, target: discord.Member, *, reason: str = ""):
|
||||||
|
"""Kicks a user, staff only."""
|
||||||
|
# Hedge-proofing the code
|
||||||
|
if target == ctx.author:
|
||||||
|
return await ctx.send("You can't do mod actions on yourself.")
|
||||||
|
elif self.check_if_target_is_staff(target):
|
||||||
|
return await ctx.send("I can't kick this user as "
|
||||||
|
"they're a member of staff.")
|
||||||
|
|
||||||
|
userlog(target.id, ctx.author, reason, "kicks", target.name)
|
||||||
|
|
||||||
|
safe_name = await commands.clean_content().convert(ctx, str(target))
|
||||||
|
|
||||||
|
dm_message = f"You were kicked from {ctx.guild.name}."
|
||||||
|
if reason:
|
||||||
|
dm_message += f" The given reason is: \"{reason}\"."
|
||||||
|
dm_message += "\n\nYou are able to rejoin the server,"\
|
||||||
|
" but please be sure to behave when participating again."
|
||||||
|
|
||||||
|
try:
|
||||||
|
await target.send(dm_message)
|
||||||
|
except discord.errors.Forbidden:
|
||||||
|
# Prevents kick issues in cases where user blocked bot
|
||||||
|
# or has DMs disabled
|
||||||
|
pass
|
||||||
|
|
||||||
|
await target.kick(reason=f"{ctx.author}, reason: {reason}")
|
||||||
|
chan_message = f"👢 **Kick**: {ctx.author.mention} kicked "\
|
||||||
|
f"{target.mention} | {safe_name}\n"\
|
||||||
|
f"🏷 __User ID__: {target.id}\n"
|
||||||
|
if reason:
|
||||||
|
chan_message += f"✏️ __Reason__: \"{reason}\""
|
||||||
|
else:
|
||||||
|
chan_message += "Please add an explanation below. In the future"\
|
||||||
|
", it is recommended to use "\
|
||||||
|
"`.kick <user> [reason]`"\
|
||||||
|
" as the reason is automatically sent to the user."
|
||||||
|
|
||||||
|
log_channel = self.bot.get_channel(config.modlog_channel)
|
||||||
|
await log_channel.send(chan_message)
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.bot_has_permissions(ban_members=True)
|
||||||
|
@commands.check(check_if_staff)
|
||||||
|
@commands.command()
|
||||||
|
async def ban(self, ctx, target: discord.Member, *, reason: str = ""):
|
||||||
|
"""Bans a user, staff only."""
|
||||||
|
# Hedge-proofing the code
|
||||||
|
if target == ctx.author:
|
||||||
|
return await ctx.send("You can't do mod actions on yourself.")
|
||||||
|
elif self.check_if_target_is_staff(target):
|
||||||
|
return await ctx.send("I can't ban this user as "
|
||||||
|
"they're a member of staff.")
|
||||||
|
|
||||||
|
userlog(target.id, ctx.author, reason, "bans", target.name)
|
||||||
|
|
||||||
|
safe_name = await commands.clean_content().convert(ctx, str(target))
|
||||||
|
|
||||||
|
dm_message = f"You were banned from {ctx.guild.name}."
|
||||||
|
if reason:
|
||||||
|
dm_message += f" The given reason is: \"{reason}\"."
|
||||||
|
dm_message += "\n\nThis ban does not expire."
|
||||||
|
|
||||||
|
try:
|
||||||
|
await target.send(dm_message)
|
||||||
|
except discord.errors.Forbidden:
|
||||||
|
# Prevents ban issues in cases where user blocked bot
|
||||||
|
# or has DMs disabled
|
||||||
|
pass
|
||||||
|
|
||||||
|
await target.ban(reason=f"{ctx.author}, reason: {reason}",
|
||||||
|
delete_message_days=0)
|
||||||
|
chan_message = f"⛔ **Ban**: {ctx.author.mention} banned "\
|
||||||
|
f"{target.mention} | {safe_name}\n"\
|
||||||
|
f"🏷 __User ID__: {target.id}\n"
|
||||||
|
if reason:
|
||||||
|
chan_message += f"✏️ __Reason__: \"{reason}\""
|
||||||
|
else:
|
||||||
|
chan_message += "Please add an explanation below. In the future"\
|
||||||
|
", it is recommended to use `.ban <user> [reason]`"\
|
||||||
|
" as the reason is automatically sent to the user."
|
||||||
|
|
||||||
|
log_channel = self.bot.get_channel(config.modlog_channel)
|
||||||
|
await log_channel.send(chan_message)
|
||||||
|
await ctx.send(f"{safe_name} is now b&. 👍")
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.bot_has_permissions(ban_members=True)
|
||||||
|
@commands.check(check_if_staff)
|
||||||
|
@commands.command(aliases=["softban"])
|
||||||
|
async def hackban(self, ctx, target: int, *, reason: str = ""):
|
||||||
|
"""Bans a user with their ID, doesn't message them, staff only."""
|
||||||
|
target_user = await self.bot.get_user_info(target)
|
||||||
|
target_member = ctx.guild.get_member(target)
|
||||||
|
# Hedge-proofing the code
|
||||||
|
if target == ctx.author.id:
|
||||||
|
return await ctx.send("You can't do mod actions on yourself.")
|
||||||
|
elif target_member and self.check_if_target_is_staff(target_member):
|
||||||
|
return await ctx.send("I can't ban this user as "
|
||||||
|
"they're a member of staff.")
|
||||||
|
|
||||||
|
userlog(target, ctx.author, reason, "bans", target_user.name)
|
||||||
|
|
||||||
|
safe_name = await commands.clean_content().convert(ctx, str(target))
|
||||||
|
|
||||||
|
await ctx.guild.ban(target_user,
|
||||||
|
reason=f"{ctx.author}, reason: {reason}",
|
||||||
|
delete_message_days=0)
|
||||||
|
chan_message = f"⛔ **Hackban**: {ctx.author.mention} banned "\
|
||||||
|
f"{target_user.mention} | {safe_name}\n"\
|
||||||
|
f"🏷 __User ID__: {target}\n"
|
||||||
|
if reason:
|
||||||
|
chan_message += f"✏️ __Reason__: \"{reason}\""
|
||||||
|
else:
|
||||||
|
chan_message += "Please add an explanation below. In the future"\
|
||||||
|
", it is recommended to use "\
|
||||||
|
"`.hackban <user> [reason]`."
|
||||||
|
|
||||||
|
log_channel = self.bot.get_channel(config.modlog_channel)
|
||||||
|
await log_channel.send(chan_message)
|
||||||
|
await ctx.send(f"{safe_name} is now b&. 👍")
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.bot_has_permissions(ban_members=True)
|
||||||
|
@commands.check(check_if_staff)
|
||||||
|
@commands.command()
|
||||||
|
async def silentban(self, ctx, target: discord.Member, *, reason: str = ""):
|
||||||
|
"""Bans a user, staff only."""
|
||||||
|
# Hedge-proofing the code
|
||||||
|
if target == ctx.author:
|
||||||
|
return await ctx.send("You can't do mod actions on yourself.")
|
||||||
|
elif self.check_if_target_is_staff(target):
|
||||||
|
return await ctx.send("I can't ban this user as "
|
||||||
|
"they're a member of staff.")
|
||||||
|
|
||||||
|
userlog(target.id, ctx.author, reason, "bans", target.name)
|
||||||
|
|
||||||
|
safe_name = await commands.clean_content().convert(ctx, str(target))
|
||||||
|
|
||||||
|
await target.ban(reason=f"{ctx.author}, reason: {reason}",
|
||||||
|
delete_message_days=0)
|
||||||
|
chan_message = f"⛔ **Silent ban**: {ctx.author.mention} banned "\
|
||||||
|
f"{target.mention} | {safe_name}\n"\
|
||||||
|
f"🏷 __User ID__: {target.id}\n"
|
||||||
|
if reason:
|
||||||
|
chan_message += f"✏️ __Reason__: \"{reason}\""
|
||||||
|
else:
|
||||||
|
chan_message += "Please add an explanation below. In the future"\
|
||||||
|
", it is recommended to use `.ban <user> [reason]`"\
|
||||||
|
" as the reason is automatically sent to the user."
|
||||||
|
|
||||||
|
log_channel = self.bot.get_channel(config.modlog_channel)
|
||||||
|
await log_channel.send(chan_message)
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.check(check_if_staff)
|
||||||
|
@commands.command()
|
||||||
|
async def approve(self, ctx, target: discord.Member,
|
||||||
|
role: str = "community"):
|
||||||
|
"""Add a role to a user (default: community), staff only."""
|
||||||
|
if role not in config.named_roles:
|
||||||
|
return await ctx.send("No such role! Available roles: " +
|
||||||
|
','.join(config.named_roles))
|
||||||
|
|
||||||
|
log_channel = self.bot.get_channel(config.modlog_channel)
|
||||||
|
target_role = ctx.guild.get_role(config.named_roles[role])
|
||||||
|
|
||||||
|
if target_role in target.roles:
|
||||||
|
return await ctx.send("Target already has this role.")
|
||||||
|
|
||||||
|
await target.add_roles(target_role, reason=str(ctx.author))
|
||||||
|
|
||||||
|
await ctx.send(f"Approved {target.mention} to `{role}` role.")
|
||||||
|
|
||||||
|
await log_channel.send(f"✅ Approved: {ctx.author.mention} added"
|
||||||
|
f" {role} to {target.mention}")
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.check(check_if_staff)
|
||||||
|
@commands.command(aliases=["unapprove"])
|
||||||
|
async def revoke(self, ctx, target: discord.Member,
|
||||||
|
role: str = "community"):
|
||||||
|
"""Remove a role from a user (default: community), staff only."""
|
||||||
|
if role not in config.named_roles:
|
||||||
|
return await ctx.send("No such role! Available roles: " +
|
||||||
|
','.join(config.named_roles))
|
||||||
|
|
||||||
|
log_channel = self.bot.get_channel(config.modlog_channel)
|
||||||
|
target_role = ctx.guild.get_role(config.named_roles[role])
|
||||||
|
|
||||||
|
if target_role not in target.roles:
|
||||||
|
return await ctx.send("Target doesn't have this role.")
|
||||||
|
|
||||||
|
await target.remove_roles(target_role, reason=str(ctx.author))
|
||||||
|
|
||||||
|
await ctx.send(f"Un-approved {target.mention} from `{role}` role.")
|
||||||
|
|
||||||
|
await log_channel.send(f"❌ Un-approved: {ctx.author.mention} removed"
|
||||||
|
f" {role} from {target.mention}")
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.check(check_if_staff)
|
||||||
|
@commands.command(aliases=["clear"])
|
||||||
|
async def purge(self, ctx, limit: int, channel: discord.TextChannel = None):
|
||||||
|
"""Clears a given number of messages, staff only."""
|
||||||
|
log_channel = self.bot.get_channel(config.modlog_channel)
|
||||||
|
if not channel:
|
||||||
|
channel = ctx.channel
|
||||||
|
await channel.purge(limit=limit)
|
||||||
|
msg = f"🗑 **Purged**: {ctx.author.mention} purged {limit} "\
|
||||||
|
f"messages in {channel.mention}."
|
||||||
|
await log_channel.send(msg)
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.check(check_if_staff)
|
||||||
|
@commands.command()
|
||||||
|
async def warn(self, ctx, target: discord.Member, *, reason: str = ""):
|
||||||
|
"""Warns a user, staff only."""
|
||||||
|
# Hedge-proofing the code
|
||||||
|
if target == ctx.author:
|
||||||
|
return await ctx.send("You can't do mod actions on yourself.")
|
||||||
|
elif self.check_if_target_is_staff(target):
|
||||||
|
return await ctx.send("I can't warn this user as "
|
||||||
|
"they're a member of staff.")
|
||||||
|
|
||||||
|
log_channel = self.bot.get_channel(config.modlog_channel)
|
||||||
|
warn_count = userlog(target.id, ctx.author, reason,
|
||||||
|
"warns", target.name)
|
||||||
|
|
||||||
|
safe_name = await commands.clean_content().convert(ctx, str(target))
|
||||||
|
chan_msg = f"⚠️ **Warned**: {ctx.author.mention} warned "\
|
||||||
|
f"{target.mention} (warn #{warn_count}) "\
|
||||||
|
f"| {safe_name}\n"
|
||||||
|
|
||||||
|
msg = f"You were warned on {ctx.guild.name}."
|
||||||
|
if reason:
|
||||||
|
msg += " The given reason is: " + reason
|
||||||
|
msg += f"\n\nPlease read the rules in {config.rules_url}. "\
|
||||||
|
f"This is warn #{warn_count}."
|
||||||
|
if warn_count == 2:
|
||||||
|
msg += " __The next warn will automatically kick.__"
|
||||||
|
if warn_count == 3:
|
||||||
|
msg += "\n\nYou were kicked because of this warning. "\
|
||||||
|
"You can join again right away. "\
|
||||||
|
"Two more warnings will result in an automatic ban."
|
||||||
|
if warn_count == 4:
|
||||||
|
msg += "\n\nYou were kicked because of this warning. "\
|
||||||
|
"This is your final warning. "\
|
||||||
|
"You can join again, but "\
|
||||||
|
"**one more warn will result in a ban**."
|
||||||
|
chan_msg += "**This resulted in an auto-kick.**\n"
|
||||||
|
if warn_count == 5:
|
||||||
|
msg += "\n\nYou were automatically banned due to five warnings."
|
||||||
|
chan_msg += "**This resulted in an auto-ban.**\n"
|
||||||
|
try:
|
||||||
|
await target.send(msg)
|
||||||
|
except discord.errors.Forbidden:
|
||||||
|
# Prevents log issues in cases where user blocked bot
|
||||||
|
# or has DMs disabled
|
||||||
|
pass
|
||||||
|
if warn_count == 3 or warn_count == 4:
|
||||||
|
await target.kick()
|
||||||
|
if warn_count >= 5: # just in case
|
||||||
|
await target.ban(reason="exceeded warn limit",
|
||||||
|
delete_message_days=0)
|
||||||
|
await ctx.send(f"{target.mention} warned. "
|
||||||
|
f"User has {warn_count} warning(s).")
|
||||||
|
|
||||||
|
if reason:
|
||||||
|
chan_msg += f"✏️ __Reason__: \"{reason}\""
|
||||||
|
else:
|
||||||
|
chan_msg += "Please add an explanation below. In the future"\
|
||||||
|
", it is recommended to use `.ban <user> [reason]`"\
|
||||||
|
" as the reason is automatically sent to the user."
|
||||||
|
await log_channel.send(chan_msg)
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.check(check_if_staff)
|
||||||
|
@commands.command(aliases=["setnick", "nick"])
|
||||||
|
async def nickname(self, ctx, target: discord.Member, *, nick: str = ""):
|
||||||
|
"""Sets a user's nickname, staff only.
|
||||||
|
|
||||||
|
Just send .nickname <user> to wipe the nickname."""
|
||||||
|
|
||||||
|
if nick:
|
||||||
|
await target.edit(nick=nick, reason=str(ctx.author))
|
||||||
|
else:
|
||||||
|
await target.edit(nick=None, reason=str(ctx.author))
|
||||||
|
|
||||||
|
await ctx.send("Successfully set nickname.")
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.check(check_if_staff)
|
||||||
|
@commands.command(aliases=['echo'])
|
||||||
|
async def say(self, ctx, *, the_text: str):
|
||||||
|
"""Repeats a given text, staff only."""
|
||||||
|
await ctx.send(the_text)
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.check(check_if_staff)
|
||||||
|
@commands.command()
|
||||||
|
async def speak(self, ctx, channel: discord.TextChannel, *, the_text: str):
|
||||||
|
"""Repeats a given text in a given channel, staff only."""
|
||||||
|
await channel.send(the_text)
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.check(check_if_staff)
|
||||||
|
@commands.command(aliases=["setplaying", "setgame"])
|
||||||
|
async def playing(self, ctx, *, game: str = ""):
|
||||||
|
"""Sets the bot's currently played game name, staff only.
|
||||||
|
|
||||||
|
Just send .playing to wipe the playing state."""
|
||||||
|
if game:
|
||||||
|
await self.bot.change_presence(activity=discord.Game(name=game))
|
||||||
|
else:
|
||||||
|
await self.bot.change_presence(activity=None)
|
||||||
|
|
||||||
|
await ctx.send("Successfully set game.")
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.check(check_if_staff)
|
||||||
|
@commands.command(aliases=["setbotnick", "botnick", "robotnick"])
|
||||||
|
async def botnickname(self, ctx, *, nick: str = ""):
|
||||||
|
"""Sets the bot's nickname, staff only.
|
||||||
|
|
||||||
|
Just send .botnickname to wipe the nickname."""
|
||||||
|
|
||||||
|
if nick:
|
||||||
|
await ctx.guild.me.edit(nick=nick, reason=str(ctx.author))
|
||||||
|
else:
|
||||||
|
await ctx.guild.me.edit(nick=None, reason=str(ctx.author))
|
||||||
|
|
||||||
|
await ctx.send("Successfully set bot nickname.")
|
||||||
|
|
||||||
|
|
||||||
|
def setup(bot):
|
||||||
|
bot.add_cog(Mod(bot))
|
|
@ -1,9 +1,8 @@
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from discord.ext.commands import Cog
|
from discord.ext.commands import Cog
|
||||||
|
from helpers.checks import check_if_staff
|
||||||
from robocop_ng.helpers.checks import check_if_staff
|
from helpers.userlogs import userlog
|
||||||
from robocop_ng.helpers.userlogs import userlog
|
|
||||||
|
|
||||||
|
|
||||||
class ModNote(Cog):
|
class ModNote(Cog):
|
||||||
|
@ -15,7 +14,8 @@ class ModNote(Cog):
|
||||||
@commands.command(aliases=["addnote"])
|
@commands.command(aliases=["addnote"])
|
||||||
async def note(self, ctx, target: discord.Member, *, note: str = ""):
|
async def note(self, ctx, target: discord.Member, *, note: str = ""):
|
||||||
"""Adds a note to a user, staff only."""
|
"""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!")
|
await ctx.send(f"{ctx.author.mention}: noted!")
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
|
@ -23,9 +23,10 @@ class ModNote(Cog):
|
||||||
@commands.command(aliases=["addnoteid"])
|
@commands.command(aliases=["addnoteid"])
|
||||||
async def noteid(self, ctx, target: int, *, note: str = ""):
|
async def noteid(self, ctx, target: int, *, note: str = ""):
|
||||||
"""Adds a note to a user by userid, staff only."""
|
"""Adds a note to a user by userid, staff only."""
|
||||||
userlog(self.bot, target, ctx.author, note, "notes")
|
userlog(target, ctx.author, note,
|
||||||
await ctx.send(f"{ctx.author.mention}: noted!")
|
"notes")
|
||||||
|
await ctx.send(f"{target.mention}: noted!")
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
def setup(bot):
|
||||||
await bot.add_cog(ModNote(bot))
|
bot.add_cog(ModNote(bot))
|
|
@ -1,10 +1,9 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from discord.ext.commands import Cog
|
from discord.ext.commands import Cog
|
||||||
|
import config
|
||||||
from robocop_ng.helpers.checks import check_if_staff
|
from helpers.checks import check_if_staff
|
||||||
|
|
||||||
|
|
||||||
class ModReact(Cog):
|
class ModReact(Cog):
|
||||||
|
@ -14,41 +13,34 @@ class ModReact(Cog):
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.check(check_if_staff)
|
@commands.check(check_if_staff)
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def clearreactsbyuser(
|
async def clearreactsbyuser(self, ctx, user: discord.Member, *,
|
||||||
self,
|
|
||||||
ctx,
|
|
||||||
user: discord.Member,
|
|
||||||
*,
|
|
||||||
channel: discord.TextChannel = None,
|
channel: discord.TextChannel = None,
|
||||||
limit: int = 50,
|
limit: int = 50):
|
||||||
):
|
|
||||||
"""Clears reacts from a given user in the given channel, staff only."""
|
"""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:
|
if not channel:
|
||||||
channel = ctx.channel
|
channel = ctx.channel
|
||||||
count = 0
|
count = 0
|
||||||
async for msg in channel.history(limit=limit):
|
async for msg in channel.history(limit=limit):
|
||||||
for react in msg.reactions:
|
for react in msg.reactions:
|
||||||
async for react_user in react.users():
|
if await react.users().find(lambda u: u == user):
|
||||||
if react_user == user:
|
|
||||||
count += 1
|
count += 1
|
||||||
await react.remove(user)
|
async for u in react.users():
|
||||||
msg = (
|
await msg.remove_reaction(react, u)
|
||||||
f"✏️ **Cleared reacts**: {ctx.author.mention} cleared "
|
msg = f"✏️ **Cleared reacts**: {ctx.author.mention} cleared "\
|
||||||
f"{user.mention}'s reacts from the last {limit} messages "
|
f"{user.mention}'s reacts from the last {limit} messages "\
|
||||||
f"in {channel.mention}."
|
f"in {channel.mention}."
|
||||||
)
|
|
||||||
await ctx.channel.send(f"Cleared {count} unique reactions")
|
await ctx.channel.send(f"Cleared {count} unique reactions")
|
||||||
await log_channel.send(msg)
|
await log_channel.send(msg)
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.check(check_if_staff)
|
@commands.check(check_if_staff)
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def clearallreacts(
|
async def clearallreacts(self, ctx, *,
|
||||||
self, ctx, *, limit: int = 50, channel: discord.TextChannel = None
|
limit: int = 50,
|
||||||
):
|
channel: discord.TextChannel = None):
|
||||||
"""Clears all reacts in a given channel, staff only. Use with care."""
|
"""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:
|
if not channel:
|
||||||
channel = ctx.channel
|
channel = ctx.channel
|
||||||
count = 0
|
count = 0
|
||||||
|
@ -56,10 +48,8 @@ class ModReact(Cog):
|
||||||
if msg.reactions:
|
if msg.reactions:
|
||||||
count += 1
|
count += 1
|
||||||
await msg.clear_reactions()
|
await msg.clear_reactions()
|
||||||
msg = (
|
msg = f"✏️ **Cleared reacts**: {ctx.author.mention} cleared all "\
|
||||||
f"✏️ **Cleared reacts**: {ctx.author.mention} cleared all "
|
|
||||||
f"reacts from the last {limit} messages in {channel.mention}."
|
f"reacts from the last {limit} messages in {channel.mention}."
|
||||||
)
|
|
||||||
await ctx.channel.send(f"Cleared reacts from {count} messages!")
|
await ctx.channel.send(f"Cleared reacts from {count} messages!")
|
||||||
await log_channel.send(msg)
|
await log_channel.send(msg)
|
||||||
|
|
||||||
|
@ -68,10 +58,8 @@ class ModReact(Cog):
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def clearreactsinteractive(self, ctx):
|
async def clearreactsinteractive(self, ctx):
|
||||||
"""Clears reacts interactively, staff only. Use with care."""
|
"""Clears reacts interactively, staff only. Use with care."""
|
||||||
msg_text = (
|
msg_text = f"{ctx.author.mention}, react to the reactions you want "\
|
||||||
f"{ctx.author.mention}, react to the reactions you want "
|
|
||||||
f"to remove. React to this message when you're done."
|
f"to remove. React to this message when you're done."
|
||||||
)
|
|
||||||
msg = await ctx.channel.send(msg_text)
|
msg = await ctx.channel.send(msg_text)
|
||||||
|
|
||||||
tasks = []
|
tasks = []
|
||||||
|
@ -86,11 +74,10 @@ class ModReact(Cog):
|
||||||
else:
|
else:
|
||||||
# remove a reaction
|
# remove a reaction
|
||||||
async def impl():
|
async def impl():
|
||||||
msg = (
|
msg = await self.bot \
|
||||||
await self.bot.get_guild(event.guild_id)
|
.get_guild(event.guild_id) \
|
||||||
.get_channel(event.channel_id)
|
.get_channel(event.channel_id) \
|
||||||
.get_message(event.message_id)
|
.get_message(event.message_id)
|
||||||
)
|
|
||||||
|
|
||||||
def check_emoji(r):
|
def check_emoji(r):
|
||||||
if event.emoji.is_custom_emoji() == r.custom_emoji:
|
if event.emoji.is_custom_emoji() == r.custom_emoji:
|
||||||
|
@ -101,17 +88,17 @@ class ModReact(Cog):
|
||||||
return event.emoji.name == r.emoji
|
return event.emoji.name == r.emoji
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for reaction in filter(check_emoji, msg.reactions):
|
for reaction in filter(check_emoji, msg.reactions):
|
||||||
async for u in reaction.users():
|
async for u in reaction.users():
|
||||||
await reaction.message.remove_reaction(reaction, u)
|
await reaction.message.remove_reaction(reaction, u)
|
||||||
|
|
||||||
# schedule immediately
|
# schedule immediately
|
||||||
tasks.append(asyncio.create_task(impl()))
|
tasks.append(asyncio.create_task(impl()))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.bot.wait_for("raw_reaction_add", timeout=120.0, check=check)
|
await self.bot.wait_for("raw_reaction_add",
|
||||||
|
timeout=120.0,
|
||||||
|
check=check)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await msg.edit(content=f"{msg_text} Timed out.")
|
await msg.edit(content=f"{msg_text} Timed out.")
|
||||||
else:
|
else:
|
||||||
|
@ -119,5 +106,5 @@ class ModReact(Cog):
|
||||||
await msg.edit(content=f"{msg_text} Done!")
|
await msg.edit(content=f"{msg_text} Done!")
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
def setup(bot):
|
||||||
await bot.add_cog(ModReact(bot))
|
bot.add_cog(ModReact(bot))
|
137
cogs/mod_timed.py
Normal file
137
cogs/mod_timed.py
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
import discord
|
||||||
|
import config
|
||||||
|
from datetime import datetime
|
||||||
|
from discord.ext import commands
|
||||||
|
from discord.ext.commands import Cog
|
||||||
|
from helpers.checks import check_if_staff
|
||||||
|
from helpers.robocronp import add_job
|
||||||
|
from helpers.userlogs import userlog
|
||||||
|
from helpers.restrictions import add_restriction
|
||||||
|
|
||||||
|
|
||||||
|
class ModTimed(Cog):
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
def check_if_target_is_staff(self, target):
|
||||||
|
return any(r.id in config.staff_role_ids for r in target.roles)
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.bot_has_permissions(ban_members=True)
|
||||||
|
@commands.check(check_if_staff)
|
||||||
|
@commands.command()
|
||||||
|
async def timeban(self, ctx, target: discord.Member,
|
||||||
|
duration: str, *, reason: str = ""):
|
||||||
|
"""Bans a user for a specified amount of time, staff only."""
|
||||||
|
# Hedge-proofing the code
|
||||||
|
if target == ctx.author:
|
||||||
|
return await ctx.send("You can't do mod actions on yourself.")
|
||||||
|
elif self.check_if_target_is_staff(target):
|
||||||
|
return await ctx.send("I can't ban this user as "
|
||||||
|
"they're a member of staff.")
|
||||||
|
|
||||||
|
expiry_timestamp = self.bot.parse_time(duration)
|
||||||
|
expiry_datetime = datetime.utcfromtimestamp(expiry_timestamp)
|
||||||
|
duration_text = self.bot.get_relative_timestamp(time_to=expiry_datetime,
|
||||||
|
include_to=True,
|
||||||
|
humanized=True)
|
||||||
|
|
||||||
|
userlog(target.id, ctx.author, f"{reason} (Timed, until "
|
||||||
|
f"{duration_text})",
|
||||||
|
"bans", target.name)
|
||||||
|
|
||||||
|
safe_name = await commands.clean_content().convert(ctx, str(target))
|
||||||
|
|
||||||
|
dm_message = f"You were banned from {ctx.guild.name}."
|
||||||
|
if reason:
|
||||||
|
dm_message += f" The given reason is: \"{reason}\"."
|
||||||
|
dm_message += f"\n\nThis ban will expire {duration_text}."
|
||||||
|
|
||||||
|
try:
|
||||||
|
await target.send(dm_message)
|
||||||
|
except discord.errors.Forbidden:
|
||||||
|
# Prevents ban issues in cases where user blocked bot
|
||||||
|
# or has DMs disabled
|
||||||
|
pass
|
||||||
|
|
||||||
|
await target.ban(reason=f"{ctx.author}, reason: {reason}",
|
||||||
|
delete_message_days=0)
|
||||||
|
chan_message = f"⛔ **Timed Ban**: {ctx.author.mention} banned "\
|
||||||
|
f"{target.mention} for {duration_text} | {safe_name}\n"\
|
||||||
|
f"🏷 __User ID__: {target.id}\n"
|
||||||
|
if reason:
|
||||||
|
chan_message += f"✏️ __Reason__: \"{reason}\""
|
||||||
|
else:
|
||||||
|
chan_message += "Please add an explanation below. In the future"\
|
||||||
|
", it is recommended to use `.ban <user> [reason]`"\
|
||||||
|
" as the reason is automatically sent to the user."
|
||||||
|
|
||||||
|
add_job("unban", target.id, {"guild": ctx.guild.id}, expiry_timestamp)
|
||||||
|
|
||||||
|
log_channel = self.bot.get_channel(config.log_channel)
|
||||||
|
await log_channel.send(chan_message)
|
||||||
|
await ctx.send(f"{safe_name} is now b&. "
|
||||||
|
f"It will expire {duration_text}. 👍")
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.check(check_if_staff)
|
||||||
|
@commands.command()
|
||||||
|
async def timemute(self, ctx, target: discord.Member,
|
||||||
|
duration: str, *, reason: str = ""):
|
||||||
|
"""Mutes a user for a specified amount of time, staff only."""
|
||||||
|
# Hedge-proofing the code
|
||||||
|
if target == ctx.author:
|
||||||
|
return await ctx.send("You can't do mod actions on yourself.")
|
||||||
|
elif self.check_if_target_is_staff(target):
|
||||||
|
return await ctx.send("I can't mute this user as "
|
||||||
|
"they're a member of staff.")
|
||||||
|
|
||||||
|
expiry_timestamp = self.bot.parse_time(duration)
|
||||||
|
expiry_datetime = datetime.utcfromtimestamp(expiry_timestamp)
|
||||||
|
duration_text = self.bot.get_relative_timestamp(time_to=expiry_datetime,
|
||||||
|
include_to=True,
|
||||||
|
humanized=True)
|
||||||
|
|
||||||
|
userlog(target.id, ctx.author, f"{reason} (Timed, until "
|
||||||
|
f"{duration_text})",
|
||||||
|
"mutes", target.name)
|
||||||
|
|
||||||
|
safe_name = await commands.clean_content().convert(ctx, str(target))
|
||||||
|
|
||||||
|
dm_message = f"You were muted!"
|
||||||
|
if reason:
|
||||||
|
dm_message += f" The given reason is: \"{reason}\"."
|
||||||
|
dm_message += f"\n\nThis mute will expire {duration_text}."
|
||||||
|
|
||||||
|
try:
|
||||||
|
await target.send(dm_message)
|
||||||
|
except discord.errors.Forbidden:
|
||||||
|
# Prevents kick issues in cases where user blocked bot
|
||||||
|
# or has DMs disabled
|
||||||
|
pass
|
||||||
|
|
||||||
|
mute_role = ctx.guild.get_role(config.mute_role)
|
||||||
|
|
||||||
|
await target.add_roles(mute_role, reason=str(ctx.author))
|
||||||
|
|
||||||
|
chan_message = f"🔇 **Timed Mute**: {ctx.author.mention} muted "\
|
||||||
|
f"{target.mention} for {duration_text} | {safe_name}\n"\
|
||||||
|
f"🏷 __User ID__: {target.id}\n"
|
||||||
|
if reason:
|
||||||
|
chan_message += f"✏️ __Reason__: \"{reason}\""
|
||||||
|
else:
|
||||||
|
chan_message += "Please add an explanation below. In the future, "\
|
||||||
|
"it is recommended to use `.mute <user> [reason]`"\
|
||||||
|
" as the reason is automatically sent to the user."
|
||||||
|
|
||||||
|
add_job("unmute", target.id, {"guild": ctx.guild.id}, expiry_timestamp)
|
||||||
|
|
||||||
|
log_channel = self.bot.get_channel(config.log_channel)
|
||||||
|
await log_channel.send(chan_message)
|
||||||
|
await ctx.send(f"{target.mention} can no longer speak. "
|
||||||
|
f"It will expire {duration_text}.")
|
||||||
|
add_restriction(target.id, config.mute_role)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(bot):
|
||||||
|
bot.add_cog(ModTimed(bot))
|
|
@ -1,27 +1,25 @@
|
||||||
import json
|
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from discord.ext.commands import Cog
|
from discord.ext.commands import Cog
|
||||||
|
import config
|
||||||
from robocop_ng.helpers.checks import check_if_staff
|
import json
|
||||||
from robocop_ng.helpers.userlogs import get_userlog, set_userlog, userlog_event_types
|
from helpers.checks import check_if_staff
|
||||||
|
from helpers.userlogs import get_userlog, set_userlog, userlog_event_types
|
||||||
|
|
||||||
|
|
||||||
class ModUserlog(Cog):
|
class ModUserlog(Cog):
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
def get_userlog_embed_for_id(
|
def get_userlog_embed_for_id(self, uid: str, name: str, own: bool = False,
|
||||||
self, uid: str, name: str, own: bool = False, event=""
|
event=""):
|
||||||
):
|
|
||||||
own_note = " Good for you!" if own else ""
|
own_note = " Good for you!" if own else ""
|
||||||
wanted_events = ["warns", "bans", "kicks", "mutes"]
|
wanted_events = ["warns", "bans", "kicks", "mutes"]
|
||||||
if event and not isinstance(event, list):
|
if event and not isinstance(event, list):
|
||||||
wanted_events = [event]
|
wanted_events = [event]
|
||||||
embed = discord.Embed(color=discord.Color.dark_red())
|
embed = discord.Embed(color=discord.Color.dark_red())
|
||||||
embed.set_author(name=f"Userlog for {name}")
|
embed.set_author(name=f"Userlog for {name}")
|
||||||
userlog = get_userlog(self.bot)
|
userlog = get_userlog()
|
||||||
|
|
||||||
if uid not in userlog:
|
if uid not in userlog:
|
||||||
embed.description = f"There are none!{own_note} (no entry)"
|
embed.description = f"There are none!{own_note} (no entry)"
|
||||||
|
@ -32,17 +30,12 @@ class ModUserlog(Cog):
|
||||||
if event_type in userlog[uid] and userlog[uid][event_type]:
|
if event_type in userlog[uid] and userlog[uid][event_type]:
|
||||||
event_name = userlog_event_types[event_type]
|
event_name = userlog_event_types[event_type]
|
||||||
for idx, event in enumerate(userlog[uid][event_type]):
|
for idx, event in enumerate(userlog[uid][event_type]):
|
||||||
issuer = (
|
issuer = "" if own else f"Issuer: {event['issuer_name']} "\
|
||||||
""
|
|
||||||
if own
|
|
||||||
else f"Issuer: {event['issuer_name']} "
|
|
||||||
f"({event['issuer_id']})\n"
|
f"({event['issuer_id']})\n"
|
||||||
)
|
embed.add_field(name=f"{event_name} {idx + 1}: "
|
||||||
embed.add_field(
|
f"{event['timestamp']}",
|
||||||
name=f"{event_name} {idx + 1}: " f"{event['timestamp']}",
|
|
||||||
value=issuer + f"Reason: {event['reason']}",
|
value=issuer + f"Reason: {event['reason']}",
|
||||||
inline=False,
|
inline=False)
|
||||||
)
|
|
||||||
|
|
||||||
if not own and "watch" in userlog[uid]:
|
if not own and "watch" in userlog[uid]:
|
||||||
watch_state = "" if userlog[uid]["watch"] else "NOT "
|
watch_state = "" if userlog[uid]["watch"] else "NOT "
|
||||||
|
@ -54,37 +47,37 @@ class ModUserlog(Cog):
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
def clear_event_from_id(self, uid: str, event_type):
|
def clear_event_from_id(self, uid: str, event_type):
|
||||||
userlog = get_userlog(self.bot)
|
userlog = get_userlog()
|
||||||
if uid not in userlog:
|
if uid not in userlog:
|
||||||
return f"<@{uid}> has no {event_type}!"
|
return f"<@{uid}> has no {event_type}!"
|
||||||
event_count = len(userlog[uid][event_type])
|
event_count = len(userlog[uid][event_type])
|
||||||
if not event_count:
|
if not event_count:
|
||||||
return f"<@{uid}> has no {event_type}!"
|
return f"<@{uid}> has no {event_type}!"
|
||||||
userlog[uid][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}!"
|
return f"<@{uid}> no longer has any {event_type}!"
|
||||||
|
|
||||||
def delete_event_from_id(self, uid: str, idx: int, 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:
|
if uid not in userlog:
|
||||||
return f"<@{uid}> has no {event_type}!"
|
return f"<@{uid}> has no {event_type}!"
|
||||||
event_count = len(userlog[uid][event_type])
|
event_count = len(userlog[uid][event_type])
|
||||||
if not event_count:
|
if not event_count:
|
||||||
return f"<@{uid}> has no {event_type}!"
|
return f"<@{uid}> has no {event_type}!"
|
||||||
if idx > event_count:
|
if idx > event_count:
|
||||||
return "Index is higher than " f"count ({event_count})!"
|
return "Index is higher than "\
|
||||||
|
f"count ({event_count})!"
|
||||||
if idx < 1:
|
if idx < 1:
|
||||||
return "Index is below 1!"
|
return "Index is below 1!"
|
||||||
event = userlog[uid][event_type][idx - 1]
|
event = userlog[uid][event_type][idx - 1]
|
||||||
event_name = userlog_event_types[event_type]
|
event_name = userlog_event_types[event_type]
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(color=discord.Color.dark_red(),
|
||||||
color=discord.Color.dark_red(),
|
title=f"{event_name} {idx} on "
|
||||||
title=f"{event_name} {idx} on " f"{event['timestamp']}",
|
f"{event['timestamp']}",
|
||||||
description=f"Issuer: {event['issuer_name']}\n"
|
description=f"Issuer: {event['issuer_name']}\n"
|
||||||
f"Reason: {event['reason']}",
|
f"Reason: {event['reason']}")
|
||||||
)
|
|
||||||
del userlog[uid][event_type][idx - 1]
|
del userlog[uid][event_type][idx - 1]
|
||||||
set_userlog(self.bot, json.dumps(userlog))
|
set_userlog(json.dumps(userlog))
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
|
@ -92,18 +85,21 @@ class ModUserlog(Cog):
|
||||||
@commands.command(aliases=["events"])
|
@commands.command(aliases=["events"])
|
||||||
async def eventtypes(self, ctx):
|
async def eventtypes(self, ctx):
|
||||||
"""Lists the available event types, staff only."""
|
"""Lists the available event types, staff only."""
|
||||||
event_list = [f"{et} ({userlog_event_types[et]})" for et in userlog_event_types]
|
event_list = [f"{et} ({userlog_event_types[et]})" for et in
|
||||||
event_text = "Available events:\n``` - " + "\n - ".join(event_list) + "```"
|
userlog_event_types]
|
||||||
|
event_text = ("Available events:\n``` - " +
|
||||||
|
"\n - ".join(event_list) +
|
||||||
|
"```")
|
||||||
await ctx.send(event_text)
|
await ctx.send(event_text)
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.check(check_if_staff)
|
@commands.check(check_if_staff)
|
||||||
@commands.command(
|
@commands.command(name="userlog",
|
||||||
name="userlog", aliases=["listwarns", "getuserlog", "listuserlog"]
|
aliases=["listwarns", "getuserlog", "listuserlog"])
|
||||||
)
|
|
||||||
async def userlog_cmd(self, ctx, target: discord.Member, event=""):
|
async def userlog_cmd(self, ctx, target: discord.Member, event=""):
|
||||||
"""Lists the userlog events for a user, staff only."""
|
"""Lists the userlog events for a user, staff only."""
|
||||||
embed = self.get_userlog_embed_for_id(str(target.id), str(target), event=event)
|
embed = self.get_userlog_embed_for_id(str(target.id), str(target),
|
||||||
|
event=event)
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
|
@ -111,16 +107,16 @@ class ModUserlog(Cog):
|
||||||
@commands.command(aliases=["listnotes", "usernotes"])
|
@commands.command(aliases=["listnotes", "usernotes"])
|
||||||
async def notes(self, ctx, target: discord.Member):
|
async def notes(self, ctx, target: discord.Member):
|
||||||
"""Lists the notes for a user, staff only."""
|
"""Lists the notes for a user, staff only."""
|
||||||
embed = self.get_userlog_embed_for_id(
|
embed = self.get_userlog_embed_for_id(str(target.id), str(target),
|
||||||
str(target.id), str(target), event="notes"
|
event="notes")
|
||||||
)
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.command(aliases=["mywarns"])
|
@commands.command(aliases=["mywarns"])
|
||||||
async def myuserlog(self, ctx):
|
async def myuserlog(self, ctx):
|
||||||
"""Lists your userlog events (warns etc)."""
|
"""Lists your userlog events (warns etc)."""
|
||||||
embed = self.get_userlog_embed_for_id(str(ctx.author.id), str(ctx.author), True)
|
embed = self.get_userlog_embed_for_id(str(ctx.author.id),
|
||||||
|
str(ctx.author), True)
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
|
@ -134,20 +130,16 @@ class ModUserlog(Cog):
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.check(check_if_staff)
|
@commands.check(check_if_staff)
|
||||||
@commands.command(aliases=["clearwarns"])
|
@commands.command(aliases=["clearwarns"])
|
||||||
async def clearevent(self, ctx, target: discord.Member, event="warns"):
|
async def clearevent(self, ctx, target: discord.Member,
|
||||||
|
event="warns"):
|
||||||
"""Clears all events of given type for a user, staff only."""
|
"""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)
|
msg = self.clear_event_from_id(str(target.id), event)
|
||||||
safe_name = await commands.clean_content(escape_markdown=True).convert(
|
safe_name = await commands.clean_content().convert(ctx, str(target))
|
||||||
ctx, str(target)
|
|
||||||
)
|
|
||||||
await ctx.send(msg)
|
await ctx.send(msg)
|
||||||
msg = (
|
msg = f"🗑 **Cleared {event}**: {ctx.author.mention} cleared"\
|
||||||
f"🗑 **Cleared {event}**: {ctx.author.mention} cleared"
|
f" all {event} events of {target.mention} | "\
|
||||||
f" all {event} events of {target.mention} | "
|
|
||||||
f"{safe_name}"
|
f"{safe_name}"
|
||||||
f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
|
|
||||||
)
|
|
||||||
await log_channel.send(msg)
|
await log_channel.send(msg)
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
|
@ -155,36 +147,29 @@ class ModUserlog(Cog):
|
||||||
@commands.command(aliases=["clearwarnsid"])
|
@commands.command(aliases=["clearwarnsid"])
|
||||||
async def cleareventid(self, ctx, target: int, event="warns"):
|
async def cleareventid(self, ctx, target: int, event="warns"):
|
||||||
"""Clears all events of given type for a userid, staff only."""
|
"""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)
|
msg = self.clear_event_from_id(str(target), event)
|
||||||
await ctx.send(msg)
|
await ctx.send(msg)
|
||||||
msg = (
|
msg = f"🗑 **Cleared {event}**: {ctx.author.mention} cleared"\
|
||||||
f"🗑 **Cleared {event}**: {ctx.author.mention} cleared"
|
|
||||||
f" all {event} events of <@{target}> "
|
f" all {event} events of <@{target}> "
|
||||||
f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
|
|
||||||
)
|
|
||||||
await log_channel.send(msg)
|
await log_channel.send(msg)
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.check(check_if_staff)
|
@commands.check(check_if_staff)
|
||||||
@commands.command(aliases=["delwarn"])
|
@commands.command(aliases=["delwarn"])
|
||||||
async def delevent(self, ctx, target: discord.Member, idx: int, event="warns"):
|
async def delevent(self, ctx, target: discord.Member, idx: int,
|
||||||
|
event="warns"):
|
||||||
"""Removes a specific event from a user, staff only."""
|
"""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)
|
del_event = self.delete_event_from_id(str(target.id), idx, event)
|
||||||
event_name = userlog_event_types[event].lower()
|
event_name = userlog_event_types[event].lower()
|
||||||
# This is hell.
|
# This is hell.
|
||||||
if isinstance(del_event, discord.Embed):
|
if isinstance(del_event, discord.Embed):
|
||||||
await ctx.send(f"{target.mention} has a {event_name} removed!")
|
await ctx.send(f"{target.mention} has a {event_name} removed!")
|
||||||
safe_name = await commands.clean_content(escape_markdown=True).convert(
|
safe_name = await commands.clean_content().convert(ctx, str(target))
|
||||||
ctx, str(target)
|
msg = f"🗑 **Deleted {event_name}**: "\
|
||||||
)
|
f"{ctx.author.mention} removed "\
|
||||||
msg = (
|
|
||||||
f"🗑 **Deleted {event_name}**: "
|
|
||||||
f"{ctx.author.mention} removed "
|
|
||||||
f"{event_name} {idx} from {target.mention} | {safe_name}"
|
f"{event_name} {idx} from {target.mention} | {safe_name}"
|
||||||
f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
|
|
||||||
)
|
|
||||||
await log_channel.send(msg, embed=del_event)
|
await log_channel.send(msg, embed=del_event)
|
||||||
else:
|
else:
|
||||||
await ctx.send(del_event)
|
await ctx.send(del_event)
|
||||||
|
@ -194,18 +179,15 @@ class ModUserlog(Cog):
|
||||||
@commands.command(aliases=["delwarnid"])
|
@commands.command(aliases=["delwarnid"])
|
||||||
async def deleventid(self, ctx, target: int, idx: int, event="warns"):
|
async def deleventid(self, ctx, target: int, idx: int, event="warns"):
|
||||||
"""Removes a specific event from a userid, staff only."""
|
"""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)
|
del_event = self.delete_event_from_id(str(target), idx, event)
|
||||||
event_name = userlog_event_types[event].lower()
|
event_name = userlog_event_types[event].lower()
|
||||||
# This is hell.
|
# This is hell.
|
||||||
if isinstance(del_event, discord.Embed):
|
if isinstance(del_event, discord.Embed):
|
||||||
await ctx.send(f"<@{target}> has a {event_name} removed!")
|
await ctx.send(f"<@{target}> has a {event_name} removed!")
|
||||||
msg = (
|
msg = f"🗑 **Deleted {event_name}**: "\
|
||||||
f"🗑 **Deleted {event_name}**: "
|
f"{ctx.author.mention} removed "\
|
||||||
f"{ctx.author.mention} removed "
|
|
||||||
f"{event_name} {idx} from <@{target}> "
|
f"{event_name} {idx} from <@{target}> "
|
||||||
f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
|
|
||||||
)
|
|
||||||
await log_channel.send(msg, embed=del_event)
|
await log_channel.send(msg, embed=del_event)
|
||||||
else:
|
else:
|
||||||
await ctx.send(del_event)
|
await ctx.send(del_event)
|
||||||
|
@ -220,30 +202,21 @@ class ModUserlog(Cog):
|
||||||
role = "@ everyone"
|
role = "@ everyone"
|
||||||
|
|
||||||
event_types = ["warns", "bans", "kicks", "mutes", "notes"]
|
event_types = ["warns", "bans", "kicks", "mutes", "notes"]
|
||||||
embed = self.get_userlog_embed_for_id(
|
embed = self.get_userlog_embed_for_id(str(user.id), str(user),
|
||||||
str(user.id), str(user), event=event_types
|
event=event_types)
|
||||||
)
|
|
||||||
|
|
||||||
user_name = await commands.clean_content(escape_markdown=True).convert(
|
await ctx.send(f"user = {user}\n"
|
||||||
ctx, user.name
|
|
||||||
)
|
|
||||||
display_name = await commands.clean_content(escape_markdown=True).convert(
|
|
||||||
ctx, user.display_name
|
|
||||||
)
|
|
||||||
|
|
||||||
await ctx.send(
|
|
||||||
f"user = {user_name}\n"
|
|
||||||
f"id = {user.id}\n"
|
f"id = {user.id}\n"
|
||||||
f"avatar = {user.display_avatar}\n"
|
f"avatar = {user.avatar_url}\n"
|
||||||
f"bot = {user.bot}\n"
|
f"bot = {user.bot}\n"
|
||||||
f"created_at = {user.created_at}\n"
|
f"created_at = {user.created_at}\n"
|
||||||
f"display_name = {display_name}\n"
|
f"display_name = {user.display_name}\n"
|
||||||
f"joined_at = {user.joined_at}\n"
|
f"joined_at = {user.joined_at}\n"
|
||||||
|
f"activities = `{user.activities}`\n"
|
||||||
f"color = {user.colour}\n"
|
f"color = {user.colour}\n"
|
||||||
f"top_role = {role}\n",
|
f"top_role = {role}\n",
|
||||||
embed=embed,
|
embed=embed)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
def setup(bot):
|
||||||
await bot.add_cog(ModUserlog(bot))
|
bot.add_cog(ModUserlog(bot))
|
|
@ -1,9 +1,8 @@
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from discord.ext.commands import Cog
|
from discord.ext.commands import Cog
|
||||||
|
from helpers.checks import check_if_staff
|
||||||
from robocop_ng.helpers.checks import check_if_staff
|
from helpers.userlogs import setwatch
|
||||||
from robocop_ng.helpers.userlogs import setwatch
|
|
||||||
|
|
||||||
|
|
||||||
class ModWatch(Cog):
|
class ModWatch(Cog):
|
||||||
|
@ -15,7 +14,7 @@ class ModWatch(Cog):
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def watch(self, ctx, target: discord.Member, *, note: str = ""):
|
async def watch(self, ctx, target: discord.Member, *, note: str = ""):
|
||||||
"""Puts a user under watch, staff only."""
|
"""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.")
|
await ctx.send(f"{ctx.author.mention}: user is now on watch.")
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
|
@ -23,7 +22,7 @@ class ModWatch(Cog):
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def watchid(self, ctx, target: int, *, note: str = ""):
|
async def watchid(self, ctx, target: int, *, note: str = ""):
|
||||||
"""Puts a user under watch by userid, staff only."""
|
"""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.")
|
await ctx.send(f"{target.mention}: user is now on watch.")
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
|
@ -31,7 +30,7 @@ class ModWatch(Cog):
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def unwatch(self, ctx, target: discord.Member, *, note: str = ""):
|
async def unwatch(self, ctx, target: discord.Member, *, note: str = ""):
|
||||||
"""Removes a user from watch, staff only."""
|
"""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.")
|
await ctx.send(f"{ctx.author.mention}: user is now not on watch.")
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
|
@ -39,9 +38,9 @@ class ModWatch(Cog):
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def unwatchid(self, ctx, target: int, *, note: str = ""):
|
async def unwatchid(self, ctx, target: int, *, note: str = ""):
|
||||||
"""Removes a user from watch by userid, staff only."""
|
"""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.")
|
await ctx.send(f"{target.mention}: user is now not on watch.")
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
def setup(bot):
|
||||||
await bot.add_cog(ModWatch(bot))
|
bot.add_cog(ModWatch(bot))
|
|
@ -1,13 +1,12 @@
|
||||||
import aiohttp
|
import config
|
||||||
import gidgethub.aiohttp
|
|
||||||
from discord import Embed
|
|
||||||
from discord.enums import MessageType
|
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from discord.ext.commands import Cog
|
from discord.ext.commands import Cog
|
||||||
|
from discord.enums import MessageType
|
||||||
from robocop_ng.helpers.checks import check_if_collaborator
|
from discord import Embed
|
||||||
from robocop_ng.helpers.checks import check_if_pin_channel
|
import aiohttp
|
||||||
|
import gidgethub.aiohttp
|
||||||
|
from helpers.checks import check_if_collaborator
|
||||||
|
from helpers.checks import check_if_pin_channel
|
||||||
|
|
||||||
class Pin(Cog):
|
class Pin(Cog):
|
||||||
"""
|
"""
|
||||||
|
@ -18,11 +17,9 @@ class Pin(Cog):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
def is_pinboard(self, msg):
|
def is_pinboard(self, msg):
|
||||||
return (
|
return msg.author == self.bot.user and \
|
||||||
msg.author == self.bot.user
|
len(msg.embeds) > 0 and \
|
||||||
and len(msg.embeds) > 0
|
msg.embeds[0].title == "Pinboard"
|
||||||
and msg.embeds[0].title == "Pinboard"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_pinboard(self, gh, channel):
|
async def get_pinboard(self, gh, channel):
|
||||||
# Find pinboard pin
|
# Find pinboard pin
|
||||||
|
@ -35,44 +32,43 @@ class Pin(Cog):
|
||||||
return (id, data["files"]["pinboard.md"]["content"])
|
return (id, data["files"]["pinboard.md"]["content"])
|
||||||
|
|
||||||
# Create pinboard pin if it does not exist
|
# Create pinboard pin if it does not exist
|
||||||
data = await gh.post(
|
data = await gh.post("/gists", data={
|
||||||
"/gists",
|
|
||||||
data={
|
|
||||||
"files": {
|
"files": {
|
||||||
"pinboard.md": {"content": "Old pins are available here:\n\n"}
|
"pinboard.md": {
|
||||||
|
"content": "Old pins are available here:\n\n"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"description": f"Pinboard for SwitchRoot #{channel.name}",
|
"description": f"Pinboard for SwitchRoot #{channel.name}",
|
||||||
"public": True,
|
"public": True
|
||||||
},
|
})
|
||||||
)
|
|
||||||
|
|
||||||
msg = await channel.send(
|
msg = await channel.send(embed=Embed(
|
||||||
embed=Embed(
|
|
||||||
title="Pinboard",
|
title="Pinboard",
|
||||||
description="Old pins are moved to the pinboard to make space for \
|
description="Old pins are moved to the pinboard to make space for \
|
||||||
new ones. Check it out!",
|
new ones. Check it out!",
|
||||||
url=data["html_url"],
|
url=data["html_url"]))
|
||||||
)
|
|
||||||
)
|
|
||||||
await msg.pin()
|
await msg.pin()
|
||||||
|
|
||||||
return (data["id"], data["files"]["pinboard.md"]["content"])
|
return (data["id"], data["files"]["pinboard.md"]["content"])
|
||||||
|
|
||||||
async def add_pin_to_pinboard(self, channel, data):
|
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
|
# Don't add to gist pinboard if we don't have an oauth token
|
||||||
return
|
return
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
gh = gidgethub.aiohttp.GitHubAPI(
|
gh = gidgethub.aiohttp.GitHubAPI(session, "RoboCop-NG",
|
||||||
session, "RoboCop-NG", oauth_token=self.bot.config.github_oauth_token
|
oauth_token=config.github_oauth_token)
|
||||||
)
|
|
||||||
(id, content) = await self.get_pinboard(gh, channel)
|
(id, content) = await self.get_pinboard(gh, channel)
|
||||||
content += "- " + data + "\n"
|
content += "- " + data + "\n"
|
||||||
|
|
||||||
await gh.patch(
|
await gh.patch(f"/gists/{id}", data={
|
||||||
f"/gists/{id}", data={"files": {"pinboard.md": {"content": content}}}
|
"files": {
|
||||||
)
|
"pinboard.md": {
|
||||||
|
"content": content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
|
@ -102,7 +98,7 @@ class Pin(Cog):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check that reaction pinning is allowd in this channel
|
# 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
|
return
|
||||||
|
|
||||||
target_guild = self.bot.get_guild(payload.guild_id)
|
target_guild = self.bot.get_guild(payload.guild_id)
|
||||||
|
@ -111,7 +107,7 @@ class Pin(Cog):
|
||||||
|
|
||||||
# Check that the user is allowed to reaction-pin
|
# Check that the user is allowed to reaction-pin
|
||||||
target_user = target_guild.get_member(payload.user_id)
|
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]:
|
if role in [role.id for role in target_user.roles]:
|
||||||
target_chan = self.bot.get_channel(payload.channel_id)
|
target_chan = self.bot.get_channel(payload.channel_id)
|
||||||
target_msg = await target_chan.get_message(payload.message_id)
|
target_msg = await target_chan.get_message(payload.message_id)
|
||||||
|
@ -140,7 +136,7 @@ class Pin(Cog):
|
||||||
break
|
break
|
||||||
|
|
||||||
# Wait for the automated "Pinned" message so we can delete it
|
# Wait for the automated "Pinned" message so we can delete it
|
||||||
waitable = self.bot.wait_for("message", check=check)
|
waitable = self.bot.wait_for('message', check=check)
|
||||||
|
|
||||||
# Pin the message
|
# Pin the message
|
||||||
await target_msg.pin()
|
await target_msg.pin()
|
||||||
|
@ -157,5 +153,5 @@ def check(msg):
|
||||||
return msg.type is MessageType.pins_add
|
return msg.type is MessageType.pins_add
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
def setup(bot):
|
||||||
await bot.add_cog(Pin(bot))
|
bot.add_cog(Pin(bot))
|
|
@ -1,12 +1,10 @@
|
||||||
|
import discord
|
||||||
import asyncio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import discord
|
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from discord.ext.commands import Cog
|
from discord.ext.commands import Cog
|
||||||
|
from helpers.robocronp import add_job, get_crontab
|
||||||
from robocop_ng.helpers.robocronp import add_job, get_crontab
|
|
||||||
|
|
||||||
|
|
||||||
class Remind(Cog):
|
class Remind(Cog):
|
||||||
|
@ -17,72 +15,55 @@ class Remind(Cog):
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def remindlist(self, ctx):
|
async def remindlist(self, ctx):
|
||||||
"""Lists your reminders."""
|
"""Lists your reminders."""
|
||||||
ctab = get_crontab(self.bot)
|
ctab = get_crontab()
|
||||||
uid = str(ctx.author.id)
|
uid = str(ctx.author.id)
|
||||||
embed = discord.Embed(title=f"Active robocronp jobs")
|
embed = discord.Embed(title=f"Active robocronp jobs")
|
||||||
for jobtimestamp in ctab["remind"]:
|
for jobtimestamp in ctab["remind"]:
|
||||||
if uid not in ctab["remind"][jobtimestamp]:
|
if uid not in ctab["remind"][jobtimestamp]:
|
||||||
continue
|
continue
|
||||||
job_details = ctab["remind"][jobtimestamp][uid]
|
job_details = ctab["remind"][jobtimestamp][uid]
|
||||||
expiry_timestr = datetime.utcfromtimestamp(int(jobtimestamp)).strftime(
|
expiry_timestr = datetime.utcfromtimestamp(int(jobtimestamp))\
|
||||||
"%Y-%m-%d %H:%M:%S (UTC)"
|
.strftime('%Y-%m-%d %H:%M:%S (UTC)')
|
||||||
)
|
embed.add_field(name=f"Reminder for {expiry_timestr}",
|
||||||
embed.add_field(
|
|
||||||
name=f"Reminder for {expiry_timestr}",
|
|
||||||
value=f"Added on: {job_details['added']}, "
|
value=f"Added on: {job_details['added']}, "
|
||||||
f"Text: {job_details['text']}",
|
f"Text: {job_details['text']}",
|
||||||
inline=False,
|
inline=False)
|
||||||
)
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@commands.cooldown(1, 60, type=commands.BucketType.user)
|
@commands.cooldown(1, 60, type=commands.BucketType.user)
|
||||||
@commands.command(aliases=["remindme"])
|
@commands.command(aliases=["remindme"])
|
||||||
async def remind(self, ctx, when: str, *, text: str = "something"):
|
async def remind(self, ctx, when: str, *, text: str = "something"):
|
||||||
"""Reminds you about 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:
|
if ctx.guild:
|
||||||
await ctx.message.delete()
|
await ctx.message.delete()
|
||||||
|
|
||||||
current_timestamp = time.time()
|
current_timestamp = time.time()
|
||||||
expiry_timestamp = self.bot.parse_time(when)
|
expiry_timestamp = self.bot.parse_time(when)
|
||||||
|
|
||||||
if current_timestamp + 5 > expiry_timestamp:
|
if current_timestamp + 5 > expiry_timestamp:
|
||||||
msg = await ctx.send(
|
msg = await ctx.send(f"{ctx.author.mention}: Minimum "
|
||||||
f"{ctx.author.mention}: Minimum remind interval is 5 seconds."
|
"remind interval is 5 seconds.")
|
||||||
)
|
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
await msg.delete()
|
await msg.delete()
|
||||||
return
|
return
|
||||||
|
|
||||||
expiry_datetime = datetime.utcfromtimestamp(expiry_timestamp)
|
expiry_datetime = datetime.utcfromtimestamp(expiry_timestamp)
|
||||||
duration_text = self.bot.get_relative_timestamp(
|
duration_text = self.bot.get_relative_timestamp(time_to=expiry_datetime,
|
||||||
time_to=expiry_datetime, include_to=True, humanized=True
|
include_to=True,
|
||||||
)
|
humanized=True)
|
||||||
|
|
||||||
safe_text = await commands.clean_content().convert(ctx, str(text))
|
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)")
|
added_on = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S (UTC)")
|
||||||
|
|
||||||
add_job(
|
add_job("remind",
|
||||||
self.bot,
|
|
||||||
"remind",
|
|
||||||
ctx.author.id,
|
ctx.author.id,
|
||||||
{"text": safe_text, "added": added_on},
|
{"text": safe_text, "added": added_on},
|
||||||
expiry_timestamp,
|
expiry_timestamp)
|
||||||
)
|
|
||||||
|
|
||||||
await ctx.send(
|
msg = await ctx.send(f"{ctx.author.mention}: I'll remind you in "
|
||||||
f"{ctx.author.mention}: I'll remind you in "
|
f"DMs about `{safe_text}` in {duration_text}.")
|
||||||
f"DMs about `{safe_text}` in {duration_text}."
|
await asyncio.sleep(5)
|
||||||
)
|
await msg.delete()
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
def setup(bot):
|
||||||
await bot.add_cog(Remind(bot))
|
bot.add_cog(Remind(bot))
|
153
cogs/robocronp.py
Normal file
153
cogs/robocronp.py
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
import asyncio
|
||||||
|
import config
|
||||||
|
import time
|
||||||
|
import discord
|
||||||
|
import traceback
|
||||||
|
from discord.ext import commands
|
||||||
|
from discord.ext.commands import Cog
|
||||||
|
from helpers.robocronp import get_crontab, delete_job
|
||||||
|
from helpers.restrictions import remove_restriction
|
||||||
|
from helpers.checks import check_if_staff
|
||||||
|
|
||||||
|
|
||||||
|
class Robocronp(Cog):
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
bot.loop.create_task(self.minutely())
|
||||||
|
bot.loop.create_task(self.hourly())
|
||||||
|
|
||||||
|
async def send_data(self):
|
||||||
|
data_files = [discord.File(fpath) for fpath in self.bot.wanted_jsons]
|
||||||
|
log_channel = self.bot.get_channel(config.botlog_channel)
|
||||||
|
await log_channel.send("Hourly data backups:", files=data_files)
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.check(check_if_staff)
|
||||||
|
@commands.command()
|
||||||
|
async def listjobs(self, ctx):
|
||||||
|
"""Lists timed robocronp jobs, staff only."""
|
||||||
|
ctab = get_crontab()
|
||||||
|
embed = discord.Embed(title=f"Active robocronp jobs")
|
||||||
|
for jobtype in ctab:
|
||||||
|
for jobtimestamp in ctab[jobtype]:
|
||||||
|
for job_name in ctab[jobtype][jobtimestamp]:
|
||||||
|
job_details = repr(ctab[jobtype][jobtimestamp][job_name])
|
||||||
|
embed.add_field(name=f"{jobtype} for {job_name}",
|
||||||
|
value=f"Timestamp: {jobtimestamp}, "
|
||||||
|
f"Details: {job_details}",
|
||||||
|
inline=False)
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.check(check_if_staff)
|
||||||
|
@commands.command(aliases=["removejob"])
|
||||||
|
async def deletejob(self, ctx, timestamp: str,
|
||||||
|
job_type: str, job_name: str):
|
||||||
|
"""Removes a timed robocronp job, staff only.
|
||||||
|
|
||||||
|
You'll need to supply:
|
||||||
|
- timestamp (like 1545981602)
|
||||||
|
- job type (like "unban")
|
||||||
|
- job name (userid, like 420332322307571713)
|
||||||
|
|
||||||
|
You can get all 3 from listjobs command."""
|
||||||
|
delete_job(timestamp, job_type, job_name)
|
||||||
|
await ctx.send(f"{ctx.author.mention}: Deleted!")
|
||||||
|
|
||||||
|
async def do_jobs(self, ctab, jobtype, timestamp):
|
||||||
|
log_channel = self.bot.get_channel(config.botlog_channel)
|
||||||
|
for job_name in ctab[jobtype][timestamp]:
|
||||||
|
try:
|
||||||
|
job_details = ctab[jobtype][timestamp][job_name]
|
||||||
|
if jobtype == "unban":
|
||||||
|
target_user = await self.bot.get_user_info(job_name)
|
||||||
|
target_guild = self.bot.get_guild(job_details["guild"])
|
||||||
|
delete_job(timestamp, jobtype, job_name)
|
||||||
|
await target_guild.unban(target_user,
|
||||||
|
reason="Robocronp: Timed "
|
||||||
|
"ban expired.")
|
||||||
|
elif jobtype == "unmute":
|
||||||
|
remove_restriction(job_name, config.mute_role)
|
||||||
|
target_guild = self.bot.get_guild(job_details["guild"])
|
||||||
|
target_member = target_guild.get_member(int(job_name))
|
||||||
|
target_role = target_guild.get_role(config.mute_role)
|
||||||
|
await target_member.remove_roles(target_role,
|
||||||
|
reason="Robocronp: Timed "
|
||||||
|
"mute expired.")
|
||||||
|
delete_job(timestamp, jobtype, job_name)
|
||||||
|
elif jobtype == "remind":
|
||||||
|
text = job_details["text"]
|
||||||
|
added_on = job_details["added"]
|
||||||
|
target = await self.bot.get_user_info(int(job_name))
|
||||||
|
if target:
|
||||||
|
await target.send("You asked to be reminded about"
|
||||||
|
f" `{text}` on {added_on}.")
|
||||||
|
delete_job(timestamp, jobtype, job_name)
|
||||||
|
except:
|
||||||
|
# Don't kill cronjobs if something goes wrong.
|
||||||
|
delete_job(timestamp, jobtype, job_name)
|
||||||
|
await log_channel.send("Crondo has errored, job deleted: ```"
|
||||||
|
f"{traceback.format_exc()}```")
|
||||||
|
|
||||||
|
async def clean_channel(self, channel_id):
|
||||||
|
log_channel = self.bot.get_channel(config.botlog_channel)
|
||||||
|
channel = self.bot.get_channel(channel_id)
|
||||||
|
try:
|
||||||
|
done_cleaning = False
|
||||||
|
count = 0
|
||||||
|
while not done_cleaning:
|
||||||
|
purge_res = await channel.purge(limit=100)
|
||||||
|
count += len(purge_res)
|
||||||
|
if len(purge_res) != 100:
|
||||||
|
done_cleaning = True
|
||||||
|
await log_channel.send(f"Wiped {count} messages from "
|
||||||
|
f"<#{channel.id}> automatically.")
|
||||||
|
except:
|
||||||
|
# Don't kill cronjobs if something goes wrong.
|
||||||
|
await log_channel.send("Cronclean has errored: ```"
|
||||||
|
f"{traceback.format_exc()}```")
|
||||||
|
|
||||||
|
async def minutely(self):
|
||||||
|
await self.bot.wait_until_ready()
|
||||||
|
log_channel = self.bot.get_channel(config.botlog_channel)
|
||||||
|
while not self.bot.is_closed():
|
||||||
|
try:
|
||||||
|
ctab = get_crontab()
|
||||||
|
timestamp = time.time()
|
||||||
|
for jobtype in ctab:
|
||||||
|
for jobtimestamp in ctab[jobtype]:
|
||||||
|
if timestamp > int(jobtimestamp):
|
||||||
|
await self.do_jobs(ctab, jobtype, jobtimestamp)
|
||||||
|
|
||||||
|
# Handle clean channels
|
||||||
|
for clean_channel in config.minutely_clean_channels:
|
||||||
|
await self.clean_channel(clean_channel)
|
||||||
|
except:
|
||||||
|
# Don't kill cronjobs if something goes wrong.
|
||||||
|
await log_channel.send("Cron-minutely has errored: ```"
|
||||||
|
f"{traceback.format_exc()}```")
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
|
async def hourly(self):
|
||||||
|
await self.bot.wait_until_ready()
|
||||||
|
log_channel = self.bot.get_channel(config.botlog_channel)
|
||||||
|
while not self.bot.is_closed():
|
||||||
|
# Your stuff that should run at boot
|
||||||
|
# and after that every hour goes here
|
||||||
|
await asyncio.sleep(3600)
|
||||||
|
try:
|
||||||
|
await self.send_data()
|
||||||
|
|
||||||
|
# Handle clean channels
|
||||||
|
for clean_channel in config.hourly_clean_channels:
|
||||||
|
await self.clean_channel(clean_channel)
|
||||||
|
except:
|
||||||
|
# Don't kill cronjobs if something goes wrong.
|
||||||
|
await log_channel.send("Cron-hourly has errored: ```"
|
||||||
|
f"{traceback.format_exc()}```")
|
||||||
|
# Your stuff that should run an hour after boot
|
||||||
|
# and after that every hour goes here
|
||||||
|
|
||||||
|
|
||||||
|
def setup(bot):
|
||||||
|
bot.add_cog(Robocronp(bot))
|
294
cogs/verification.py
Normal file
294
cogs/verification.py
Normal file
|
@ -0,0 +1,294 @@
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
from discord.ext.commands import Cog
|
||||||
|
import asyncio
|
||||||
|
import config
|
||||||
|
import random
|
||||||
|
from inspect import cleandoc
|
||||||
|
import hashlib
|
||||||
|
from helpers.checks import check_if_staff
|
||||||
|
|
||||||
|
|
||||||
|
welcome_header = """
|
||||||
|
<:ReSwitched:326421448543567872> __**Welcome to ReSwitched!**__
|
||||||
|
|
||||||
|
__**Be sure you read the following rules and information before participating. If you came here to ask about "backups", this is NOT the place.**__
|
||||||
|
|
||||||
|
__**Got questions about Nintendo Switch hacking? Before asking in the server, please see our FAQ at <https://reswitched.team/faq/> to see if your question has already been answered.**__
|
||||||
|
|
||||||
|
:bookmark_tabs:__Rules:__
|
||||||
|
"""
|
||||||
|
|
||||||
|
welcome_rules = (
|
||||||
|
# 1
|
||||||
|
"""
|
||||||
|
Read all the rules before participating in chat. Not reading the rules is *not* an excuse for breaking them.
|
||||||
|
• It's suggested that you read channel topics and pins before asking questions as well, as some questions may have already been answered in those.
|
||||||
|
""",
|
||||||
|
|
||||||
|
# 2
|
||||||
|
"""
|
||||||
|
Be nice to each other. It's fine to disagree, it's not fine to insult or attack other people.
|
||||||
|
• You may disagree with anyone or anything you like, but you should try to keep it to opinions, and not people. Avoid vitriol.
|
||||||
|
• Constant antagonistic behavior is considered uncivil and appropriate action will be taken.
|
||||||
|
• The use of derogatory slurs -- sexist, racist, homophobic, transphobic, or otherwise -- is unacceptable and may be grounds for an immediate ban.
|
||||||
|
""",
|
||||||
|
|
||||||
|
# 3
|
||||||
|
'If you have concerns about another user, please take up your concerns with a staff member (someone with the "mod" role in the sidebar) in private. Don\'t publicly call other users out.',
|
||||||
|
|
||||||
|
# 4
|
||||||
|
"""
|
||||||
|
From time to time, we may mention everyone in the server. We do this when we feel something important is going on that requires attention. Complaining about these pings may result in a ban.
|
||||||
|
• To disable notifications for these pings, suppress them in "ReSwitched → Notification Settings".
|
||||||
|
""",
|
||||||
|
|
||||||
|
# 5
|
||||||
|
"""
|
||||||
|
Don't spam.
|
||||||
|
• For excessively long text, use a service like <https://0bin.net/>.
|
||||||
|
""",
|
||||||
|
|
||||||
|
# 6
|
||||||
|
"Don't brigade, raid, or otherwise attack other people or communities. Don't discuss participation in these attacks. This may warrant an immediate permanent ban.",
|
||||||
|
|
||||||
|
# 7
|
||||||
|
'Off-topic content goes to #off-topic. Keep low-quality content like memes out.',
|
||||||
|
|
||||||
|
# 8
|
||||||
|
'Trying to evade, look for loopholes, or stay borderline within the rules will be treated as breaking them.',
|
||||||
|
|
||||||
|
# 9
|
||||||
|
"""
|
||||||
|
Absolutely no piracy or related discussion. This includes:
|
||||||
|
• "Backups", even if you legally own a copy of the game.
|
||||||
|
• "Installable" NSPs, XCIs, and NCAs; this **includes** installable homebrew (i.e. on the Home Menu instead of within nx-hbmenu).
|
||||||
|
• Signature and ES patches, also known as "sigpatches"
|
||||||
|
• Usage of piracy-focused groups' (Team Xecuter, etc.) hardware and software, such as SX OS.
|
||||||
|
This is a zero-tolerance, non-negotiable policy that is enforced strictly and swiftly, up to and including instant bans without warning.
|
||||||
|
""",
|
||||||
|
|
||||||
|
# 10
|
||||||
|
'The first character of your server nickname should be alphanumeric if you wish to talk in chat.'
|
||||||
|
)
|
||||||
|
|
||||||
|
welcome_footer = (
|
||||||
|
"""
|
||||||
|
:hash: __Channel Breakdown:__
|
||||||
|
#news - Used exclusively for updates on ReSwitched progress and community information. Most major announcements are passed through this channel and whenever something is posted there it's usually something you'll want to look at.
|
||||||
|
|
||||||
|
#switch-hacking-meta - For "meta-discussion" related to hacking the switch. This is where we talk *about* the switch hacking that's going on, and where you can get clarification about the hacks that exist and the work that's being done.
|
||||||
|
|
||||||
|
#user-support - End-user focused support, mainly between users. Ask your questions about using switch homebrew here.
|
||||||
|
|
||||||
|
#tool-support - Developer focused support. Ask your questions about using PegaSwitch, libtransistor, Mephisto, and other tools here.
|
||||||
|
|
||||||
|
#hack-n-all - General hacking, hardware and software development channel for hacking on things *other* than the switch. This is a great place to ask about hacking other systems-- and for the community to have technical discussions.
|
||||||
|
""",
|
||||||
|
|
||||||
|
"""
|
||||||
|
#switch-hacking-general - Channel for everyone working on hacking the switch-- both in an exploit and a low-level hardware sense. This is where a lot of our in-the-open development goes on. Note that this isn't the place for developing homebrew-- we have #homebrew-development for that!
|
||||||
|
|
||||||
|
#homebrew-development - Discussion about the development of homebrew goes there. Feel free to show off your latest creation here.
|
||||||
|
|
||||||
|
#off-topic - Channel for discussion of anything that doesn't belong in #general. Anything goes, so long as you make sure to follow the rules and be on your best behavior.
|
||||||
|
|
||||||
|
#toolchain-development - Discussion about the development of libtransistor itself goes there.
|
||||||
|
|
||||||
|
#cfw-development - Development discussion regarding custom firmware (CFW) projects, such as Atmosphère. This channel is meant for the discussion accompanying active development.
|
||||||
|
|
||||||
|
#bot-cmds - Channel for excessive/random use of Robocop's various commands.
|
||||||
|
|
||||||
|
**If you are still not sure how to get access to the other channels, please read the rules again.**
|
||||||
|
**If you have questions about the rules, feel free to ask here!**
|
||||||
|
|
||||||
|
**Note: This channel is completely automated (aside from responding to questions about the rules). If your message didn't give you access to the other channels, you failed the test. Feel free to try again.**
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
hidden_term_line = ' • When you have finished reading all of the rules, send a message in this channel that includes the {0} hash of your discord "name#discriminator" (for example, {0}(User#1234)), and we\'ll grant you access to the other channels. You can find your "name#discriminator" (your username followed by a ‘#’ and four numbers) under the discord channel list.'
|
||||||
|
|
||||||
|
class Verification(Cog):
|
||||||
|
def __init__(self, bot, hash_choice):
|
||||||
|
self.bot = bot
|
||||||
|
self.hash_choice = random.choice(tuple(hashlib.algorithms_guaranteed))
|
||||||
|
|
||||||
|
@commands.check(check_if_staff)
|
||||||
|
@commands.command()
|
||||||
|
async def reset(self, ctx, limit: int = 100, force: bool = False):
|
||||||
|
"""Wipes messages and pastes the welcome message again. Staff only."""
|
||||||
|
if ctx.message.channel.id != config.welcome_channel and not force:
|
||||||
|
await ctx.send(f"This command is limited to"
|
||||||
|
f" <#{config.welcome_channel}>, unless forced.")
|
||||||
|
return
|
||||||
|
|
||||||
|
await ctx.channel.purge(limit=limit)
|
||||||
|
|
||||||
|
await ctx.send(welcome_header)
|
||||||
|
rules = ['**{}**. {}'.format(i, cleandoc(r)) for i, r in
|
||||||
|
enumerate(welcome_rules, 1)]
|
||||||
|
rule_choice = random.randint(2, len(rules))
|
||||||
|
rules[rule_choice - 1] += '\n' + hidden_term_line.format(self.hash_choice.upper())
|
||||||
|
msg = f"🗑 **Reset**: {ctx.author.mention} cleared {limit} messages "\
|
||||||
|
f" in {ctx.channel.mention}"
|
||||||
|
msg += f"\n💬 __Current challenge location__: under rule {rule_choice}"
|
||||||
|
log_channel = self.bot.get_channel(config.log_channel)
|
||||||
|
await log_channel.send(msg)
|
||||||
|
|
||||||
|
# find rule that puts us over 2,000 characters, if any
|
||||||
|
total = 0
|
||||||
|
messages = []
|
||||||
|
current_message = ""
|
||||||
|
for item in rules:
|
||||||
|
total += len(item) + 2 # \n\n
|
||||||
|
if total < 2000:
|
||||||
|
current_message += item + "\n\n"
|
||||||
|
else:
|
||||||
|
# we've hit the limit; split!
|
||||||
|
messages += [current_message]
|
||||||
|
current_message = "\n\u200B\n" + item + "\n\u200B\n"
|
||||||
|
total = 0
|
||||||
|
messages += [current_message]
|
||||||
|
|
||||||
|
for item in messages:
|
||||||
|
await ctx.send(item)
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
for x in welcome_footer:
|
||||||
|
await ctx.send(cleandoc(x))
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
async def process_message(self, message):
|
||||||
|
"""Big code that makes me want to shoot myself
|
||||||
|
Not really a rewrite but more of a port
|
||||||
|
|
||||||
|
Git blame tells me that I should blame/credit Robin Lambertz"""
|
||||||
|
if message.channel.id == config.welcome_channel:
|
||||||
|
# Assign common stuff into variables to make stuff less of a mess
|
||||||
|
member = message.author
|
||||||
|
full_name = str(member)
|
||||||
|
discrim = str(member.discriminator)
|
||||||
|
guild = message.guild
|
||||||
|
chan = message.channel
|
||||||
|
mcl = message.content.lower()
|
||||||
|
|
||||||
|
# Reply to users that insult the bot
|
||||||
|
oof = ["bad", "broken", "buggy", "bugged",
|
||||||
|
"stupid", "dumb", "silly", "fuck", "heck", "h*ck"]
|
||||||
|
if "bot" in mcl and any(insult in mcl for insult in oof):
|
||||||
|
snark = random.choice(["bad human",
|
||||||
|
"no u",
|
||||||
|
"no u, rtfm",
|
||||||
|
"pebkac"])
|
||||||
|
return await chan.send(snark)
|
||||||
|
|
||||||
|
# Get the role we will give in case of success
|
||||||
|
success_role = guild.get_role(config.participant_role)
|
||||||
|
|
||||||
|
# Get a list of stuff we'll allow and will consider close
|
||||||
|
allowed_names = [f"@{full_name}", full_name, str(member.id)]
|
||||||
|
close_names = [f"@{member.name}", member.name, discrim,
|
||||||
|
f"#{discrim}"]
|
||||||
|
# Now add the same things but with newlines at the end of them
|
||||||
|
allowed_names += [(an + '\n') for an in allowed_names]
|
||||||
|
close_names += [(cn + '\n') for cn in close_names]
|
||||||
|
allowed_names += [(an + '\r\n') for an in allowed_names]
|
||||||
|
close_names += [(cn + '\r\n') for cn in close_names]
|
||||||
|
# [ ͡° ͜ᔦ ͡°] 𝐖𝐞𝐥𝐜𝐨𝐦𝐞 𝐭𝐨 𝐌𝐚𝐜 𝐎𝐒 𝟗.
|
||||||
|
allowed_names += [(an + '\r') for an in allowed_names]
|
||||||
|
close_names += [(cn + '\r') for cn in close_names]
|
||||||
|
|
||||||
|
# Finally, hash the stuff so that we can access them later :)
|
||||||
|
hash_allow = [hashlib.new(self.hash_choice, name.encode('utf-8')).hexdigest()
|
||||||
|
for name in allowed_names]
|
||||||
|
|
||||||
|
# I'm not even going to attempt to break those into lines jfc
|
||||||
|
if any(allow in mcl for allow in hash_allow):
|
||||||
|
await member.add_roles(success_role)
|
||||||
|
await chan.purge(limit=100, check=lambda m: m.author == message.author or (m.author == self.bot.user and message.author.mention in m.content))
|
||||||
|
elif full_name in message.content or str(member.id) in message.content or member.name in message.content or discrim in message.content:
|
||||||
|
no_text = ":no_entry: Incorrect. You need to do something *specific* with your name and discriminator instead of just posting it. Please re-read the rules carefully and look up any terms you are not familiar with."
|
||||||
|
rand_num = random.randint(1, 100)
|
||||||
|
if rand_num == 42:
|
||||||
|
no_text = "you're doing it wrong"
|
||||||
|
elif rand_num == 43:
|
||||||
|
no_text = "ugh, wrong, read the rules."
|
||||||
|
elif rand_num == 44:
|
||||||
|
no_text = "\"The definition of insanity is doing the same thing over and over again, but expecting different results.\"\n-Albert Einstein"
|
||||||
|
await chan.send(f"{message.author.mention} {no_text}")
|
||||||
|
|
||||||
|
@Cog.listener()
|
||||||
|
async def on_message(self, message):
|
||||||
|
if message.author.bot:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.process_message(message)
|
||||||
|
except discord.errors.Forbidden:
|
||||||
|
chan = self.bot.get_channel(message.channel)
|
||||||
|
await chan.send("💢 I don't have permission to do this.")
|
||||||
|
|
||||||
|
@Cog.listener()
|
||||||
|
async def on_message_edit(self, before, after):
|
||||||
|
if after.author.bot:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.process_message(after)
|
||||||
|
except discord.errors.Forbidden:
|
||||||
|
chan = self.bot.get_channel(after.channel)
|
||||||
|
await chan.send("💢 I don't have permission to do this.")
|
||||||
|
|
||||||
|
# @commands.guild_only()
|
||||||
|
# @commands.command()
|
||||||
|
# async def verify(self, ctx, *, verification_string: str):
|
||||||
|
# """Does verification.
|
||||||
|
|
||||||
|
# See text on top of #verification for more info."""
|
||||||
|
|
||||||
|
# await ctx.message.delete()
|
||||||
|
|
||||||
|
# veriflogs_channel = ctx.guild.get_channel(config.veriflogs_chanid)
|
||||||
|
# verification_role = ctx.guild.get_role(config.read_rules_roleid)
|
||||||
|
# verification_wanted = config.verification_code\
|
||||||
|
# .replace("[discrim]", ctx.author.discriminator)
|
||||||
|
|
||||||
|
# # Do checks on if the user can even attempt to verify
|
||||||
|
# if ctx.channel.id != config.verification_chanid:
|
||||||
|
# resp = await ctx.send("This command can only be used "
|
||||||
|
# f"on <#{config.verification_chanid}>.")
|
||||||
|
# await asyncio.sleep(config.sleep_secs)
|
||||||
|
# return await resp.delete()
|
||||||
|
|
||||||
|
# if verification_role in ctx.author.roles:
|
||||||
|
# resp = await ctx.send("This command can only by those without "
|
||||||
|
# f"<@&{config.read_rules_roleid}> role.")
|
||||||
|
# await asyncio.sleep(config.sleep_secs)
|
||||||
|
# return await resp.delete()
|
||||||
|
|
||||||
|
# # Log verification attempt
|
||||||
|
# await self.bot.update_logs("Verification Attempt",
|
||||||
|
# ctx.author.id,
|
||||||
|
# veriflogs_channel,
|
||||||
|
# log_text=verification_string,
|
||||||
|
# digdepth=50, result=-1)
|
||||||
|
|
||||||
|
# # Check verification code
|
||||||
|
# if verification_string.lower().strip() == verification_wanted:
|
||||||
|
# resp = await ctx.send("Success! Welcome to the "
|
||||||
|
# f"club, {str(ctx.author)}.")
|
||||||
|
# await self.bot.update_logs("Verification Attempt",
|
||||||
|
# ctx.author.id,
|
||||||
|
# veriflogs_channel,
|
||||||
|
# digdepth=50, result=0)
|
||||||
|
# await asyncio.sleep(config.sleep_secs)
|
||||||
|
# await ctx.author.add_roles(verification_role)
|
||||||
|
# await resp.delete()
|
||||||
|
# else:
|
||||||
|
# resp = await ctx.send(f"Incorrect password, {str(ctx.author)}.")
|
||||||
|
# await asyncio.sleep(config.sleep_secs)
|
||||||
|
# await resp.delete()
|
||||||
|
|
||||||
|
|
||||||
|
def setup(bot):
|
||||||
|
bot.add_cog(Verification(bot))
|
99
config_template.py
Normal file
99
config_template.py
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
# Basic bot config, insert your token here, update description if you want
|
||||||
|
prefixes = [".", "!"]
|
||||||
|
token = "token-goes-here"
|
||||||
|
bot_description = "Robocop-NG, the moderation bot of ReSwitched."
|
||||||
|
|
||||||
|
# If you forked robocop-ng, put your repo here
|
||||||
|
source_url = "https://github.com/reswitched/robocop-ng"
|
||||||
|
rules_url = "https://reswitched.team/discord/#rules"
|
||||||
|
|
||||||
|
# The bot description to be used in .robocop embed
|
||||||
|
embed_desc = "Robocop-NG is developed by [Ave](https://github.com/aveao)"\
|
||||||
|
" and [tomGER](https://github.com/tumGER), and is a rewrite "\
|
||||||
|
"of Robocop.\nRobocop is based on Kurisu by 916253 and ihaveamac."
|
||||||
|
|
||||||
|
|
||||||
|
# Minimum account age required to join the guild
|
||||||
|
# If user's account creation is shorter than the time delta given here
|
||||||
|
# then user will be kicked and informed
|
||||||
|
min_age = datetime.timedelta(minutes=15)
|
||||||
|
|
||||||
|
# The bot will only work in these guilds
|
||||||
|
guild_whitelist = [
|
||||||
|
269333940928512010 # ReSwitched discord
|
||||||
|
]
|
||||||
|
|
||||||
|
# Named roles to be used with .approve and .revoke
|
||||||
|
# Example: .approve User hacker
|
||||||
|
named_roles = {
|
||||||
|
"community": 420010997877833731,
|
||||||
|
"hacker": 364508795038072833,
|
||||||
|
"participant": 434353085926866946
|
||||||
|
}
|
||||||
|
|
||||||
|
# The bot manager and staff roles
|
||||||
|
# Bot manager can run eval, exit and other destructive commands
|
||||||
|
# Staff can run administrative commands
|
||||||
|
bot_manager_role_id = 466447265863696394 # Bot management role in ReSwitched
|
||||||
|
staff_role_ids = [364647829248933888, # Team role in ReSwitched
|
||||||
|
360138431524765707, # Mod role in ReSwitched
|
||||||
|
466447265863696394, # Bot management role in ReSwitched
|
||||||
|
360138163156549632, # Admin role in ReSwitched
|
||||||
|
287289529986187266] # Wizard role in ReSwitched
|
||||||
|
|
||||||
|
# Various log channels used to log bot and guild's activity
|
||||||
|
# You can use same channel for multiple log types
|
||||||
|
# Spylog channel logs suspicious messages or messages by members under watch
|
||||||
|
# Invites created with .invite will direct to the welcome channel.
|
||||||
|
log_channel = 290958160414375946 # server-logs in ReSwitched
|
||||||
|
botlog_channel = 529070282409771048 # bot-logs channel in ReSwitched
|
||||||
|
modlog_channel = 542114169244221452 # mod-logs channel in ReSwitched
|
||||||
|
spylog_channel = 548304839294189579 # spy channel in ReSwitched
|
||||||
|
welcome_channel = 326416669058662401 # newcomers channel in ReSwitched
|
||||||
|
|
||||||
|
# These channel entries are used to determine which roles will be given
|
||||||
|
# access when we unmute on them
|
||||||
|
general_channels = [420029476634886144,
|
||||||
|
414949821003202562,
|
||||||
|
383368936466546698,
|
||||||
|
343244421044633602,
|
||||||
|
491316901692178432,
|
||||||
|
539212260350885908] # Channels everyone can access
|
||||||
|
community_channels = [269333940928512010,
|
||||||
|
438839875970662400,
|
||||||
|
404722395845361668,
|
||||||
|
435687501068501002,
|
||||||
|
286612533757083648] # Channels requiring community role
|
||||||
|
|
||||||
|
# Controls which roles are blocked during lockdown
|
||||||
|
lockdown_configs = {
|
||||||
|
# Used as a default value for channels without a config
|
||||||
|
"default": {
|
||||||
|
"channels": general_channels,
|
||||||
|
"roles": [named_roles["participant"]]
|
||||||
|
},
|
||||||
|
"community": {
|
||||||
|
"channels": community_channels,
|
||||||
|
"roles": [named_roles["community"], named_roles["hacker"]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mute role is applied to users when they're muted
|
||||||
|
# As we no longer have mute role on ReSwitched, I set it to 0 here
|
||||||
|
mute_role = 0 # Mute role in ReSwitched
|
||||||
|
|
||||||
|
# Channels that will be cleaned every minute/hour
|
||||||
|
minutely_clean_channels = []
|
||||||
|
hourly_clean_channels = []
|
||||||
|
|
||||||
|
# Edited and deletes messages in these channels will be logged
|
||||||
|
spy_channels = general_channels
|
||||||
|
|
||||||
|
# Channels and roles where users can pin messages
|
||||||
|
allowed_pin_channels = []
|
||||||
|
allowed_pin_roles = []
|
||||||
|
|
||||||
|
# Used for the pinboard. Leave empty if you don't wish for a gist pinboard.
|
||||||
|
github_oauth_token = ""
|
|
@ -1,4 +0,0 @@
|
||||||
(import (fetchTarball
|
|
||||||
"https://github.com/edolstra/flake-compat/archive/master.tar.gz") {
|
|
||||||
src = builtins.fetchGit ./.;
|
|
||||||
}).defaultNix
|
|
175
flake.lock
175
flake.lock
|
@ -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
133
flake.nix
|
@ -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;
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -16,25 +16,17 @@ def check_if_bot_manager(ctx):
|
||||||
def check_if_staff_or_ot(ctx):
|
def check_if_staff_or_ot(ctx):
|
||||||
if not ctx.guild:
|
if not ctx.guild:
|
||||||
return True
|
return True
|
||||||
is_ot = ctx.channel.name == "off-topic"
|
is_ot = (ctx.channel.name == "off-topic")
|
||||||
is_bot_cmds = ctx.channel.name == "bot-cmds"
|
is_bot_cmds = (ctx.channel.name == "bot-cmds")
|
||||||
is_staff = any(r.id in config.staff_role_ids for r in ctx.author.roles)
|
is_staff = any(r.id in config.staff_role_ids for r in ctx.author.roles)
|
||||||
return is_ot or is_staff or is_bot_cmds
|
return (is_ot or is_staff or is_bot_cmds)
|
||||||
|
|
||||||
|
|
||||||
def check_if_staff_or_dm(ctx):
|
|
||||||
if not ctx.guild:
|
|
||||||
return True
|
|
||||||
return any(r.id in config.staff_role_ids for r in ctx.author.roles)
|
|
||||||
|
|
||||||
|
|
||||||
def check_if_collaborator(ctx):
|
def check_if_collaborator(ctx):
|
||||||
if not ctx.guild:
|
if not ctx.guild:
|
||||||
return False
|
return False
|
||||||
return any(
|
return any(r.id in config.staff_role_ids + config.allowed_pin_roles
|
||||||
r.id in config.staff_role_ids + config.allowed_pin_roles
|
for r in ctx.author.roles)
|
||||||
for r in ctx.author.roles
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def check_if_pin_channel(ctx):
|
def check_if_pin_channel(ctx):
|
File diff suppressed because it is too large
Load diff
42
helpers/restrictions.py
Normal file
42
helpers/restrictions.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def get_restrictions():
|
||||||
|
with open("data/restrictions.json", "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def set_restrictions(contents):
|
||||||
|
with open("data/restrictions.json", "w") as f:
|
||||||
|
f.write(contents)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_restrictions(uid):
|
||||||
|
uid = str(uid)
|
||||||
|
with open("data/restrictions.json", "r") as f:
|
||||||
|
rsts = json.load(f)
|
||||||
|
if uid in rsts:
|
||||||
|
return rsts[uid]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def add_restriction(uid, rst):
|
||||||
|
# mostly from kurisu source, credits go to ihaveamac
|
||||||
|
uid = str(uid)
|
||||||
|
rsts = get_restrictions()
|
||||||
|
if uid not in rsts:
|
||||||
|
rsts[uid] = []
|
||||||
|
if rst not in rsts[uid]:
|
||||||
|
rsts[uid].append(rst)
|
||||||
|
set_restrictions(json.dumps(rsts))
|
||||||
|
|
||||||
|
|
||||||
|
def remove_restriction(uid, rst):
|
||||||
|
# mostly from kurisu source, credits go to ihaveamac
|
||||||
|
uid = str(uid)
|
||||||
|
rsts = get_restrictions()
|
||||||
|
if uid not in rsts:
|
||||||
|
rsts[uid] = []
|
||||||
|
if rst in rsts[uid]:
|
||||||
|
rsts[uid].remove(rst)
|
||||||
|
set_restrictions(json.dumps(rsts))
|
37
helpers/robocronp.py
Normal file
37
helpers/robocronp.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
|
||||||
|
|
||||||
|
def get_crontab():
|
||||||
|
with open("data/robocronptab.json", "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def set_crontab(contents):
|
||||||
|
with open("data/robocronptab.json", "w") as f:
|
||||||
|
f.write(contents)
|
||||||
|
|
||||||
|
|
||||||
|
def add_job(job_type, job_name, job_details, timestamp):
|
||||||
|
timestamp = str(math.floor(timestamp))
|
||||||
|
job_name = str(job_name)
|
||||||
|
ctab = get_crontab()
|
||||||
|
|
||||||
|
if job_type not in ctab:
|
||||||
|
ctab[job_type] = {}
|
||||||
|
|
||||||
|
if timestamp not in ctab[job_type]:
|
||||||
|
ctab[job_type][timestamp] = {}
|
||||||
|
|
||||||
|
ctab[job_type][timestamp][job_name] = job_details
|
||||||
|
set_crontab(json.dumps(ctab))
|
||||||
|
|
||||||
|
|
||||||
|
def delete_job(timestamp, job_type, job_name):
|
||||||
|
timestamp = str(timestamp)
|
||||||
|
job_name = str(job_name)
|
||||||
|
ctab = get_crontab()
|
||||||
|
|
||||||
|
del ctab[job_type][timestamp][job_name]
|
||||||
|
|
||||||
|
set_crontab(json.dumps(ctab))
|
63
helpers/userlogs.py
Normal file
63
helpers/userlogs.py
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
userlog_event_types = {"warns": "Warn",
|
||||||
|
"bans": "Ban",
|
||||||
|
"kicks": "Kick",
|
||||||
|
"mutes": "Mute",
|
||||||
|
"notes": "Note"}
|
||||||
|
|
||||||
|
|
||||||
|
def get_userlog():
|
||||||
|
with open("data/userlog.json", "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def set_userlog(contents):
|
||||||
|
with open("data/userlog.json", "w") as f:
|
||||||
|
f.write(contents)
|
||||||
|
|
||||||
|
|
||||||
|
def userlog(uid, issuer, reason, event_type, uname: str = ""):
|
||||||
|
userlogs = get_userlog()
|
||||||
|
uid = str(uid)
|
||||||
|
if uid not in userlogs:
|
||||||
|
userlogs[uid] = {"warns": [],
|
||||||
|
"mutes": [],
|
||||||
|
"kicks": [],
|
||||||
|
"bans": [],
|
||||||
|
"notes": [],
|
||||||
|
"watch": False,
|
||||||
|
"name": "n/a"}
|
||||||
|
if uname:
|
||||||
|
userlogs[uid]["name"] = uname
|
||||||
|
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||||
|
log_data = {"issuer_id": issuer.id,
|
||||||
|
"issuer_name": f"{issuer}",
|
||||||
|
"reason": reason,
|
||||||
|
"timestamp": timestamp}
|
||||||
|
if event_type not in userlogs[uid]:
|
||||||
|
userlogs[uid][event_type] = []
|
||||||
|
userlogs[uid][event_type].append(log_data)
|
||||||
|
set_userlog(json.dumps(userlogs))
|
||||||
|
return len(userlogs[uid][event_type])
|
||||||
|
|
||||||
|
|
||||||
|
def setwatch(uid, issuer, watch_state, uname: str = ""):
|
||||||
|
userlogs = get_userlog()
|
||||||
|
uid = str(uid)
|
||||||
|
# Can we reduce code repetition here?
|
||||||
|
if uid not in userlogs:
|
||||||
|
userlogs[uid] = {"warns": [],
|
||||||
|
"mutes": [],
|
||||||
|
"kicks": [],
|
||||||
|
"bans": [],
|
||||||
|
"notes": [],
|
||||||
|
"watch": False,
|
||||||
|
"name": "n/a"}
|
||||||
|
if uname:
|
||||||
|
userlogs[uid]["name"] = uname
|
||||||
|
|
||||||
|
userlogs[uid]["watch"] = watch_state
|
||||||
|
set_userlog(json.dumps(userlogs))
|
||||||
|
return
|
686
poetry.lock
generated
686
poetry.lock
generated
|
@ -1,686 +0,0 @@
|
||||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aiohttp"
|
|
||||||
version = "3.9.5"
|
|
||||||
description = "Async http client/server framework (asyncio)"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7"},
|
|
||||||
{file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c"},
|
|
||||||
{file = "aiohttp-3.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a"},
|
|
||||||
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430"},
|
|
||||||
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3"},
|
|
||||||
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b"},
|
|
||||||
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72"},
|
|
||||||
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0"},
|
|
||||||
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558"},
|
|
||||||
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db"},
|
|
||||||
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f"},
|
|
||||||
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832"},
|
|
||||||
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10"},
|
|
||||||
{file = "aiohttp-3.9.5-cp310-cp310-win32.whl", hash = "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb"},
|
|
||||||
{file = "aiohttp-3.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb"},
|
|
||||||
{file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342"},
|
|
||||||
{file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d"},
|
|
||||||
{file = "aiohttp-3.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424"},
|
|
||||||
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee"},
|
|
||||||
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2"},
|
|
||||||
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233"},
|
|
||||||
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595"},
|
|
||||||
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6"},
|
|
||||||
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d"},
|
|
||||||
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323"},
|
|
||||||
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9"},
|
|
||||||
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771"},
|
|
||||||
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75"},
|
|
||||||
{file = "aiohttp-3.9.5-cp311-cp311-win32.whl", hash = "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6"},
|
|
||||||
{file = "aiohttp-3.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a"},
|
|
||||||
{file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678"},
|
|
||||||
{file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c"},
|
|
||||||
{file = "aiohttp-3.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f"},
|
|
||||||
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4"},
|
|
||||||
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c"},
|
|
||||||
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa"},
|
|
||||||
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58"},
|
|
||||||
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf"},
|
|
||||||
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f"},
|
|
||||||
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81"},
|
|
||||||
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a"},
|
|
||||||
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a"},
|
|
||||||
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da"},
|
|
||||||
{file = "aiohttp-3.9.5-cp312-cp312-win32.whl", hash = "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59"},
|
|
||||||
{file = "aiohttp-3.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888"},
|
|
||||||
{file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8"},
|
|
||||||
{file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8"},
|
|
||||||
{file = "aiohttp-3.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78"},
|
|
||||||
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b"},
|
|
||||||
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de"},
|
|
||||||
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac"},
|
|
||||||
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11"},
|
|
||||||
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd"},
|
|
||||||
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1"},
|
|
||||||
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e"},
|
|
||||||
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156"},
|
|
||||||
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031"},
|
|
||||||
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe"},
|
|
||||||
{file = "aiohttp-3.9.5-cp38-cp38-win32.whl", hash = "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da"},
|
|
||||||
{file = "aiohttp-3.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a"},
|
|
||||||
{file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed"},
|
|
||||||
{file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a"},
|
|
||||||
{file = "aiohttp-3.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372"},
|
|
||||||
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb"},
|
|
||||||
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24"},
|
|
||||||
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2"},
|
|
||||||
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106"},
|
|
||||||
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b"},
|
|
||||||
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475"},
|
|
||||||
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed"},
|
|
||||||
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c"},
|
|
||||||
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46"},
|
|
||||||
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2"},
|
|
||||||
{file = "aiohttp-3.9.5-cp39-cp39-win32.whl", hash = "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09"},
|
|
||||||
{file = "aiohttp-3.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1"},
|
|
||||||
{file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
aiosignal = ">=1.1.2"
|
|
||||||
attrs = ">=17.3.0"
|
|
||||||
frozenlist = ">=1.1.1"
|
|
||||||
multidict = ">=4.5,<7.0"
|
|
||||||
yarl = ">=1.0,<2.0"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
speedups = ["Brotli", "aiodns", "brotlicffi"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aiosignal"
|
|
||||||
version = "1.3.1"
|
|
||||||
description = "aiosignal: a list of registered asynchronous callbacks"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"},
|
|
||||||
{file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
frozenlist = ">=1.1.0"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "attrs"
|
|
||||||
version = "23.2.0"
|
|
||||||
description = "Classes Without Boilerplate"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"},
|
|
||||||
{file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
|
|
||||||
dev = ["attrs[tests]", "pre-commit"]
|
|
||||||
docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
|
|
||||||
tests = ["attrs[tests-no-zope]", "zope-interface"]
|
|
||||||
tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"]
|
|
||||||
tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cffi"
|
|
||||||
version = "1.16.0"
|
|
||||||
description = "Foreign Function Interface for Python calling C code."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"},
|
|
||||||
{file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"},
|
|
||||||
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"},
|
|
||||||
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"},
|
|
||||||
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"},
|
|
||||||
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"},
|
|
||||||
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"},
|
|
||||||
{file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"},
|
|
||||||
{file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"},
|
|
||||||
{file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"},
|
|
||||||
{file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"},
|
|
||||||
{file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"},
|
|
||||||
{file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"},
|
|
||||||
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"},
|
|
||||||
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"},
|
|
||||||
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"},
|
|
||||||
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"},
|
|
||||||
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"},
|
|
||||||
{file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"},
|
|
||||||
{file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"},
|
|
||||||
{file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"},
|
|
||||||
{file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"},
|
|
||||||
{file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"},
|
|
||||||
{file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"},
|
|
||||||
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"},
|
|
||||||
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"},
|
|
||||||
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"},
|
|
||||||
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"},
|
|
||||||
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"},
|
|
||||||
{file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"},
|
|
||||||
{file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"},
|
|
||||||
{file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"},
|
|
||||||
{file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"},
|
|
||||||
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"},
|
|
||||||
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"},
|
|
||||||
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"},
|
|
||||||
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"},
|
|
||||||
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"},
|
|
||||||
{file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"},
|
|
||||||
{file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"},
|
|
||||||
{file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"},
|
|
||||||
{file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"},
|
|
||||||
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"},
|
|
||||||
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"},
|
|
||||||
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"},
|
|
||||||
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"},
|
|
||||||
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"},
|
|
||||||
{file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"},
|
|
||||||
{file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"},
|
|
||||||
{file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"},
|
|
||||||
{file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"},
|
|
||||||
{file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
pycparser = "*"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cryptography"
|
|
||||||
version = "42.0.7"
|
|
||||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a987f840718078212fdf4504d0fd4c6effe34a7e4740378e59d47696e8dfb477"},
|
|
||||||
{file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bd13b5e9b543532453de08bcdc3cc7cebec6f9883e886fd20a92f26940fd3e7a"},
|
|
||||||
{file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a79165431551042cc9d1d90e6145d5d0d3ab0f2d66326c201d9b0e7f5bf43604"},
|
|
||||||
{file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a47787a5e3649008a1102d3df55424e86606c9bae6fb77ac59afe06d234605f8"},
|
|
||||||
{file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:02c0eee2d7133bdbbc5e24441258d5d2244beb31da5ed19fbb80315f4bbbff55"},
|
|
||||||
{file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5e44507bf8d14b36b8389b226665d597bc0f18ea035d75b4e53c7b1ea84583cc"},
|
|
||||||
{file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7f8b25fa616d8b846aef64b15c606bb0828dbc35faf90566eb139aa9cff67af2"},
|
|
||||||
{file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:93a3209f6bb2b33e725ed08ee0991b92976dfdcf4e8b38646540674fc7508e13"},
|
|
||||||
{file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e6b8f1881dac458c34778d0a424ae5769de30544fc678eac51c1c8bb2183e9da"},
|
|
||||||
{file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3de9a45d3b2b7d8088c3fbf1ed4395dfeff79d07842217b38df14ef09ce1d8d7"},
|
|
||||||
{file = "cryptography-42.0.7-cp37-abi3-win32.whl", hash = "sha256:789caea816c6704f63f6241a519bfa347f72fbd67ba28d04636b7c6b7da94b0b"},
|
|
||||||
{file = "cryptography-42.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:8cb8ce7c3347fcf9446f201dc30e2d5a3c898d009126010cbd1f443f28b52678"},
|
|
||||||
{file = "cryptography-42.0.7-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:a3a5ac8b56fe37f3125e5b72b61dcde43283e5370827f5233893d461b7360cd4"},
|
|
||||||
{file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:779245e13b9a6638df14641d029add5dc17edbef6ec915688f3acb9e720a5858"},
|
|
||||||
{file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d563795db98b4cd57742a78a288cdbdc9daedac29f2239793071fe114f13785"},
|
|
||||||
{file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:31adb7d06fe4383226c3e963471f6837742889b3c4caa55aac20ad951bc8ffda"},
|
|
||||||
{file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:efd0bf5205240182e0f13bcaea41be4fdf5c22c5129fc7ced4a0282ac86998c9"},
|
|
||||||
{file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a9bc127cdc4ecf87a5ea22a2556cab6c7eda2923f84e4f3cc588e8470ce4e42e"},
|
|
||||||
{file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3577d029bc3f4827dd5bf8bf7710cac13527b470bbf1820a3f394adb38ed7d5f"},
|
|
||||||
{file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2e47577f9b18723fa294b0ea9a17d5e53a227867a0a4904a1a076d1646d45ca1"},
|
|
||||||
{file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1a58839984d9cb34c855197043eaae2c187d930ca6d644612843b4fe8513c886"},
|
|
||||||
{file = "cryptography-42.0.7-cp39-abi3-win32.whl", hash = "sha256:e6b79d0adb01aae87e8a44c2b64bc3f3fe59515280e00fb6d57a7267a2583cda"},
|
|
||||||
{file = "cryptography-42.0.7-cp39-abi3-win_amd64.whl", hash = "sha256:16268d46086bb8ad5bf0a2b5544d8a9ed87a0e33f5e77dd3c3301e63d941a83b"},
|
|
||||||
{file = "cryptography-42.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2954fccea107026512b15afb4aa664a5640cd0af630e2ee3962f2602693f0c82"},
|
|
||||||
{file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:362e7197754c231797ec45ee081f3088a27a47c6c01eff2ac83f60f85a50fe60"},
|
|
||||||
{file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4f698edacf9c9e0371112792558d2f705b5645076cc0aaae02f816a0171770fd"},
|
|
||||||
{file = "cryptography-42.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5482e789294854c28237bba77c4c83be698be740e31a3ae5e879ee5444166582"},
|
|
||||||
{file = "cryptography-42.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e9b2a6309f14c0497f348d08a065d52f3020656f675819fc405fb63bbcd26562"},
|
|
||||||
{file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d8e3098721b84392ee45af2dd554c947c32cc52f862b6a3ae982dbb90f577f14"},
|
|
||||||
{file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c65f96dad14f8528a447414125e1fc8feb2ad5a272b8f68477abbcc1ea7d94b9"},
|
|
||||||
{file = "cryptography-42.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36017400817987670037fbb0324d71489b6ead6231c9604f8fc1f7d008087c68"},
|
|
||||||
{file = "cryptography-42.0.7.tar.gz", hash = "sha256:ecbfbc00bf55888edda9868a4cf927205de8499e7fabe6c050322298382953f2"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
|
|
||||||
docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
|
|
||||||
nox = ["nox"]
|
|
||||||
pep8test = ["check-sdist", "click", "mypy", "ruff"]
|
|
||||||
sdist = ["build"]
|
|
||||||
ssh = ["bcrypt (>=3.1.5)"]
|
|
||||||
test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
|
|
||||||
test-randomorder = ["pytest-randomly"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "discord-py"
|
|
||||||
version = "2.3.2"
|
|
||||||
description = "A Python wrapper for the Discord API"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8.0"
|
|
||||||
files = [
|
|
||||||
{file = "discord.py-2.3.2-py3-none-any.whl", hash = "sha256:9da4679fc3cb10c64b388284700dc998663e0e57328283bbfcfc2525ec5960a6"},
|
|
||||||
{file = "discord.py-2.3.2.tar.gz", hash = "sha256:4560f70f2eddba7e83370ecebd237ac09fbb4980dc66507482b0c0e5b8f76b9c"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
aiohttp = ">=3.7.4,<4"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
docs = ["sphinx (==4.4.0)", "sphinxcontrib-trio (==1.1.2)", "sphinxcontrib-websupport", "typing-extensions (>=4.3,<5)"]
|
|
||||||
speed = ["Brotli", "aiodns (>=1.1)", "cchardet (==2.1.7)", "orjson (>=3.5.4)"]
|
|
||||||
test = ["coverage[toml]", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mock", "typing-extensions (>=4.3,<5)"]
|
|
||||||
voice = ["PyNaCl (>=1.3.0,<1.6)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "frozenlist"
|
|
||||||
version = "1.4.1"
|
|
||||||
description = "A list-like structure which implements collections.abc.MutableSequence"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"},
|
|
||||||
{file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"},
|
|
||||||
{file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"},
|
|
||||||
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"},
|
|
||||||
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"},
|
|
||||||
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"},
|
|
||||||
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"},
|
|
||||||
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"},
|
|
||||||
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"},
|
|
||||||
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"},
|
|
||||||
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"},
|
|
||||||
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"},
|
|
||||||
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"},
|
|
||||||
{file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"},
|
|
||||||
{file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"},
|
|
||||||
{file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"},
|
|
||||||
{file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"},
|
|
||||||
{file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"},
|
|
||||||
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"},
|
|
||||||
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"},
|
|
||||||
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"},
|
|
||||||
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"},
|
|
||||||
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"},
|
|
||||||
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"},
|
|
||||||
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"},
|
|
||||||
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"},
|
|
||||||
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"},
|
|
||||||
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"},
|
|
||||||
{file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"},
|
|
||||||
{file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"},
|
|
||||||
{file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"},
|
|
||||||
{file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"},
|
|
||||||
{file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"},
|
|
||||||
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"},
|
|
||||||
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"},
|
|
||||||
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"},
|
|
||||||
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"},
|
|
||||||
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"},
|
|
||||||
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"},
|
|
||||||
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"},
|
|
||||||
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"},
|
|
||||||
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"},
|
|
||||||
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"},
|
|
||||||
{file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"},
|
|
||||||
{file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"},
|
|
||||||
{file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"},
|
|
||||||
{file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"},
|
|
||||||
{file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"},
|
|
||||||
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"},
|
|
||||||
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"},
|
|
||||||
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"},
|
|
||||||
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"},
|
|
||||||
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"},
|
|
||||||
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"},
|
|
||||||
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"},
|
|
||||||
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"},
|
|
||||||
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"},
|
|
||||||
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"},
|
|
||||||
{file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"},
|
|
||||||
{file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"},
|
|
||||||
{file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"},
|
|
||||||
{file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"},
|
|
||||||
{file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"},
|
|
||||||
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"},
|
|
||||||
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"},
|
|
||||||
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"},
|
|
||||||
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"},
|
|
||||||
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"},
|
|
||||||
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"},
|
|
||||||
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"},
|
|
||||||
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"},
|
|
||||||
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"},
|
|
||||||
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"},
|
|
||||||
{file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"},
|
|
||||||
{file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"},
|
|
||||||
{file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"},
|
|
||||||
{file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gidgethub"
|
|
||||||
version = "5.3.0"
|
|
||||||
description = "An async GitHub API library"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "gidgethub-5.3.0-py3-none-any.whl", hash = "sha256:4dd92f2252d12756b13f9dd15cde322bfb0d625b6fb5d680da1567ec74b462c0"},
|
|
||||||
{file = "gidgethub-5.3.0.tar.gz", hash = "sha256:9ece7d37fbceb819b80560e7ed58f936e48a65d37ec5f56db79145156b426a25"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
PyJWT = {version = ">=2.4.0", extras = ["crypto"]}
|
|
||||||
uritemplate = ">=3.0.1"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
aiohttp = ["aiohttp"]
|
|
||||||
dev = ["aiohttp", "black", "coverage[toml] (>=5.0.3)", "httpx", "mypy", "pytest-cov", "pytest-xdist", "tornado"]
|
|
||||||
doc = ["sphinx (>=4.0.0)", "sphinx-rtd-theme (>=0.5.2)"]
|
|
||||||
httpx = ["httpx (>=0.16.1)"]
|
|
||||||
test = ["importlib-resources", "pytest (>=5.4.1)", "pytest-asyncio", "pytest-tornasync"]
|
|
||||||
tornado = ["tornado"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "humanize"
|
|
||||||
version = "4.9.0"
|
|
||||||
description = "Python humanize utilities"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "humanize-4.9.0-py3-none-any.whl", hash = "sha256:ce284a76d5b1377fd8836733b983bfb0b76f1aa1c090de2566fcf008d7f6ab16"},
|
|
||||||
{file = "humanize-4.9.0.tar.gz", hash = "sha256:582a265c931c683a7e9b8ed9559089dea7edcf6cc95be39a3cbc2c5d5ac2bcfa"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
tests = ["freezegun", "pytest", "pytest-cov"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "idna"
|
|
||||||
version = "3.7"
|
|
||||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.5"
|
|
||||||
files = [
|
|
||||||
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
|
|
||||||
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "multidict"
|
|
||||||
version = "6.0.5"
|
|
||||||
description = "multidict implementation"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"},
|
|
||||||
{file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"},
|
|
||||||
{file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"},
|
|
||||||
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"},
|
|
||||||
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"},
|
|
||||||
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"},
|
|
||||||
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"},
|
|
||||||
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"},
|
|
||||||
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"},
|
|
||||||
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"},
|
|
||||||
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"},
|
|
||||||
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"},
|
|
||||||
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"},
|
|
||||||
{file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"},
|
|
||||||
{file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"},
|
|
||||||
{file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"},
|
|
||||||
{file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"},
|
|
||||||
{file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"},
|
|
||||||
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"},
|
|
||||||
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"},
|
|
||||||
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"},
|
|
||||||
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"},
|
|
||||||
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"},
|
|
||||||
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"},
|
|
||||||
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"},
|
|
||||||
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"},
|
|
||||||
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"},
|
|
||||||
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"},
|
|
||||||
{file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"},
|
|
||||||
{file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"},
|
|
||||||
{file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"},
|
|
||||||
{file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"},
|
|
||||||
{file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"},
|
|
||||||
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"},
|
|
||||||
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"},
|
|
||||||
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"},
|
|
||||||
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"},
|
|
||||||
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"},
|
|
||||||
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"},
|
|
||||||
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"},
|
|
||||||
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"},
|
|
||||||
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"},
|
|
||||||
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"},
|
|
||||||
{file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"},
|
|
||||||
{file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"},
|
|
||||||
{file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"},
|
|
||||||
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"},
|
|
||||||
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"},
|
|
||||||
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"},
|
|
||||||
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"},
|
|
||||||
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"},
|
|
||||||
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"},
|
|
||||||
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"},
|
|
||||||
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"},
|
|
||||||
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"},
|
|
||||||
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"},
|
|
||||||
{file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"},
|
|
||||||
{file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"},
|
|
||||||
{file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"},
|
|
||||||
{file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"},
|
|
||||||
{file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"},
|
|
||||||
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"},
|
|
||||||
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"},
|
|
||||||
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"},
|
|
||||||
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"},
|
|
||||||
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"},
|
|
||||||
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"},
|
|
||||||
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"},
|
|
||||||
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"},
|
|
||||||
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"},
|
|
||||||
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"},
|
|
||||||
{file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"},
|
|
||||||
{file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"},
|
|
||||||
{file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"},
|
|
||||||
{file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"},
|
|
||||||
{file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"},
|
|
||||||
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"},
|
|
||||||
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"},
|
|
||||||
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"},
|
|
||||||
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"},
|
|
||||||
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"},
|
|
||||||
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"},
|
|
||||||
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"},
|
|
||||||
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"},
|
|
||||||
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"},
|
|
||||||
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"},
|
|
||||||
{file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"},
|
|
||||||
{file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"},
|
|
||||||
{file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"},
|
|
||||||
{file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parsedatetime"
|
|
||||||
version = "2.6"
|
|
||||||
description = "Parse human-readable date/time text."
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
files = [
|
|
||||||
{file = "parsedatetime-2.6-py3-none-any.whl", hash = "sha256:cb96edd7016872f58479e35879294258c71437195760746faffedb692aef000b"},
|
|
||||||
{file = "parsedatetime-2.6.tar.gz", hash = "sha256:4cb368fbb18a0b7231f4d76119165451c8d2e35951455dfee97c62a87b04d455"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pycparser"
|
|
||||||
version = "2.22"
|
|
||||||
description = "C parser in Python"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
|
|
||||||
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pyjwt"
|
|
||||||
version = "2.8.0"
|
|
||||||
description = "JSON Web Token implementation in Python"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"},
|
|
||||||
{file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""}
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
crypto = ["cryptography (>=3.4.0)"]
|
|
||||||
dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
|
|
||||||
docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
|
|
||||||
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "python-dateutil"
|
|
||||||
version = "2.9.0.post0"
|
|
||||||
description = "Extensions to the standard Python datetime module"
|
|
||||||
optional = false
|
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
|
||||||
files = [
|
|
||||||
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
|
|
||||||
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
six = ">=1.5"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "six"
|
|
||||||
version = "1.16.0"
|
|
||||||
description = "Python 2 and 3 compatibility utilities"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
|
||||||
files = [
|
|
||||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
|
||||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "uritemplate"
|
|
||||||
version = "4.1.1"
|
|
||||||
description = "Implementation of RFC 6570 URI Templates"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.6"
|
|
||||||
files = [
|
|
||||||
{file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"},
|
|
||||||
{file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "yarl"
|
|
||||||
version = "1.9.4"
|
|
||||||
description = "Yet another URL library"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"},
|
|
||||||
{file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"},
|
|
||||||
{file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"},
|
|
||||||
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"},
|
|
||||||
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"},
|
|
||||||
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"},
|
|
||||||
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"},
|
|
||||||
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"},
|
|
||||||
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"},
|
|
||||||
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"},
|
|
||||||
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"},
|
|
||||||
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"},
|
|
||||||
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"},
|
|
||||||
{file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"},
|
|
||||||
{file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"},
|
|
||||||
{file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"},
|
|
||||||
{file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"},
|
|
||||||
{file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"},
|
|
||||||
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"},
|
|
||||||
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"},
|
|
||||||
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"},
|
|
||||||
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"},
|
|
||||||
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"},
|
|
||||||
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"},
|
|
||||||
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"},
|
|
||||||
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"},
|
|
||||||
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"},
|
|
||||||
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"},
|
|
||||||
{file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"},
|
|
||||||
{file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"},
|
|
||||||
{file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"},
|
|
||||||
{file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"},
|
|
||||||
{file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"},
|
|
||||||
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"},
|
|
||||||
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"},
|
|
||||||
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"},
|
|
||||||
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"},
|
|
||||||
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"},
|
|
||||||
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"},
|
|
||||||
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"},
|
|
||||||
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"},
|
|
||||||
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"},
|
|
||||||
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"},
|
|
||||||
{file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"},
|
|
||||||
{file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"},
|
|
||||||
{file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"},
|
|
||||||
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"},
|
|
||||||
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"},
|
|
||||||
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"},
|
|
||||||
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"},
|
|
||||||
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"},
|
|
||||||
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"},
|
|
||||||
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"},
|
|
||||||
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"},
|
|
||||||
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"},
|
|
||||||
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"},
|
|
||||||
{file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"},
|
|
||||||
{file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"},
|
|
||||||
{file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"},
|
|
||||||
{file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"},
|
|
||||||
{file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"},
|
|
||||||
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"},
|
|
||||||
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"},
|
|
||||||
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"},
|
|
||||||
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"},
|
|
||||||
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"},
|
|
||||||
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"},
|
|
||||||
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"},
|
|
||||||
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"},
|
|
||||||
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"},
|
|
||||||
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"},
|
|
||||||
{file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"},
|
|
||||||
{file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"},
|
|
||||||
{file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"},
|
|
||||||
{file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"},
|
|
||||||
{file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"},
|
|
||||||
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"},
|
|
||||||
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"},
|
|
||||||
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"},
|
|
||||||
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"},
|
|
||||||
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"},
|
|
||||||
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"},
|
|
||||||
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"},
|
|
||||||
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"},
|
|
||||||
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"},
|
|
||||||
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"},
|
|
||||||
{file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"},
|
|
||||||
{file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"},
|
|
||||||
{file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"},
|
|
||||||
{file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
idna = ">=2.0"
|
|
||||||
multidict = ">=4.0"
|
|
||||||
|
|
||||||
[metadata]
|
|
||||||
lock-version = "2.0"
|
|
||||||
python-versions = "^3.11"
|
|
||||||
content-hash = "1453fcd147cbf31437ecaa4b37deb0f5d880579a2b93b5f668c28c154ed299d8"
|
|
|
@ -1,23 +0,0 @@
|
||||||
[tool.poetry]
|
|
||||||
name = "robocop_ng"
|
|
||||||
version = "1.0.1"
|
|
||||||
description = "Discord bot for handling ReSwitched moderation tasks and such, (n)ext-(g)en rewrite of Robocop"
|
|
||||||
authors = ["ReSwitched Team"]
|
|
||||||
license = "MIT"
|
|
||||||
readme = "README.md"
|
|
||||||
repository = "https://github.com/reswitched/robocop-ng"
|
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
|
||||||
python = "^3.11"
|
|
||||||
python-dateutil = "^2.8.2"
|
|
||||||
humanize = "^4.8.0"
|
|
||||||
parsedatetime = "^2.6"
|
|
||||||
aiohttp = "^3.9.3"
|
|
||||||
gidgethub = "^5.3.0"
|
|
||||||
"discord.py" = "^2.3.2"
|
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["poetry-core"]
|
|
||||||
build-backend = "poetry.core.masonry.api"
|
|
8
requirements.txt
Normal file
8
requirements.txt
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
git+https://github.com/Rapptz/discord.py@rewrite
|
||||||
|
|
||||||
|
asyncio
|
||||||
|
python-dateutil
|
||||||
|
humanize
|
||||||
|
parsedatetime
|
||||||
|
aiohttp
|
||||||
|
gidgethub
|
|
@ -1,296 +0,0 @@
|
||||||
import asyncio
|
|
||||||
import logging.handlers
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
import discord
|
|
||||||
from discord.ext import commands
|
|
||||||
from discord.ext.commands import CommandError, Context
|
|
||||||
|
|
||||||
from robocop_ng.helpers.notifications import report_critical_error
|
|
||||||
|
|
||||||
if len(sys.argv[1:]) != 1:
|
|
||||||
sys.stderr.write("usage: <state_dir>")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
state_dir = os.path.abspath(sys.argv[1])
|
|
||||||
sys.path.append(state_dir)
|
|
||||||
|
|
||||||
import config
|
|
||||||
|
|
||||||
script_name = os.path.basename(__file__).split(".")[0]
|
|
||||||
|
|
||||||
log_file_name = f"{script_name}.log"
|
|
||||||
|
|
||||||
# Limit of discord (non-nitro) is 8MB (not MiB)
|
|
||||||
max_file_size = 1000 * 1000 * 8
|
|
||||||
backup_count = 3
|
|
||||||
file_handler = logging.handlers.RotatingFileHandler(
|
|
||||||
filename=log_file_name, maxBytes=max_file_size, backupCount=backup_count
|
|
||||||
)
|
|
||||||
stdout_handler = logging.StreamHandler(sys.stdout)
|
|
||||||
|
|
||||||
log_format = logging.Formatter(
|
|
||||||
"[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s"
|
|
||||||
)
|
|
||||||
file_handler.setFormatter(log_format)
|
|
||||||
stdout_handler.setFormatter(log_format)
|
|
||||||
|
|
||||||
log = logging.getLogger("discord")
|
|
||||||
log.setLevel(logging.INFO)
|
|
||||||
log.addHandler(file_handler)
|
|
||||||
log.addHandler(stdout_handler)
|
|
||||||
|
|
||||||
|
|
||||||
def get_prefix(bot, message):
|
|
||||||
prefixes = config.prefixes
|
|
||||||
|
|
||||||
return commands.when_mentioned_or(*prefixes)(bot, message)
|
|
||||||
|
|
||||||
|
|
||||||
wanted_jsons = [
|
|
||||||
"data/restrictions.json",
|
|
||||||
"data/robocronptab.json",
|
|
||||||
"data/userlog.json",
|
|
||||||
"data/invites.json",
|
|
||||||
"data/macros.json",
|
|
||||||
"data/persistent_roles.json",
|
|
||||||
"data/disabled_ids.json",
|
|
||||||
]
|
|
||||||
|
|
||||||
if not os.path.exists(os.path.join(state_dir, "data")):
|
|
||||||
os.makedirs(os.path.join(state_dir, "data"))
|
|
||||||
|
|
||||||
for wanted_json_idx in range(len(wanted_jsons)):
|
|
||||||
wanted_jsons[wanted_json_idx] = os.path.join(
|
|
||||||
state_dir, wanted_jsons[wanted_json_idx]
|
|
||||||
)
|
|
||||||
if not os.path.isfile(wanted_jsons[wanted_json_idx]):
|
|
||||||
with open(wanted_jsons[wanted_json_idx], "w") as file:
|
|
||||||
file.write("{}")
|
|
||||||
|
|
||||||
intents = discord.Intents.all()
|
|
||||||
intents.typing = False
|
|
||||||
|
|
||||||
bot = commands.Bot(
|
|
||||||
command_prefix=get_prefix, description=config.bot_description, intents=intents
|
|
||||||
)
|
|
||||||
bot.help_command = commands.DefaultHelpCommand(dm_help=True)
|
|
||||||
|
|
||||||
bot.log = log
|
|
||||||
bot.config = config
|
|
||||||
bot.script_name = script_name
|
|
||||||
bot.state_dir = state_dir
|
|
||||||
bot.wanted_jsons = wanted_jsons
|
|
||||||
|
|
||||||
|
|
||||||
async def get_channel_safe(self, channel_id: int):
|
|
||||||
res = self.get_channel(channel_id)
|
|
||||||
if res is None:
|
|
||||||
res = await self.fetch_channel(channel_id)
|
|
||||||
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
commands.Bot.get_channel_safe = get_channel_safe
|
|
||||||
|
|
||||||
|
|
||||||
@bot.event
|
|
||||||
async def on_ready():
|
|
||||||
aioh = {"User-Agent": f"{script_name}/1.0'"}
|
|
||||||
bot.aiosession = aiohttp.ClientSession(headers=aioh)
|
|
||||||
bot.app_info = await bot.application_info()
|
|
||||||
bot.botlog_channel = await bot.get_channel_safe(config.botlog_channel)
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
f"\nLogged in as: {bot.user.name} - "
|
|
||||||
f"{bot.user.id}\ndpy version: {discord.__version__}\n"
|
|
||||||
)
|
|
||||||
game_name = f"{config.prefixes[0]}help"
|
|
||||||
|
|
||||||
# Send "Robocop has started! x has y members!"
|
|
||||||
guild = bot.botlog_channel.guild
|
|
||||||
msg = (
|
|
||||||
f"{bot.user.name} has started! "
|
|
||||||
f"{guild.name} has {guild.member_count} members!"
|
|
||||||
)
|
|
||||||
|
|
||||||
data_files = [discord.File(fpath) for fpath in wanted_jsons]
|
|
||||||
await bot.botlog_channel.send(msg, files=data_files)
|
|
||||||
|
|
||||||
activity = discord.Activity(name=game_name, type=discord.ActivityType.listening)
|
|
||||||
|
|
||||||
await bot.change_presence(activity=activity)
|
|
||||||
|
|
||||||
|
|
||||||
@bot.event
|
|
||||||
async def on_command(ctx):
|
|
||||||
log_text = (
|
|
||||||
f"{ctx.message.author} ({ctx.message.author.id}): " f'"{ctx.message.content}" '
|
|
||||||
)
|
|
||||||
if ctx.guild: # was too long for tertiary if
|
|
||||||
log_text += (
|
|
||||||
f'on "{ctx.channel.name}" ({ctx.channel.id}) '
|
|
||||||
f'at "{ctx.guild.name}" ({ctx.guild.id})'
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
log_text += f"on DMs ({ctx.channel.id})"
|
|
||||||
log.info(log_text)
|
|
||||||
|
|
||||||
|
|
||||||
@bot.event
|
|
||||||
async def on_error(event: str, *args, **kwargs):
|
|
||||||
log.exception(f"Error on {event}:")
|
|
||||||
|
|
||||||
exception = sys.exc_info()[1]
|
|
||||||
is_report_allowed = any(
|
|
||||||
[
|
|
||||||
not isinstance(exception, x)
|
|
||||||
for x in [
|
|
||||||
discord.RateLimited,
|
|
||||||
discord.GatewayNotFound,
|
|
||||||
discord.InteractionResponded,
|
|
||||||
discord.LoginFailure,
|
|
||||||
]
|
|
||||||
]
|
|
||||||
)
|
|
||||||
if exception is not None and is_report_allowed:
|
|
||||||
await report_critical_error(
|
|
||||||
bot,
|
|
||||||
exception,
|
|
||||||
additional_info={"Event": event, "args": args, "kwargs": kwargs},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bot.event
|
|
||||||
async def on_command_error(ctx: Context, error: CommandError):
|
|
||||||
error_text = str(error)
|
|
||||||
|
|
||||||
err_msg = (
|
|
||||||
f'Error with "{ctx.message.content}" from '
|
|
||||||
f'"{ctx.message.author} ({ctx.message.author.id}) '
|
|
||||||
f"of type {type(error)}: {error_text}"
|
|
||||||
)
|
|
||||||
|
|
||||||
log.exception(err_msg)
|
|
||||||
|
|
||||||
if not isinstance(error, commands.CommandNotFound):
|
|
||||||
err_msg = bot.escape_message(err_msg)
|
|
||||||
await bot.botlog_channel.send(err_msg)
|
|
||||||
|
|
||||||
if isinstance(error, commands.NoPrivateMessage):
|
|
||||||
return await ctx.send("This command doesn't work on DMs.")
|
|
||||||
elif isinstance(error, commands.MissingPermissions):
|
|
||||||
roles_needed = "\n- ".join(error.missing_perms)
|
|
||||||
return await ctx.send(
|
|
||||||
f"{ctx.author.mention}: You don't have the right"
|
|
||||||
" permissions to run this command. You need: "
|
|
||||||
f"```- {roles_needed}```"
|
|
||||||
)
|
|
||||||
elif isinstance(error, commands.BotMissingPermissions):
|
|
||||||
roles_needed = "\n-".join(error.missing_perms)
|
|
||||||
return await ctx.send(
|
|
||||||
f"{ctx.author.mention}: Bot doesn't have "
|
|
||||||
"the right permissions to run this command. "
|
|
||||||
"Please add the following roles: "
|
|
||||||
f"```- {roles_needed}```"
|
|
||||||
)
|
|
||||||
elif isinstance(error, commands.CommandOnCooldown):
|
|
||||||
return await ctx.send(
|
|
||||||
f"{ctx.author.mention}: You're being "
|
|
||||||
"ratelimited. Try in "
|
|
||||||
f"{error.retry_after:.1f} seconds."
|
|
||||||
)
|
|
||||||
elif isinstance(error, commands.CheckFailure):
|
|
||||||
return await ctx.send(
|
|
||||||
f"{ctx.author.mention}: Check failed. "
|
|
||||||
"You might not have the right permissions "
|
|
||||||
"to run this command, or you may not be able "
|
|
||||||
"to run this command in the current channel."
|
|
||||||
)
|
|
||||||
elif isinstance(error, commands.CommandInvokeError) and (
|
|
||||||
"Cannot send messages to this user" in error_text
|
|
||||||
):
|
|
||||||
return await ctx.send(
|
|
||||||
f"{ctx.author.mention}: I can't DM you.\n"
|
|
||||||
"You might have me blocked or have DMs "
|
|
||||||
f"blocked globally or for {ctx.guild.name}.\n"
|
|
||||||
"Please resolve that, then "
|
|
||||||
"run the command again."
|
|
||||||
)
|
|
||||||
elif isinstance(error, commands.CommandNotFound):
|
|
||||||
# Nothing to do when command is not found.
|
|
||||||
return
|
|
||||||
|
|
||||||
help_text = (
|
|
||||||
f"Usage of this command is: ```{ctx.prefix}{ctx.command.name} "
|
|
||||||
f"{ctx.command.signature}```\nPlease see `{ctx.prefix}help "
|
|
||||||
f"{ctx.command.name}` for more info about this command."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Keep a list of commands that involve mentioning users
|
|
||||||
# and can involve users leaving/getting banned
|
|
||||||
# noinspection NonAsciiCharacters,PyPep8Naming
|
|
||||||
ಠ_ಠ = ["warn", "kick", "ban"]
|
|
||||||
|
|
||||||
if isinstance(error, commands.BadArgument):
|
|
||||||
# and if said commands get used, add a specific notice.
|
|
||||||
if ctx.command.name in ಠ_ಠ:
|
|
||||||
help_text = (
|
|
||||||
"This probably means that user left (or already got kicked/banned).\n"
|
|
||||||
+ help_text
|
|
||||||
)
|
|
||||||
|
|
||||||
return await ctx.send(
|
|
||||||
f"{ctx.author.mention}: You gave incorrect arguments. {help_text}"
|
|
||||||
)
|
|
||||||
elif isinstance(error, commands.MissingRequiredArgument):
|
|
||||||
return await ctx.send(
|
|
||||||
f"{ctx.author.mention}: You gave incomplete arguments. {help_text}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bot.event
|
|
||||||
async def on_message(message):
|
|
||||||
if message.author.bot:
|
|
||||||
return
|
|
||||||
|
|
||||||
if (message.guild) and (message.guild.id not in config.guild_whitelist):
|
|
||||||
return
|
|
||||||
|
|
||||||
# Ignore messages in newcomers channel, unless it's potentially
|
|
||||||
# an allowed command
|
|
||||||
welcome_allowed = ["reset", "kick", "ban", "warn"]
|
|
||||||
if message.channel.id == config.welcome_channel and not any(
|
|
||||||
cmd in message.content for cmd in welcome_allowed
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
ctx = await bot.get_context(message)
|
|
||||||
await bot.invoke(ctx)
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
async with bot:
|
|
||||||
if len(config.guild_whitelist) == 1:
|
|
||||||
invite_url = discord.utils.oauth_url(
|
|
||||||
config.client_id,
|
|
||||||
guild=discord.Object(config.guild_whitelist[0]),
|
|
||||||
disable_guild_select=True,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
invite_url = discord.utils.oauth_url(config.client_id)
|
|
||||||
|
|
||||||
log.info(f"\nInvite URL: {invite_url}\n")
|
|
||||||
|
|
||||||
for cog in config.initial_cogs:
|
|
||||||
try:
|
|
||||||
await bot.load_extension(f"robocop_ng.{cog}")
|
|
||||||
except Exception as e:
|
|
||||||
log.exception(f"Failed to load cog {cog}:", e)
|
|
||||||
await bot.start(config.token)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(main())
|
|
2
robocop_ng/assets/.gitignore
vendored
2
robocop_ng/assets/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
||||||
*.otf
|
|
||||||
*.ttf
|
|
Binary file not shown.
Before Width: | Height: | Size: 6.6 KiB |
Binary file not shown.
Before Width: | Height: | Size: 7.6 KiB |
|
@ -1,177 +0,0 @@
|
||||||
import inspect
|
|
||||||
import re
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
import discord
|
|
||||||
from discord.ext import commands
|
|
||||||
from discord.ext.commands import Cog
|
|
||||||
|
|
||||||
from robocop_ng.helpers.checks import check_if_bot_manager
|
|
||||||
|
|
||||||
|
|
||||||
class Admin(Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
self.last_eval_result = None
|
|
||||||
self.previous_eval_code = None
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_bot_manager)
|
|
||||||
@commands.command(name="exit", aliases=["quit", "bye"])
|
|
||||||
async def _exit(self, ctx):
|
|
||||||
"""Shuts down the bot, bot manager only."""
|
|
||||||
await ctx.send(":wave: Goodbye!")
|
|
||||||
await self.bot.close()
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_bot_manager)
|
|
||||||
@commands.command()
|
|
||||||
async def fetchlog(self, ctx):
|
|
||||||
"""Returns log"""
|
|
||||||
await ctx.send(
|
|
||||||
"Here's the current log file:",
|
|
||||||
file=discord.File(f"{self.bot.script_name}.log"),
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_bot_manager)
|
|
||||||
@commands.command()
|
|
||||||
async def fetchdata(self, ctx):
|
|
||||||
"""Returns data files"""
|
|
||||||
data_files = [discord.File(fpath) for fpath in self.bot.wanted_jsons]
|
|
||||||
await ctx.send("Here you go:", files=data_files)
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_bot_manager)
|
|
||||||
@commands.command(name="eval")
|
|
||||||
async def _eval(self, ctx, *, code: str):
|
|
||||||
"""Evaluates some code, bot manager only."""
|
|
||||||
try:
|
|
||||||
code = code.strip("` ")
|
|
||||||
|
|
||||||
env = {
|
|
||||||
"bot": self.bot,
|
|
||||||
"ctx": ctx,
|
|
||||||
"message": ctx.message,
|
|
||||||
"server": ctx.guild,
|
|
||||||
"guild": ctx.guild,
|
|
||||||
"channel": ctx.message.channel,
|
|
||||||
"author": ctx.message.author,
|
|
||||||
# modules
|
|
||||||
"discord": discord,
|
|
||||||
"commands": commands,
|
|
||||||
# utilities
|
|
||||||
"_get": discord.utils.get,
|
|
||||||
"_find": discord.utils.find,
|
|
||||||
# last result
|
|
||||||
"_": self.last_eval_result,
|
|
||||||
"_p": self.previous_eval_code,
|
|
||||||
}
|
|
||||||
env.update(globals())
|
|
||||||
|
|
||||||
self.bot.log.info(f"Evaling {repr(code)}:")
|
|
||||||
result = eval(code, env)
|
|
||||||
if inspect.isawaitable(result):
|
|
||||||
result = await result
|
|
||||||
|
|
||||||
if result is not None:
|
|
||||||
self.last_eval_result = result
|
|
||||||
|
|
||||||
self.previous_eval_code = code
|
|
||||||
|
|
||||||
sliced_message = await self.bot.slice_message(
|
|
||||||
repr(result), prefix="```", suffix="```"
|
|
||||||
)
|
|
||||||
for msg in sliced_message:
|
|
||||||
await ctx.send(msg)
|
|
||||||
except:
|
|
||||||
sliced_message = await self.bot.slice_message(
|
|
||||||
traceback.format_exc(), prefix="```", suffix="```"
|
|
||||||
)
|
|
||||||
for msg in sliced_message:
|
|
||||||
await ctx.send(msg)
|
|
||||||
|
|
||||||
async def cog_load_actions(self, cog_name):
|
|
||||||
if cog_name == "verification":
|
|
||||||
verif_channel = self.bot.get_channel(self.bot.config.welcome_channel)
|
|
||||||
await self.bot.do_resetalgo(verif_channel, "cog load")
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_bot_manager)
|
|
||||||
@commands.command()
|
|
||||||
async def pull(self, ctx, auto=False):
|
|
||||||
"""Does a git pull, bot manager only."""
|
|
||||||
tmp = await ctx.send("Pulling...")
|
|
||||||
git_output = await self.bot.async_call_shell("git pull")
|
|
||||||
await tmp.edit(content=f"Pull complete. Output: ```{git_output}```")
|
|
||||||
if auto:
|
|
||||||
cogs_to_reload = re.findall(r"cogs/([a-z_]*).py[ ]*\|", git_output)
|
|
||||||
for cog in cogs_to_reload:
|
|
||||||
cog_name = "robocop_ng.cogs." + cog
|
|
||||||
if cog_name not in self.bot.config.initial_cogs:
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.bot.unload_extension(cog_name)
|
|
||||||
await self.bot.load_extension(cog_name)
|
|
||||||
self.bot.log.info(f"Reloaded ext {cog}")
|
|
||||||
await ctx.send(f":white_check_mark: `{cog}` successfully reloaded.")
|
|
||||||
await self.cog_load_actions(cog)
|
|
||||||
except:
|
|
||||||
await ctx.send(
|
|
||||||
f":x: Cog reloading failed, traceback: "
|
|
||||||
f"```\n{traceback.format_exc()}\n```"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_bot_manager)
|
|
||||||
@commands.command()
|
|
||||||
async def load(self, ctx, ext: str):
|
|
||||||
"""Loads a cog, bot manager only."""
|
|
||||||
try:
|
|
||||||
await self.bot.load_extension("robocop_ng.cogs." + ext)
|
|
||||||
await self.cog_load_actions(ext)
|
|
||||||
except:
|
|
||||||
await ctx.send(
|
|
||||||
f":x: Cog loading failed, traceback: "
|
|
||||||
f"```\n{traceback.format_exc()}\n```"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
self.bot.log.info(f"Loaded ext {ext}")
|
|
||||||
await ctx.send(f":white_check_mark: `{ext}` successfully loaded.")
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_bot_manager)
|
|
||||||
@commands.command()
|
|
||||||
async def unload(self, ctx, ext: str):
|
|
||||||
"""Unloads a cog, bot manager only."""
|
|
||||||
await self.bot.unload_extension("robocop_ng.cogs." + ext)
|
|
||||||
self.bot.log.info(f"Unloaded ext {ext}")
|
|
||||||
await ctx.send(f":white_check_mark: `{ext}` successfully unloaded.")
|
|
||||||
|
|
||||||
@commands.check(check_if_bot_manager)
|
|
||||||
@commands.command()
|
|
||||||
async def reload(self, ctx, ext="_"):
|
|
||||||
"""Reloads a cog, bot manager only."""
|
|
||||||
if ext == "_":
|
|
||||||
ext = self.lastreload
|
|
||||||
else:
|
|
||||||
self.lastreload = ext
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.bot.unload_extension("robocop_ng.cogs." + ext)
|
|
||||||
await self.bot.load_extension("robocop_ng.cogs." + ext)
|
|
||||||
await self.cog_load_actions(ext)
|
|
||||||
except:
|
|
||||||
await ctx.send(
|
|
||||||
f":x: Cog reloading failed, traceback: "
|
|
||||||
f"```\n{traceback.format_exc()}\n```"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
self.bot.log.info(f"Reloaded ext {ext}")
|
|
||||||
await ctx.send(f":white_check_mark: `{ext}` successfully reloaded.")
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(Admin(bot))
|
|
|
@ -1,67 +0,0 @@
|
||||||
import time
|
|
||||||
|
|
||||||
import discord
|
|
||||||
from discord.ext import commands
|
|
||||||
from discord.ext.commands import Cog
|
|
||||||
|
|
||||||
|
|
||||||
class Basic(Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
async def hello(self, ctx):
|
|
||||||
"""Says hello. Duh."""
|
|
||||||
await ctx.send(f"Hello {ctx.author.mention}!")
|
|
||||||
|
|
||||||
@commands.cooldown(1, 10, type=commands.BucketType.user)
|
|
||||||
@commands.command(name="hex")
|
|
||||||
async def _hex(self, ctx, num: int):
|
|
||||||
"""Converts base 10 to 16 (for emummc sector calculation)"""
|
|
||||||
hex_val = hex(num).upper().replace("0X", "0x")
|
|
||||||
await ctx.send(f"{ctx.author.mention}: {hex_val}")
|
|
||||||
|
|
||||||
@commands.cooldown(1, 10, type=commands.BucketType.user)
|
|
||||||
@commands.command(name="dec")
|
|
||||||
async def _dec(self, ctx, num):
|
|
||||||
"""Converts base 16 to 10"""
|
|
||||||
await ctx.send(f"{ctx.author.mention}: {int(num, 16)}")
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.command()
|
|
||||||
async def membercount(self, ctx):
|
|
||||||
"""Prints the member count of the server."""
|
|
||||||
await ctx.send(f"{ctx.guild.name} has {ctx.guild.member_count} members!")
|
|
||||||
|
|
||||||
@commands.command(aliases=["robocopng", "robocop-ng"])
|
|
||||||
async def robocop(self, ctx):
|
|
||||||
"""Shows a quick embed with bot info."""
|
|
||||||
embed = discord.Embed(
|
|
||||||
title="Robocop-NG",
|
|
||||||
url=self.bot.config.source_url,
|
|
||||||
description=self.bot.config.embed_desc,
|
|
||||||
)
|
|
||||||
|
|
||||||
embed.set_thumbnail(url=str(self.bot.user.display_avatar))
|
|
||||||
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
|
|
||||||
@commands.command(aliases=["p"])
|
|
||||||
async def ping(self, ctx):
|
|
||||||
"""Shows ping values to discord.
|
|
||||||
|
|
||||||
RTT = Round-trip time, time taken to send a message to discord
|
|
||||||
GW = Gateway Ping"""
|
|
||||||
before = time.monotonic()
|
|
||||||
tmp = await ctx.send("Calculating ping...")
|
|
||||||
after = time.monotonic()
|
|
||||||
rtt_ms = (after - before) * 1000
|
|
||||||
gw_ms = self.bot.latency * 1000
|
|
||||||
|
|
||||||
message_text = f":ping_pong:\nrtt: `{rtt_ms:.1f}ms`\ngw: `{gw_ms:.1f}ms`"
|
|
||||||
self.bot.log.info(message_text)
|
|
||||||
await tmp.edit(content=message_text)
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(Basic(bot))
|
|
|
@ -1,29 +0,0 @@
|
||||||
from discord.ext import commands
|
|
||||||
from discord.ext.commands import Cog
|
|
||||||
|
|
||||||
|
|
||||||
class BasicReswitched(Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.command()
|
|
||||||
async def communitycount(self, ctx):
|
|
||||||
"""Prints the community member count of the server."""
|
|
||||||
community = ctx.guild.get_role(self.bot.config.named_roles["community"])
|
|
||||||
await ctx.send(
|
|
||||||
f"{ctx.guild.name} has {len(community.members)} community members!"
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.command()
|
|
||||||
async def hackercount(self, ctx):
|
|
||||||
"""Prints the hacker member count of the server."""
|
|
||||||
h4x0r = ctx.guild.get_role(self.bot.config.named_roles["hacker"])
|
|
||||||
await ctx.send(
|
|
||||||
f"{ctx.guild.name} has {len(h4x0r.members)} people with hacker role!"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(BasicReswitched(bot))
|
|
|
@ -1,40 +0,0 @@
|
||||||
import json
|
|
||||||
import os
|
|
||||||
|
|
||||||
import discord
|
|
||||||
from discord.ext import commands
|
|
||||||
from discord.ext.commands import Cog
|
|
||||||
|
|
||||||
from robocop_ng.helpers.checks import check_if_collaborator
|
|
||||||
from robocop_ng.helpers.invites import add_invite
|
|
||||||
|
|
||||||
|
|
||||||
class Invites(Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_collaborator)
|
|
||||||
async def invite(self, ctx):
|
|
||||||
welcome_channel = self.bot.get_channel(self.bot.config.welcome_channel)
|
|
||||||
author = ctx.message.author
|
|
||||||
reason = f"Created by {str(author)} ({author.id})"
|
|
||||||
invite = await welcome_channel.create_invite(
|
|
||||||
max_age=0, max_uses=1, temporary=True, unique=True, reason=reason
|
|
||||||
)
|
|
||||||
|
|
||||||
add_invite(self.bot, invite.id, invite.url, 1, invite.code)
|
|
||||||
|
|
||||||
await ctx.message.add_reaction("🆗")
|
|
||||||
try:
|
|
||||||
await ctx.author.send(f"Created single-use invite {invite.url}")
|
|
||||||
except discord.errors.Forbidden:
|
|
||||||
await ctx.send(
|
|
||||||
f"{ctx.author.mention} I could not send you the \
|
|
||||||
invite. Send me a DM so I can reply to you."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(Invites(bot))
|
|
|
@ -1,37 +0,0 @@
|
||||||
from discord.ext import commands
|
|
||||||
from discord.ext.commands import Cog
|
|
||||||
|
|
||||||
|
|
||||||
class Legacy(Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
@commands.command(hidden=True, aliases=["removehacker"])
|
|
||||||
async def probate(self, ctx):
|
|
||||||
"""Use .revoke <user> <role>"""
|
|
||||||
await ctx.send(
|
|
||||||
"This command was replaced with `.revoke <user> <role>`"
|
|
||||||
" on Robocop-NG, please use that instead."
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.command(hidden=True)
|
|
||||||
async def softlock(self, ctx):
|
|
||||||
"""Use .lock True"""
|
|
||||||
await ctx.send(
|
|
||||||
"This command was replaced with `.lock True`"
|
|
||||||
" on Robocop-NG, please use that instead.\n"
|
|
||||||
"Also... good luck, and sorry for taking your time. "
|
|
||||||
"Lockdown rarely means anything good."
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.command(hidden=True, aliases=["addhacker"])
|
|
||||||
async def unprobate(self, ctx):
|
|
||||||
"""Use .approve <user> <role>"""
|
|
||||||
await ctx.send(
|
|
||||||
"This command was replaced with `.approve <user> <role>`"
|
|
||||||
" on Robocop-NG, please use that instead."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(Legacy(bot))
|
|
|
@ -1,79 +0,0 @@
|
||||||
import discord
|
|
||||||
from discord.ext import commands
|
|
||||||
from discord.ext.commands import Cog
|
|
||||||
|
|
||||||
|
|
||||||
class Links(Cog):
|
|
||||||
"""
|
|
||||||
Commands for easily linking to projects.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
@commands.command(hidden=True)
|
|
||||||
async def pegaswitch(self, ctx):
|
|
||||||
"""Link to the Pegaswitch repo"""
|
|
||||||
await ctx.send("https://github.com/reswitched/pegaswitch")
|
|
||||||
|
|
||||||
@commands.command(hidden=True, aliases=["atmos"])
|
|
||||||
async def atmosphere(self, ctx):
|
|
||||||
"""Link to the Atmosphere repo"""
|
|
||||||
await ctx.send("https://github.com/atmosphere-nx/atmosphere")
|
|
||||||
|
|
||||||
@commands.command(hidden=True, aliases=["xyproblem"])
|
|
||||||
async def xy(self, ctx):
|
|
||||||
"""Link to the "What is the XY problem?" post from SE"""
|
|
||||||
await ctx.send(
|
|
||||||
"<https://meta.stackexchange.com/q/66377/285481>\n\n"
|
|
||||||
"TL;DR: It's asking about your attempted solution "
|
|
||||||
"rather than your actual problem.\n"
|
|
||||||
"It's perfectly okay to want to learn about a "
|
|
||||||
"solution, but please be clear about your intentions "
|
|
||||||
"if you're not actually trying to solve a problem."
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.command(hidden=True, aliases=["guides", "link"])
|
|
||||||
async def guide(self, ctx):
|
|
||||||
"""Link to the guides"""
|
|
||||||
await ctx.send(self.bot.config.links_guide_text)
|
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
async def source(self, ctx):
|
|
||||||
"""Gives link to source code."""
|
|
||||||
await ctx.send(
|
|
||||||
f"You can find my source at {self.bot.config.source_url}. "
|
|
||||||
"Serious PRs and issues welcome!"
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
async def rules(self, ctx, *, targetuser: discord.Member = None):
|
|
||||||
"""Post a link to the Rules"""
|
|
||||||
if not targetuser:
|
|
||||||
targetuser = ctx.author
|
|
||||||
await ctx.send(
|
|
||||||
f"{targetuser.mention}: A link to the rules "
|
|
||||||
f"can be found here: {self.bot.config.rules_url}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
async def community(self, ctx, *, targetuser: discord.Member = None):
|
|
||||||
"""Post a link to the community section of the rules"""
|
|
||||||
if not targetuser:
|
|
||||||
targetuser = ctx.author
|
|
||||||
await ctx.send(
|
|
||||||
f"{targetuser.mention}: "
|
|
||||||
"https://reswitched.github.io/discord/#member-roles-breakdown"
|
|
||||||
"\n\n"
|
|
||||||
"Community role allows access to the set of channels "
|
|
||||||
"on the community category (#off-topic, "
|
|
||||||
"#homebrew-development, #switch-hacking-general etc)."
|
|
||||||
"\n\n"
|
|
||||||
"What you need to get the role is to be around, "
|
|
||||||
"be helpful and nice to people and "
|
|
||||||
"show an understanding of rules."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(Links(bot))
|
|
|
@ -1,392 +0,0 @@
|
||||||
import io
|
|
||||||
import os.path
|
|
||||||
|
|
||||||
import discord
|
|
||||||
from discord.ext import commands
|
|
||||||
from discord.ext.commands import Cog
|
|
||||||
|
|
||||||
|
|
||||||
class Lists(Cog):
|
|
||||||
"""
|
|
||||||
Manages channels that are dedicated to lists.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
# Helpers
|
|
||||||
|
|
||||||
def check_if_target_is_staff(self, target):
|
|
||||||
return any(r.id in self.bot.config.staff_role_ids for r in target.roles)
|
|
||||||
|
|
||||||
def is_edit(self, emoji):
|
|
||||||
return str(emoji)[0] == "✏" or str(emoji)[0] == "📝"
|
|
||||||
|
|
||||||
def is_delete(self, emoji):
|
|
||||||
return str(emoji)[0] == "❌" or str(emoji)[0] == "❎"
|
|
||||||
|
|
||||||
def is_recycle(self, emoji):
|
|
||||||
return str(emoji)[0] == "♻"
|
|
||||||
|
|
||||||
def is_insert_above(self, emoji):
|
|
||||||
return str(emoji)[0] == "⤴️" or str(emoji)[0] == "⬆"
|
|
||||||
|
|
||||||
def is_insert_below(self, emoji):
|
|
||||||
return str(emoji)[0] == "⤵️" or str(emoji)[0] == "⬇"
|
|
||||||
|
|
||||||
def is_reaction_valid(self, reaction):
|
|
||||||
allowed_reactions = [
|
|
||||||
"✏",
|
|
||||||
"📝",
|
|
||||||
"❌",
|
|
||||||
"❎",
|
|
||||||
"♻",
|
|
||||||
"⤴️",
|
|
||||||
"⬆",
|
|
||||||
"⬇",
|
|
||||||
"⤵️",
|
|
||||||
]
|
|
||||||
return str(reaction.emoji)[0] in allowed_reactions
|
|
||||||
|
|
||||||
async def find_reactions(self, user_id, channel_id, limit=None):
|
|
||||||
reactions = []
|
|
||||||
channel = self.bot.get_channel(channel_id)
|
|
||||||
async for message in channel.history(limit=limit):
|
|
||||||
if len(message.reactions) == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
for reaction in message.reactions:
|
|
||||||
users = await reaction.users().flatten()
|
|
||||||
user_ids = map(lambda user: user.id, users)
|
|
||||||
if user_id in user_ids:
|
|
||||||
reactions.append(reaction)
|
|
||||||
|
|
||||||
return reactions
|
|
||||||
|
|
||||||
def create_log_message(self, emoji, action, user, channel, reason=""):
|
|
||||||
msg = (
|
|
||||||
f"{emoji} **{action}** \n"
|
|
||||||
f"from {self.bot.escape_message(user.name)} ({user.id}), in {channel.mention}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if reason != "":
|
|
||||||
msg += f":\n`{reason}`"
|
|
||||||
|
|
||||||
return msg
|
|
||||||
|
|
||||||
async def clean_up_raw_text_file_message(self, message):
|
|
||||||
embeds = message.embeds
|
|
||||||
if len(embeds) == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
fields = embeds[0].fields
|
|
||||||
for field in fields:
|
|
||||||
if field.name == "Message ID":
|
|
||||||
files_channel = self.bot.get_channel(self.bot.config.list_files_channel)
|
|
||||||
file_message = await files_channel.fetch_message(int(field.value))
|
|
||||||
await file_message.delete()
|
|
||||||
|
|
||||||
await message.edit(embed=None)
|
|
||||||
|
|
||||||
async def cache_message(self, message):
|
|
||||||
msg = {
|
|
||||||
"has_attachment": False,
|
|
||||||
"attachment_filename": "",
|
|
||||||
"attachment_data": b"",
|
|
||||||
"content": message.content,
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(message.attachments) != 0:
|
|
||||||
attachment = next(
|
|
||||||
(
|
|
||||||
a
|
|
||||||
for a in message.attachments
|
|
||||||
if os.path.splitext(a.filename)[1] in [".png", ".jpg", ".jpeg"]
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
if attachment is not None:
|
|
||||||
msg["has_attachment"] = True
|
|
||||||
msg["attachment_filename"] = attachment.filename
|
|
||||||
msg["attachment_data"] = await attachment.read()
|
|
||||||
|
|
||||||
return msg
|
|
||||||
|
|
||||||
async def send_cached_message(self, channel, message):
|
|
||||||
if message["has_attachment"] == True:
|
|
||||||
file = discord.File(
|
|
||||||
io.BytesIO(message["attachment_data"]),
|
|
||||||
filename=message["attachment_filename"],
|
|
||||||
)
|
|
||||||
await channel.send(content=message["content"], file=file)
|
|
||||||
else:
|
|
||||||
await channel.send(content=message["content"])
|
|
||||||
|
|
||||||
# Commands
|
|
||||||
|
|
||||||
@commands.command(aliases=["list"])
|
|
||||||
async def listitem(self, ctx, channel: discord.TextChannel, number: int):
|
|
||||||
"""Link to a specific list item."""
|
|
||||||
if number <= 0:
|
|
||||||
await ctx.send(f"Number must be greater than 0.")
|
|
||||||
return
|
|
||||||
|
|
||||||
if channel.id not in self.bot.config.list_channels:
|
|
||||||
await ctx.send(f"{channel.mention} is not a list channel.")
|
|
||||||
return
|
|
||||||
|
|
||||||
counter = 0
|
|
||||||
async for message in channel.history(limit=None, oldest_first=True):
|
|
||||||
if message.content.strip():
|
|
||||||
counter += 1
|
|
||||||
|
|
||||||
if counter == number:
|
|
||||||
embed = discord.Embed(
|
|
||||||
title=f"Item #{number} in #{channel.name}",
|
|
||||||
description=message.content,
|
|
||||||
url=message.jump_url,
|
|
||||||
)
|
|
||||||
await ctx.send(content="", embed=embed)
|
|
||||||
return
|
|
||||||
|
|
||||||
await ctx.send(f"Unable to find item #{number} in {channel.mention}.")
|
|
||||||
|
|
||||||
# Listeners
|
|
||||||
|
|
||||||
@Cog.listener()
|
|
||||||
async def on_raw_reaction_add(self, payload):
|
|
||||||
await self.bot.wait_until_ready()
|
|
||||||
|
|
||||||
# We only care about reactions in Rules, and Support FAQ
|
|
||||||
if payload.channel_id not in self.bot.config.list_channels:
|
|
||||||
return
|
|
||||||
|
|
||||||
channel = self.bot.get_channel(payload.channel_id)
|
|
||||||
message = await channel.fetch_message(payload.message_id)
|
|
||||||
member = channel.guild.get_member(payload.user_id)
|
|
||||||
user = self.bot.get_user(payload.user_id)
|
|
||||||
reaction = next(
|
|
||||||
(
|
|
||||||
reaction
|
|
||||||
for reaction in message.reactions
|
|
||||||
if str(reaction.emoji) == str(payload.emoji)
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
if reaction is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Only staff can add reactions in these channels.
|
|
||||||
if not self.check_if_target_is_staff(member):
|
|
||||||
await reaction.remove(user)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Reactions are only allowed on messages from the bot.
|
|
||||||
if not message.author.bot:
|
|
||||||
await reaction.remove(user)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Only certain reactions are allowed.
|
|
||||||
if not self.is_reaction_valid(reaction):
|
|
||||||
await reaction.remove(user)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Remove all other reactions from user in this channel.
|
|
||||||
for r in await self.find_reactions(payload.user_id, payload.channel_id):
|
|
||||||
if r.message.id != message.id or (
|
|
||||||
r.message.id == message.id and str(r.emoji) != str(reaction.emoji)
|
|
||||||
):
|
|
||||||
await r.remove(user)
|
|
||||||
|
|
||||||
# When editing we want to provide the user a copy of the raw text.
|
|
||||||
if self.is_edit(reaction.emoji) and self.bot.config.list_files_channel != 0:
|
|
||||||
files_channel = self.bot.get_channel(self.bot.config.list_files_channel)
|
|
||||||
file = discord.File(
|
|
||||||
io.BytesIO(message.content.encode("utf-8")),
|
|
||||||
filename=f"{message.id}.txt",
|
|
||||||
)
|
|
||||||
file_message = await files_channel.send(file=file)
|
|
||||||
|
|
||||||
embed = discord.Embed(
|
|
||||||
title="Click here to get the raw text to modify.",
|
|
||||||
url=f"{file_message.attachments[0].url}?",
|
|
||||||
)
|
|
||||||
embed.add_field(name="Message ID", value=file_message.id, inline=False)
|
|
||||||
await message.edit(embed=embed)
|
|
||||||
|
|
||||||
@Cog.listener()
|
|
||||||
async def on_raw_reaction_remove(self, payload):
|
|
||||||
await self.bot.wait_until_ready()
|
|
||||||
|
|
||||||
# We only care about reactions in Rules, and Support FAQ
|
|
||||||
if payload.channel_id not in self.bot.config.list_channels:
|
|
||||||
return
|
|
||||||
|
|
||||||
channel = self.bot.get_channel(payload.channel_id)
|
|
||||||
message = await channel.fetch_message(payload.message_id)
|
|
||||||
|
|
||||||
# Reaction was removed from a message we don"t care about.
|
|
||||||
if not message.author.bot:
|
|
||||||
return
|
|
||||||
|
|
||||||
# We want to remove the embed we added.
|
|
||||||
if self.is_edit(payload.emoji) and self.bot.config.list_files_channel != 0:
|
|
||||||
await self.clean_up_raw_text_file_message(message)
|
|
||||||
|
|
||||||
@Cog.listener()
|
|
||||||
async def on_message(self, message):
|
|
||||||
await self.bot.wait_until_ready()
|
|
||||||
|
|
||||||
# We only care about messages in Rules, and Support FAQ
|
|
||||||
if message.channel.id not in self.bot.config.list_channels:
|
|
||||||
return
|
|
||||||
|
|
||||||
# We don"t care about messages from bots.
|
|
||||||
if message.author.bot:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Only staff can modify lists.
|
|
||||||
if not self.check_if_target_is_staff(message.author):
|
|
||||||
await message.delete()
|
|
||||||
return
|
|
||||||
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.log_channel)
|
|
||||||
channel = message.channel
|
|
||||||
content = message.content
|
|
||||||
user = message.author
|
|
||||||
|
|
||||||
attachment_filename = None
|
|
||||||
attachment_data = None
|
|
||||||
if len(message.attachments) != 0:
|
|
||||||
# Lists will only reupload the first image.
|
|
||||||
attachment = next(
|
|
||||||
(
|
|
||||||
a
|
|
||||||
for a in message.attachments
|
|
||||||
if os.path.splitext(a.filename)[1] in [".png", ".jpg", ".jpeg"]
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
if attachment is not None:
|
|
||||||
attachment_filename = attachment.filename
|
|
||||||
attachment_data = await attachment.read()
|
|
||||||
|
|
||||||
await message.delete()
|
|
||||||
|
|
||||||
reactions = await self.find_reactions(user.id, channel.id)
|
|
||||||
|
|
||||||
# Add to the end of the list if there is no reactions or somehow more
|
|
||||||
# than one.
|
|
||||||
if len(reactions) != 1:
|
|
||||||
if attachment_filename is not None and attachment_data is not None:
|
|
||||||
file = discord.File(
|
|
||||||
io.BytesIO(attachment_data), filename=attachment_filename
|
|
||||||
)
|
|
||||||
await channel.send(content=content, file=file)
|
|
||||||
else:
|
|
||||||
await channel.send(content)
|
|
||||||
|
|
||||||
for reaction in reactions:
|
|
||||||
await reaction.remove(user)
|
|
||||||
|
|
||||||
await log_channel.send(
|
|
||||||
self.create_log_message("💬", "List item added:", user, channel)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
targeted_reaction = reactions[0]
|
|
||||||
targeted_message = targeted_reaction.message
|
|
||||||
|
|
||||||
if self.is_edit(targeted_reaction):
|
|
||||||
if self.bot.config.list_files_channel != 0:
|
|
||||||
await self.clean_up_raw_text_file_message(targeted_message)
|
|
||||||
await targeted_message.edit(content=content)
|
|
||||||
await targeted_reaction.remove(user)
|
|
||||||
|
|
||||||
await log_channel.send(
|
|
||||||
self.create_log_message("📝", "List item edited:", user, channel)
|
|
||||||
)
|
|
||||||
|
|
||||||
elif self.is_delete(targeted_reaction):
|
|
||||||
await targeted_message.delete()
|
|
||||||
|
|
||||||
await log_channel.send(
|
|
||||||
self.create_log_message(
|
|
||||||
"❌", "List item deleted:", user, channel, content
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
elif self.is_recycle(targeted_reaction):
|
|
||||||
messages = [await self.cache_message(targeted_message)]
|
|
||||||
|
|
||||||
for message in await channel.history(
|
|
||||||
limit=None, after=targeted_message, oldest_first=True
|
|
||||||
).flatten():
|
|
||||||
messages.append(await self.cache_message(message))
|
|
||||||
|
|
||||||
await channel.purge(limit=len(messages) + 1, bulk=True)
|
|
||||||
|
|
||||||
for message in messages:
|
|
||||||
await self.send_cached_message(channel, message)
|
|
||||||
|
|
||||||
await log_channel.send(
|
|
||||||
self.create_log_message(
|
|
||||||
"♻", "List item recycled:", user, channel, content
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
elif self.is_insert_above(targeted_reaction):
|
|
||||||
messages = [await self.cache_message(targeted_message)]
|
|
||||||
|
|
||||||
for message in await channel.history(
|
|
||||||
limit=None, after=targeted_message, oldest_first=True
|
|
||||||
).flatten():
|
|
||||||
messages.append(await self.cache_message(message))
|
|
||||||
|
|
||||||
await channel.purge(limit=len(messages) + 1, bulk=True)
|
|
||||||
|
|
||||||
if attachment_filename is not None and attachment_data is not None:
|
|
||||||
file = discord.File(
|
|
||||||
io.BytesIO(attachment_data), filename=attachment_filename
|
|
||||||
)
|
|
||||||
await channel.send(content=content, file=file)
|
|
||||||
else:
|
|
||||||
await channel.send(content)
|
|
||||||
|
|
||||||
for message in messages:
|
|
||||||
await self.send_cached_message(channel, message)
|
|
||||||
|
|
||||||
await log_channel.send(
|
|
||||||
self.create_log_message("💬", "List item added:", user, channel)
|
|
||||||
)
|
|
||||||
|
|
||||||
elif self.is_insert_below(targeted_reaction):
|
|
||||||
await targeted_reaction.remove(user)
|
|
||||||
|
|
||||||
messages = []
|
|
||||||
|
|
||||||
for message in await channel.history(
|
|
||||||
limit=None, after=targeted_message, oldest_first=True
|
|
||||||
).flatten():
|
|
||||||
messages.append(await self.cache_message(message))
|
|
||||||
|
|
||||||
await channel.purge(limit=len(messages), bulk=True)
|
|
||||||
|
|
||||||
if attachment_filename is not None and attachment_data is not None:
|
|
||||||
file = discord.File(
|
|
||||||
io.BytesIO(attachment_data), filename=attachment_filename
|
|
||||||
)
|
|
||||||
await channel.send(content=content, file=file)
|
|
||||||
else:
|
|
||||||
await channel.send(content)
|
|
||||||
|
|
||||||
for message in messages:
|
|
||||||
await self.send_cached_message(channel, message)
|
|
||||||
|
|
||||||
await log_channel.send(
|
|
||||||
self.create_log_message("💬", "List item added:", user, channel)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(Lists(bot))
|
|
|
@ -1,710 +0,0 @@
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
from discord import Colour, Embed, Message, Attachment
|
|
||||||
from discord.ext import commands
|
|
||||||
from discord.ext.commands import Cog, Context, BucketType
|
|
||||||
|
|
||||||
from robocop_ng.helpers.checks import check_if_staff
|
|
||||||
from robocop_ng.helpers.disabled_ids import (
|
|
||||||
add_disabled_app_id,
|
|
||||||
is_app_id_valid,
|
|
||||||
remove_disabled_app_id,
|
|
||||||
get_disabled_ids,
|
|
||||||
is_app_id_disabled,
|
|
||||||
is_build_id_valid,
|
|
||||||
add_disabled_build_id,
|
|
||||||
remove_disabled_build_id,
|
|
||||||
is_build_id_disabled,
|
|
||||||
is_ro_section_disabled,
|
|
||||||
is_ro_section_valid,
|
|
||||||
add_disabled_ro_section,
|
|
||||||
remove_disabled_ro_section,
|
|
||||||
remove_disable_id,
|
|
||||||
)
|
|
||||||
from robocop_ng.helpers.disabled_paths import (
|
|
||||||
is_path_disabled,
|
|
||||||
get_disabled_paths,
|
|
||||||
add_disabled_path,
|
|
||||||
remove_disabled_path,
|
|
||||||
)
|
|
||||||
from robocop_ng.helpers.ryujinx_log_analyser import LogAnalyser
|
|
||||||
|
|
||||||
logging.basicConfig(
|
|
||||||
format="%(asctime)s (%(levelname)s) %(message)s (Line %(lineno)d)",
|
|
||||||
level=logging.INFO,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class LogFileReader(Cog):
|
|
||||||
@staticmethod
|
|
||||||
def is_valid_log_name(attachment: Attachment) -> tuple[bool, bool]:
|
|
||||||
filename = attachment.filename
|
|
||||||
ryujinx_log_file_regex = re.compile(r"^Ryujinx_.*\.log$")
|
|
||||||
log_file = re.compile(r"^.*\.log|.*\.txt$")
|
|
||||||
is_ryujinx_log_file = re.match(ryujinx_log_file_regex, filename) is not None
|
|
||||||
is_log_file = re.match(log_file, filename) is not None
|
|
||||||
|
|
||||||
return is_log_file, is_ryujinx_log_file
|
|
||||||
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
self.bot_log_allowed_channels = self.bot.config.bot_log_allowed_channels
|
|
||||||
self.disallowed_named_roles = ["pirate"]
|
|
||||||
self.ryujinx_blue = Colour(0x4A90E2)
|
|
||||||
self.uploaded_log_info = []
|
|
||||||
|
|
||||||
self.disallowed_roles = [
|
|
||||||
self.bot.config.named_roles[x] for x in self.disallowed_named_roles
|
|
||||||
]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def download_file(log_url):
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
# Grabs first and last few bytes of log file to prevent abuse from large files
|
|
||||||
headers = {"Range": "bytes=0-60000, -6000"}
|
|
||||||
async with session.get(log_url, headers=headers) as response:
|
|
||||||
return await response.text("UTF-8")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_log_valid(log_file: str) -> bool:
|
|
||||||
app_info = LogAnalyser.get_app_info(log_file)
|
|
||||||
is_homebrew = LogAnalyser.is_homebrew(log_file)
|
|
||||||
if app_info is None or is_homebrew:
|
|
||||||
return True
|
|
||||||
game_name, app_id, another_app_id, build_ids, main_ro_section = app_info
|
|
||||||
if (
|
|
||||||
game_name is None
|
|
||||||
or app_id is None
|
|
||||||
or another_app_id is None
|
|
||||||
or build_ids is None
|
|
||||||
or main_ro_section is None
|
|
||||||
):
|
|
||||||
return False
|
|
||||||
return app_id == another_app_id
|
|
||||||
|
|
||||||
def is_game_blocked(self, log_file: str) -> bool:
|
|
||||||
app_info = LogAnalyser.get_app_info(log_file)
|
|
||||||
if app_info is None:
|
|
||||||
return False
|
|
||||||
game_name, app_id, another_app_id, build_ids, main_ro_section = app_info
|
|
||||||
if is_app_id_disabled(self.bot, app_id) or is_app_id_disabled(
|
|
||||||
self.bot, another_app_id
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
for bid in build_ids:
|
|
||||||
if is_build_id_disabled(self.bot, bid):
|
|
||||||
return True
|
|
||||||
return is_ro_section_disabled(self.bot, main_ro_section)
|
|
||||||
|
|
||||||
def contains_blocked_paths(self, log_file: str) -> Optional[str]:
|
|
||||||
filepaths = LogAnalyser.get_filepaths(log_file)
|
|
||||||
if filepaths is None:
|
|
||||||
return None
|
|
||||||
for filepath in filepaths:
|
|
||||||
if is_path_disabled(self.bot, filepath):
|
|
||||||
return filepath
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def blocked_game_action(self, message: Message) -> Embed:
|
|
||||||
warn_command = self.bot.get_command("warn")
|
|
||||||
if warn_command is not None:
|
|
||||||
warn_message = await message.reply(
|
|
||||||
".warn This log contains a blocked game."
|
|
||||||
)
|
|
||||||
warn_context = await self.bot.get_context(warn_message)
|
|
||||||
await warn_context.invoke(
|
|
||||||
warn_command,
|
|
||||||
target=None,
|
|
||||||
reason="This log contains a blocked game.",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logging.error(
|
|
||||||
f"Couldn't find 'warn' command. Unable to warn {message.author} for uploading a log of a blocked game."
|
|
||||||
)
|
|
||||||
|
|
||||||
pirate_role = message.guild.get_role(self.bot.config.named_roles["pirate"])
|
|
||||||
await message.author.add_roles(pirate_role)
|
|
||||||
|
|
||||||
embed = Embed(
|
|
||||||
title="⛔ Blocked game detected ⛔",
|
|
||||||
colour=Colour(0xFF0000),
|
|
||||||
description="This log contains a blocked game and has been removed.\n"
|
|
||||||
"The user has been warned and the pirate role has been applied.",
|
|
||||||
)
|
|
||||||
embed.set_footer(text=f"Log uploaded by @{message.author.name}")
|
|
||||||
await message.delete()
|
|
||||||
return embed
|
|
||||||
|
|
||||||
async def blocked_path_action(self, message: Message, blocked_path: str) -> Embed:
|
|
||||||
warn_command = self.bot.get_command("warn")
|
|
||||||
if warn_command is not None:
|
|
||||||
warn_message = await message.reply(
|
|
||||||
".warn This log contains blocked content in paths."
|
|
||||||
)
|
|
||||||
warn_context = await self.bot.get_context(warn_message)
|
|
||||||
await warn_context.invoke(
|
|
||||||
warn_command,
|
|
||||||
target=None,
|
|
||||||
reason=f"This log contains blocked content in paths: '{blocked_path}'",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logging.error(
|
|
||||||
f"Couldn't find 'warn' command. Unable to warn {message.author} for uploading a log "
|
|
||||||
f"containing a blocked content in paths."
|
|
||||||
)
|
|
||||||
|
|
||||||
pirate_role = message.guild.get_role(self.bot.config.named_roles["pirate"])
|
|
||||||
await message.author.add_roles(pirate_role)
|
|
||||||
|
|
||||||
embed = Embed(
|
|
||||||
title="⛔ Blocked content in path detected ⛔",
|
|
||||||
colour=Colour(0xFF0000),
|
|
||||||
description="This log contains paths containing blocked content and has been removed.\n"
|
|
||||||
"The user has been warned and the pirate role has been applied.",
|
|
||||||
)
|
|
||||||
embed.set_footer(text=f"Log uploaded by @{message.author.name}")
|
|
||||||
await message.delete()
|
|
||||||
return embed
|
|
||||||
|
|
||||||
def format_analysed_log(self, author_name: str, analysed_log):
|
|
||||||
cleaned_game_name = re.sub(
|
|
||||||
r"\s\[(64|32)-bit\]$", "", analysed_log["game_info"]["game_name"]
|
|
||||||
)
|
|
||||||
analysed_log["game_info"]["game_name"] = cleaned_game_name
|
|
||||||
|
|
||||||
hardware_info = " | ".join(
|
|
||||||
(
|
|
||||||
f"**CPU:** {analysed_log['hardware_info']['cpu']}",
|
|
||||||
f"**GPU:** {analysed_log['hardware_info']['gpu']}",
|
|
||||||
f"**RAM:** {analysed_log['hardware_info']['ram']}",
|
|
||||||
f"**OS:** {analysed_log['hardware_info']['os']}",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
system_settings_info = "\n".join(
|
|
||||||
(
|
|
||||||
f"**Audio Backend:** `{analysed_log['settings']['audio_backend']}`",
|
|
||||||
f"**Console Mode:** `{analysed_log['settings']['docked']}`",
|
|
||||||
f"**PPTC Cache:** `{analysed_log['settings']['pptc']}`",
|
|
||||||
f"**Shader Cache:** `{analysed_log['settings']['shader_cache']}`",
|
|
||||||
f"**V-Sync:** `{analysed_log['settings']['vsync']}`",
|
|
||||||
f"**Hypervisor:** `{analysed_log['settings']['hypervisor']}`",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
graphics_settings_info = "\n".join(
|
|
||||||
(
|
|
||||||
f"**Graphics Backend:** `{analysed_log['settings']['graphics_backend']}`",
|
|
||||||
f"**Resolution:** `{analysed_log['settings']['resolution_scale']}`",
|
|
||||||
f"**Anisotropic Filtering:** `{analysed_log['settings']['anisotropic_filtering']}`",
|
|
||||||
f"**Aspect Ratio:** `{analysed_log['settings']['aspect_ratio']}`",
|
|
||||||
f"**Texture Recompression:** `{analysed_log['settings']['texture_recompression']}`",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
ryujinx_info = " | ".join(
|
|
||||||
(
|
|
||||||
f"**Version:** {analysed_log['emu_info']['ryu_version']}",
|
|
||||||
f"**Firmware:** {analysed_log['emu_info']['ryu_firmware']}",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
log_embed = Embed(title=f"{cleaned_game_name}", colour=self.ryujinx_blue)
|
|
||||||
log_embed.set_footer(text=f"Log uploaded by {author_name}")
|
|
||||||
log_embed.add_field(
|
|
||||||
name="General Info",
|
|
||||||
value=" | ".join((ryujinx_info, hardware_info)),
|
|
||||||
inline=False,
|
|
||||||
)
|
|
||||||
log_embed.add_field(
|
|
||||||
name="System Settings",
|
|
||||||
value=system_settings_info,
|
|
||||||
inline=True,
|
|
||||||
)
|
|
||||||
log_embed.add_field(
|
|
||||||
name="Graphics Settings",
|
|
||||||
value=graphics_settings_info,
|
|
||||||
inline=True,
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
cleaned_game_name == "Unknown"
|
|
||||||
and analysed_log["game_info"]["errors"] == "No errors found in log"
|
|
||||||
):
|
|
||||||
log_embed.add_field(
|
|
||||||
name="Empty Log",
|
|
||||||
value=f"""The log file appears to be empty. To get a proper log, follow these steps:
|
|
||||||
1) In Logging settings, ensure `Enable Logging to File` is checked.
|
|
||||||
2) Ensure the following default logs are enabled: `Info`, `Warning`, `Error`, `Guest` and `Stub`.
|
|
||||||
3) Start a game up.
|
|
||||||
4) Play until your issue occurs.
|
|
||||||
5) Upload the latest log file which is larger than 3KB.""",
|
|
||||||
inline=False,
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
cleaned_game_name == "Unknown"
|
|
||||||
and analysed_log["game_info"]["errors"] != "No errors found in log"
|
|
||||||
):
|
|
||||||
log_embed.add_field(
|
|
||||||
name="Latest Error Snippet",
|
|
||||||
value=analysed_log["game_info"]["errors"],
|
|
||||||
inline=False,
|
|
||||||
)
|
|
||||||
log_embed.add_field(
|
|
||||||
name="No Game Boot Detected",
|
|
||||||
value=f"""No game boot has been detected in log file. To get a proper log, follow these steps:
|
|
||||||
1) In Logging settings, ensure `Enable Logging to File` is checked.
|
|
||||||
2) Ensure the following default logs are enabled: `Info`, `Warning`, `Error`, `Guest` and `Stub`.
|
|
||||||
3) Start a game up.
|
|
||||||
4) Play until your issue occurs.
|
|
||||||
5) Upload the latest log file which is larger than 3KB.""",
|
|
||||||
inline=False,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
log_embed.add_field(
|
|
||||||
name="Latest Error Snippet",
|
|
||||||
value=analysed_log["game_info"]["errors"],
|
|
||||||
inline=False,
|
|
||||||
)
|
|
||||||
log_embed.add_field(
|
|
||||||
name="Mods", value=analysed_log["game_info"]["mods"], inline=False
|
|
||||||
)
|
|
||||||
log_embed.add_field(
|
|
||||||
name="Cheats", value=analysed_log["game_info"]["cheats"], inline=False
|
|
||||||
)
|
|
||||||
|
|
||||||
log_embed.add_field(
|
|
||||||
name="Notes",
|
|
||||||
value=analysed_log["game_info"]["notes"],
|
|
||||||
inline=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
return log_embed
|
|
||||||
|
|
||||||
async def log_file_read(self, message):
|
|
||||||
attached_log = message.attachments[0]
|
|
||||||
author_name = f"@{message.author.name}"
|
|
||||||
log_file = await self.download_file(attached_log.url)
|
|
||||||
|
|
||||||
if self.is_game_blocked(log_file):
|
|
||||||
return await self.blocked_game_action(message)
|
|
||||||
blocked_path = self.contains_blocked_paths(log_file)
|
|
||||||
if blocked_path:
|
|
||||||
return await self.blocked_path_action(message, blocked_path)
|
|
||||||
|
|
||||||
for role in message.author.roles:
|
|
||||||
if role.id in self.disallowed_roles:
|
|
||||||
embed = Embed(
|
|
||||||
colour=Colour(0xFF0000),
|
|
||||||
description="I'm not allowed to analyse this log.",
|
|
||||||
)
|
|
||||||
embed.set_footer(text=f"Log uploaded by {author_name}")
|
|
||||||
return embed
|
|
||||||
|
|
||||||
if not self.is_log_valid(log_file):
|
|
||||||
embed = Embed(
|
|
||||||
title="⚠️ Modified log detected ⚠️",
|
|
||||||
colour=Colour(0xFCFC00),
|
|
||||||
description="This log contains manually modified information and won't be analysed.",
|
|
||||||
)
|
|
||||||
embed.set_footer(text=f"Log uploaded by {author_name}")
|
|
||||||
return embed
|
|
||||||
|
|
||||||
try:
|
|
||||||
analyser = LogAnalyser(log_file)
|
|
||||||
except ValueError:
|
|
||||||
return Embed(
|
|
||||||
colour=self.ryujinx_blue,
|
|
||||||
description="This log file appears to be invalid. Please make sure to upload a Ryujinx log file.",
|
|
||||||
)
|
|
||||||
|
|
||||||
is_channel_allowed = False
|
|
||||||
for allowed_channel_id in self.bot.config.bot_log_allowed_channels.values():
|
|
||||||
if message.channel.id == allowed_channel_id:
|
|
||||||
is_channel_allowed = True
|
|
||||||
break
|
|
||||||
|
|
||||||
return self.format_analysed_log(
|
|
||||||
author_name,
|
|
||||||
analyser.analyse_discord(
|
|
||||||
is_channel_allowed,
|
|
||||||
self.bot.config.bot_log_allowed_channels["pr-testing"],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command(
|
|
||||||
aliases=["disallow_log_id", "forbid_log_id", "block_id", "blockid"]
|
|
||||||
)
|
|
||||||
async def disable_log_id(
|
|
||||||
self, ctx: Context, disable_id: str, block_id_type: str, *, block_id: str
|
|
||||||
):
|
|
||||||
match block_id_type.lower():
|
|
||||||
case "app" | "app_id" | "appid" | "tid" | "title_id":
|
|
||||||
if not is_app_id_valid(block_id):
|
|
||||||
return await ctx.send("The specified app id is invalid.")
|
|
||||||
|
|
||||||
if add_disabled_app_id(self.bot, disable_id, block_id):
|
|
||||||
return await ctx.send(
|
|
||||||
f"Application id '{block_id}' is now blocked!"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return await ctx.send(
|
|
||||||
f"Application id '{block_id}' is already blocked."
|
|
||||||
)
|
|
||||||
case "build" | "build_id" | "bid":
|
|
||||||
if not is_build_id_valid(block_id):
|
|
||||||
return await ctx.send("The specified build id is invalid.")
|
|
||||||
|
|
||||||
if add_disabled_build_id(self.bot, disable_id, block_id):
|
|
||||||
return await ctx.send(f"Build id '{block_id}' is now blocked!")
|
|
||||||
else:
|
|
||||||
return await ctx.send(f"Build id '{block_id}' is already blocked.")
|
|
||||||
case "ro_section" | "rosection":
|
|
||||||
ro_section_snippet = block_id.strip("`").splitlines()
|
|
||||||
ro_section_snippet = [
|
|
||||||
line for line in ro_section_snippet if len(line.strip()) > 0
|
|
||||||
]
|
|
||||||
|
|
||||||
ro_section_info_regex = re.search(
|
|
||||||
r"PrintRoSectionInfo: main:", ro_section_snippet[0]
|
|
||||||
)
|
|
||||||
if ro_section_info_regex is None:
|
|
||||||
ro_section_snippet.insert(0, "PrintRoSectionInfo: main:")
|
|
||||||
|
|
||||||
ro_section = LogAnalyser.get_main_ro_section(
|
|
||||||
"\n".join(ro_section_snippet)
|
|
||||||
)
|
|
||||||
if ro_section is not None and is_ro_section_valid(ro_section):
|
|
||||||
if add_disabled_ro_section(self.bot, disable_id, ro_section):
|
|
||||||
return await ctx.send(
|
|
||||||
f"The specified read-only section for '{disable_id}' is now blocked."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return await ctx.send(
|
|
||||||
f"The specified read-only section for '{disable_id}' is already blocked."
|
|
||||||
)
|
|
||||||
case _:
|
|
||||||
return await ctx.send(
|
|
||||||
"The specified id type is invalid. Valid id types are: ['app_id', 'build_id', 'ro_section']"
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command(
|
|
||||||
aliases=[
|
|
||||||
"allow_log_id",
|
|
||||||
"unblock_log_id",
|
|
||||||
"unblock_id",
|
|
||||||
"allow_id",
|
|
||||||
"unblockid",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
async def enable_log_id(self, ctx: Context, disable_id: str, block_id_type="all"):
|
|
||||||
match block_id_type.lower():
|
|
||||||
case "all":
|
|
||||||
if remove_disable_id(self.bot, disable_id):
|
|
||||||
return await ctx.send(
|
|
||||||
f"All ids for '{disable_id}' are now unblocked!"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return await ctx.send(f"No blocked ids for '{disable_id}' found.")
|
|
||||||
case "app" | "app_id" | "appid" | "tid" | "title_id":
|
|
||||||
if remove_disabled_app_id(self.bot, disable_id):
|
|
||||||
return await ctx.send(
|
|
||||||
f"Application id for '{disable_id}' is now unblocked!"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return await ctx.send(
|
|
||||||
f"No blocked application id for '{disable_id}' found."
|
|
||||||
)
|
|
||||||
case "build" | "build_id" | "bid":
|
|
||||||
if remove_disabled_build_id(self.bot, disable_id):
|
|
||||||
return await ctx.send(
|
|
||||||
f"Build id for '{disable_id}' is now unblocked!"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return await ctx.send(f"No blocked build id '{disable_id}' found.")
|
|
||||||
case "ro_section" | "rosection":
|
|
||||||
if remove_disabled_ro_section(self.bot, disable_id):
|
|
||||||
return await ctx.send(
|
|
||||||
f"Read-only section for '{disable_id}' is now unblocked!"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return await ctx.send(
|
|
||||||
f"No blocked read-only section for '{disable_id}' found."
|
|
||||||
)
|
|
||||||
case _:
|
|
||||||
return await ctx.send(
|
|
||||||
"The specified id type is invalid. Valid id types are: ['all', 'app_id', 'build_id', 'ro_section']"
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command(
|
|
||||||
aliases=[
|
|
||||||
"disabled_ids",
|
|
||||||
"blocked_ids",
|
|
||||||
"listblockedids",
|
|
||||||
"list_blocked_log_ids",
|
|
||||||
"list_blocked_ids",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
async def list_disabled_ids(self, ctx: Context):
|
|
||||||
disabled_ids = get_disabled_ids(self.bot)
|
|
||||||
id_types = {"app_id": "AppID", "build_id": "BID", "ro_section": "RoSection"}
|
|
||||||
|
|
||||||
message = "**Blocking analysis of the following IDs:**\n"
|
|
||||||
for name, entry in disabled_ids.items():
|
|
||||||
message += f"- {name}:\n"
|
|
||||||
for id_type, title in id_types.items():
|
|
||||||
if len(entry[id_type]) > 0:
|
|
||||||
if id_type != "ro_section":
|
|
||||||
message += f" - __{title}__: {entry[id_type]}\n"
|
|
||||||
else:
|
|
||||||
message += f" - __{title}__\n"
|
|
||||||
message += "\n"
|
|
||||||
return await ctx.send(message)
|
|
||||||
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command(
|
|
||||||
aliases=[
|
|
||||||
"get_blocked_ro_section",
|
|
||||||
"disabled_ro_section",
|
|
||||||
"blocked_ro_section",
|
|
||||||
"list_disabled_ro_section",
|
|
||||||
"list_blocked_ro_section",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
async def get_disabled_ro_section(self, ctx: Context, disable_id: str):
|
|
||||||
disabled_ids = get_disabled_ids(self.bot)
|
|
||||||
disable_id = disable_id.lower()
|
|
||||||
if (
|
|
||||||
disable_id in disabled_ids.keys()
|
|
||||||
and len(disabled_ids[disable_id]["ro_section"]) > 0
|
|
||||||
):
|
|
||||||
message = f"**Blocked read-only section for '{disable_id}'**:\n"
|
|
||||||
message += "```\n"
|
|
||||||
for key, content in disabled_ids[disable_id]["ro_section"].items():
|
|
||||||
match key:
|
|
||||||
case "module":
|
|
||||||
message += f"Module: {content}\n"
|
|
||||||
case "sdk_libraries":
|
|
||||||
message += f"SDK Libraries: \n"
|
|
||||||
for entry in content:
|
|
||||||
message += f" SDK {entry}\n"
|
|
||||||
message += "\n"
|
|
||||||
message += "```"
|
|
||||||
return await ctx.send(message)
|
|
||||||
else:
|
|
||||||
return await ctx.send(f"No read-only section blocked for '{disable_id}'.")
|
|
||||||
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command(
|
|
||||||
aliases=["disallow_path", "forbid_path", "block_path", "blockpath"]
|
|
||||||
)
|
|
||||||
async def disable_path(self, ctx: Context, block_path: str):
|
|
||||||
if add_disabled_path(self.bot, block_path):
|
|
||||||
return await ctx.send(f"Path content `{block_path}` is now blocked!")
|
|
||||||
else:
|
|
||||||
return await ctx.send(f"Path content `{block_path}` is already blocked.")
|
|
||||||
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command(
|
|
||||||
aliases=[
|
|
||||||
"allow_path",
|
|
||||||
"unblock_path",
|
|
||||||
"unblockpath",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
async def enable_path(self, ctx: Context, block_path: str):
|
|
||||||
if remove_disabled_path(self.bot, block_path):
|
|
||||||
return await ctx.send(f"Path content `{block_path}` is now unblocked!")
|
|
||||||
else:
|
|
||||||
return await ctx.send(f"No blocked path content '{block_path}' found.")
|
|
||||||
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command(
|
|
||||||
aliases=[
|
|
||||||
"disabled_paths",
|
|
||||||
"blocked_paths",
|
|
||||||
"listdisabledpaths",
|
|
||||||
"listblockedpaths",
|
|
||||||
"list_blocked_paths",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
async def list_disabled_paths(self, ctx: Context):
|
|
||||||
messages = []
|
|
||||||
disabled_paths = get_disabled_paths(self.bot)
|
|
||||||
|
|
||||||
message = (
|
|
||||||
"**Blocking analysis of logs containing the following content in paths:**\n"
|
|
||||||
)
|
|
||||||
for entry in disabled_paths:
|
|
||||||
if len(message) >= 1500:
|
|
||||||
messages.append(message)
|
|
||||||
message = f"- `{entry}`\n"
|
|
||||||
else:
|
|
||||||
message += f"- `{entry}`\n"
|
|
||||||
|
|
||||||
if message not in messages:
|
|
||||||
# Add the last message as well
|
|
||||||
messages.append(message)
|
|
||||||
|
|
||||||
for msg in messages:
|
|
||||||
await ctx.send(msg)
|
|
||||||
|
|
||||||
async def analyse_log_message(self, message: Message, attachment_index=0):
|
|
||||||
author_id = message.author.id
|
|
||||||
author_mention = message.author.mention
|
|
||||||
filename = message.attachments[attachment_index].filename
|
|
||||||
filesize = message.attachments[attachment_index].size
|
|
||||||
# Any message over 2000 chars is uploaded as message.txt, so this is accounted for
|
|
||||||
log_file_link = message.jump_url
|
|
||||||
|
|
||||||
uploaded_logs_exist = [
|
|
||||||
True for elem in self.uploaded_log_info if filename in elem.values()
|
|
||||||
]
|
|
||||||
if not any(uploaded_logs_exist):
|
|
||||||
reply_message = await message.channel.send(
|
|
||||||
"Log detected, parsing...", reference=message
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
embed = await self.log_file_read(message)
|
|
||||||
if "Ryujinx_" in filename:
|
|
||||||
self.uploaded_log_info.append(
|
|
||||||
{
|
|
||||||
"filename": filename,
|
|
||||||
"file_size": filesize,
|
|
||||||
"link": log_file_link,
|
|
||||||
"author": author_id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
# Avoid duplicate log file analysis, at least temporarily; keep track of the last few filenames of uploaded logs
|
|
||||||
# this should help support channels not be flooded with too many log files
|
|
||||||
# fmt: off
|
|
||||||
self.uploaded_log_info = self.uploaded_log_info[-5:]
|
|
||||||
# fmt: on
|
|
||||||
return await reply_message.edit(content=None, embed=embed)
|
|
||||||
except UnicodeDecodeError as error:
|
|
||||||
await reply_message.edit(
|
|
||||||
content=author_mention,
|
|
||||||
embed=Embed(
|
|
||||||
description="This log file appears to be invalid. Please re-check and re-upload your log file.",
|
|
||||||
colour=self.ryujinx_blue,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
logging.warning(error)
|
|
||||||
except Exception as error:
|
|
||||||
await reply_message.edit(
|
|
||||||
content=f"Error: Couldn't parse log; parser threw `{type(error).__name__}` exception."
|
|
||||||
)
|
|
||||||
logging.warning(error)
|
|
||||||
else:
|
|
||||||
duplicate_log_file = next(
|
|
||||||
(
|
|
||||||
elem
|
|
||||||
for elem in self.uploaded_log_info
|
|
||||||
if elem["filename"] == filename
|
|
||||||
and elem["file_size"] == filesize
|
|
||||||
and elem["author"] == author_id
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
await message.channel.send(
|
|
||||||
content=author_mention,
|
|
||||||
embed=Embed(
|
|
||||||
description=f"The log file `{filename}` appears to be a duplicate [already uploaded here]({duplicate_log_file['link']}). Please upload a more recent file.",
|
|
||||||
colour=self.ryujinx_blue,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.cooldown(3, 30, BucketType.channel)
|
|
||||||
@commands.command(
|
|
||||||
aliases=["analyselog", "analyse_log", "analyze", "analyzelog", "analyze_log"]
|
|
||||||
)
|
|
||||||
async def analyse(self, ctx: Context, attachment_number=1):
|
|
||||||
await ctx.message.delete()
|
|
||||||
if ctx.message.reference is not None:
|
|
||||||
message = await ctx.fetch_message(ctx.message.reference.message_id)
|
|
||||||
if len(message.attachments) >= attachment_number:
|
|
||||||
attachment = message.attachments[attachment_number - 1]
|
|
||||||
is_log_file, _ = self.is_valid_log_name(attachment)
|
|
||||||
|
|
||||||
if is_log_file:
|
|
||||||
return await self.analyse_log_message(
|
|
||||||
message, attachment_number - 1
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return await ctx.send(
|
|
||||||
f"The attached log file '{attachment.filename}' is not valid.",
|
|
||||||
reference=ctx.message.reference,
|
|
||||||
)
|
|
||||||
|
|
||||||
return await ctx.send(
|
|
||||||
"Please use `.analyse` as a reply to a message with an attached log file."
|
|
||||||
)
|
|
||||||
|
|
||||||
@Cog.listener()
|
|
||||||
async def on_message(self, message: Message):
|
|
||||||
await self.bot.wait_until_ready()
|
|
||||||
if message.author.bot:
|
|
||||||
return
|
|
||||||
for attachment in message.attachments:
|
|
||||||
is_log_file, is_ryujinx_log_file = self.is_valid_log_name(attachment)
|
|
||||||
|
|
||||||
if is_log_file and not is_ryujinx_log_file:
|
|
||||||
attached_log = message.attachments[0]
|
|
||||||
log_file = await self.download_file(attached_log.url)
|
|
||||||
# Large files show a header value when not downloaded completely
|
|
||||||
# this regex makes sure that the log text to read starts from the first timestamp, ignoring headers
|
|
||||||
log_file_header_regex = re.compile(
|
|
||||||
r"\d{2}:\d{2}:\d{2}\.\d{3}.*", re.DOTALL
|
|
||||||
)
|
|
||||||
log_file_match = re.search(log_file_header_regex, log_file)
|
|
||||||
if log_file_match:
|
|
||||||
log_file = log_file_match.group(0)
|
|
||||||
if self.is_game_blocked(log_file):
|
|
||||||
return await message.channel.send(
|
|
||||||
content=None, embed=await self.blocked_game_action(message)
|
|
||||||
)
|
|
||||||
blocked_path = self.contains_blocked_paths(log_file)
|
|
||||||
if blocked_path:
|
|
||||||
return await message.channel.send(
|
|
||||||
content=None,
|
|
||||||
embed=await self.blocked_path_action(message, blocked_path),
|
|
||||||
)
|
|
||||||
elif (
|
|
||||||
is_log_file
|
|
||||||
and is_ryujinx_log_file
|
|
||||||
and message.channel.id in self.bot_log_allowed_channels.values()
|
|
||||||
):
|
|
||||||
return await self.analyse_log_message(
|
|
||||||
message, message.attachments.index(attachment)
|
|
||||||
)
|
|
||||||
elif (
|
|
||||||
is_log_file
|
|
||||||
and is_ryujinx_log_file
|
|
||||||
and message.channel.id not in self.bot_log_allowed_channels.values()
|
|
||||||
):
|
|
||||||
return await message.author.send(
|
|
||||||
content=message.author.mention,
|
|
||||||
embed=Embed(
|
|
||||||
description="\n".join(
|
|
||||||
(
|
|
||||||
f"Please upload Ryujinx log files to the correct location:\n",
|
|
||||||
f'<#{self.bot.config.bot_log_allowed_channels["windows-support"]}>: Windows help and troubleshooting',
|
|
||||||
f'<#{self.bot.config.bot_log_allowed_channels["linux-support"]}>: Linux help and troubleshooting',
|
|
||||||
f'<#{self.bot.config.bot_log_allowed_channels["macos-support"]}>: macOS help and troubleshooting',
|
|
||||||
f'<#{self.bot.config.bot_log_allowed_channels["patreon-support"]}>: Help and troubleshooting for Patreon subscribers',
|
|
||||||
f'<#{self.bot.config.bot_log_allowed_channels["development"]}>: Ryujinx development discussion',
|
|
||||||
f'<#{self.bot.config.bot_log_allowed_channels["pr-testing"]}>: Discussion of in-progress pull request builds',
|
|
||||||
)
|
|
||||||
),
|
|
||||||
colour=self.ryujinx_blue,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(LogFileReader(bot))
|
|
|
@ -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))
|
|
|
@ -1,246 +0,0 @@
|
||||||
import datetime
|
|
||||||
import math
|
|
||||||
import platform
|
|
||||||
import random
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import discord
|
|
||||||
from discord.ext import commands
|
|
||||||
from discord.ext.commands import Cog
|
|
||||||
|
|
||||||
from robocop_ng.helpers.checks import check_if_staff_or_ot
|
|
||||||
|
|
||||||
|
|
||||||
class Meme(Cog):
|
|
||||||
"""
|
|
||||||
Meme commands.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
def c_to_f(self, c):
|
|
||||||
"""this is where we take memes too far"""
|
|
||||||
return math.floor(9.0 / 5.0 * c + 32)
|
|
||||||
|
|
||||||
def c_to_k(self, c):
|
|
||||||
"""this is where we take memes REALLY far"""
|
|
||||||
return math.floor(c + 273.15)
|
|
||||||
|
|
||||||
@commands.check(check_if_staff_or_ot)
|
|
||||||
@commands.command(hidden=True, name="warm")
|
|
||||||
async def warm_member(self, ctx, user: Optional[discord.Member]):
|
|
||||||
"""Warms a user :3"""
|
|
||||||
if user is None and ctx.message.reference is None:
|
|
||||||
celsius = random.randint(15, 20)
|
|
||||||
fahrenheit = self.c_to_f(celsius)
|
|
||||||
kelvin = self.c_to_k(celsius)
|
|
||||||
await ctx.send(
|
|
||||||
f"{ctx.author.mention} tries to warm themself."
|
|
||||||
f" User is now {celsius}°C "
|
|
||||||
f"({fahrenheit}°F, {kelvin}K).\n"
|
|
||||||
"You might have more success warming someone else :3"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if user is None:
|
|
||||||
user = (
|
|
||||||
await ctx.channel.fetch_message(ctx.message.reference.message_id)
|
|
||||||
).author
|
|
||||||
|
|
||||||
celsius = random.randint(15, 100)
|
|
||||||
fahrenheit = self.c_to_f(celsius)
|
|
||||||
kelvin = self.c_to_k(celsius)
|
|
||||||
await ctx.send(
|
|
||||||
f"{user.mention} warmed."
|
|
||||||
f" User is now {celsius}°C "
|
|
||||||
f"({fahrenheit}°F, {kelvin}K)."
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.check(check_if_staff_or_ot)
|
|
||||||
@commands.command(hidden=True)
|
|
||||||
async def lick(self, ctx, user: Optional[discord.Member]):
|
|
||||||
"""licks a user :?"""
|
|
||||||
if user is None and ctx.message.reference is None:
|
|
||||||
await ctx.send(f"{ctx.author.mention} licks their lips! 👅")
|
|
||||||
else:
|
|
||||||
if user is None:
|
|
||||||
user = (
|
|
||||||
await ctx.channel.fetch_message(ctx.message.reference.message_id)
|
|
||||||
).author
|
|
||||||
await ctx.send(f"{user.mention} has been licked! 👅")
|
|
||||||
|
|
||||||
@commands.check(check_if_staff_or_ot)
|
|
||||||
@commands.command(hidden=True, name="chill", aliases=["cold"])
|
|
||||||
async def chill_member(self, ctx, user: Optional[discord.Member]):
|
|
||||||
"""Chills a user >:3"""
|
|
||||||
if user is None and ctx.message.reference is None:
|
|
||||||
celsius = random.randint(-75, 10)
|
|
||||||
fahrenheit = self.c_to_f(celsius)
|
|
||||||
kelvin = self.c_to_k(celsius)
|
|
||||||
await ctx.send(
|
|
||||||
f"{ctx.author.mention} chills themself."
|
|
||||||
f" User is now {celsius}°C "
|
|
||||||
f"({fahrenheit}°F, {kelvin}K).\n"
|
|
||||||
"🧊 Don't be so hard on yourself. 😔"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if user is None:
|
|
||||||
user = (
|
|
||||||
await ctx.channel.fetch_message(ctx.message.reference.message_id)
|
|
||||||
).author
|
|
||||||
celsius = random.randint(-50, 15)
|
|
||||||
fahrenheit = self.c_to_f(celsius)
|
|
||||||
kelvin = self.c_to_k(celsius)
|
|
||||||
await ctx.send(
|
|
||||||
f"{user.mention} chilled."
|
|
||||||
f" User is now {celsius}°C "
|
|
||||||
f"({fahrenheit}°F, {kelvin}K)."
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.check(check_if_staff_or_ot)
|
|
||||||
@commands.command(hidden=True, aliases=["thank", "reswitchedgold"])
|
|
||||||
async def gild(self, ctx, user: Optional[discord.Member]):
|
|
||||||
"""Gives a star to a user"""
|
|
||||||
if user is None and ctx.message.reference is None:
|
|
||||||
await ctx.send(f"No stars for you, {ctx.author.mention}!")
|
|
||||||
else:
|
|
||||||
if user is None:
|
|
||||||
user = (
|
|
||||||
await ctx.channel.fetch_message(ctx.message.reference.message_id)
|
|
||||||
).author
|
|
||||||
await ctx.send(f"{user.mention} gets a :star:, yay!")
|
|
||||||
|
|
||||||
@commands.check(check_if_staff_or_ot)
|
|
||||||
@commands.command(
|
|
||||||
hidden=True, aliases=["reswitchedsilver", "silv3r", "reswitchedsilv3r"]
|
|
||||||
)
|
|
||||||
async def silver(self, ctx, user: Optional[discord.Member]):
|
|
||||||
"""Gives a user ReSwitched Silver™"""
|
|
||||||
if user is None and ctx.message.reference is None:
|
|
||||||
await ctx.send(f"{ctx.author.mention}, you can't reward yourself.")
|
|
||||||
else:
|
|
||||||
if user is None:
|
|
||||||
user = (
|
|
||||||
await ctx.channel.fetch_message(ctx.message.reference.message_id)
|
|
||||||
).author
|
|
||||||
embed = discord.Embed(
|
|
||||||
title="ReSwitched Silver™!",
|
|
||||||
description=f"Here's your ReSwitched Silver™," f"{user.mention}!",
|
|
||||||
)
|
|
||||||
embed.set_image(
|
|
||||||
url="https://cdn.discordapp.com/emojis/548623626916724747.png?v=1"
|
|
||||||
)
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
|
|
||||||
@commands.check(check_if_staff_or_ot)
|
|
||||||
@commands.command(hidden=True)
|
|
||||||
async def btwiuse(self, ctx):
|
|
||||||
"""btw i use arch"""
|
|
||||||
uname = platform.uname()
|
|
||||||
await ctx.send(
|
|
||||||
f"BTW I use {platform.python_implementation()} "
|
|
||||||
f"{platform.python_version()} on {uname.system} "
|
|
||||||
f"{uname.release}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.check(check_if_staff_or_ot)
|
|
||||||
@commands.command(hidden=True)
|
|
||||||
async def yahaha(self, ctx):
|
|
||||||
"""secret command"""
|
|
||||||
await ctx.send(f"🍂 you found me 🍂")
|
|
||||||
|
|
||||||
@commands.check(check_if_staff_or_ot)
|
|
||||||
@commands.command(hidden=True)
|
|
||||||
async def blackalabi(self, ctx):
|
|
||||||
"""secret command"""
|
|
||||||
await ctx.send("https://elixi.re/i/discord.png")
|
|
||||||
|
|
||||||
@commands.check(check_if_staff_or_ot)
|
|
||||||
@commands.command(hidden=True)
|
|
||||||
async def peng(self, ctx):
|
|
||||||
"""heck tomger"""
|
|
||||||
await ctx.send(f"🐧")
|
|
||||||
|
|
||||||
@commands.check(check_if_staff_or_ot)
|
|
||||||
@commands.command(hidden=True, aliases=["outstanding"])
|
|
||||||
async def outstandingmove(self, ctx):
|
|
||||||
"""Posts the outstanding move meme"""
|
|
||||||
await ctx.send(
|
|
||||||
"https://cdn.discordapp.com/attachments"
|
|
||||||
"/371047036348268545/528413677007929344"
|
|
||||||
"/image0-5.jpg"
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.check(check_if_staff_or_ot)
|
|
||||||
@commands.command(hidden=True)
|
|
||||||
async def bones(self, ctx):
|
|
||||||
await ctx.send("https://cdn.discordapp.com/emojis/443501365843591169.png?v=1")
|
|
||||||
|
|
||||||
@commands.check(check_if_staff_or_ot)
|
|
||||||
@commands.command(hidden=True)
|
|
||||||
async def headpat(self, ctx):
|
|
||||||
await ctx.send("https://cdn.discordapp.com/emojis/465650811909701642.png?v=1")
|
|
||||||
|
|
||||||
@commands.check(check_if_staff_or_ot)
|
|
||||||
@commands.command(
|
|
||||||
hidden=True, aliases=["when", "etawhen", "emunand", "emummc", "thermosphere"]
|
|
||||||
)
|
|
||||||
async def eta(self, ctx):
|
|
||||||
await ctx.send("June 15.")
|
|
||||||
|
|
||||||
@commands.check(check_if_staff_or_ot)
|
|
||||||
@commands.command(hidden=True, name="bam")
|
|
||||||
async def bam_member(self, ctx, target: Optional[discord.Member]):
|
|
||||||
"""Bams a user owo"""
|
|
||||||
if target is None and ctx.message.reference is None:
|
|
||||||
await ctx.reply("https://tenor.com/view/bonk-gif-26414884")
|
|
||||||
else:
|
|
||||||
if ctx.message.reference is not None:
|
|
||||||
target = (
|
|
||||||
await ctx.channel.fetch_message(ctx.message.reference.message_id)
|
|
||||||
).author
|
|
||||||
if target == ctx.author:
|
|
||||||
if target.id == 181627658520625152:
|
|
||||||
return await ctx.send(
|
|
||||||
"https://cdn.discordapp.com/attachments/286612533757083648/403080855402315796/rehedge.PNG"
|
|
||||||
)
|
|
||||||
return await ctx.send("hedgeberg#7337 is ̶n͢ow b̕&̡.̷ 👍̡")
|
|
||||||
elif target == self.bot.user:
|
|
||||||
return await ctx.send(
|
|
||||||
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
|
|
||||||
)
|
|
||||||
|
|
||||||
safe_name = await commands.clean_content(escape_markdown=True).convert(
|
|
||||||
ctx, str(target)
|
|
||||||
)
|
|
||||||
await ctx.send(f"{safe_name} is ̶n͢ow b̕&̡.̷ 👍̡")
|
|
||||||
|
|
||||||
@commands.command(hidden=True)
|
|
||||||
async def memebercount(self, ctx):
|
|
||||||
"""Checks memeber count, as requested by dvdfreitag"""
|
|
||||||
await ctx.send("There's like, uhhhhh a bunch")
|
|
||||||
|
|
||||||
@commands.command(hidden=True)
|
|
||||||
async def frolics(self, ctx):
|
|
||||||
"""test"""
|
|
||||||
await ctx.send("https://www.youtube.com/watch?v=VmarNEsjpDI")
|
|
||||||
|
|
||||||
@commands.command(
|
|
||||||
hidden=True,
|
|
||||||
aliases=[
|
|
||||||
"yotld",
|
|
||||||
"yold",
|
|
||||||
"yoltd",
|
|
||||||
"yearoflinuxondesktop",
|
|
||||||
"yearoflinuxonthedesktop",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def yearoflinux(self, ctx):
|
|
||||||
"""Shows the year of Linux on the desktop"""
|
|
||||||
await ctx.send(
|
|
||||||
f"{datetime.datetime.now().year} is the year of Linux on the Desktop"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(Meme(bot))
|
|
|
@ -1,933 +0,0 @@
|
||||||
import io
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import discord
|
|
||||||
from discord.ext import commands
|
|
||||||
from discord.ext.commands import Cog, Context
|
|
||||||
|
|
||||||
from robocop_ng.helpers.checks import check_if_staff, check_if_bot_manager
|
|
||||||
from robocop_ng.helpers.restrictions import add_restriction, remove_restriction
|
|
||||||
from robocop_ng.helpers.userlogs import userlog
|
|
||||||
|
|
||||||
|
|
||||||
class Mod(Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
def check_if_target_is_staff(self, target):
|
|
||||||
return any(r.id in self.bot.config.staff_role_ids for r in target.roles)
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_bot_manager)
|
|
||||||
@commands.command()
|
|
||||||
async def setguildicon(self, ctx, url):
|
|
||||||
"""Changes guild icon, bot manager only."""
|
|
||||||
img_bytes = await self.bot.aiogetbytes(url)
|
|
||||||
await ctx.guild.edit(icon=img_bytes, reason=str(ctx.author))
|
|
||||||
await ctx.send(f"Done!")
|
|
||||||
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
|
|
||||||
log_msg = (
|
|
||||||
f"✏️ **Guild Icon Update**: {ctx.author} changed the guild icon."
|
|
||||||
f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
|
|
||||||
)
|
|
||||||
img_filename = url.split("/")[-1].split("#")[0] # hacky
|
|
||||||
img_file = discord.File(io.BytesIO(img_bytes), filename=img_filename)
|
|
||||||
await log_channel.send(log_msg, file=img_file)
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command()
|
|
||||||
async def mute(self, ctx, target: Optional[discord.Member], *, reason: str = ""):
|
|
||||||
"""Mutes a user, staff only."""
|
|
||||||
if target is None and ctx.message.reference is None:
|
|
||||||
return await ctx.send(
|
|
||||||
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if ctx.message.reference is not None:
|
|
||||||
if target is not None:
|
|
||||||
reason = str(target) + reason
|
|
||||||
target = (
|
|
||||||
await ctx.channel.fetch_message(ctx.message.reference.message_id)
|
|
||||||
).author
|
|
||||||
# Hedge-proofing the code
|
|
||||||
if target == ctx.author:
|
|
||||||
return await ctx.send("You can't do mod actions on yourself.")
|
|
||||||
elif target == self.bot.user:
|
|
||||||
return await ctx.send(
|
|
||||||
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
|
|
||||||
)
|
|
||||||
elif self.check_if_target_is_staff(target):
|
|
||||||
return await ctx.send(
|
|
||||||
"I can't mute this user as they're a member of staff."
|
|
||||||
)
|
|
||||||
|
|
||||||
userlog(self.bot, target.id, ctx.author, reason, "mutes", target.name)
|
|
||||||
|
|
||||||
safe_name = await commands.clean_content(escape_markdown=True).convert(
|
|
||||||
ctx, str(target)
|
|
||||||
)
|
|
||||||
|
|
||||||
dm_message = f"You were muted!"
|
|
||||||
if reason:
|
|
||||||
dm_message += f' The given reason is: "{reason}".'
|
|
||||||
|
|
||||||
try:
|
|
||||||
await target.send(dm_message)
|
|
||||||
except discord.errors.Forbidden:
|
|
||||||
# Prevents kick issues in cases where user blocked bot
|
|
||||||
# or has DMs disabled
|
|
||||||
pass
|
|
||||||
|
|
||||||
mute_role = ctx.guild.get_role(self.bot.config.mute_role)
|
|
||||||
|
|
||||||
await target.add_roles(mute_role, reason=str(ctx.author))
|
|
||||||
|
|
||||||
chan_message = (
|
|
||||||
f"🔇 **Muted**: {str(ctx.author)} muted "
|
|
||||||
f"{target.mention} | {safe_name}\n"
|
|
||||||
f"🏷 __User ID__: {target.id}\n"
|
|
||||||
)
|
|
||||||
if reason:
|
|
||||||
chan_message += f'✏️ __Reason__: "{reason}"'
|
|
||||||
else:
|
|
||||||
chan_message += (
|
|
||||||
"Please add an explanation below. In the future, "
|
|
||||||
"it is recommended to use `.mute <user> [reason]`"
|
|
||||||
" as the reason is automatically sent to the user."
|
|
||||||
)
|
|
||||||
|
|
||||||
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
|
|
||||||
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
|
|
||||||
await log_channel.send(chan_message)
|
|
||||||
await ctx.send(f"{target.mention} can no longer speak.")
|
|
||||||
add_restriction(self.bot, target.id, self.bot.config.mute_role)
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command()
|
|
||||||
async def unmute(self, ctx, target: discord.Member):
|
|
||||||
"""Unmutes a user, staff only."""
|
|
||||||
safe_name = await commands.clean_content(escape_markdown=True).convert(
|
|
||||||
ctx, str(target)
|
|
||||||
)
|
|
||||||
|
|
||||||
mute_role = ctx.guild.get_role(self.bot.config.mute_role)
|
|
||||||
await target.remove_roles(mute_role, reason=str(ctx.author))
|
|
||||||
|
|
||||||
chan_message = (
|
|
||||||
f"🔈 **Unmuted**: {str(ctx.author)} unmuted "
|
|
||||||
f"{target.mention} | {safe_name}\n"
|
|
||||||
f"🏷 __User ID__: {target.id}\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
|
|
||||||
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
|
|
||||||
await log_channel.send(chan_message)
|
|
||||||
await ctx.send(f"{target.mention} can now speak again.")
|
|
||||||
remove_restriction(self.bot, target.id, self.bot.config.mute_role)
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.bot_has_permissions(kick_members=True)
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command()
|
|
||||||
async def kick(self, ctx, target: Optional[discord.Member], *, reason: str = ""):
|
|
||||||
"""Kicks a user, staff only."""
|
|
||||||
if target is None and ctx.message.reference is None:
|
|
||||||
return await ctx.send(
|
|
||||||
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if ctx.message.reference is not None:
|
|
||||||
if target is not None:
|
|
||||||
reason = str(target) + reason
|
|
||||||
target = (
|
|
||||||
await ctx.channel.fetch_message(ctx.message.reference.message_id)
|
|
||||||
).author
|
|
||||||
# Hedge-proofing the code
|
|
||||||
if target == ctx.author:
|
|
||||||
return await ctx.send("You can't do mod actions on yourself.")
|
|
||||||
elif target == self.bot.user:
|
|
||||||
return await ctx.send(
|
|
||||||
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
|
|
||||||
)
|
|
||||||
elif self.check_if_target_is_staff(target):
|
|
||||||
return await ctx.send(
|
|
||||||
"I can't kick this user as they're a member of staff."
|
|
||||||
)
|
|
||||||
|
|
||||||
userlog(self.bot, target.id, ctx.author, reason, "kicks", target.name)
|
|
||||||
|
|
||||||
safe_name = await commands.clean_content(escape_markdown=True).convert(
|
|
||||||
ctx, str(target)
|
|
||||||
)
|
|
||||||
|
|
||||||
dm_message = f"You were kicked from {ctx.guild.name}."
|
|
||||||
if reason:
|
|
||||||
dm_message += f' The given reason is: "{reason}".'
|
|
||||||
dm_message += (
|
|
||||||
"\n\nYou are able to rejoin the server,"
|
|
||||||
" but please be sure to behave when participating again."
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await target.send(dm_message)
|
|
||||||
except discord.errors.Forbidden:
|
|
||||||
# Prevents kick issues in cases where user blocked bot
|
|
||||||
# or has DMs disabled
|
|
||||||
pass
|
|
||||||
|
|
||||||
await target.kick(reason=f"{ctx.author}, reason: {reason}")
|
|
||||||
chan_message = (
|
|
||||||
f"👢 **Kick**: {str(ctx.author)} kicked "
|
|
||||||
f"{target.mention} | {safe_name}\n"
|
|
||||||
f"🏷 __User ID__: {target.id}\n"
|
|
||||||
)
|
|
||||||
if reason:
|
|
||||||
chan_message += f'✏️ __Reason__: "{reason}"'
|
|
||||||
else:
|
|
||||||
chan_message += (
|
|
||||||
"Please add an explanation below. In the future"
|
|
||||||
", it is recommended to use "
|
|
||||||
"`.kick <user> [reason]`"
|
|
||||||
" as the reason is automatically sent to the user."
|
|
||||||
)
|
|
||||||
|
|
||||||
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
|
|
||||||
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
|
|
||||||
await log_channel.send(chan_message)
|
|
||||||
await ctx.send(f"👢 {safe_name}, 👍.")
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.bot_has_permissions(ban_members=True)
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command(aliases=["yeet"])
|
|
||||||
async def ban(self, ctx, target: Optional[discord.Member], *, reason: str = ""):
|
|
||||||
"""Bans a user, staff only."""
|
|
||||||
if target is None and ctx.message.reference is None:
|
|
||||||
return await ctx.send(
|
|
||||||
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if ctx.message.reference is not None:
|
|
||||||
if target is not None:
|
|
||||||
reason = str(target) + reason
|
|
||||||
target = (
|
|
||||||
await ctx.channel.fetch_message(ctx.message.reference.message_id)
|
|
||||||
).author
|
|
||||||
# Hedge-proofing the code
|
|
||||||
if target == ctx.author:
|
|
||||||
if target.id == 181627658520625152:
|
|
||||||
return await ctx.send(
|
|
||||||
"https://cdn.discordapp.com/attachments/286612533757083648/403080855402315796/rehedge.PNG"
|
|
||||||
)
|
|
||||||
return await ctx.send("hedgeberg#7337 is now b&. 👍")
|
|
||||||
elif target == self.bot.user:
|
|
||||||
return await ctx.send(
|
|
||||||
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
|
|
||||||
)
|
|
||||||
elif self.check_if_target_is_staff(target):
|
|
||||||
return await ctx.send("I can't ban this user as they're a member of staff.")
|
|
||||||
|
|
||||||
userlog(self.bot, target.id, ctx.author, reason, "bans", target.name)
|
|
||||||
|
|
||||||
safe_name = await commands.clean_content(escape_markdown=True).convert(
|
|
||||||
ctx, str(target)
|
|
||||||
)
|
|
||||||
|
|
||||||
dm_message = f"You were banned from {ctx.guild.name}."
|
|
||||||
if reason:
|
|
||||||
dm_message += f' The given reason is: "{reason}".'
|
|
||||||
dm_message += "\n\nThis ban does not expire."
|
|
||||||
|
|
||||||
try:
|
|
||||||
await target.send(dm_message)
|
|
||||||
except discord.errors.Forbidden:
|
|
||||||
# Prevents ban issues in cases where user blocked bot
|
|
||||||
# or has DMs disabled
|
|
||||||
pass
|
|
||||||
|
|
||||||
await target.ban(
|
|
||||||
reason=f"{ctx.author}, reason: {reason}", delete_message_days=0
|
|
||||||
)
|
|
||||||
chan_message = (
|
|
||||||
f"⛔ **Ban**: {str(ctx.author)} banned "
|
|
||||||
f"{target.mention} | {safe_name}\n"
|
|
||||||
f"🏷 __User ID__: {target.id}\n"
|
|
||||||
)
|
|
||||||
if reason:
|
|
||||||
chan_message += f'✏️ __Reason__: "{reason}"'
|
|
||||||
else:
|
|
||||||
chan_message += (
|
|
||||||
"Please add an explanation below. In the future"
|
|
||||||
", it is recommended to use `.ban <user> [reason]`"
|
|
||||||
" as the reason is automatically sent to the user."
|
|
||||||
)
|
|
||||||
|
|
||||||
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
|
|
||||||
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
|
|
||||||
await log_channel.send(chan_message)
|
|
||||||
await ctx.send(f"{safe_name} is now b&. 👍")
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.bot_has_permissions(ban_members=True)
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command()
|
|
||||||
async def bandel(
|
|
||||||
self, ctx, day_count: int, target: Optional[discord.Member], *, reason: str = ""
|
|
||||||
):
|
|
||||||
"""Bans a user for a given number of days, staff only."""
|
|
||||||
if target is None and ctx.message.reference is None:
|
|
||||||
return await ctx.send(
|
|
||||||
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if ctx.message.reference is not None:
|
|
||||||
if target is not None:
|
|
||||||
reason = str(target) + reason
|
|
||||||
target = (
|
|
||||||
await ctx.channel.fetch_message(ctx.message.reference.message_id)
|
|
||||||
).author
|
|
||||||
# Hedge-proofing the code
|
|
||||||
if target == ctx.author:
|
|
||||||
if target.id == 181627658520625152:
|
|
||||||
return await ctx.send(
|
|
||||||
"https://cdn.discordapp.com/attachments/286612533757083648/403080855402315796/rehedge.PNG"
|
|
||||||
)
|
|
||||||
return await ctx.send("hedgeberg#7337 is now b&. 👍")
|
|
||||||
elif target == self.bot.user:
|
|
||||||
return await ctx.send(
|
|
||||||
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
|
|
||||||
)
|
|
||||||
elif self.check_if_target_is_staff(target):
|
|
||||||
return await ctx.send("I can't ban this user as they're a member of staff.")
|
|
||||||
|
|
||||||
if day_count < 0 or day_count > 7:
|
|
||||||
return await ctx.send(
|
|
||||||
"Message delete day count needs to be between 0 and 7 days."
|
|
||||||
)
|
|
||||||
|
|
||||||
userlog(self.bot, target.id, ctx.author, reason, "bans", target.name)
|
|
||||||
|
|
||||||
safe_name = await commands.clean_content(escape_markdown=True).convert(
|
|
||||||
ctx, str(target)
|
|
||||||
)
|
|
||||||
|
|
||||||
dm_message = f"You were banned from {ctx.guild.name}."
|
|
||||||
if reason:
|
|
||||||
dm_message += f' The given reason is: "{reason}".'
|
|
||||||
dm_message += "\n\nThis ban does not expire."
|
|
||||||
|
|
||||||
try:
|
|
||||||
await target.send(dm_message)
|
|
||||||
except discord.errors.Forbidden:
|
|
||||||
# Prevents ban issues in cases where user blocked bot
|
|
||||||
# or has DMs disabled
|
|
||||||
pass
|
|
||||||
|
|
||||||
await target.ban(
|
|
||||||
reason=f"{ctx.author}, days of message deletions: {day_count}, reason: {reason}",
|
|
||||||
delete_message_days=day_count,
|
|
||||||
)
|
|
||||||
chan_message = (
|
|
||||||
f"⛔ **Ban**: {str(ctx.author)} banned with {day_count} of messages deleted "
|
|
||||||
f"{target.mention} | {safe_name}\n"
|
|
||||||
f"🏷 __User ID__: {target.id}\n"
|
|
||||||
)
|
|
||||||
if reason:
|
|
||||||
chan_message += f'✏️ __Reason__: "{reason}"'
|
|
||||||
else:
|
|
||||||
chan_message += (
|
|
||||||
"Please add an explanation below. In the future"
|
|
||||||
", it is recommended to use `.bandel <daycount> <user> [reason]`"
|
|
||||||
" as the reason is automatically sent to the user."
|
|
||||||
)
|
|
||||||
|
|
||||||
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
|
|
||||||
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
|
|
||||||
await log_channel.send(chan_message)
|
|
||||||
await ctx.send(
|
|
||||||
f"{safe_name} is now b&, with {day_count} days of messages deleted. 👍"
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.bot_has_permissions(ban_members=True)
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command(aliases=["softban"])
|
|
||||||
async def hackban(self, ctx, target: int, *, reason: str = ""):
|
|
||||||
"""Bans a user with their ID, doesn't message them, staff only."""
|
|
||||||
target_user = await self.bot.fetch_user(target)
|
|
||||||
target_member = ctx.guild.get_member(target)
|
|
||||||
# Hedge-proofing the code
|
|
||||||
if target == ctx.author.id:
|
|
||||||
return await ctx.send("You can't do mod actions on yourself.")
|
|
||||||
elif target == self.bot.user:
|
|
||||||
return await ctx.send(
|
|
||||||
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
|
|
||||||
)
|
|
||||||
elif target_member and self.check_if_target_is_staff(target_member):
|
|
||||||
return await ctx.send("I can't ban this user as they're a member of staff.")
|
|
||||||
|
|
||||||
userlog(self.bot, target, ctx.author, reason, "bans", target_user.name)
|
|
||||||
|
|
||||||
safe_name = await commands.clean_content(escape_markdown=True).convert(
|
|
||||||
ctx, str(target)
|
|
||||||
)
|
|
||||||
|
|
||||||
await ctx.guild.ban(
|
|
||||||
target_user, reason=f"{ctx.author}, reason: {reason}", delete_message_days=0
|
|
||||||
)
|
|
||||||
chan_message = (
|
|
||||||
f"⛔ **Hackban**: {str(ctx.author)} banned "
|
|
||||||
f"{target_user.mention} | {safe_name}\n"
|
|
||||||
f"🏷 __User ID__: {target}\n"
|
|
||||||
)
|
|
||||||
if reason:
|
|
||||||
chan_message += f'✏️ __Reason__: "{reason}"'
|
|
||||||
else:
|
|
||||||
chan_message += (
|
|
||||||
"Please add an explanation below. In the future"
|
|
||||||
", it is recommended to use "
|
|
||||||
"`.hackban <user> [reason]`."
|
|
||||||
)
|
|
||||||
|
|
||||||
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
|
|
||||||
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
|
|
||||||
await log_channel.send(chan_message)
|
|
||||||
await ctx.send(f"{safe_name} is now b&. 👍")
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.bot_has_permissions(ban_members=True)
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command()
|
|
||||||
async def massban(self, ctx, *, targets: str):
|
|
||||||
"""Bans users with their IDs, doesn't message them, staff only."""
|
|
||||||
targets_int = [int(target) for target in targets.strip().split(" ")]
|
|
||||||
for target in targets_int:
|
|
||||||
target_user = await self.bot.fetch_user(target)
|
|
||||||
target_member = ctx.guild.get_member(target)
|
|
||||||
# Hedge-proofing the code
|
|
||||||
if target == ctx.author.id:
|
|
||||||
await ctx.send(f"(re: {target}) You can't do mod actions on yourself.")
|
|
||||||
continue
|
|
||||||
elif target == self.bot.user:
|
|
||||||
await ctx.send(
|
|
||||||
f"(re: {target}) I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
elif target_member and self.check_if_target_is_staff(target_member):
|
|
||||||
await ctx.send(
|
|
||||||
f"(re: {target}) I can't ban this user as they're a member of staff."
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
userlog(self.bot, target, ctx.author, f"massban", "bans", target_user.name)
|
|
||||||
|
|
||||||
safe_name = await commands.clean_content(escape_markdown=True).convert(
|
|
||||||
ctx, str(target)
|
|
||||||
)
|
|
||||||
|
|
||||||
await ctx.guild.ban(
|
|
||||||
target_user,
|
|
||||||
reason=f"{ctx.author}, reason: massban",
|
|
||||||
delete_message_days=0,
|
|
||||||
)
|
|
||||||
chan_message = (
|
|
||||||
f"⛔ **Massban**: {str(ctx.author)} banned "
|
|
||||||
f"{target_user.mention} | {safe_name}\n"
|
|
||||||
f"🏷 __User ID__: {target}\n"
|
|
||||||
"Please add an explanation below."
|
|
||||||
)
|
|
||||||
|
|
||||||
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
|
|
||||||
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
|
|
||||||
await log_channel.send(chan_message)
|
|
||||||
await ctx.send(f"All {len(targets_int)} users are now b&. 👍")
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.bot_has_permissions(ban_members=True)
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command()
|
|
||||||
async def unban(self, ctx, target: int, *, reason: str = ""):
|
|
||||||
"""Unbans a user with their ID, doesn't message them, staff only."""
|
|
||||||
target_user = await self.bot.fetch_user(target)
|
|
||||||
|
|
||||||
safe_name = await commands.clean_content(escape_markdown=True).convert(
|
|
||||||
ctx, str(target)
|
|
||||||
)
|
|
||||||
|
|
||||||
await ctx.guild.unban(target_user, reason=f"{ctx.author}, reason: {reason}")
|
|
||||||
chan_message = (
|
|
||||||
f"⚠️ **Unban**: {str(ctx.author)} unbanned "
|
|
||||||
f"{target_user.mention} | {safe_name}\n"
|
|
||||||
f"🏷 __User ID__: {target}\n"
|
|
||||||
)
|
|
||||||
if reason:
|
|
||||||
chan_message += f'✏️ __Reason__: "{reason}"'
|
|
||||||
else:
|
|
||||||
chan_message += (
|
|
||||||
"Please add an explanation below. In the future"
|
|
||||||
", it is recommended to use "
|
|
||||||
"`.unban <user id> [reason]`."
|
|
||||||
)
|
|
||||||
|
|
||||||
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
|
|
||||||
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
|
|
||||||
await log_channel.send(chan_message)
|
|
||||||
await ctx.send(f"{safe_name} is now unb&.")
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.bot_has_permissions(ban_members=True)
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command()
|
|
||||||
async def silentban(self, ctx, target: discord.Member, *, reason: str = ""):
|
|
||||||
"""Bans a user, staff only."""
|
|
||||||
# Hedge-proofing the code
|
|
||||||
if target == ctx.author:
|
|
||||||
return await ctx.send("You can't do mod actions on yourself.")
|
|
||||||
elif target == self.bot.user:
|
|
||||||
return await ctx.send(
|
|
||||||
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
|
|
||||||
)
|
|
||||||
elif self.check_if_target_is_staff(target):
|
|
||||||
return await ctx.send("I can't ban this user as they're a member of staff.")
|
|
||||||
|
|
||||||
userlog(self.bot, target.id, ctx.author, reason, "bans", target.name)
|
|
||||||
|
|
||||||
safe_name = await commands.clean_content(escape_markdown=True).convert(
|
|
||||||
ctx, str(target)
|
|
||||||
)
|
|
||||||
|
|
||||||
await target.ban(
|
|
||||||
reason=f"{ctx.author}, reason: {reason}", delete_message_days=0
|
|
||||||
)
|
|
||||||
chan_message = (
|
|
||||||
f"⛔ **Silent ban**: {str(ctx.author)} banned "
|
|
||||||
f"{target.mention} | {safe_name}\n"
|
|
||||||
f"🏷 __User ID__: {target.id}\n"
|
|
||||||
)
|
|
||||||
if reason:
|
|
||||||
chan_message += f'✏️ __Reason__: "{reason}"'
|
|
||||||
else:
|
|
||||||
chan_message += (
|
|
||||||
"Please add an explanation below. In the future"
|
|
||||||
", it is recommended to use `.ban <user> [reason]`"
|
|
||||||
" as the reason is automatically sent to the user."
|
|
||||||
)
|
|
||||||
|
|
||||||
chan_message += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
|
|
||||||
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
|
|
||||||
await log_channel.send(chan_message)
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command()
|
|
||||||
async def approve(
|
|
||||||
self, ctx, target: Optional[discord.Member], role: str = "community"
|
|
||||||
):
|
|
||||||
"""Add a role to a user (default: community), staff only."""
|
|
||||||
if role not in self.bot.config.named_roles:
|
|
||||||
return await ctx.send(
|
|
||||||
"No such role! Available roles: "
|
|
||||||
+ ",".join(self.bot.config.named_roles)
|
|
||||||
)
|
|
||||||
|
|
||||||
if target is None and ctx.message.reference is None:
|
|
||||||
return await ctx.send(
|
|
||||||
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if ctx.message.reference is not None:
|
|
||||||
target = (
|
|
||||||
await ctx.channel.fetch_message(ctx.message.reference.message_id)
|
|
||||||
).author
|
|
||||||
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
|
|
||||||
target_role = ctx.guild.get_role(self.bot.config.named_roles[role])
|
|
||||||
|
|
||||||
if target_role in target.roles:
|
|
||||||
return await ctx.send("Target already has this role.")
|
|
||||||
|
|
||||||
await target.add_roles(target_role, reason=str(ctx.author))
|
|
||||||
|
|
||||||
await ctx.send(f"Approved {target.mention} to `{role}` role.")
|
|
||||||
|
|
||||||
await log_channel.send(
|
|
||||||
f"✅ Approved: {str(ctx.author)} added"
|
|
||||||
f" {role} to {target.mention}"
|
|
||||||
f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command(aliases=["unapprove"])
|
|
||||||
async def revoke(
|
|
||||||
self, ctx, target: Optional[discord.Member], role: str = "community"
|
|
||||||
):
|
|
||||||
"""Remove a role from a user (default: community), staff only."""
|
|
||||||
if role not in self.bot.config.named_roles:
|
|
||||||
return await ctx.send(
|
|
||||||
"No such role! Available roles: "
|
|
||||||
+ ",".join(self.bot.config.named_roles)
|
|
||||||
)
|
|
||||||
|
|
||||||
if target is None and ctx.message.reference is None:
|
|
||||||
return await ctx.send(
|
|
||||||
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if ctx.message.reference is not None:
|
|
||||||
target = (
|
|
||||||
await ctx.channel.fetch_message(ctx.message.reference.message_id)
|
|
||||||
).author
|
|
||||||
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
|
|
||||||
target_role = ctx.guild.get_role(self.bot.config.named_roles[role])
|
|
||||||
|
|
||||||
if target_role not in target.roles:
|
|
||||||
return await ctx.send("Target doesn't have this role.")
|
|
||||||
|
|
||||||
await target.remove_roles(target_role, reason=str(ctx.author))
|
|
||||||
|
|
||||||
await ctx.send(f"Un-approved {target.mention} from `{role}` role.")
|
|
||||||
|
|
||||||
await log_channel.send(
|
|
||||||
f"❌ Un-approved: {str(ctx.author)} removed"
|
|
||||||
f" {role} from {target.mention}"
|
|
||||||
f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command(aliases=["clear"])
|
|
||||||
async def purge(self, ctx, limit: int, channel: discord.TextChannel = None):
|
|
||||||
"""Clears a given number of messages, staff only."""
|
|
||||||
modlog_channel = self.bot.get_channel(self.bot.config.modlog_channel)
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.log_channel)
|
|
||||||
if not channel:
|
|
||||||
channel = ctx.channel
|
|
||||||
|
|
||||||
purged_log_jump_url = ""
|
|
||||||
for deleted_message in await channel.purge(limit=limit):
|
|
||||||
msg = (
|
|
||||||
"🗑️ **Message purged**: \n"
|
|
||||||
f"from {self.bot.escape_message(deleted_message.author.name)} "
|
|
||||||
f"({deleted_message.author.id}), in {deleted_message.channel.mention}:\n"
|
|
||||||
f"`{deleted_message.clean_content}`"
|
|
||||||
)
|
|
||||||
if len(purged_log_jump_url) == 0:
|
|
||||||
purged_log_jump_url = (await log_channel.send(msg)).jump_url
|
|
||||||
else:
|
|
||||||
await log_channel.send(msg)
|
|
||||||
|
|
||||||
msg = (
|
|
||||||
f"🗑 **Purged**: {str(ctx.author)} purged {limit} "
|
|
||||||
f"messages in {channel.mention}."
|
|
||||||
f"\n🔗 __Jump__: <{purged_log_jump_url}>"
|
|
||||||
)
|
|
||||||
await modlog_channel.send(msg)
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command()
|
|
||||||
async def warn(self, ctx, target: Optional[discord.Member], *, reason: str = ""):
|
|
||||||
"""Warns a user, staff only."""
|
|
||||||
if target is None and ctx.message.reference is None:
|
|
||||||
return await ctx.send(
|
|
||||||
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if ctx.message.reference is not None:
|
|
||||||
if target is not None:
|
|
||||||
reason = str(target) + reason
|
|
||||||
target = (
|
|
||||||
await ctx.channel.fetch_message(ctx.message.reference.message_id)
|
|
||||||
).author
|
|
||||||
# Hedge-proofing the code
|
|
||||||
if target == ctx.author:
|
|
||||||
return await ctx.send("You can't do mod actions on yourself.")
|
|
||||||
elif target == self.bot.user:
|
|
||||||
return await ctx.send(
|
|
||||||
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
|
|
||||||
)
|
|
||||||
elif self.check_if_target_is_staff(target):
|
|
||||||
return await ctx.send(
|
|
||||||
"I can't warn this user as they're a member of staff."
|
|
||||||
)
|
|
||||||
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
|
|
||||||
warn_count = userlog(
|
|
||||||
self.bot, target.id, ctx.author, reason, "warns", target.name
|
|
||||||
)
|
|
||||||
|
|
||||||
safe_name = await commands.clean_content(escape_markdown=True).convert(
|
|
||||||
ctx, str(target)
|
|
||||||
)
|
|
||||||
chan_msg = (
|
|
||||||
f"⚠️ **Warned**: {str(ctx.author)} warned "
|
|
||||||
f"{target.mention} (warn #{warn_count}) "
|
|
||||||
f"| {safe_name}\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
msg = f"You were warned on {ctx.guild.name}."
|
|
||||||
if reason:
|
|
||||||
msg += " The given reason is: " + reason
|
|
||||||
msg += (
|
|
||||||
f"\n\nPlease read the rules in {self.bot.config.rules_url}. "
|
|
||||||
f"This is warn #{warn_count}."
|
|
||||||
)
|
|
||||||
if warn_count == 2:
|
|
||||||
msg += " __The next warn will automatically kick.__"
|
|
||||||
if warn_count == 3:
|
|
||||||
msg += (
|
|
||||||
"\n\nYou were kicked because of this warning. "
|
|
||||||
"This is your final warning. "
|
|
||||||
"You can join again, but "
|
|
||||||
"**one more warn will result in a ban**."
|
|
||||||
)
|
|
||||||
chan_msg += "**This resulted in an auto-kick.**\n"
|
|
||||||
if warn_count == 4:
|
|
||||||
msg += "\n\nYou were automatically banned due to four warnings."
|
|
||||||
chan_msg += "**This resulted in an auto-ban.**\n"
|
|
||||||
try:
|
|
||||||
await target.send(msg)
|
|
||||||
except discord.errors.Forbidden:
|
|
||||||
# Prevents log issues in cases where user blocked bot
|
|
||||||
# or has DMs disabled
|
|
||||||
pass
|
|
||||||
if warn_count == 3:
|
|
||||||
await target.kick()
|
|
||||||
if warn_count >= 4: # just in case
|
|
||||||
await target.ban(reason="exceeded warn limit", delete_message_days=0)
|
|
||||||
await ctx.send(
|
|
||||||
f"{target.mention} warned. " f"User has {warn_count} warning(s)."
|
|
||||||
)
|
|
||||||
|
|
||||||
if reason:
|
|
||||||
chan_msg += f'✏️ __Reason__: "{reason}"'
|
|
||||||
else:
|
|
||||||
chan_msg += (
|
|
||||||
"Please add an explanation below. In the future"
|
|
||||||
", it is recommended to use `.warn <user> [reason]`"
|
|
||||||
" as the reason is automatically sent to the user."
|
|
||||||
)
|
|
||||||
|
|
||||||
chan_msg += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
|
|
||||||
|
|
||||||
await log_channel.send(chan_msg)
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.bot_has_permissions(ban_members=True)
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command(aliases=["softwarn"])
|
|
||||||
async def hackwarn(self, ctx, target: int, *, reason: str = ""):
|
|
||||||
"""Warns a user with their ID, doesn't message them, staff only."""
|
|
||||||
target_user = await self.bot.fetch_user(target)
|
|
||||||
target_member = ctx.guild.get_member(target)
|
|
||||||
# Hedge-proofing the code
|
|
||||||
if target == ctx.author.id:
|
|
||||||
return await ctx.send("You can't do mod actions on yourself.")
|
|
||||||
elif target == self.bot.user:
|
|
||||||
return await ctx.send(
|
|
||||||
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
|
|
||||||
)
|
|
||||||
elif target_member and self.check_if_target_is_staff(target_member):
|
|
||||||
return await ctx.send(
|
|
||||||
"I can't warn this user as they're a member of staff."
|
|
||||||
)
|
|
||||||
|
|
||||||
warn_count = userlog(
|
|
||||||
self.bot, target, ctx.author, reason, "warns", target_user.name
|
|
||||||
)
|
|
||||||
|
|
||||||
safe_name = await commands.clean_content(escape_markdown=True).convert(
|
|
||||||
ctx, str(target)
|
|
||||||
)
|
|
||||||
|
|
||||||
chan_msg = (
|
|
||||||
f"⚠️ **Hackwarned**: {str(ctx.author)} warned "
|
|
||||||
f"{target_user.mention} (warn #{warn_count}) | {safe_name}\n"
|
|
||||||
f"🏷 __User ID__: {target}\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
if warn_count == 4:
|
|
||||||
userlog(
|
|
||||||
self.bot,
|
|
||||||
target,
|
|
||||||
ctx.author,
|
|
||||||
"exceeded warn limit",
|
|
||||||
"bans",
|
|
||||||
target_user.name,
|
|
||||||
)
|
|
||||||
chan_msg += "**This resulted in an auto-hackban.**\n"
|
|
||||||
await ctx.guild.ban(
|
|
||||||
target_user,
|
|
||||||
reason=f"{ctx.author}, reason: exceeded warn limit",
|
|
||||||
delete_message_days=0,
|
|
||||||
)
|
|
||||||
|
|
||||||
if reason:
|
|
||||||
chan_msg += f'✏️ __Reason__: "{reason}"'
|
|
||||||
else:
|
|
||||||
chan_msg += (
|
|
||||||
"Please add an explanation below. In the future"
|
|
||||||
", it is recommended to use "
|
|
||||||
"`.hackwarn <user> [reason]`."
|
|
||||||
)
|
|
||||||
|
|
||||||
chan_msg += f"\n🔗 __Jump__: <{ctx.message.jump_url}>"
|
|
||||||
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.modlog_channel)
|
|
||||||
await log_channel.send(chan_msg)
|
|
||||||
await ctx.send(f"{safe_name} warned. " f"User has {warn_count} warning(s).")
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command(aliases=["setnick", "nick"])
|
|
||||||
async def nickname(self, ctx, target: Optional[discord.Member], *, nick: str = ""):
|
|
||||||
"""Sets a user's nickname, staff only.
|
|
||||||
|
|
||||||
Just send .nickname <user> to wipe the nickname."""
|
|
||||||
if target is None and ctx.message.reference is None:
|
|
||||||
return await ctx.send(
|
|
||||||
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if ctx.message.reference is not None:
|
|
||||||
target = (
|
|
||||||
await ctx.channel.fetch_message(ctx.message.reference.message_id)
|
|
||||||
).author
|
|
||||||
|
|
||||||
try:
|
|
||||||
if nick:
|
|
||||||
await target.edit(nick=nick, reason=str(ctx.author))
|
|
||||||
else:
|
|
||||||
await target.edit(nick=None, reason=str(ctx.author))
|
|
||||||
|
|
||||||
await ctx.send("Successfully set nickname.")
|
|
||||||
except discord.errors.Forbidden:
|
|
||||||
await ctx.send(
|
|
||||||
"I don't have the permission to set that user's nickname.\n"
|
|
||||||
"User's top role may be above mine, or I may lack Manage Nicknames permission."
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command(aliases=["echo"])
|
|
||||||
async def say(self, ctx, *, the_text: str):
|
|
||||||
"""Repeats a given text, staff only."""
|
|
||||||
await ctx.send(the_text)
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command()
|
|
||||||
async def speak(self, ctx, channel: discord.TextChannel, *, the_text: str):
|
|
||||||
"""Repeats a given text in a given channel, staff only."""
|
|
||||||
await channel.send(the_text)
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command(aliases=["setplaying", "setgame"])
|
|
||||||
async def playing(self, ctx, *, game: str = ""):
|
|
||||||
"""Sets the bot's currently played game name, staff only.
|
|
||||||
|
|
||||||
Just send .playing to wipe the playing state."""
|
|
||||||
if game:
|
|
||||||
await self.bot.change_presence(activity=discord.Game(name=game))
|
|
||||||
else:
|
|
||||||
await self.bot.change_presence(activity=None)
|
|
||||||
|
|
||||||
await ctx.send("Successfully set game.")
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command(aliases=["setbotnick", "botnick", "robotnick"])
|
|
||||||
async def botnickname(self, ctx, *, nick: str = ""):
|
|
||||||
"""Sets the bot's nickname, staff only.
|
|
||||||
|
|
||||||
Just send .botnickname to wipe the nickname."""
|
|
||||||
|
|
||||||
if nick:
|
|
||||||
await ctx.guild.me.edit(nick=nick, reason=str(ctx.author))
|
|
||||||
else:
|
|
||||||
await ctx.guild.me.edit(nick=None, reason=str(ctx.author))
|
|
||||||
|
|
||||||
await ctx.send("Successfully set bot nickname.")
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command()
|
|
||||||
async def move(self, ctx, channelTo: discord.TextChannel, *, limit: int):
|
|
||||||
"""Move a user to another channel, staff only.
|
|
||||||
|
|
||||||
!move {channel to move to} {number of messages}"""
|
|
||||||
# get a list of the messages
|
|
||||||
fetchedMessages = []
|
|
||||||
|
|
||||||
async for message in ctx.channel.history(limit=limit + 1):
|
|
||||||
fetchedMessages.append(message)
|
|
||||||
|
|
||||||
# delete all of those messages from the channel
|
|
||||||
for i in fetchedMessages:
|
|
||||||
await i.delete()
|
|
||||||
|
|
||||||
# invert the list and remove the last message (gets rid of the command message)
|
|
||||||
fetchedMessages = fetchedMessages[::-1]
|
|
||||||
fetchedMessages = fetchedMessages[:-1]
|
|
||||||
|
|
||||||
# Loop over the messages fetched
|
|
||||||
for message in fetchedMessages:
|
|
||||||
# if the message is embedded already
|
|
||||||
if message.embeds:
|
|
||||||
# set the embed message to the old embed object
|
|
||||||
embedMessage = message.embeds[0]
|
|
||||||
# else
|
|
||||||
else:
|
|
||||||
# Create embed message object and set content to original
|
|
||||||
embedMessage = discord.Embed(description=message.content)
|
|
||||||
|
|
||||||
avatar_url = None
|
|
||||||
|
|
||||||
if message.author.display_avatar is not None:
|
|
||||||
avatar_url = str(message.author.display_avatar)
|
|
||||||
|
|
||||||
# set the embed message author to original author
|
|
||||||
embedMessage.set_author(name=message.author, icon_url=avatar_url)
|
|
||||||
# if message has attachments add them
|
|
||||||
if message.attachments:
|
|
||||||
for i in message.attachments:
|
|
||||||
embedMessage.set_image(url=i.proxy_url)
|
|
||||||
|
|
||||||
# Send to the desired channel
|
|
||||||
await channelTo.send(embed=embedMessage)
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command(aliases=["slow"])
|
|
||||||
async def slowmode(
|
|
||||||
self, ctx: Context, seconds: int, channel: Optional[discord.TextChannel] = None
|
|
||||||
):
|
|
||||||
if channel is None:
|
|
||||||
channel = ctx.channel
|
|
||||||
|
|
||||||
if seconds > 21600 or seconds < 0:
|
|
||||||
return await ctx.send("Seconds can't be above '21600' or less then '0'")
|
|
||||||
|
|
||||||
await channel.edit(
|
|
||||||
slowmode_delay=seconds, reason=f"{str(ctx.author)} set the slowmode"
|
|
||||||
)
|
|
||||||
await ctx.send(f"Set the slowmode delay in this channel to {seconds} seconds!")
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(Mod(bot))
|
|
|
@ -1,45 +0,0 @@
|
||||||
from discord.ext import commands
|
|
||||||
from discord.ext.commands import Cog
|
|
||||||
|
|
||||||
from robocop_ng.helpers.checks import check_if_staff
|
|
||||||
|
|
||||||
|
|
||||||
class ModReswitched(Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.command(aliases=["pingmods", "summonmods"])
|
|
||||||
async def pingmod(self, ctx):
|
|
||||||
"""Pings mods, only use when there's an emergency."""
|
|
||||||
can_ping = any(r.id in self.bot.config.pingmods_allow for r in ctx.author.roles)
|
|
||||||
if can_ping:
|
|
||||||
await ctx.send(
|
|
||||||
f"<@&{self.bot.config.pingmods_role}>: {ctx.author.mention} needs assistance."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await ctx.send(
|
|
||||||
f"{ctx.author.mention}: You need the community role to be able to ping the entire mod team, please pick an online mod (not staff, please!), and ping them instead."
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command(aliases=["togglemod"])
|
|
||||||
async def modtoggle(self, ctx):
|
|
||||||
"""Toggles your mod role, staff only."""
|
|
||||||
target_role = ctx.guild.get_role(self.bot.config.modtoggle_role)
|
|
||||||
|
|
||||||
if target_role in ctx.author.roles:
|
|
||||||
await ctx.author.remove_roles(
|
|
||||||
target_role, reason="Staff self-unassigned mod role"
|
|
||||||
)
|
|
||||||
await ctx.send(f"{ctx.author.mention}: Removed your mod role.")
|
|
||||||
else:
|
|
||||||
await ctx.author.add_roles(
|
|
||||||
target_role, reason="Staff self-assigned mod role"
|
|
||||||
)
|
|
||||||
await ctx.send(f"{ctx.author.mention}: Gave you mod role.")
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(ModReswitched(bot))
|
|
|
@ -1,188 +0,0 @@
|
||||||
from datetime import datetime
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import discord
|
|
||||||
from discord.ext import commands
|
|
||||||
from discord.ext.commands import Cog
|
|
||||||
|
|
||||||
from robocop_ng.helpers.checks import check_if_staff
|
|
||||||
from robocop_ng.helpers.restrictions import add_restriction
|
|
||||||
from robocop_ng.helpers.robocronp import add_job
|
|
||||||
from robocop_ng.helpers.userlogs import userlog
|
|
||||||
|
|
||||||
|
|
||||||
class ModTimed(Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
def check_if_target_is_staff(self, target):
|
|
||||||
return any(r.id in self.bot.config.staff_role_ids for r in target.roles)
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.bot_has_permissions(ban_members=True)
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command()
|
|
||||||
async def timeban(
|
|
||||||
self, ctx, target: Optional[discord.Member], duration: str, *, reason: str = ""
|
|
||||||
):
|
|
||||||
"""Bans a user for a specified amount of time, staff only."""
|
|
||||||
if target is None and ctx.message.reference is None:
|
|
||||||
return await ctx.send(
|
|
||||||
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if ctx.message.reference is not None:
|
|
||||||
if target is not None:
|
|
||||||
duration = str(target) + duration
|
|
||||||
target = (
|
|
||||||
await ctx.channel.fetch_message(ctx.message.reference.message_id)
|
|
||||||
).author
|
|
||||||
# Hedge-proofing the code
|
|
||||||
if target == ctx.author:
|
|
||||||
return await ctx.send("You can't do mod actions on yourself.")
|
|
||||||
elif self.check_if_target_is_staff(target):
|
|
||||||
return await ctx.send("I can't ban this user as they're a member of staff.")
|
|
||||||
|
|
||||||
expiry_timestamp = self.bot.parse_time(duration)
|
|
||||||
expiry_datetime = datetime.utcfromtimestamp(expiry_timestamp)
|
|
||||||
duration_text = self.bot.get_relative_timestamp(
|
|
||||||
time_to=expiry_datetime, include_to=True, humanized=True
|
|
||||||
)
|
|
||||||
|
|
||||||
userlog(
|
|
||||||
self.bot,
|
|
||||||
target.id,
|
|
||||||
ctx.author,
|
|
||||||
f"{reason} (Timed, until " f"{duration_text})",
|
|
||||||
"bans",
|
|
||||||
target.name,
|
|
||||||
)
|
|
||||||
|
|
||||||
safe_name = await commands.clean_content(escape_markdown=True).convert(
|
|
||||||
ctx, str(target)
|
|
||||||
)
|
|
||||||
|
|
||||||
dm_message = f"You were banned from {ctx.guild.name}."
|
|
||||||
if reason:
|
|
||||||
dm_message += f' The given reason is: "{reason}".'
|
|
||||||
dm_message += f"\n\nThis ban will expire {duration_text}."
|
|
||||||
|
|
||||||
try:
|
|
||||||
await target.send(dm_message)
|
|
||||||
except discord.errors.Forbidden:
|
|
||||||
# Prevents ban issues in cases where user blocked bot
|
|
||||||
# or has DMs disabled
|
|
||||||
pass
|
|
||||||
|
|
||||||
await target.ban(
|
|
||||||
reason=f"{ctx.author}, reason: {reason}", delete_message_days=0
|
|
||||||
)
|
|
||||||
chan_message = (
|
|
||||||
f"⛔ **Timed Ban**: {ctx.author.mention} banned "
|
|
||||||
f"{target.mention} for {duration_text} | {safe_name}\n"
|
|
||||||
f"🏷 __User ID__: {target.id}\n"
|
|
||||||
)
|
|
||||||
if reason:
|
|
||||||
chan_message += f'✏️ __Reason__: "{reason}"'
|
|
||||||
else:
|
|
||||||
chan_message += (
|
|
||||||
"Please add an explanation below. In the future"
|
|
||||||
", it is recommended to use `.ban <user> [reason]`"
|
|
||||||
" as the reason is automatically sent to the user."
|
|
||||||
)
|
|
||||||
|
|
||||||
add_job(self.bot, "unban", target.id, {"guild": ctx.guild.id}, expiry_timestamp)
|
|
||||||
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.log_channel)
|
|
||||||
await log_channel.send(chan_message)
|
|
||||||
await ctx.send(f"{safe_name} is now b&. " f"It will expire {duration_text}. 👍")
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command()
|
|
||||||
async def timemute(
|
|
||||||
self, ctx, target: Optional[discord.Member], duration: str, *, reason: str = ""
|
|
||||||
):
|
|
||||||
"""Mutes a user for a specified amount of time, staff only."""
|
|
||||||
if target is None and ctx.message.reference is None:
|
|
||||||
return await ctx.send(
|
|
||||||
f"I'm sorry {ctx.author.mention}, I'm afraid I can't do that."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if ctx.message.reference is not None:
|
|
||||||
if target is not None:
|
|
||||||
duration = str(target) + duration
|
|
||||||
target = (
|
|
||||||
await ctx.channel.fetch_message(ctx.message.reference.message_id)
|
|
||||||
).author
|
|
||||||
# Hedge-proofing the code
|
|
||||||
if target == ctx.author:
|
|
||||||
return await ctx.send("You can't do mod actions on yourself.")
|
|
||||||
elif self.check_if_target_is_staff(target):
|
|
||||||
return await ctx.send(
|
|
||||||
"I can't mute this user as they're a member of staff."
|
|
||||||
)
|
|
||||||
|
|
||||||
expiry_timestamp = self.bot.parse_time(duration)
|
|
||||||
expiry_datetime = datetime.utcfromtimestamp(expiry_timestamp)
|
|
||||||
duration_text = self.bot.get_relative_timestamp(
|
|
||||||
time_to=expiry_datetime, include_to=True, humanized=True
|
|
||||||
)
|
|
||||||
|
|
||||||
userlog(
|
|
||||||
self.bot,
|
|
||||||
target.id,
|
|
||||||
ctx.author,
|
|
||||||
f"{reason} (Timed, until " f"{duration_text})",
|
|
||||||
"mutes",
|
|
||||||
target.name,
|
|
||||||
)
|
|
||||||
|
|
||||||
safe_name = await commands.clean_content(escape_markdown=True).convert(
|
|
||||||
ctx, str(target)
|
|
||||||
)
|
|
||||||
|
|
||||||
dm_message = f"You were muted!"
|
|
||||||
if reason:
|
|
||||||
dm_message += f' The given reason is: "{reason}".'
|
|
||||||
dm_message += f"\n\nThis mute will expire {duration_text}."
|
|
||||||
|
|
||||||
try:
|
|
||||||
await target.send(dm_message)
|
|
||||||
except discord.errors.Forbidden:
|
|
||||||
# Prevents kick issues in cases where user blocked bot
|
|
||||||
# or has DMs disabled
|
|
||||||
pass
|
|
||||||
|
|
||||||
mute_role = ctx.guild.get_role(self.bot.config.mute_role)
|
|
||||||
|
|
||||||
await target.add_roles(mute_role, reason=str(ctx.author))
|
|
||||||
|
|
||||||
chan_message = (
|
|
||||||
f"🔇 **Timed Mute**: {ctx.author.mention} muted "
|
|
||||||
f"{target.mention} for {duration_text} | {safe_name}\n"
|
|
||||||
f"🏷 __User ID__: {target.id}\n"
|
|
||||||
)
|
|
||||||
if reason:
|
|
||||||
chan_message += f'✏️ __Reason__: "{reason}"'
|
|
||||||
else:
|
|
||||||
chan_message += (
|
|
||||||
"Please add an explanation below. In the future, "
|
|
||||||
"it is recommended to use `.mute <user> [reason]`"
|
|
||||||
" as the reason is automatically sent to the user."
|
|
||||||
)
|
|
||||||
|
|
||||||
add_job(
|
|
||||||
self.bot, "unmute", target.id, {"guild": ctx.guild.id}, expiry_timestamp
|
|
||||||
)
|
|
||||||
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.log_channel)
|
|
||||||
await log_channel.send(chan_message)
|
|
||||||
await ctx.send(
|
|
||||||
f"{target.mention} can no longer speak. " f"It will expire {duration_text}."
|
|
||||||
)
|
|
||||||
add_restriction(self.bot, target.id, self.bot.config.mute_role)
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(ModTimed(bot))
|
|
|
@ -1,179 +0,0 @@
|
||||||
import time
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
import discord
|
|
||||||
from discord.ext import commands, tasks
|
|
||||||
from discord.ext.commands import Cog
|
|
||||||
|
|
||||||
from robocop_ng.helpers.checks import check_if_staff
|
|
||||||
from robocop_ng.helpers.restrictions import remove_restriction
|
|
||||||
from robocop_ng.helpers.robocronp import get_crontab, delete_job
|
|
||||||
|
|
||||||
|
|
||||||
class Robocronp(Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
self.minutely.start()
|
|
||||||
self.hourly.start()
|
|
||||||
self.daily.start()
|
|
||||||
|
|
||||||
def cog_unload(self):
|
|
||||||
self.minutely.cancel()
|
|
||||||
self.hourly.cancel()
|
|
||||||
self.daily.cancel()
|
|
||||||
|
|
||||||
async def send_data(self):
|
|
||||||
await self.bot.wait_until_ready()
|
|
||||||
data_files = [discord.File(fpath) for fpath in self.bot.wanted_jsons]
|
|
||||||
log_channel = await self.bot.get_channel_safe(self.bot.config.botlog_channel)
|
|
||||||
await log_channel.send("Hourly data backups:", files=data_files)
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command()
|
|
||||||
async def listjobs(self, ctx):
|
|
||||||
"""Lists timed robocronp jobs, staff only."""
|
|
||||||
ctab = get_crontab(self.bot)
|
|
||||||
embed = discord.Embed(title=f"Active robocronp jobs")
|
|
||||||
for jobtype in ctab:
|
|
||||||
for jobtimestamp in ctab[jobtype]:
|
|
||||||
for job_name in ctab[jobtype][jobtimestamp]:
|
|
||||||
job_details = repr(ctab[jobtype][jobtimestamp][job_name])
|
|
||||||
embed.add_field(
|
|
||||||
name=f"{jobtype} for {job_name}",
|
|
||||||
value=f"Timestamp: {jobtimestamp}, Details: {job_details}",
|
|
||||||
inline=False,
|
|
||||||
)
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command(aliases=["removejob"])
|
|
||||||
async def deletejob(self, ctx, timestamp: str, job_type: str, job_name: str):
|
|
||||||
"""Removes a timed robocronp job, staff only.
|
|
||||||
|
|
||||||
You'll need to supply:
|
|
||||||
- timestamp (like 1545981602)
|
|
||||||
- job type (like "unban")
|
|
||||||
- job name (userid, like 420332322307571713)
|
|
||||||
|
|
||||||
You can get all 3 from listjobs command."""
|
|
||||||
delete_job(self.bot, timestamp, job_type, job_name)
|
|
||||||
await ctx.send(f"{ctx.author.mention}: Deleted!")
|
|
||||||
|
|
||||||
async def do_jobs(self, ctab, jobtype, timestamp):
|
|
||||||
await self.bot.wait_until_ready()
|
|
||||||
log_channel = await self.bot.get_channel_safe(self.bot.config.botlog_channel)
|
|
||||||
for job_name in ctab[jobtype][timestamp]:
|
|
||||||
try:
|
|
||||||
job_details = ctab[jobtype][timestamp][job_name]
|
|
||||||
if jobtype == "unban":
|
|
||||||
target_user = await self.bot.fetch_user(job_name)
|
|
||||||
target_guild = self.bot.get_guild(job_details["guild"])
|
|
||||||
delete_job(self.bot, timestamp, jobtype, job_name)
|
|
||||||
await target_guild.unban(
|
|
||||||
target_user, reason="Robocronp: Timed ban expired."
|
|
||||||
)
|
|
||||||
elif jobtype == "unmute":
|
|
||||||
remove_restriction(self.bot, job_name, self.bot.config.mute_role)
|
|
||||||
target_guild = self.bot.get_guild(job_details["guild"])
|
|
||||||
target_member = target_guild.get_member(int(job_name))
|
|
||||||
target_role = target_guild.get_role(self.bot.config.mute_role)
|
|
||||||
await target_member.remove_roles(
|
|
||||||
target_role, reason="Robocronp: Timed mute expired."
|
|
||||||
)
|
|
||||||
delete_job(self.bot, timestamp, jobtype, job_name)
|
|
||||||
elif jobtype == "remind":
|
|
||||||
text = job_details["text"]
|
|
||||||
added_on = job_details["added"]
|
|
||||||
target = await self.bot.fetch_user(int(job_name))
|
|
||||||
if target:
|
|
||||||
await target.send(
|
|
||||||
f"You asked to be reminded about `{text}` on {added_on}."
|
|
||||||
)
|
|
||||||
delete_job(self.bot, timestamp, jobtype, job_name)
|
|
||||||
except:
|
|
||||||
# Don't kill cronjobs if something goes wrong.
|
|
||||||
delete_job(self.bot, timestamp, jobtype, job_name)
|
|
||||||
await log_channel.send(
|
|
||||||
"Crondo has errored, job deleted: ```"
|
|
||||||
f"{traceback.format_exc()}```"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def clean_channel(self, channel_id):
|
|
||||||
await self.bot.wait_until_ready()
|
|
||||||
log_channel = await self.bot.get_channel_safe(self.bot.config.botlog_channel)
|
|
||||||
channel = await self.bot.get_channel_safe(channel_id)
|
|
||||||
try:
|
|
||||||
done_cleaning = False
|
|
||||||
count = 0
|
|
||||||
while not done_cleaning:
|
|
||||||
purge_res = await channel.purge(limit=100)
|
|
||||||
count += len(purge_res)
|
|
||||||
if len(purge_res) != 100:
|
|
||||||
done_cleaning = True
|
|
||||||
await log_channel.send(
|
|
||||||
f"Wiped {count} messages from <#{channel.id}> automatically."
|
|
||||||
)
|
|
||||||
except:
|
|
||||||
# Don't kill cronjobs if something goes wrong.
|
|
||||||
await log_channel.send(
|
|
||||||
f"Cronclean has errored: ```{traceback.format_exc()}```"
|
|
||||||
)
|
|
||||||
|
|
||||||
@tasks.loop(minutes=1)
|
|
||||||
async def minutely(self):
|
|
||||||
await self.bot.wait_until_ready()
|
|
||||||
log_channel = await self.bot.get_channel_safe(self.bot.config.botlog_channel)
|
|
||||||
try:
|
|
||||||
ctab = get_crontab(self.bot)
|
|
||||||
timestamp = time.time()
|
|
||||||
for jobtype in ctab:
|
|
||||||
for jobtimestamp in ctab[jobtype]:
|
|
||||||
if timestamp > int(jobtimestamp):
|
|
||||||
await self.do_jobs(ctab, jobtype, jobtimestamp)
|
|
||||||
|
|
||||||
# Handle clean channels
|
|
||||||
for clean_channel in self.bot.config.minutely_clean_channels:
|
|
||||||
await self.clean_channel(clean_channel)
|
|
||||||
except:
|
|
||||||
# Don't kill cronjobs if something goes wrong.
|
|
||||||
await log_channel.send(
|
|
||||||
f"Cron-minutely has errored: ```{traceback.format_exc()}```"
|
|
||||||
)
|
|
||||||
|
|
||||||
@tasks.loop(hours=1)
|
|
||||||
async def hourly(self):
|
|
||||||
await self.bot.wait_until_ready()
|
|
||||||
log_channel = await self.bot.get_channel_safe(self.bot.config.botlog_channel)
|
|
||||||
try:
|
|
||||||
await self.send_data()
|
|
||||||
# Handle clean channels
|
|
||||||
for clean_channel in self.bot.config.hourly_clean_channels:
|
|
||||||
await self.clean_channel(clean_channel)
|
|
||||||
except:
|
|
||||||
# Don't kill cronjobs if something goes wrong.
|
|
||||||
await log_channel.send(
|
|
||||||
f"Cron-hourly has errored: ```{traceback.format_exc()}```"
|
|
||||||
)
|
|
||||||
|
|
||||||
@tasks.loop(hours=24)
|
|
||||||
async def daily(self):
|
|
||||||
await self.bot.wait_until_ready()
|
|
||||||
log_channel = await self.bot.get_channel_safe(self.bot.config.botlog_channel)
|
|
||||||
try:
|
|
||||||
# Reset verification and algorithm
|
|
||||||
if "cogs.verification" in self.bot.config.initial_cogs:
|
|
||||||
verif_channel = await self.bot.get_channel_safe(
|
|
||||||
self.bot.config.welcome_channel
|
|
||||||
)
|
|
||||||
await self.bot.do_resetalgo(verif_channel, "daily robocronp")
|
|
||||||
except:
|
|
||||||
# Don't kill cronjobs if something goes wrong.
|
|
||||||
await log_channel.send(
|
|
||||||
f"Cron-daily has errored: ```{traceback.format_exc()}```"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(Robocronp(bot))
|
|
|
@ -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))
|
|
|
@ -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))
|
|
|
@ -1,100 +0,0 @@
|
||||||
import discord
|
|
||||||
from discord.ext import commands
|
|
||||||
from discord.ext.commands import Cog
|
|
||||||
|
|
||||||
from robocop_ng.helpers.checks import check_if_staff
|
|
||||||
|
|
||||||
|
|
||||||
class RyujinxVerification(Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
# Export reset channel functions
|
|
||||||
self.bot.do_reset = self.do_reset
|
|
||||||
self.bot.do_resetalgo = self.do_resetalgo
|
|
||||||
|
|
||||||
@Cog.listener()
|
|
||||||
async def on_member_join(self, member):
|
|
||||||
await self.bot.wait_until_ready()
|
|
||||||
|
|
||||||
if member.guild.id not in self.bot.config.guild_whitelist:
|
|
||||||
return
|
|
||||||
|
|
||||||
join_channel = self.bot.get_channel(self.bot.config.welcome_channel)
|
|
||||||
|
|
||||||
if join_channel is not None:
|
|
||||||
await join_channel.send(
|
|
||||||
"Hello {0.mention}! Welcome to Ryujinx! Please read the <#411271165429022730>, and then type the verifying command here to gain access to the rest of the channels.\n\nIf you need help with basic common questions, visit the <#585288848704143371> channel after joining.\n\nIf you need help with Animal Crossing visit the <#692104087889641472> channel for common issues and solutions. If you need help that is not Animal Crossing related, please visit the <#410208610455519243> channel after verifying.".format(
|
|
||||||
member
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def process_message(self, message):
|
|
||||||
"""Process the verification process"""
|
|
||||||
if message.channel.id == self.bot.config.welcome_channel:
|
|
||||||
# Assign common stuff into variables to make stuff less of a mess
|
|
||||||
mcl = message.content.lower()
|
|
||||||
|
|
||||||
# Get the role we will give in case of success
|
|
||||||
success_role = message.guild.get_role(self.bot.config.participant_role)
|
|
||||||
|
|
||||||
if self.bot.config.verification_string == mcl:
|
|
||||||
await message.author.add_roles(success_role)
|
|
||||||
await message.delete()
|
|
||||||
|
|
||||||
@Cog.listener()
|
|
||||||
async def on_message(self, message):
|
|
||||||
if message.author.bot:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.process_message(message)
|
|
||||||
except discord.errors.Forbidden:
|
|
||||||
chan = self.bot.get_channel(message.channel)
|
|
||||||
await chan.send("💢 I don't have permission to do this.")
|
|
||||||
|
|
||||||
@Cog.listener()
|
|
||||||
async def on_message_edit(self, before, after):
|
|
||||||
if after.author.bot:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.process_message(after)
|
|
||||||
except discord.errors.Forbidden:
|
|
||||||
chan = self.bot.get_channel(after.channel)
|
|
||||||
await chan.send("💢 I don't have permission to do this.")
|
|
||||||
|
|
||||||
@Cog.listener()
|
|
||||||
async def on_member_join(self, member):
|
|
||||||
await self.bot.wait_until_ready()
|
|
||||||
|
|
||||||
if member.guild.id not in self.bot.config.guild_whitelist:
|
|
||||||
return
|
|
||||||
|
|
||||||
join_channel = self.bot.get_channel(self.bot.config.welcome_channel)
|
|
||||||
|
|
||||||
if join_channel is not None:
|
|
||||||
await join_channel.send(self.bot.config.join_message.format(member))
|
|
||||||
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command()
|
|
||||||
async def reset(self, ctx, limit: int = 100, force: bool = False):
|
|
||||||
"""Wipes messages and pastes the welcome message again. Staff only."""
|
|
||||||
if ctx.message.channel.id != self.bot.config.welcome_channel and not force:
|
|
||||||
await ctx.send(
|
|
||||||
f"This command is limited to"
|
|
||||||
f" <#{self.bot.config.welcome_channel}>, unless forced."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
await self.do_reset(ctx.channel, ctx.author.mention, limit)
|
|
||||||
|
|
||||||
async def do_reset(self, channel, author, limit: int = 100):
|
|
||||||
await channel.purge(limit=limit)
|
|
||||||
|
|
||||||
async def do_resetalgo(self, channel, author, limit: int = 100):
|
|
||||||
# We only auto clear the channel daily
|
|
||||||
await self.do_reset(channel, author)
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(RyujinxVerification(bot))
|
|
|
@ -1,48 +0,0 @@
|
||||||
from discord.ext import commands
|
|
||||||
from discord.ext.commands import Cog
|
|
||||||
|
|
||||||
from robocop_ng.helpers.checks import check_if_staff_or_ot
|
|
||||||
|
|
||||||
|
|
||||||
class SAR(Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.command()
|
|
||||||
@commands.check(check_if_staff_or_ot)
|
|
||||||
async def sar(self, ctx):
|
|
||||||
"""Lists self assignable roles."""
|
|
||||||
return await ctx.send(
|
|
||||||
"Self assignable roles in this guild: "
|
|
||||||
+ ",".join(self.bot.config.self_assignable_roles)
|
|
||||||
+ f"\n\nRun `{self.bot.config.prefixes[0]}iam role_name_goes_here` to get or remove one."
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.cooldown(1, 30, type=commands.BucketType.user)
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.command(aliases=["iamnot"])
|
|
||||||
@commands.check(check_if_staff_or_ot)
|
|
||||||
async def iam(self, ctx, role: str):
|
|
||||||
"""Gets you a self assignable role."""
|
|
||||||
if role not in self.bot.config.self_assignable_roles:
|
|
||||||
return await ctx.send(
|
|
||||||
"There's no self assignable role with that name. Run .sar to see what you can self assign."
|
|
||||||
)
|
|
||||||
|
|
||||||
target_role = ctx.guild.get_role(self.bot.config.self_assignable_roles[role])
|
|
||||||
|
|
||||||
if target_role in ctx.author.roles:
|
|
||||||
await ctx.author.remove_roles(target_role, reason=str(ctx.author))
|
|
||||||
await ctx.send(
|
|
||||||
f"{ctx.author.mention}: Successfully removed your `{role}` role. Run the command again if you want to add it again."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await ctx.author.add_roles(target_role, reason=str(ctx.author))
|
|
||||||
await ctx.send(
|
|
||||||
f"{ctx.author.mention}: Successfully gave you the `{role}` role. Run the command again if you want to remove it."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(SAR(bot))
|
|
|
@ -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))
|
|
|
@ -1,227 +0,0 @@
|
||||||
import asyncio
|
|
||||||
import hashlib
|
|
||||||
import itertools
|
|
||||||
import random
|
|
||||||
from inspect import cleandoc
|
|
||||||
|
|
||||||
import discord
|
|
||||||
from discord.ext import commands
|
|
||||||
from discord.ext.commands import Cog
|
|
||||||
|
|
||||||
from robocop_ng.helpers.checks import check_if_staff
|
|
||||||
|
|
||||||
|
|
||||||
class Verification(Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
self.hash_choice = random.choice(self.bot.config.welcome_hashes)
|
|
||||||
|
|
||||||
# Export reset channel functions
|
|
||||||
self.bot.do_reset = self.do_reset
|
|
||||||
self.bot.do_resetalgo = self.do_resetalgo
|
|
||||||
|
|
||||||
async def do_reset(self, channel, author, limit: int = 100):
|
|
||||||
await channel.purge(limit=limit)
|
|
||||||
|
|
||||||
await channel.send(self.bot.config.welcome_header)
|
|
||||||
rules = [
|
|
||||||
"**{}**. {}".format(i, cleandoc(r))
|
|
||||||
for i, r in enumerate(self.bot.config.welcome_rules, 1)
|
|
||||||
]
|
|
||||||
rule_choice = random.randint(2, len(rules))
|
|
||||||
hash_choice_str = self.hash_choice.upper()
|
|
||||||
if hash_choice_str == "BLAKE2B":
|
|
||||||
hash_choice_str += "-512"
|
|
||||||
elif hash_choice_str == "BLAKE2S":
|
|
||||||
hash_choice_str += "-256"
|
|
||||||
rules[rule_choice - 1] += "\n" + self.bot.config.hidden_term_line.format(
|
|
||||||
hash_choice_str
|
|
||||||
)
|
|
||||||
msg = (
|
|
||||||
f"🗑 **Reset**: {author} cleared {limit} messages " f" in {channel.mention}"
|
|
||||||
)
|
|
||||||
msg += f"\n💬 __Current challenge location__: under rule {rule_choice}"
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.log_channel)
|
|
||||||
await log_channel.send(msg)
|
|
||||||
|
|
||||||
# find rule that puts us over 2,000 characters, if any
|
|
||||||
total = 0
|
|
||||||
messages = []
|
|
||||||
current_message = ""
|
|
||||||
for item in rules:
|
|
||||||
total += len(item) + 2 # \n\n
|
|
||||||
if total < 2000:
|
|
||||||
current_message += item + "\n\n"
|
|
||||||
else:
|
|
||||||
# we've hit the limit; split!
|
|
||||||
messages += [current_message]
|
|
||||||
current_message = "\n\u200B\n" + item + "\n\u200B\n"
|
|
||||||
total = 0
|
|
||||||
messages += [current_message]
|
|
||||||
|
|
||||||
for item in messages:
|
|
||||||
await channel.send(item)
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
for x in self.bot.config.welcome_footer:
|
|
||||||
await channel.send(cleandoc(x))
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
async def do_resetalgo(self, channel, author, limit: int = 100):
|
|
||||||
# randomize hash_choice on reset
|
|
||||||
self.hash_choice = random.choice(tuple(self.bot.config.welcome_hashes))
|
|
||||||
|
|
||||||
msg = (
|
|
||||||
f"📘 **Reset Algorithm**: {author} reset " f"algorithm in {channel.mention}"
|
|
||||||
)
|
|
||||||
msg += f"\n💬 __Current algorithm__: {self.hash_choice.upper()}"
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.log_channel)
|
|
||||||
await log_channel.send(msg)
|
|
||||||
|
|
||||||
await self.do_reset(channel, author)
|
|
||||||
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command()
|
|
||||||
async def reset(self, ctx, limit: int = 100, force: bool = False):
|
|
||||||
"""Wipes messages and pastes the welcome message again. Staff only."""
|
|
||||||
if ctx.message.channel.id != self.bot.config.welcome_channel and not force:
|
|
||||||
await ctx.send(
|
|
||||||
f"This command is limited to"
|
|
||||||
f" <#{self.bot.config.welcome_channel}>, unless forced."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
await self.do_reset(ctx.channel, ctx.author.mention, limit)
|
|
||||||
|
|
||||||
@commands.check(check_if_staff)
|
|
||||||
@commands.command()
|
|
||||||
async def resetalgo(self, ctx, limit: int = 100, force: bool = False):
|
|
||||||
"""Resets the verification algorithm and does what reset does. Staff only."""
|
|
||||||
if ctx.message.channel.id != self.bot.config.welcome_channel and not force:
|
|
||||||
await ctx.send(
|
|
||||||
f"This command is limited to"
|
|
||||||
f" <#{self.bot.config.welcome_channel}>, unless forced."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
await self.do_resetalgo(ctx.channel, ctx.author.mention, limit)
|
|
||||||
|
|
||||||
async def process_message(self, message):
|
|
||||||
"""Big code that makes me want to shoot myself
|
|
||||||
Not really a rewrite but more of a port
|
|
||||||
|
|
||||||
Git blame tells me that I should blame/credit Robin Lambertz"""
|
|
||||||
if message.channel.id == self.bot.config.welcome_channel:
|
|
||||||
# Assign common stuff into variables to make stuff less of a mess
|
|
||||||
member = message.author
|
|
||||||
full_name = str(member)
|
|
||||||
discrim = str(member.discriminator)
|
|
||||||
guild = message.guild
|
|
||||||
chan = message.channel
|
|
||||||
mcl = message.content.lower()
|
|
||||||
|
|
||||||
# Reply to users that insult the bot
|
|
||||||
oof = [
|
|
||||||
"bad",
|
|
||||||
"broken",
|
|
||||||
"buggy",
|
|
||||||
"bugged",
|
|
||||||
"stupid",
|
|
||||||
"dumb",
|
|
||||||
"silly",
|
|
||||||
"fuck",
|
|
||||||
"heck",
|
|
||||||
"h*ck",
|
|
||||||
]
|
|
||||||
if "bot" in mcl and any(insult in mcl for insult in oof):
|
|
||||||
snark = random.choice(["bad human", "no u", "no u, rtfm", "pebkac"])
|
|
||||||
return await chan.send(snark)
|
|
||||||
|
|
||||||
# Get the role we will give in case of success
|
|
||||||
success_role = guild.get_role(self.bot.config.named_roles["participant"])
|
|
||||||
|
|
||||||
# Get a list of stuff we'll allow and will consider close
|
|
||||||
allowed_names = [f"@{full_name}", full_name, str(member.id)]
|
|
||||||
close_names = [f"@{member.name}", member.name, discrim, f"#{discrim}"]
|
|
||||||
# Now add the same things but with newlines at the end of them
|
|
||||||
allowed_names += [(an + "\n") for an in allowed_names]
|
|
||||||
close_names += [(cn + "\n") for cn in close_names]
|
|
||||||
allowed_names += [(an + "\r\n") for an in allowed_names]
|
|
||||||
close_names += [(cn + "\r\n") for cn in close_names]
|
|
||||||
# [ ͡° ͜ᔦ ͡°] 𝐖𝐞𝐥𝐜𝐨𝐦𝐞 𝐭𝐨 𝐌𝐚𝐜 𝐎𝐒 𝟗.
|
|
||||||
allowed_names += [(an + "\r") for an in allowed_names]
|
|
||||||
close_names += [(cn + "\r") for cn in close_names]
|
|
||||||
|
|
||||||
# Finally, hash the stuff so that we can access them later :)
|
|
||||||
hash_allow = [
|
|
||||||
hashlib.new(self.hash_choice, name.encode("utf-8")).hexdigest()
|
|
||||||
for name in allowed_names
|
|
||||||
]
|
|
||||||
|
|
||||||
# I'm not even going to attempt to break those into lines jfc
|
|
||||||
if any(allow in mcl for allow in hash_allow):
|
|
||||||
await member.add_roles(success_role)
|
|
||||||
return await chan.purge(
|
|
||||||
limit=100,
|
|
||||||
check=lambda m: m.author == message.author
|
|
||||||
or (
|
|
||||||
m.author == self.bot.user
|
|
||||||
and message.author.mention in m.content
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Detect if the user uses the wrong hash algorithm
|
|
||||||
wrong_hash_algos = list(
|
|
||||||
set(self.bot.config.welcome_hashes) - {self.hash_choice}
|
|
||||||
)
|
|
||||||
for algo in wrong_hash_algos:
|
|
||||||
for name in itertools.chain(allowed_names, close_names):
|
|
||||||
if hashlib.new(algo, name.encode("utf-8")).hexdigest() in mcl:
|
|
||||||
log_channel = self.bot.get_channel(self.bot.config.log_channel)
|
|
||||||
await log_channel.send(
|
|
||||||
f"User {message.author.mention} tried verification with algo {algo} instead of {self.hash_choice}."
|
|
||||||
)
|
|
||||||
return await chan.send(
|
|
||||||
f"{message.author.mention} :no_entry: Close, but not quite. Go back and re-read!"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
full_name in message.content
|
|
||||||
or str(member.id) in message.content
|
|
||||||
or member.name in message.content
|
|
||||||
or discrim in message.content
|
|
||||||
):
|
|
||||||
no_text = ":no_entry: Incorrect. You need to do something *specific* with your name and discriminator instead of just posting it. Please re-read the rules carefully and look up any terms you are not familiar with."
|
|
||||||
rand_num = random.randint(1, 100)
|
|
||||||
if rand_num == 42:
|
|
||||||
no_text = "you're doing it wrong"
|
|
||||||
elif rand_num == 43:
|
|
||||||
no_text = "ugh, wrong, read the rules."
|
|
||||||
elif rand_num == 44:
|
|
||||||
no_text = '"The definition of insanity is doing the same thing over and over again, but expecting different results."\n-Albert Einstein'
|
|
||||||
await chan.send(f"{message.author.mention} {no_text}")
|
|
||||||
|
|
||||||
@Cog.listener()
|
|
||||||
async def on_message(self, message):
|
|
||||||
if message.author.bot:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.process_message(message)
|
|
||||||
except discord.errors.Forbidden:
|
|
||||||
chan = self.bot.get_channel(message.channel)
|
|
||||||
await chan.send("💢 I don't have permission to do this.")
|
|
||||||
|
|
||||||
@Cog.listener()
|
|
||||||
async def on_message_edit(self, before, after):
|
|
||||||
if after.author.bot:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.process_message(after)
|
|
||||||
except discord.errors.Forbidden:
|
|
||||||
chan = self.bot.get_channel(after.channel)
|
|
||||||
await chan.send("💢 I don't have permission to do this.")
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(Verification(bot))
|
|
|
@ -1,153 +0,0 @@
|
||||||
import asyncio
|
|
||||||
import base64
|
|
||||||
import hmac
|
|
||||||
import re
|
|
||||||
import secrets
|
|
||||||
|
|
||||||
from discord.ext.commands import Cog
|
|
||||||
|
|
||||||
|
|
||||||
class YubicoOTP(Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
self.otp_re = re.compile("((cc|vv)[cbdefghijklnrtuv]{42})$")
|
|
||||||
self.api_servers = [
|
|
||||||
"https://api.yubico.com",
|
|
||||||
"https://api2.yubico.com",
|
|
||||||
"https://api3.yubico.com",
|
|
||||||
"https://api4.yubico.com",
|
|
||||||
"https://api5.yubico.com",
|
|
||||||
]
|
|
||||||
self.reuse_responses = ["BAD_OTP", "REPLAYED_OTP"]
|
|
||||||
self.bad_responses = [
|
|
||||||
"MISSING_PARAMETER",
|
|
||||||
"NO_SUCH_CLIENT",
|
|
||||||
"OPERATION_NOT_ALLOWED",
|
|
||||||
]
|
|
||||||
self.modhex_to_hex_conversion_map = {
|
|
||||||
"c": "0",
|
|
||||||
"b": "1",
|
|
||||||
"d": "2",
|
|
||||||
"e": "3",
|
|
||||||
"f": "4",
|
|
||||||
"g": "5",
|
|
||||||
"h": "6",
|
|
||||||
"i": "7",
|
|
||||||
"j": "8",
|
|
||||||
"k": "9",
|
|
||||||
"l": "a",
|
|
||||||
"n": "b",
|
|
||||||
"r": "c",
|
|
||||||
"t": "d",
|
|
||||||
"u": "e",
|
|
||||||
"v": "f",
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_serial(self, otp):
|
|
||||||
"""Get OTP from serial, based on code by linuxgemini"""
|
|
||||||
if otp[:2] != "cc":
|
|
||||||
return False
|
|
||||||
|
|
||||||
hexconv = []
|
|
||||||
|
|
||||||
for modhexletter in otp[0:12]:
|
|
||||||
hexconv.append(self.modhex_to_hex_conversion_map[modhexletter])
|
|
||||||
|
|
||||||
return int("".join(hexconv), 16)
|
|
||||||
|
|
||||||
def calc_signature(self, text):
|
|
||||||
key = base64.b64decode(self.bot.config.yubico_otp_secret)
|
|
||||||
signature_bytes = hmac.digest(key, text.encode(), "SHA1")
|
|
||||||
return base64.b64encode(signature_bytes).decode()
|
|
||||||
|
|
||||||
def validate_response_signature(self, response_dict):
|
|
||||||
yubico_signature = response_dict["h"]
|
|
||||||
to_sign = ""
|
|
||||||
for key in sorted(response_dict.keys()):
|
|
||||||
if key == "h":
|
|
||||||
continue
|
|
||||||
to_sign += f"{key}={response_dict[key]}&"
|
|
||||||
our_signature = self.calc_signature(to_sign.strip("&"))
|
|
||||||
return our_signature == yubico_signature
|
|
||||||
|
|
||||||
async def validate_yubico_otp(self, otp):
|
|
||||||
nonce = secrets.token_hex(15) # Random number in the valid range
|
|
||||||
params = f"id={self.bot.config.yubico_otp_client_id}&nonce={nonce}&otp={otp}"
|
|
||||||
|
|
||||||
# If secret is supplied, sign our request
|
|
||||||
if self.bot.config.yubico_otp_secret:
|
|
||||||
params += "&h=" + self.calc_signature(params)
|
|
||||||
|
|
||||||
for api_server in self.api_servers:
|
|
||||||
url = f"{api_server}/wsapi/2.0/verify?{params}"
|
|
||||||
try:
|
|
||||||
resp = await self.bot.aiosession.get(url)
|
|
||||||
assert resp.status == 200
|
|
||||||
except Exception as ex:
|
|
||||||
self.bot.log.warning(f"Got {repr(ex)} on {api_server} with otp {otp}.")
|
|
||||||
continue
|
|
||||||
resptext = await resp.text()
|
|
||||||
|
|
||||||
# Turn the fields to a python dict for easier parsing
|
|
||||||
datafields = resptext.strip().split("\r\n")
|
|
||||||
datafields = {
|
|
||||||
line[: line.index("=")]: line[line.index("=") + 1 :]
|
|
||||||
for line in datafields
|
|
||||||
}
|
|
||||||
|
|
||||||
# Verify nonce
|
|
||||||
assert datafields["nonce"] == nonce
|
|
||||||
|
|
||||||
# Verify signature if secret is present
|
|
||||||
if self.bot.config.yubico_otp_secret:
|
|
||||||
assert self.validate_response_signature(datafields)
|
|
||||||
|
|
||||||
# If we got a success, then return True
|
|
||||||
if datafields["status"] == "OK":
|
|
||||||
return True
|
|
||||||
elif datafields["status"] in self.reuse_responses:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# If status isn't an expected one, log it
|
|
||||||
self.bot.log.warning(
|
|
||||||
f"Got {repr(datafields)} on {api_server} with otp {otp} and nonce {nonce}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# If we fucked up in a way we can't recover from, just return None
|
|
||||||
if datafields["status"] in self.bad_responses:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Return None if we fail to get responses from any server
|
|
||||||
return None
|
|
||||||
|
|
||||||
@Cog.listener()
|
|
||||||
async def on_message(self, message):
|
|
||||||
await self.bot.wait_until_ready()
|
|
||||||
otps = self.otp_re.findall(message.content.strip())
|
|
||||||
if otps:
|
|
||||||
otp = otps[0][0]
|
|
||||||
# Validate OTP
|
|
||||||
validation_result = await self.validate_yubico_otp(otp)
|
|
||||||
if validation_result is not True:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Derive serial and a string to use it
|
|
||||||
serial = self.get_serial(otp)
|
|
||||||
serial_str = f" (serial: `{serial}`)" if serial else ""
|
|
||||||
|
|
||||||
# If the message content is _just_ the OTP code, delete it toos
|
|
||||||
if message.content.strip() == otp:
|
|
||||||
await message.delete()
|
|
||||||
|
|
||||||
# If OTP is valid, tell user that it was revoked
|
|
||||||
msg = await message.channel.send(
|
|
||||||
f"{message.author.mention}: Ate Yubico OTP `{otp}`{serial_str}"
|
|
||||||
". This message will self destruct in 5 seconds."
|
|
||||||
)
|
|
||||||
# and delete message after 5s to help SNR
|
|
||||||
await asyncio.sleep(5)
|
|
||||||
await msg.delete()
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(YubicoOTP(bot))
|
|
|
@ -1,343 +0,0 @@
|
||||||
import datetime
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
# Basic bot config, insert your token here, update description if you want
|
|
||||||
prefixes = [".", "!"]
|
|
||||||
client_id = 0
|
|
||||||
token = "token-goes-here"
|
|
||||||
bot_description = "Robocop-NG, the moderation bot of ReSwitched."
|
|
||||||
|
|
||||||
# If you forked robocop-ng, put your repo here
|
|
||||||
source_url = "https://github.com/reswitched/robocop-ng"
|
|
||||||
rules_url = "https://reswitched.github.io/discord/#rules"
|
|
||||||
|
|
||||||
# The bot description to be used in .robocop embed
|
|
||||||
embed_desc = (
|
|
||||||
"Robocop-NG is developed by [Ave](https://github.com/aveao)"
|
|
||||||
" and [tomGER](https://github.com/tumGER), and is a rewrite "
|
|
||||||
"of Robocop.\nRobocop is based on Kurisu by 916253 and ihaveamac."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# The cogs the bot will load on startup.
|
|
||||||
initial_cogs = [
|
|
||||||
"cogs.common",
|
|
||||||
"cogs.admin",
|
|
||||||
"cogs.verification",
|
|
||||||
"cogs.mod",
|
|
||||||
"cogs.mod_note",
|
|
||||||
"cogs.mod_reacts",
|
|
||||||
"cogs.mod_userlog",
|
|
||||||
"cogs.mod_timed",
|
|
||||||
"cogs.mod_watch",
|
|
||||||
"cogs.basic",
|
|
||||||
"cogs.logs",
|
|
||||||
"cogs.err",
|
|
||||||
"cogs.lockdown",
|
|
||||||
"cogs.legacy",
|
|
||||||
"cogs.links",
|
|
||||||
"cogs.remind",
|
|
||||||
"cogs.robocronp",
|
|
||||||
"cogs.meme",
|
|
||||||
"cogs.invites",
|
|
||||||
"cogs.yubicootp",
|
|
||||||
]
|
|
||||||
|
|
||||||
# The following cogs are also available but aren't loaded by default:
|
|
||||||
# cogs.imagemanip - Adds a meme command called .cox.
|
|
||||||
# Requires Pillow to be installed with pip.
|
|
||||||
# cogs.lists - Allows managing list channels (rules, FAQ) easily through the bot
|
|
||||||
# PR'd in at: https://github.com/reswitched/robocop-ng/pull/65
|
|
||||||
# cogs.pin - Lets users pin important messages
|
|
||||||
# and sends pins above limit to a github gist
|
|
||||||
|
|
||||||
|
|
||||||
# The string that users need to say to get past verification
|
|
||||||
verification_string = "go read the rules, not the code"
|
|
||||||
|
|
||||||
|
|
||||||
# Minimum account age required to join the guild
|
|
||||||
# If user's account creation is shorter than the time delta given here
|
|
||||||
# then user will be kicked and informed
|
|
||||||
min_age = datetime.timedelta(minutes=15)
|
|
||||||
|
|
||||||
# The bot will only work in these guilds
|
|
||||||
guild_whitelist = [269333940928512010] # ReSwitched discord
|
|
||||||
|
|
||||||
# Custom invite URL codes
|
|
||||||
vanity_codes = {269333940928512010: "reswitched"}
|
|
||||||
|
|
||||||
# Named roles to be used with .approve and .revoke
|
|
||||||
# Example: .approve User hacker
|
|
||||||
named_roles = {
|
|
||||||
"community": 420010997877833731,
|
|
||||||
"hacker": 364508795038072833,
|
|
||||||
"participant": 434353085926866946,
|
|
||||||
"pirate": 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
# The bot manager and staff roles
|
|
||||||
# Bot manager can run eval, exit and other destructive commands
|
|
||||||
# Staff can run administrative commands
|
|
||||||
bot_manager_role_id = 466447265863696394 # Bot management role in ReSwitched
|
|
||||||
staff_role_ids = [
|
|
||||||
364647829248933888, # Team role in ReSwitched
|
|
||||||
360138431524765707, # Mod role in ReSwitched
|
|
||||||
466447265863696394, # Bot management role in ReSwitched
|
|
||||||
360138163156549632, # Admin role in ReSwitched
|
|
||||||
287289529986187266, # Wizard role in ReSwitched
|
|
||||||
]
|
|
||||||
|
|
||||||
# Various log channels used to log bot and guild's activity
|
|
||||||
# You can use same channel for multiple log types
|
|
||||||
# Spylog channel logs suspicious messages or messages by members under watch
|
|
||||||
# Invites created with .invite will direct to the welcome channel.
|
|
||||||
log_channel = 290958160414375946 # server-logs in ReSwitched
|
|
||||||
botlog_channel = 529070282409771048 # bot-logs channel in ReSwitched
|
|
||||||
modlog_channel = 542114169244221452 # mod-logs channel in ReSwitched
|
|
||||||
spylog_channel = 548304839294189579 # spy channel in ReSwitched
|
|
||||||
welcome_channel = 326416669058662401 # newcomers channel in ReSwitched
|
|
||||||
|
|
||||||
# These channel entries are used to determine which roles will be given
|
|
||||||
# access when we unmute on them
|
|
||||||
general_channels = [
|
|
||||||
420029476634886144,
|
|
||||||
414949821003202562,
|
|
||||||
383368936466546698,
|
|
||||||
343244421044633602,
|
|
||||||
491316901692178432,
|
|
||||||
539212260350885908,
|
|
||||||
] # Channels everyone can access
|
|
||||||
community_channels = [
|
|
||||||
269333940928512010,
|
|
||||||
438839875970662400,
|
|
||||||
404722395845361668,
|
|
||||||
435687501068501002,
|
|
||||||
286612533757083648,
|
|
||||||
] # Channels requiring community role
|
|
||||||
|
|
||||||
# Controls which roles are blocked during lockdown
|
|
||||||
lockdown_configs = {
|
|
||||||
# Used as a default value for channels without a config
|
|
||||||
"default": {"channels": general_channels, "roles": [named_roles["participant"]]},
|
|
||||||
"community": {
|
|
||||||
"channels": community_channels,
|
|
||||||
"roles": [named_roles["community"], named_roles["hacker"]],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
# Mute role is applied to users when they're muted
|
|
||||||
# As we no longer have mute role on ReSwitched, I set it to 0 here
|
|
||||||
mute_role = 0 # Mute role in ReSwitched
|
|
||||||
|
|
||||||
# Channels that will be cleaned every minute/hour.
|
|
||||||
# This feature isn't very good rn.
|
|
||||||
# See https://github.com/reswitched/robocop-ng/issues/23
|
|
||||||
minutely_clean_channels = []
|
|
||||||
hourly_clean_channels = []
|
|
||||||
|
|
||||||
# Edited and deletes messages in these channels will be logged
|
|
||||||
spy_channels = general_channels
|
|
||||||
|
|
||||||
# All lower case, no spaces, nothing non-alphanumeric
|
|
||||||
suspect_words = [
|
|
||||||
"deepsea", # piracy-enabling cfw
|
|
||||||
"sx", # piracy-enabling cfw
|
|
||||||
"tx", # piracy-enabling cfw
|
|
||||||
"reinx", # piracy-enabling cfw
|
|
||||||
"gomanx", # piracy-enabling cfw
|
|
||||||
"neutos", # piracy-enabling cfw
|
|
||||||
"underpack", # piracy-enabling cfw
|
|
||||||
"underos", # piracy-enabling cfw
|
|
||||||
"tinfoil", # title manager
|
|
||||||
"dz", # title manager
|
|
||||||
"goldleaf", # potential title manager
|
|
||||||
"lithium", # title manager
|
|
||||||
"cracked", # older term for pirated games
|
|
||||||
"xci", # "backup" format
|
|
||||||
"xcz", # "backup" format
|
|
||||||
"nsz", # "backup" format
|
|
||||||
"hbg", # piracy source
|
|
||||||
"jits", # piracy source
|
|
||||||
]
|
|
||||||
|
|
||||||
# List of words that will be ignored if they match one of the
|
|
||||||
# suspect_words (This is used to remove false positives)
|
|
||||||
suspect_ignored_words = [
|
|
||||||
"excit",
|
|
||||||
"s/x",
|
|
||||||
"3dsx",
|
|
||||||
"psx",
|
|
||||||
"txt",
|
|
||||||
"s(x",
|
|
||||||
"txd",
|
|
||||||
"t=x",
|
|
||||||
"osx",
|
|
||||||
"rtx",
|
|
||||||
"shift-x",
|
|
||||||
"users/x",
|
|
||||||
"tx1",
|
|
||||||
"tx2",
|
|
||||||
"tcptx",
|
|
||||||
"udptx",
|
|
||||||
"ctx",
|
|
||||||
"jit's",
|
|
||||||
]
|
|
||||||
|
|
||||||
# == For cogs.links ==
|
|
||||||
links_guide_text = """**Generic starter guides:**
|
|
||||||
Nintendo Homebrew's Guide: <https://nh-server.github.io/switch-guide/>
|
|
||||||
|
|
||||||
**Specific guides:**
|
|
||||||
Manually Updating/Downgrading (with HOS): <https://switch.homebrew.guide/usingcfw/manualupgrade>
|
|
||||||
Manually Repairing/Downgrading (without HOS): <https://switch.homebrew.guide/usingcfw/manualchoiupgrade>
|
|
||||||
How to set up a Homebrew development environment: <https://devkitpro.org/wiki/Getting_Started>
|
|
||||||
Getting full RAM in homebrew without NSPs: As of Atmosphere 0.8.6, hold R while opening any game.
|
|
||||||
Check if a switch is vulnerable to RCM through serial: <https://akdm.github.io/ssnc/checker/>
|
|
||||||
"""
|
|
||||||
|
|
||||||
# == For cogs.verification ==
|
|
||||||
# ReSwitched verification system is rather unique.
|
|
||||||
# You might want to reimplement it.
|
|
||||||
# If you do, use a different name for easier upstream merge.
|
|
||||||
|
|
||||||
# https://docs.python.org/3.7/library/hashlib.html#shake-variable-length-digests
|
|
||||||
_welcome_blacklisted_hashes = {"shake_128", "shake_256"}
|
|
||||||
|
|
||||||
# List of hashes that are to be used during verification
|
|
||||||
welcome_hashes = tuple(hashlib.algorithms_guaranteed - _welcome_blacklisted_hashes)
|
|
||||||
|
|
||||||
# Header before rules in #newcomers - https://elixi.re/i/opviq90y.png
|
|
||||||
welcome_header = """
|
|
||||||
<:ReSwitched:326421448543567872> __**Welcome to ReSwitched!**__
|
|
||||||
|
|
||||||
__**Be sure you read the following rules and information before participating. If you came here to ask about "backups", this is NOT the place.**__
|
|
||||||
|
|
||||||
__**Got questions about Nintendo Switch hacking? Before asking in the server, please see our FAQ at <https://reswitched.github.io/faq/> to see if your question has already been answered.**__
|
|
||||||
|
|
||||||
__**This is a server for technical discussion and development support. If you are looking for end-user support, the Nintendo Homebrew discord server may be a better fit: <https://discord.gg/C29hYvh>.**__
|
|
||||||
|
|
||||||
:bookmark_tabs:__Rules:__
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Rules in #newcomers - https://elixi.re/i/dp3enq5i.png
|
|
||||||
welcome_rules = (
|
|
||||||
# 1
|
|
||||||
"""
|
|
||||||
Read all the rules before participating in chat. Not reading the rules is *not* an excuse for breaking them.
|
|
||||||
• It's suggested that you read channel topics and pins before asking questions as well, as some questions may have already been answered in those.
|
|
||||||
""",
|
|
||||||
# 2
|
|
||||||
"""
|
|
||||||
Be nice to each other. It's fine to disagree, it's not fine to insult or attack other people.
|
|
||||||
• You may disagree with anyone or anything you like, but you should try to keep it to opinions, and not people. Avoid vitriol.
|
|
||||||
• Constant antagonistic behavior is considered uncivil and appropriate action will be taken.
|
|
||||||
• The use of derogatory slurs -- sexist, racist, homophobic, transphobic, or otherwise -- is unacceptable and may be grounds for an immediate ban.
|
|
||||||
""",
|
|
||||||
# 3
|
|
||||||
'If you have concerns about another user, please take up your concerns with a staff member (someone with the "mod" role in the sidebar) in private. Don\'t publicly call other users out.',
|
|
||||||
# 4
|
|
||||||
"""
|
|
||||||
From time to time, we may mention everyone in the server. We do this when we feel something important is going on that requires attention. Complaining about these pings may result in a ban.
|
|
||||||
• To disable notifications for these pings, suppress them in "ReSwitched → Notification Settings".
|
|
||||||
""",
|
|
||||||
# 5
|
|
||||||
"""
|
|
||||||
Don't spam.
|
|
||||||
• For excessively long text, use a service like <https://0bin.net/>.
|
|
||||||
""",
|
|
||||||
# 6
|
|
||||||
"Don't brigade, raid, or otherwise attack other people or communities. Don't discuss participation in these attacks. This may warrant an immediate permanent ban.",
|
|
||||||
# 7
|
|
||||||
"Off-topic content goes to #off-topic. Keep low-quality content like memes out.",
|
|
||||||
# 8
|
|
||||||
"Trying to evade, look for loopholes, or stay borderline within the rules will be treated as breaking them.",
|
|
||||||
# 9
|
|
||||||
"""
|
|
||||||
Absolutely no piracy or related discussion. This includes:
|
|
||||||
• "Backups", even if you legally own a copy of the game.
|
|
||||||
• "Installable" NSPs, XCIs, and NCAs; this **includes** installable homebrew (i.e. on the Home Menu instead of within nx-hbmenu).
|
|
||||||
• Signature and ES patches, also known as "sigpatches"
|
|
||||||
• Usage of piracy-focused groups' (Team Xecuter, etc.) hardware and software, such as SX OS.
|
|
||||||
This is a zero-tolerance, non-negotiable policy that is enforced strictly and swiftly, up to and including instant bans without warning.
|
|
||||||
""",
|
|
||||||
# 10
|
|
||||||
"The first character of your server nickname should be alphanumeric if you wish to talk in chat.",
|
|
||||||
# 11
|
|
||||||
"""
|
|
||||||
Do not boost the server.
|
|
||||||
• ReSwitched neither wants nor needs your server boosts, and your money is better off elsewhere. Consider the EFF (or a charity of your choice).
|
|
||||||
• Boosting the server is liable to get you kicked (to remove the nitro boost role), and/or warned. Roles you possessed prior to the kick may not be restored in a timely fashion.
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Footer after rules in #newcomers - https://elixi.re/i/uhfiecib.png
|
|
||||||
welcome_footer = (
|
|
||||||
"""
|
|
||||||
:hash: __Channel Breakdown:__
|
|
||||||
#news - Used exclusively for updates on ReSwitched progress and community information. Most major announcements are passed through this channel and whenever something is posted there it's usually something you'll want to look at.
|
|
||||||
|
|
||||||
#switch-hacking-meta - For "meta-discussion" related to hacking the switch. This is where we talk *about* the switch hacking that's going on, and where you can get clarification about the hacks that exist and the work that's being done.
|
|
||||||
|
|
||||||
#user-support - End-user focused support, mainly between users. Ask your questions about using switch homebrew here.
|
|
||||||
|
|
||||||
#tool-support - Developer focused support. Ask your questions about using PegaSwitch, libtransistor, Mephisto, and other tools here.
|
|
||||||
|
|
||||||
#hack-n-all - General hacking, hardware and software development channel for hacking on things *other* than the switch. This is a great place to ask about hacking other systems-- and for the community to have technical discussions.
|
|
||||||
""",
|
|
||||||
"""
|
|
||||||
#switch-hacking-general - Channel for everyone working on hacking the switch-- both in an exploit and a low-level hardware sense. This is where a lot of our in-the-open development goes on. Note that this isn't the place for developing homebrew-- we have #homebrew-development for that!
|
|
||||||
|
|
||||||
#homebrew-development - Discussion about the development of homebrew goes there. Feel free to show off your latest creation here.
|
|
||||||
|
|
||||||
#off-topic - Channel for discussion of anything that doesn't belong in #general. Anything goes, so long as you make sure to follow the rules and be on your best behavior.
|
|
||||||
|
|
||||||
#toolchain-development - Discussion about the development of libtransistor itself goes there.
|
|
||||||
|
|
||||||
#cfw-development - Development discussion regarding custom firmware (CFW) projects, such as Atmosphère. This channel is meant for the discussion accompanying active development.
|
|
||||||
|
|
||||||
#bot-cmds - Channel for excessive/random use of Robocop's various commands.
|
|
||||||
|
|
||||||
**If you are still not sure how to get access to the other channels, please read the rules again.**
|
|
||||||
**If you have questions about the rules, feel free to ask here!**
|
|
||||||
|
|
||||||
**Note: This channel is completely automated (aside from responding to questions about the rules). If your message didn't give you access to the other channels, you failed the test. Feel free to try again.**
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Line to be hidden in rules
|
|
||||||
hidden_term_line = ' • When you have finished reading all of the rules, send a message in this channel that includes the {0} hex digest of your discord "name#discriminator", and bot will automatically grant you access to the other channels. You can find your "name#discriminator" (your username followed by a ‘#’ and four numbers) under the discord channel list.'
|
|
||||||
|
|
||||||
# == Only if you want to use cogs.pin ==
|
|
||||||
# Used for the pinboard. Leave empty if you don't wish for a gist pinboard.
|
|
||||||
github_oauth_token = ""
|
|
||||||
|
|
||||||
# Channels and roles where users can pin messages
|
|
||||||
allowed_pin_channels = []
|
|
||||||
allowed_pin_roles = []
|
|
||||||
|
|
||||||
# Channel to upload text files while editing list items. (They are cleaned up.)
|
|
||||||
list_files_channel = 0
|
|
||||||
|
|
||||||
# == Only if you want to use cogs.lists ==
|
|
||||||
# Channels that are lists that are controlled by the lists cog.
|
|
||||||
list_channels = []
|
|
||||||
|
|
||||||
# == Only if you want to use cogs.sar ==
|
|
||||||
self_assignable_roles = {
|
|
||||||
"streamnotifs": 715158689060880384,
|
|
||||||
}
|
|
||||||
|
|
||||||
# == Only if you want to use cogs.mod_reswitched ==
|
|
||||||
pingmods_allow = [named_roles["community"]] + staff_role_ids
|
|
||||||
pingmods_role = 360138431524765707
|
|
||||||
modtoggle_role = 360138431524765707
|
|
||||||
|
|
||||||
# == Only if you want to use cogs.yubicootp ==
|
|
||||||
# Optiona: Get your own from https://upgrade.yubico.com/getapikey/
|
|
||||||
yubico_otp_client_id = 1
|
|
||||||
# Note: You can keep client ID on 1, it will function.
|
|
||||||
yubico_otp_secret = ""
|
|
||||||
# Optional: If you provide a secret, requests will be signed
|
|
||||||
# and responses will be verified.
|
|
|
@ -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 {}
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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)
|
|
|
@ -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
|
|
|
@ -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)
|
|
|
@ -1,47 +0,0 @@
|
||||||
import json
|
|
||||||
import os
|
|
||||||
|
|
||||||
from robocop_ng.helpers.data_loader import read_json
|
|
||||||
|
|
||||||
|
|
||||||
def get_restrictions_path(bot):
|
|
||||||
return os.path.join(bot.state_dir, "data/restrictions.json")
|
|
||||||
|
|
||||||
|
|
||||||
def get_restrictions(bot):
|
|
||||||
return read_json(bot, get_restrictions_path(bot))
|
|
||||||
|
|
||||||
|
|
||||||
def set_restrictions(bot, contents):
|
|
||||||
with open(get_restrictions_path(bot), "w") as f:
|
|
||||||
f.write(contents)
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_restrictions(bot, uid):
|
|
||||||
uid = str(uid)
|
|
||||||
rsts = get_restrictions(bot)
|
|
||||||
if uid in rsts:
|
|
||||||
return rsts[uid]
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def add_restriction(bot, uid, rst):
|
|
||||||
# mostly from kurisu source, credits go to ihaveamac
|
|
||||||
uid = str(uid)
|
|
||||||
rsts = get_restrictions(bot)
|
|
||||||
if uid not in rsts:
|
|
||||||
rsts[uid] = []
|
|
||||||
if rst not in rsts[uid]:
|
|
||||||
rsts[uid].append(rst)
|
|
||||||
set_restrictions(bot, json.dumps(rsts))
|
|
||||||
|
|
||||||
|
|
||||||
def remove_restriction(bot, uid, rst):
|
|
||||||
# mostly from kurisu source, credits go to ihaveamac
|
|
||||||
uid = str(uid)
|
|
||||||
rsts = get_restrictions(bot)
|
|
||||||
if uid not in rsts:
|
|
||||||
rsts[uid] = []
|
|
||||||
if rst in rsts[uid]:
|
|
||||||
rsts[uid].remove(rst)
|
|
||||||
set_restrictions(bot, json.dumps(rsts))
|
|
|
@ -1,43 +0,0 @@
|
||||||
import json
|
|
||||||
import math
|
|
||||||
import os
|
|
||||||
|
|
||||||
from robocop_ng.helpers.data_loader import read_json
|
|
||||||
|
|
||||||
|
|
||||||
def get_crontab_path(bot):
|
|
||||||
return os.path.join(bot.state_dir, "data/robocronptab.json")
|
|
||||||
|
|
||||||
|
|
||||||
def get_crontab(bot):
|
|
||||||
return read_json(bot, get_crontab_path(bot))
|
|
||||||
|
|
||||||
|
|
||||||
def set_crontab(bot, contents):
|
|
||||||
with open(get_crontab_path(bot), "w") as f:
|
|
||||||
f.write(contents)
|
|
||||||
|
|
||||||
|
|
||||||
def add_job(bot, job_type, job_name, job_details, timestamp):
|
|
||||||
timestamp = str(math.floor(timestamp))
|
|
||||||
job_name = str(job_name)
|
|
||||||
ctab = get_crontab(bot)
|
|
||||||
|
|
||||||
if job_type not in ctab:
|
|
||||||
ctab[job_type] = {}
|
|
||||||
|
|
||||||
if timestamp not in ctab[job_type]:
|
|
||||||
ctab[job_type][timestamp] = {}
|
|
||||||
|
|
||||||
ctab[job_type][timestamp][job_name] = job_details
|
|
||||||
set_crontab(bot, json.dumps(ctab))
|
|
||||||
|
|
||||||
|
|
||||||
def delete_job(bot, timestamp, job_type, job_name):
|
|
||||||
timestamp = str(timestamp)
|
|
||||||
job_name = str(job_name)
|
|
||||||
ctab = get_crontab(bot)
|
|
||||||
|
|
||||||
del ctab[job_type][timestamp][job_name]
|
|
||||||
|
|
||||||
set_crontab(bot, json.dumps(ctab))
|
|
|
@ -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 []
|
|
|
@ -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))
|
|
|
@ -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)
|
|
|
@ -1,70 +0,0 @@
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
|
|
||||||
from robocop_ng.helpers.data_loader import read_json
|
|
||||||
|
|
||||||
userlog_event_types = {
|
|
||||||
"warns": "Warn",
|
|
||||||
"bans": "Ban",
|
|
||||||
"kicks": "Kick",
|
|
||||||
"mutes": "Mute",
|
|
||||||
"notes": "Note",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_userlog_path(bot):
|
|
||||||
return os.path.join(bot.state_dir, "data/userlog.json")
|
|
||||||
|
|
||||||
|
|
||||||
def get_userlog(bot):
|
|
||||||
return read_json(bot, get_userlog_path(bot))
|
|
||||||
|
|
||||||
|
|
||||||
def set_userlog(bot, contents):
|
|
||||||
with open(get_userlog_path(bot), "w") as f:
|
|
||||||
f.write(contents)
|
|
||||||
|
|
||||||
|
|
||||||
def fill_userlog(bot, userid, uname):
|
|
||||||
userlogs = get_userlog(bot)
|
|
||||||
uid = str(userid)
|
|
||||||
if uid not in userlogs:
|
|
||||||
userlogs[uid] = {
|
|
||||||
"warns": [],
|
|
||||||
"mutes": [],
|
|
||||||
"kicks": [],
|
|
||||||
"bans": [],
|
|
||||||
"notes": [],
|
|
||||||
"watch": False,
|
|
||||||
"name": "n/a",
|
|
||||||
}
|
|
||||||
if uname:
|
|
||||||
userlogs[uid]["name"] = uname
|
|
||||||
|
|
||||||
return userlogs, uid
|
|
||||||
|
|
||||||
|
|
||||||
def userlog(bot, uid, issuer, reason, event_type, uname: str = ""):
|
|
||||||
userlogs, uid = fill_userlog(bot, uid, uname)
|
|
||||||
|
|
||||||
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
|
||||||
log_data = {
|
|
||||||
"issuer_id": issuer.id,
|
|
||||||
"issuer_name": f"{issuer}",
|
|
||||||
"reason": reason,
|
|
||||||
"timestamp": timestamp,
|
|
||||||
}
|
|
||||||
if event_type not in userlogs[uid]:
|
|
||||||
userlogs[uid][event_type] = []
|
|
||||||
userlogs[uid][event_type].append(log_data)
|
|
||||||
set_userlog(bot, json.dumps(userlogs))
|
|
||||||
return len(userlogs[uid][event_type])
|
|
||||||
|
|
||||||
|
|
||||||
def setwatch(bot, uid, issuer, watch_state, uname: str = ""):
|
|
||||||
userlogs, uid = fill_userlog(bot, uid, uname)
|
|
||||||
|
|
||||||
userlogs[uid]["watch"] = watch_state
|
|
||||||
set_userlog(bot, json.dumps(userlogs))
|
|
||||||
return
|
|
|
@ -1,4 +0,0 @@
|
||||||
(import (fetchTarball
|
|
||||||
"https://github.com/edolstra/flake-compat/archive/master.tar.gz") {
|
|
||||||
src = builtins.fetchGit ./.;
|
|
||||||
}).shellNix
|
|
Loading…
Reference in a new issue