├── .github ├── images │ ├── logo.png │ └── wlogo.png ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── codeql-analysis.yml │ └── black.yml └── CONTRIBUTORS.md ├── kekids ├── __init__.py ├── info.json └── kekid.py ├── lockitup ├── __init__.py ├── info.json └── lockitup.py ├── customapps ├── __init__.py ├── info.json └── main.py ├── Makefile ├── README.md ├── decancer ├── __init__.py ├── info.json ├── randomnames.py └── decancer.py ├── allutils ├── __init__.py ├── info.json ├── formats.py ├── time.py └── main.py ├── info.json ├── setup.py ├── LICENSE ├── .gitignore ├── info.yaml └── .tools └── generate_info.py /.github/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kablekompany/Kable-Kogs/HEAD/.github/images/logo.png -------------------------------------------------------------------------------- /.github/images/wlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kablekompany/Kable-Kogs/HEAD/.github/images/wlogo.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ['kablekompany'] 4 | patreon: 'kablekompany' 5 | # custom: ['https://cash.app/$KableKo'] 6 | -------------------------------------------------------------------------------- /kekids/__init__.py: -------------------------------------------------------------------------------- 1 | from .kekid import IDKick 2 | 3 | __red_end_user_data_statement__ = ( 4 | "This cog does not persistently store data or metadata about users." 5 | ) 6 | 7 | 8 | def setup(bot): 9 | bot.add_cog(IDKick(bot)) 10 | -------------------------------------------------------------------------------- /lockitup/__init__.py: -------------------------------------------------------------------------------- 1 | from .lockitup import LockItUp 2 | 3 | __red_end_user_data_statement__ = ( 4 | "This cog does not persistently store data or metadata about users." 5 | ) 6 | 7 | 8 | def setup(bot): 9 | bot.add_cog(LockItUp(bot)) 10 | -------------------------------------------------------------------------------- /customapps/__init__.py: -------------------------------------------------------------------------------- 1 | from .main import CustomApps 2 | 3 | __red_end_user_data_statement__ = ( 4 | "This cog does not persistently store data or metadata about users." 5 | ) 6 | 7 | 8 | def setup(bot): 9 | cog = CustomApps(bot) 10 | bot.add_cog(cog) 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | reformat: 4 | black --target-version py38 -l 99 `git ls-files "*.py" "*.pyi"` 5 | isort --profile=black `git ls-files "*.py"` 6 | autoflake -r -i `git ls-files "*.py"` 7 | stylecheck: 8 | black --check --target-version py38 -l 99 `git ls-files "*.py" "*.pyi"` 9 | isort --check-only --profile=black `git ls-files "*.py"` 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### DISCLAIMER 2 | 3 | :warning: This repo is not an approved plugin-repo from [CogBoard](cogboard.red/ "Click me BRO!") at this time 4 | > *Install at your own risk, and you assume any liability* 5 | 6 | # THESE COGS ARE CURRENTLY OUT-OF-DATE AND MAY NOT WORK WITH RED'S CURRENT VERSIONING 7 | # THIS REPO IS CURRENTLY NOT BEING MAINTAINED 8 | -------------------------------------------------------------------------------- /decancer/__init__.py: -------------------------------------------------------------------------------- 1 | # from .dehoister import Decancer 2 | from .decancer import Decancer 3 | 4 | __red_end_user_data_statement__ = ( 5 | "This cog does not persistently store data or metadata about users." 6 | ) 7 | 8 | 9 | async def setup(bot): 10 | cog = Decancer(bot) 11 | await cog.initialize() 12 | bot.add_cog(cog) 13 | -------------------------------------------------------------------------------- /allutils/__init__.py: -------------------------------------------------------------------------------- 1 | from .main import AllUtils 2 | 3 | __red_end_user_data_statement__ = ( 4 | "This cog does not persistently store data or metadata about users." 5 | ) 6 | 7 | # majorly source from https://github.com/Rapptz/RoboDanny/blob/rewrite/cogs/meta.py and modified to work with Red 8 | def setup(bot): 9 | bot.add_cog(AllUtils(bot)) 10 | -------------------------------------------------------------------------------- /info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Kable-Kogs", 3 | "short": "Cogs by KableKompany (KableKompany#0001).", 4 | "description": "culmination of rewrites and combination cogs for use with Red.", 5 | "install_msg": "Thanks for using Kable-Kogs. Please note this repo is unapproved \u2014 some features may be missing. Report issues at https://github.com/KableKompany/Kable-Kogs", 6 | "author": [ 7 | "KableKompany (KableKompany#0001)" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="Kable-Kogs", 8 | version="1.0.1", 9 | author="Trent Kable", 10 | author_email="trent@kablekompany.com", 11 | description="Cogs for Kr0nos", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/kableko/Kable-Kogs", 15 | packages=setuptools.find_packages(), 16 | classifiers=[ 17 | "Programming Language :: Python :: 3", 18 | "License :: OSI Approved :: MIT License", 19 | "Operating System :: OS Independent", 20 | ], 21 | ) 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /kekids/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "KekIDs", 3 | "short": "Walk confidently and wear big shoes.", 4 | "description": "Masskick users by ID", 5 | "end_user_data_statement": "This cog does not persistently store data or metadata about users.", 6 | "install_msg": "Thanks for installing KekIDs. If anything doesn't work, you can report it at .", 7 | "author": [ 8 | "KableKompany (KableKompany#0001)" 9 | ], 10 | "required_cogs": {}, 11 | "requirements": [], 12 | "tags": [ 13 | "moderation", 14 | "tools", 15 | "utility" 16 | ], 17 | "hidden": false, 18 | "disabled": false, 19 | "type": "COG" 20 | } 21 | -------------------------------------------------------------------------------- /allutils/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AllUtils", 3 | "short": "Grab meta, make polls. Bitchin", 4 | "description": "Various utility functions and commands from R.Danny. Grab meta, make polls, FDB.", 5 | "end_user_data_statement": "This cog does not store meta data", 6 | "install_msg": "Thanks for installing AllUtils. If anything doesn't work, you can report it at .", 7 | "author": [ 8 | "KableKompany (KableKompany#0001)" 9 | ], 10 | "required_cogs": {}, 11 | "requirements": [ 12 | "parsedatetime" 13 | ], 14 | "tags": [ 15 | "tools", 16 | "utility" 17 | ], 18 | "hidden": true, 19 | "disabled": false, 20 | "type": "COG" 21 | } 22 | -------------------------------------------------------------------------------- /customapps/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CustomApps", 3 | "short": "Customize Staff apps for your server", 4 | "description": "Add additional questions of your choice to staff applications making them specific to your server. Can retrieve filled in apps that users have completed. Responses are sent back to server via webhook. AIO Setup.", 5 | "end_user_data_statement": "This cog stores application response that can be deleted at request", 6 | "install_msg": "Thanks for installing CustomApps. If anything doesn't work, you can report it at .", 7 | "author": [ 8 | "KableKompany (KableKompany#0001)" 9 | ], 10 | "required_cogs": {}, 11 | "requirements": [], 12 | "tags": [ 13 | "staff-apps", 14 | "tools", 15 | "utility" 16 | ], 17 | "hidden": false, 18 | "disabled": false, 19 | "type": "COG" 20 | } 21 | -------------------------------------------------------------------------------- /decancer/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Decancer", 3 | "short": "Decancer users names removing special and accented chars. `[p]decancerset` to get started if you're already using redbot core modlog", 4 | "description": "Clears special characters, and accented characters from usernames. Customize default fallback name on unsuccessful decancer and translates glyphs", 5 | "end_user_data_statement": "This cog does not persistently store data or metadata about users.", 6 | "install_msg": "Thanks for installing Decancer. If anything doesn't work, you can report it at .", 7 | "author": [ 8 | "KableKompany (KableKompany#0001)" 9 | ], 10 | "required_cogs": {}, 11 | "requirements": [ 12 | "stringcase", 13 | "unidecode" 14 | ], 15 | "tags": [ 16 | "decancer", 17 | "moderation", 18 | "decancer" 19 | ], 20 | "hidden": false, 21 | "disabled": false, 22 | "type": "COG" 23 | } 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /lockitup/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LockItUp", 3 | "short": "Lockdown a list of channels, a channel, or the whole server.", 4 | "description": "Lockdown a list of channels and output a lock message in each of those channels as well as an unlock message when that lockdown is rescinded. For a second level of configuration, you can add a special role to take denied overrides on lockdown and give positive overrides on unlock. Webhook error logging in server for debugging, and role toggle for everyone role perms for message sending. Lock voice channels, and singular channels too.", 5 | "end_user_data_statement": "This cog does not persistently store data or metadata about users.", 6 | "install_msg": "Thanks for installing LockItUp. If anything doesn't work, you can report it at .", 7 | "author": [ 8 | "KableKompany (KableKompany#0001)" 9 | ], 10 | "required_cogs": {}, 11 | "requirements": [], 12 | "tags": [ 13 | "moderation", 14 | "lockdown" 15 | ], 16 | "hidden": false, 17 | "disabled": false, 18 | "type": "COG" 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Trent Kable 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | # The branches below must be a subset of the branches above 9 | branches: 10 | - master 11 | schedule: 12 | - cron: '0 0 * * 0' 13 | 14 | jobs: 15 | analyze: 16 | name: Analyze 17 | runs-on: ubuntu-latest 18 | 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | # Override automatic language detection by changing the below list 23 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 24 | language: ['python'] 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v2 29 | with: 30 | # We must fetch at least the immediate parents so that if this is 31 | # a pull request then we can checkout the head. 32 | fetch-depth: 2 33 | 34 | # If this run was triggered by a pull request event, then checkout 35 | # the head of the pull request instead of the merge commit. 36 | - run: git checkout HEAD^2 37 | if: ${{ github.event_name == 'pull_request' }} 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v1 42 | with: 43 | languages: ${{ matrix.language }} 44 | 45 | - name: Perform CodeQL Analysis 46 | uses: github/codeql-action/analyze@v1 47 | -------------------------------------------------------------------------------- /.github/workflows/black.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [push, pull_request] 4 | 5 | 6 | jobs: 7 | black: 8 | name: Style Reformatting 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/setup-python@v1 13 | with: 14 | python_version: "3.8" 15 | - run: "python -m pip install black isort autoflake" 16 | name: Install requirements 17 | - run: "make reformat" 18 | continue-on-error: true 19 | name: Style reformatting 20 | - name: Commit changes 21 | continue-on-error: true 22 | run: | 23 | git config --local committer.email "noreply@github.com" 24 | git config --local committer.name "GitHub" 25 | git config --local author.email "${{ github.actor }}@users.noreply.github.com" 26 | git config --local author.name "{{ github.actor }}" 27 | git add -A 28 | git commit -m "Style Reformatting" 29 | git push "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" HEAD:${GITHUB_REF#refs/heads/} 30 | 31 | 32 | lint_python: 33 | name: Lint Python 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v2 37 | with: 38 | ref: ${{ env.ref }} 39 | - uses: actions/setup-python@v1 40 | with: 41 | python_version: "3.8" 42 | - run: "python -m pip install flake8" 43 | name: Install Flake8 44 | - run: "python -m flake8 . --count --select=E9,F7,F82 --show-source" 45 | name: Flake8 Linting 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | .history/ 131 | lockitup/sharkylock.py 132 | .refs 133 | .venv/ 134 | .vscode 135 | httpkount/ 136 | !info.yaml 137 | .ci/ 138 | 139 | # macOS shit 140 | .DS_Store 141 | 142 | **/diff.py -------------------------------------------------------------------------------- /.github/CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | 6 | ## CONTRIBUTORS 7 | - **[predacogs](https://github.com/predaAa/predacogs)**: Predä 8 | - **[phen-cogs](https://github.com/phenom4n4n/phen-cogs)**: PhenoM4n4n#0055 9 | - **[drapercogs](https://github.com/Drapersniper/drapercogs)**: Draper 10 | - **[Toxic-Cogs](https://github.com/NeuroAssassin/Toxic-Cogs)**: Neuro Assassin 11 | - **[aikaterna-cogs](https://github.com/aikaterna/aikaterna-cogs)**: aikaterna (aikaterna#1393) 12 | - **[Fixator10-Cogs](https://github.com/fixator10/Fixator10-Cogs)**: Fixator10 13 | - **[CogsByAdrian](https://github.com/designbyadrian/CogsByAdrian)**: designbyadrian 14 | - **[Dav-Cogs](https://github.com/Dav-Git/Dav-Cogs)**: Dav#6998 15 | - **[Colts-Cogs](https://github.com/PredaaA/Colts-Cogs)**: ColtOutram, Predä 。#1001 16 | - **[Flare-Cogs](https://github.com/flaree/Flare-Cogs)**: flare (flare#0001) 17 | - **[FluffyCogs](https://github.com/zephyrkul/FluffyCogs)**: Zephyrkul (Zephyrkul#1089) 18 | - **[imgwelcome](https://github.com/aikaterna/imgwelcome)**: aikaterna (aikaterna#1393) 19 | - **[JackCogs](https://github.com/jack1142/JackCogs)**: jack1142 (Jackenmen#6607) 20 | - **[Jumper-Plugins](https://github.com/Redjumpman/Jumper-Plugins/)**: Redjumpman (Redjumpman#1337) 21 | - **[kennnyshiwa-cogs](https://github.com/kennnyshiwa/kennnyshiwa-cogs)**: Kennnyshiwa, Beryju, preda 22 | - **[Laggrons-Dumb-Cogs](https://github.com/retke/Laggrons-Dumb-Cogs)**: El Laggron 23 | - **[NIXCOGS](https://github.com/NIXC/NIXCOGS)**: NIXC (NIN) 24 | - **[PCXCogs](https://github.com/PhasecoreX/PCXCogs)**: PhasecoreX (PhasecoreX#0635) 25 | - **[SauriCogs](https://github.com/elijabesu/SauriCogs/)**: saurichable 26 | - **[Sentinel](https://github.com/Kowlin/Sentinel)**: Kowlin 27 | - **[SharkyTheKing](https://github.com/SharkyTheKing/Sharky)**: Sharky The King#0001 28 | - **[SinbadCogs](https://github.com/mikeshardmind/SinbadCogs)**: mikeshardmind (Sinbad), DiscordLiz 29 | - **[Squid-Plugins](https://github.com/tekulvw/Squid-Plugins)**: Will (tekulvw) 30 | - **[Stone-Cogs](https://github.com/Stonedestroyer/Stone-Cogs)**: Stonedestroyer 31 | - **[Tobo-Cogs](https://github.com/PredaaA/Tobo-Cogs)**: Tobotimus 32 | - **[Trusty-cogs](https://github.com/TrustyJAID/Trusty-cogs/)**: TrustyJAID 33 | - **[Wyn-RedV3Cogs](https://github.com/Wyn10/Wyn-RedV3Cogs)**: Wyn10 34 | - **[Red-DiscordBot Contributors](https://github.com/Cog-Creators/Red-DiscordBot/graphs/contributors)***: Contributor List for Red by TwentySix 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /kekids/kekid.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | 4 | import discord 5 | from redbot.core import checks, commands, i18n, modlog 6 | from redbot.core.commands import BadArgument, Converter 7 | from redbot.core.utils.chat_formatting import bold, humanize_number, pagify 8 | from redbot.core.utils.mod import get_audit_reason, is_allowed_by_hierarchy 9 | 10 | _id_regex = re.compile(r"([0-9]{15,21})$") 11 | _mention_regex = re.compile(r"<@!?([0-9]{15,21})>$") 12 | 13 | 14 | log = logging.getLogger("red.kko-kogs.idkick") 15 | 16 | 17 | # from Red-DiscordBot Core Mod cog 18 | class RawUserIds(Converter): 19 | async def convert(self, ctx, argument): 20 | if match := _id_regex.match(argument) or _mention_regex.match(argument): 21 | return int(match.group(1)) 22 | 23 | raise BadArgument("{} doesn't look like a valid user ID.").format(argument) 24 | 25 | 26 | class IDKick(commands.Cog): 27 | def __init__(self, bot): 28 | self.bot = bot 29 | 30 | """ 31 | Kick a list of IDs from server. 32 | """ 33 | 34 | @commands.command() 35 | @commands.guild_only() 36 | @commands.bot_has_permissions(kick_members=True) 37 | @checks.admin_or_permissions(kick_members=True) 38 | async def idkick( 39 | self, 40 | ctx: commands.Context, 41 | user_ids: commands.Greedy[RawUserIds], 42 | *, 43 | reason: str = None, 44 | ): 45 | """Kick a list of users. 46 | 47 | If a reason is specified, it will be the reason that shows up 48 | in the audit log. 49 | """ 50 | kicked = [] 51 | errors = {} 52 | 53 | async def show_results(): 54 | text = "Kicked {num} users from the server.".format(num=humanize_number(len(kicked))) 55 | if errors: 56 | text += "\nErrors:\n" 57 | text += "\n".join(errors.values()) 58 | 59 | for p in pagify(text): 60 | await ctx.send(p) 61 | 62 | # def remove_processed(ids): 63 | # return [_id for _id in ids if _id not in kicked and _id not in errors] 64 | 65 | user_ids = list(set(user_ids)) # No dupes 66 | 67 | author = ctx.author 68 | guild = ctx.guild 69 | 70 | if not user_ids: 71 | await ctx.send_help() 72 | return 73 | 74 | if not guild.me.guild_permissions.kick_members: 75 | return await ctx.send("I lack the permissions to do this.") 76 | 77 | for user_id in user_ids: 78 | user = discord.Object(id=user_id) 79 | audit_reason = get_audit_reason(author, reason) 80 | queue_entry = (guild.id, user_id) 81 | try: 82 | await guild.kick(user, reason=audit_reason) 83 | log.info("{}({}) kicked {}".format(author.name, author.id, user_id)) 84 | except discord.NotFound: 85 | errors[user_id] = "User {user_id} does not exist.".format(user_id=user_id) 86 | continue 87 | except discord.Forbidden: 88 | errors[user_id] = "Could not kick {user_id}: missing permissions.".format( 89 | user_id=user_id 90 | ) 91 | continue 92 | else: 93 | kicked.append(user_id) 94 | await show_results() 95 | -------------------------------------------------------------------------------- /allutils/formats.py: -------------------------------------------------------------------------------- 1 | from redbot.core.commands import BadArgument 2 | from redbot.core.utils.chat_formatting import inline 3 | 4 | 5 | class plural: 6 | def __init__(self, value): 7 | self.value = value 8 | 9 | def __format__(self, format_spec): 10 | v = self.value 11 | singular, sep, plural = format_spec.partition("|") 12 | plural = plural or f"{singular}s" 13 | if abs(v) != 1: 14 | return f"{v} {plural}" 15 | return f"{v} {singular}" 16 | 17 | 18 | def human_join(seq, delim=", ", final="or"): 19 | size = len(seq) 20 | if size == 0: 21 | return "" 22 | 23 | if size == 1: 24 | return seq[0] 25 | 26 | if size == 2: 27 | return f"{seq[0]} {final} {seq[1]}" 28 | 29 | return delim.join(seq[:-1]) + f" {final} {seq[-1]}" 30 | 31 | 32 | class TabularData: 33 | def __init__(self): 34 | self._widths = [] 35 | self._columns = [] 36 | self._rows = [] 37 | 38 | def set_columns(self, columns): 39 | self._columns = columns 40 | self._widths = [len(c) + 2 for c in columns] 41 | 42 | def add_row(self, row): 43 | rows = [str(r) for r in row] 44 | self._rows.append(rows) 45 | for index, element in enumerate(rows): 46 | width = len(element) + 2 47 | if width > self._widths[index]: 48 | self._widths[index] = width 49 | 50 | def add_rows(self, rows): 51 | for row in rows: 52 | self.add_row(row) 53 | 54 | def render(self): 55 | """Renders a table in rST format. 56 | 57 | Example: 58 | 59 | +-------+-----+ 60 | | Name | Age | 61 | +-------+-----+ 62 | | Alice | 24 | 63 | | Bob | 19 | 64 | +-------+-----+ 65 | """ 66 | 67 | sep = "+".join("-" * w for w in self._widths) 68 | sep = f"+{sep}+" 69 | 70 | to_draw = [sep] 71 | 72 | def get_entry(d): 73 | elem = "|".join(f"{e:^{self._widths[i]}}" for i, e in enumerate(d)) 74 | return f"|{elem}|" 75 | 76 | to_draw.append(get_entry(self._columns)) 77 | to_draw.append(sep) 78 | 79 | for row in self._rows: 80 | to_draw.append(get_entry(row)) 81 | 82 | to_draw.append(sep) 83 | return "\n".join(to_draw) 84 | 85 | 86 | def positive_int(arg: str) -> int: 87 | arg = arg.replace(",", "") 88 | arg = arg.replace(" ", "") 89 | arg = arg.replace("k", "000") 90 | arg = arg.replace("million", "000000") 91 | arg = arg.replace("mil", "000000") 92 | arg = arg.replace("m", "000000") 93 | try: 94 | ret = int(arg) 95 | except ValueError: 96 | raise BadArgument("{arg} is not an integer.".format(arg=inline(arg))) 97 | if ret <= 0: 98 | raise BadArgument("{arg} is not a positive integer.".format(arg=inline(arg))) 99 | if ret >= 10000000000: 100 | raise BadArgument( 101 | "{arg} is no where near an amount of coins you'll reach, idiot".format(arg=inline(arg)) 102 | ) 103 | return ret 104 | 105 | 106 | def hundred_int(arg: str): 107 | try: 108 | ret = int(arg) 109 | except ValueError: 110 | raise BadArgument("{arg} is not an integer.".format(arg=inline(arg))) 111 | if ret < 0 or ret > 100: 112 | raise BadArgument(f"`{arg}` must be an integer between 0 and 100.") 113 | return ret 114 | -------------------------------------------------------------------------------- /info.yaml: -------------------------------------------------------------------------------- 1 | # jack 2 | repo: 3 | name: Kable-Kogs 4 | short: Cogs by KableKompany (KableKompany#0001). 5 | description: Culmination of rewrites and combination cogs for use wit Red 6 | install_msg: >- 7 | Thanks for using {repo_name}. Please note this repo is unapproved — some features may be missing. Report issues at https://github.com/KableKompany/Kable-Kogs 8 | author: 9 | - KableKompany (KableKompany#0001) 10 | 11 | shared_fields: 12 | install_msg: >- 13 | Thanks for installing {cog_name}. If anything doesn't work, you can report it 14 | at . 15 | author: 16 | - KableKompany (KableKompany#0001) 17 | hidden: false 18 | disabled: false 19 | type: COG 20 | 21 | cogs: 22 | allutils: 23 | name: AllUtils 24 | short: Grab meta, make polls. Bitchin' 25 | description: Various utility functions and commands from R.Danny. Grab meta, make polls, FDB. 26 | end_user_data_statement: >- 27 | This cog stores application response that can be deleted at request 28 | install_msg: >- 29 | {shared_fields.install_msg} 30 | tags: 31 | - banmessage 32 | - tools 33 | - utility 34 | customapps: 35 | name: CustomApps 36 | short: Customize Staff apps for your server 37 | description: Add additional questions of your choice to staff applications making them specific to your server. Can retrieve filled in apps that users have completed. Responses are sent back to server via webhook. AIO Setup. 38 | end_user_data_statement: >- 39 | This cog stores application response that can be deleted at request 40 | tags: 41 | - staff-apps 42 | - tools 43 | - utility 44 | decancer: 45 | name: Decancer 46 | short: Decancer users names removing special and accented chars. `[p]decancerset` to get started if you're already using redbot core modlog 47 | description: Clears special characters, and accented characters from usernames. Customize default fallback name on unsuccessful decancer and translates glyphs 48 | end_user_data_statement: >- 49 | This cog does not persistently store data or metadata about users. 50 | install_msg: >- 51 | {shared_fields.install_msg} 52 | 53 | tags: 54 | - utility 55 | - moderation 56 | - dehoist 57 | kekids: 58 | name: KekIDs 59 | short: >- 60 | Walk confidently and wear big shoes. Kick a list of users from your server. EzPz 61 | description: >- 62 | Masskick users by ID 63 | end_user_data_statement: >- 64 | This cog does not persistently store data or metadata about users. 65 | class_docstring: Kick users en masse by mention or ID 66 | tags: 67 | - moderation 68 | - tools 69 | - utility 70 | lockitup: 71 | name: LockItUp 72 | short: Lockdown a list of channels, a channel, or the whole server. 73 | description: Lockdown a list of channels and output a lock message in each of those channels as well as an unlock message when that lockdown is rescinded. For a second level of configuration, you can add a special role to take denied overrides on lockdown and give positive overrides on unlock. Webhook error logging in server for debugging, and role toggle for everyone role perms for message sending. Lock voice channels, and singular channels too. 74 | end_user_data_statement: >- 75 | This cog does not persistently store data or metadata about users. 76 | install_msg: >- 77 | {shared_fields.install_msg} 78 | tags: 79 | - moderation 80 | - lockdown 81 | -------------------------------------------------------------------------------- /decancer/randomnames.py: -------------------------------------------------------------------------------- 1 | properNouns = [ 2 | "Donald Trump", 3 | "Joe Biden", 4 | "Ninja", 5 | "Proton Bomb", 6 | "He Who Shall Not Be Named", 7 | "Definitely Phen's Alt", 8 | "Cable Company without K", 9 | "Probably Sharts", 10 | ] 11 | 12 | nouns = [ 13 | "Dog", 14 | "Cat", 15 | "Gamer", 16 | "Ork", 17 | "Memer", 18 | "Robot", 19 | "Programmer", 20 | "Player", 21 | "Doctor", 22 | "Communist", 23 | "Apple", 24 | "Godfather", 25 | "Mafia", 26 | "Detective", 27 | "Politician", 28 | ] 29 | 30 | adjectives = [ 31 | "Fast", 32 | "Defiant", 33 | "Homeless", 34 | "Adorable", 35 | "Delightful", 36 | "Homely", 37 | "Quaint", 38 | "Adventurous", 39 | "Depressed", 40 | "Horrible", 41 | "Aggressive", 42 | "Determined", 43 | "Hungry", 44 | "Real", 45 | "Agreeable", 46 | "Different", 47 | "Hurt", 48 | "Relieved", 49 | "Alert", 50 | "Difficult", 51 | "Repulsive", 52 | "Alive", 53 | "Disgusted", 54 | "Ill", 55 | "Rich", 56 | "Amused", 57 | "Distinct", 58 | "Important", 59 | "Angry", 60 | "Disturbed", 61 | "Impossible", 62 | "Scary", 63 | "Annoyed", 64 | "Dizzy", 65 | "Inexpensive", 66 | "Selfish", 67 | "Annoying", 68 | "Doubtful", 69 | "Innocent", 70 | "Shiny", 71 | "Anxious", 72 | "Drab", 73 | "Inquisitive", 74 | "Shy", 75 | "Arrogant", 76 | "Dull", 77 | "Itchy", 78 | "Silly", 79 | "Ashamed", 80 | "Sleepy", 81 | "Attractive", 82 | "Eager", 83 | "Jealous", 84 | "Smiling", 85 | "Average", 86 | "Easy", 87 | "Jittery", 88 | "Smoggy", 89 | "Awful", 90 | "Elated", 91 | "Jolly", 92 | "Sore", 93 | "Elegant", 94 | "Joyous", 95 | "Sparkling", 96 | "Bad", 97 | "Embarrassed", 98 | "Splendid", 99 | "Beautiful", 100 | "Enchanting", 101 | "Kind", 102 | "Spotless", 103 | "Better", 104 | "Encouraging", 105 | "Stormy", 106 | "Bewildered", 107 | "Energetic", 108 | "Lazy", 109 | "Strange", 110 | "Enthusiastic", 111 | "Light", 112 | "Stupid", 113 | "Bloody", 114 | "Envious", 115 | "Lively", 116 | "Successful", 117 | "Blue", 118 | "Evil", 119 | "Lonely", 120 | "Super", 121 | "Blue-eyed", 122 | "Excited", 123 | "Long", 124 | "Blushing", 125 | "Expensive", 126 | "Lovely", 127 | "Talented", 128 | "Bored", 129 | "Exuberant", 130 | "Lucky", 131 | "Tame", 132 | "Brainy", 133 | "Tender", 134 | "Brave", 135 | "Fair", 136 | "Magnificent", 137 | "Tense", 138 | "Breakable", 139 | "Faithful", 140 | "Misty", 141 | "Terrible", 142 | "Bright", 143 | "Famous", 144 | "Modern", 145 | "Tasty", 146 | "Busy", 147 | "Fancy", 148 | "Motionless", 149 | "Thankful", 150 | "Fantastic", 151 | "Muddy", 152 | "Thoughtful", 153 | "Calm", 154 | "Fierce", 155 | "Mushy", 156 | "Thoughtless", 157 | "Careful", 158 | "Filthy", 159 | "Mysterious", 160 | "Tired", 161 | "Cautious", 162 | "Fine", 163 | "Tough", 164 | "Charming", 165 | "Foolish", 166 | "Nasty", 167 | "Troubled", 168 | "Cheerful", 169 | "Fragile", 170 | "Naughty", 171 | "Clean", 172 | "Frail", 173 | "Nervous", 174 | "Ugliest", 175 | "Clear", 176 | "Frantic", 177 | "Nice", 178 | "Ugly", 179 | "Clever", 180 | "Friendly", 181 | "Nutty", 182 | "Uninterested", 183 | "Cloudy", 184 | "Frightened", 185 | "Unsightly", 186 | "Clumsy", 187 | "Funny", 188 | "Obedient", 189 | "Unusual", 190 | "Colorful", 191 | "Obnoxious", 192 | "Upset", 193 | "Combative", 194 | "Gentle", 195 | "Odd", 196 | "Uptight", 197 | "Comfortable", 198 | "Gifted", 199 | "Old-fashioned", 200 | "Concerned", 201 | "Glamorous", 202 | "Open", 203 | "Vast", 204 | "Condemned", 205 | "Gleaming", 206 | "Outrageous", 207 | "Victorious", 208 | "Confused", 209 | "Glorious", 210 | "Outstanding", 211 | "Vivacious", 212 | "Cooperative", 213 | "Good", 214 | "Courageous", 215 | "Gorgeous", 216 | "Panicky", 217 | "Wandering", 218 | "Crazy", 219 | "Graceful", 220 | "Perfect", 221 | "Weary", 222 | "Creepy", 223 | "Grieving", 224 | "Plain", 225 | "Wicked", 226 | "Crowded", 227 | "Grotesque", 228 | "Pleasant", 229 | "Wide-eyed", 230 | "Cruel", 231 | "Grumpy", 232 | "Poised", 233 | "Wild", 234 | "Curious", 235 | "Poor", 236 | "Witty", 237 | "Cute", 238 | "Handsome", 239 | "Powerful", 240 | "Worrisome", 241 | "Happy", 242 | "Precious", 243 | "Worried", 244 | "Dangerous", 245 | "Healthy", 246 | "Prickly", 247 | "Wrong", 248 | "Dark", 249 | "Helpful", 250 | "Proud", 251 | "Dead", 252 | "Helpless", 253 | "Putrid", 254 | "Zany", 255 | "Defeated", 256 | "Hilarious", 257 | "Puzzled", 258 | "Zealous", 259 | "Dank", 260 | "Sexy", 261 | "Darth", 262 | ] 263 | -------------------------------------------------------------------------------- /allutils/time.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import re 3 | 4 | import parsedatetime as pdt 5 | from dateutil.relativedelta import relativedelta 6 | from discord.ext import commands 7 | 8 | from .formats import human_join, plural 9 | 10 | # Monkey patch mins and secs into the units 11 | units = pdt.pdtLocales["en_US"].units 12 | units["minutes"].append("mins") 13 | units["seconds"].append("secs") 14 | 15 | # this can be faced with other cogs if any of the functions are needed 16 | class ShortTime: 17 | compiled = re.compile( 18 | """(?:(?P[0-9])(?:years?|y))? # e.g. 2y 19 | (?:(?P[0-9]{1,2})(?:months?|mo))? # e.g. 2months 20 | (?:(?P[0-9]{1,4})(?:weeks?|w))? # e.g. 10w 21 | (?:(?P[0-9]{1,5})(?:days?|d))? # e.g. 14d 22 | (?:(?P[0-9]{1,5})(?:hours?|h))? # e.g. 12h 23 | (?:(?P[0-9]{1,5})(?:minutes?|m))? # e.g. 10m 24 | (?:(?P[0-9]{1,5})(?:seconds?|s))? # e.g. 15s 25 | """, 26 | re.VERBOSE, 27 | ) 28 | 29 | def __init__(self, argument, *, now=None): 30 | match = self.compiled.fullmatch(argument) 31 | if match is None or not match.group(0): 32 | raise commands.BadArgument("invalid time provided") 33 | 34 | data = {k: int(v) for k, v in match.groupdict(default=0).items()} 35 | now = now or datetime.datetime.utcnow() 36 | self.dt = now + relativedelta(**data) 37 | 38 | @classmethod 39 | async def convert(cls, ctx, argument): 40 | return cls(argument, now=ctx.message.created_at) 41 | 42 | 43 | class HumanTime: 44 | calendar = pdt.Calendar(version=pdt.VERSION_CONTEXT_STYLE) 45 | 46 | def __init__(self, argument, *, now=None): 47 | now = now or datetime.datetime.utcnow() 48 | dt, status = self.calendar.parseDT(argument, sourceTime=now) 49 | if not status.hasDateOrTime: 50 | raise commands.BadArgument('invalid time provided, try e.g. "tomorrow" or "3 days"') 51 | 52 | if not status.hasTime: 53 | # replace it with the current time 54 | dt = dt.replace( 55 | hour=now.hour, minute=now.minute, second=now.second, microsecond=now.microsecond 56 | ) 57 | 58 | self.dt = dt 59 | self._past = dt < now 60 | 61 | @classmethod 62 | async def convert(cls, ctx, argument): 63 | return cls(argument, now=ctx.message.created_at) 64 | 65 | 66 | class Time(HumanTime): 67 | def __init__(self, argument, *, now=None): 68 | try: 69 | o = ShortTime(argument, now=now) 70 | except Exception as e: 71 | super().__init__(argument) 72 | else: 73 | self.dt = o.dt 74 | self._past = False 75 | 76 | 77 | class FutureTime(Time): 78 | def __init__(self, argument, *, now=None): 79 | super().__init__(argument, now=now) 80 | 81 | if self._past: 82 | raise commands.BadArgument("this time is in the past") 83 | 84 | 85 | class UserFriendlyTime(commands.Converter): 86 | """That way quotes aren't absolutely necessary.""" 87 | 88 | def __init__(self, converter=None, *, default=None): 89 | if isinstance(converter, type) and issubclass(converter, commands.Converter): 90 | converter = converter() 91 | 92 | if converter is not None and not isinstance(converter, commands.Converter): 93 | raise TypeError("commands.Converter subclass necessary.") 94 | 95 | self.converter = converter 96 | self.default = default 97 | 98 | async def check_constraints(self, ctx, now, remaining): 99 | if self.dt < now: 100 | raise commands.BadArgument("This time is in the past.") 101 | 102 | if not remaining: 103 | if self.default is None: 104 | raise commands.BadArgument("Missing argument after the time.") 105 | remaining = self.default 106 | 107 | if self.converter is not None: 108 | self.arg = await self.converter.convert(ctx, remaining) 109 | else: 110 | self.arg = remaining 111 | return self 112 | 113 | def copy(self): 114 | cls = self.__class__ 115 | obj = cls.__new__(cls) 116 | obj.converter = self.converter 117 | obj.default = self.default 118 | return obj 119 | 120 | async def convert(self, ctx, argument): 121 | # Create a copy of ourselves to prevent race conditions from two 122 | # events modifying the same instance of a converter 123 | result = self.copy() 124 | try: 125 | calendar = HumanTime.calendar 126 | regex = ShortTime.compiled 127 | now = ctx.message.created_at 128 | 129 | match = regex.match(argument) 130 | if match is not None and match.group(0): 131 | data = {k: int(v) for k, v in match.groupdict(default=0).items()} 132 | remaining = argument[match.end() :].strip() 133 | result.dt = now + relativedelta(**data) 134 | return await result.check_constraints(ctx, now, remaining) 135 | 136 | # apparently nlp does not like "from now" 137 | # it likes "from x" in other cases though so let me handle the 'now' case 138 | if argument.endswith("from now"): 139 | argument = argument[:-8].strip() 140 | 141 | if argument[0:2] == "me" and argument[0:6] in ( 142 | "me to ", 143 | "me in ", 144 | "me at ", 145 | ): 146 | argument = argument[6:] 147 | 148 | elements = calendar.nlp(argument, sourceTime=now) 149 | if elements is None or len(elements) == 0: 150 | raise commands.BadArgument( 151 | 'Invalid time provided, try e.g. "tomorrow" or "3 days".' 152 | ) 153 | 154 | # handle the following cases: 155 | # "date time" foo 156 | # date time foo 157 | # foo date time 158 | 159 | # first the first two cases: 160 | dt, status, begin, end, dt_string = elements[0] 161 | 162 | if not status.hasDateOrTime: 163 | raise commands.BadArgument( 164 | 'Invalid time provided, try e.g. "tomorrow" or "3 days".' 165 | ) 166 | 167 | if begin not in (0, 1) and end != len(argument): 168 | raise commands.BadArgument( 169 | "Time is either in an inappropriate location, which " 170 | "must be either at the end or beginning of your input, " 171 | "or I just flat out did not understand what you meant. Sorry." 172 | ) 173 | 174 | if not status.hasTime: 175 | # replace it with the current time 176 | dt = dt.replace( 177 | hour=now.hour, 178 | minute=now.minute, 179 | second=now.second, 180 | microsecond=now.microsecond, 181 | ) 182 | 183 | # if midnight is provided, just default to next day 184 | if status.accuracy == pdt.pdtContext.ACU_HALFDAY: 185 | dt = dt.replace(day=now.day + 1) 186 | 187 | result.dt = dt 188 | 189 | if begin in (0, 1): 190 | if begin == 1: 191 | # check if it's quoted: 192 | if argument[0] != '"': 193 | raise commands.BadArgument("Expected quote before time input...") 194 | 195 | if end >= len(argument) or argument[end] != '"': 196 | raise commands.BadArgument("If the time is quoted, you must unquote it.") 197 | 198 | remaining = argument[end + 1 :].lstrip(" ,.!") 199 | else: 200 | remaining = argument[end:].lstrip(" ,.!") 201 | elif len(argument) == end: 202 | remaining = argument[:begin].strip() 203 | 204 | return await result.check_constraints(ctx, now, remaining) 205 | except: 206 | import traceback 207 | 208 | traceback.print_exc() 209 | raise 210 | 211 | 212 | def human_timedelta(dt, *, source=None, accuracy=3, brief=False, suffix=True): 213 | now = source or datetime.datetime.utcnow() 214 | # Microsecond free zone 215 | now = now.replace(microsecond=0) 216 | dt = dt.replace(microsecond=0) 217 | 218 | # This implementation uses relativedelta instead of the much more obvious 219 | # divmod approach with seconds because the seconds approach is not entirely 220 | # accurate once you go over 1 week in terms of accuracy since you have to 221 | # hardcode a month as 30 or 31 days. 222 | # A query like "11 months" can be interpreted as "!1 months and 6 days" 223 | if dt > now: 224 | delta = relativedelta(dt, now) 225 | suffix = "" 226 | else: 227 | delta = relativedelta(now, dt) 228 | suffix = " ago" if suffix else "" 229 | 230 | attrs = [ 231 | ("year", "y"), 232 | ("month", "mo"), 233 | ("day", "d"), 234 | ("hour", "h"), 235 | ("minute", "m"), 236 | ("second", "s"), 237 | ] 238 | 239 | output = [] 240 | for attr, brief_attr in attrs: 241 | elem = getattr(delta, attr + "s") 242 | if not elem: 243 | continue 244 | 245 | if attr == "day": 246 | weeks = delta.weeks 247 | if weeks: 248 | elem -= weeks * 7 249 | if not brief: 250 | output.append(format(plural(weeks), "week")) 251 | else: 252 | output.append(f"{weeks}w") 253 | 254 | if elem <= 0: 255 | continue 256 | 257 | if brief: 258 | output.append(f"{elem}{brief_attr}") 259 | else: 260 | output.append(format(plural(elem), attr)) 261 | 262 | if accuracy is not None: 263 | output = output[:accuracy] 264 | 265 | if len(output) == 0: 266 | return "now" 267 | if not brief: 268 | return human_join(output, final="and") + suffix 269 | else: 270 | return " ".join(output) + suffix 271 | -------------------------------------------------------------------------------- /allutils/main.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | from typing import Union 3 | 4 | import discord 5 | from redbot.core import checks, commands 6 | 7 | from . import formats, time 8 | 9 | 10 | class FetchedUser(commands.Converter): 11 | async def convert(self, ctx, argument): 12 | if not argument.isdigit(): 13 | raise commands.BadArgument("Not a valid user ID.") 14 | try: 15 | return await ctx.bot.get_or_fetch_user(argument) 16 | except discord.NotFound: 17 | raise commands.BadArgument("User not found.") from None 18 | except discord.HTTPException: 19 | raise commands.BadArgument("An error occurred while fetching the user.") from None 20 | 21 | 22 | class AllUtils(commands.Cog): 23 | """Grab meta, make polls. Bitchin'""" 24 | 25 | def __init__(self, bot): 26 | self.bot = bot 27 | 28 | @commands.group(name="get", cooldown_after_parsing=True) 29 | async def get_that(self, ctx: commands.Context): 30 | """Group commands for fetching various information""" 31 | 32 | @get_that.command(aliases=["av"]) 33 | @commands.cooldown(1, 5, commands.BucketType.user) 34 | @commands.guild_only() 35 | async def avatar(self, ctx, *, user: Union[discord.Member, FetchedUser] = None): 36 | """Shows a user's enlarged avatar (if possible). 37 | 38 | Works to fetch user if not in server (must provide UserID) 39 | """ 40 | embed = discord.Embed() 41 | user = user or ctx.author 42 | avatar = user.avatar_url_as(static_format="png") 43 | embed.set_author(name=str(user), url=avatar) 44 | embed.set_image(url=avatar) 45 | embed.colour = await ctx.embed_colour() 46 | await ctx.send(embed=embed) 47 | 48 | @get_that.command(hidden=True, aliases=["ui"]) 49 | @commands.cooldown(1, 5, commands.BucketType.user) 50 | async def userinfo(self, ctx, *, user: Union[discord.Member, FetchedUser] = None): 51 | """Shows info about a user. 52 | 53 | Must supply UserID 54 | """ 55 | 56 | user = user or ctx.author 57 | if ctx.guild and isinstance(user, discord.User): 58 | user = ctx.guild.get_member(user.id) or user 59 | 60 | e = discord.Embed() 61 | roles = [role.name.replace("@", "@\u200b") for role in getattr(user, "roles", [])] 62 | shared = sum(g.get_member(user.id) is not None for g in self.bot.guilds) 63 | e.set_author(name=str(user)) 64 | 65 | def format_date(dt): 66 | if dt is None: 67 | return "N/A" 68 | return f"{dt:%Y-%m-%d %H:%M} ({time.human_timedelta(dt, accuracy=3)})" 69 | 70 | e.add_field(name="ID", value=user.id, inline=False) 71 | e.add_field(name="Servers", value=f"{shared} shared", inline=False) 72 | e.add_field( 73 | name="Joined", value=format_date(getattr(user, "joined_at", None)), inline=False 74 | ) 75 | e.add_field(name="Created", value=format_date(user.created_at), inline=False) 76 | 77 | voice = getattr(user, "voice", None) 78 | if voice is not None: 79 | vc = voice.channel 80 | other_people = len(vc.members) - 1 81 | voice = ( 82 | f"{vc.name} with {other_people} others" 83 | if other_people 84 | else f"{vc.name} by themselves" 85 | ) 86 | e.add_field(name="Voice", value=voice, inline=False) 87 | 88 | if roles: 89 | e.add_field( 90 | name="Roles", 91 | value=", ".join(roles) if len(roles) < 10 else f"{len(roles)} roles", 92 | inline=False, 93 | ) 94 | 95 | colour = user.colour 96 | if colour.value: 97 | e.colour = colour 98 | 99 | if user.avatar: 100 | e.set_thumbnail(url=user.avatar_url) 101 | 102 | if isinstance(user, discord.User): 103 | e.set_footer(text="This member is not in this server.") 104 | 105 | await ctx.send(embed=e) 106 | 107 | @get_that.command(aliases=["guildinfo", "si"], usage="") 108 | @commands.cooldown(1, 5, commands.BucketType.user) 109 | @commands.guild_only() 110 | async def serverinfo(self, ctx, *, guild_id: int = None): 111 | """Shows info about the current server.""" 112 | 113 | if guild_id is not None and await self.bot.is_owner(ctx.author): 114 | guild = self.bot.get_guild(guild_id) 115 | if guild is None: 116 | return await ctx.send(f"Invalid Guild ID given.") 117 | else: 118 | guild = ctx.guild 119 | 120 | roles = [role.name.replace("@", "@\u200b") for role in guild.roles] 121 | 122 | # figure out what channels are 'secret' 123 | everyone = guild.default_role 124 | everyone_perms = everyone.permissions.value 125 | secret = Counter() 126 | totals = Counter() 127 | for channel in guild.channels: 128 | allow, deny = channel.overwrites_for(everyone).pair() 129 | perms = discord.Permissions((everyone_perms & ~deny.value) | allow.value) 130 | channel_type = type(channel) 131 | totals[channel_type] += 1 132 | if not perms.read_messages: 133 | secret[channel_type] += 1 134 | elif isinstance(channel, discord.VoiceChannel) and ( 135 | not perms.connect or not perms.speak 136 | ): 137 | secret[channel_type] += 1 138 | 139 | # member_by_status = Counter(str(m.status) for m in guild.members) 140 | 141 | e = discord.Embed() 142 | e.title = guild.name 143 | e.description = f"**ID**: {guild.id}\n**Owner**: {guild.owner}" 144 | if guild.icon: 145 | e.set_thumbnail(url=guild.icon_url) 146 | 147 | channel_info = [] 148 | key_to_emoji = { 149 | discord.TextChannel: "<:channel:777109611395678218>", 150 | discord.VoiceChannel: "<:voice:777109848499290113>", 151 | } 152 | for key, total in totals.items(): 153 | secrets = secret[key] 154 | try: 155 | emoji = key_to_emoji[key] 156 | except KeyError: 157 | continue 158 | 159 | if secrets: 160 | channel_info.append(f"{emoji} {total} ({secrets} locked)") 161 | else: 162 | channel_info.append(f"{emoji} {total}") 163 | 164 | features = set(guild.features) 165 | all_features = { 166 | "PARTNERED": "Partnered", 167 | "VERIFIED": "Verified", 168 | "DISCOVERABLE": "Server Discovery", 169 | "COMMUNITY": "Community Server", 170 | "FEATURABLE": "Featured", 171 | "WELCOME_SCREEN_ENABLED": "Welcome Screen", 172 | "INVITE_SPLASH": "Invite Splash", 173 | "VIP_REGIONS": "VIP Voice Servers", 174 | "VANITY_URL": "Vanity Invite", 175 | "COMMERCE": "Commerce", 176 | "LURKABLE": "Lurkable", 177 | "NEWS": "News Channels", 178 | "ANIMATED_ICON": "Animated Icon", 179 | "BANNER": "Banner", 180 | } 181 | 182 | info = [ 183 | f"<:agree:749441222954844241>: {label}" 184 | for feature, label in all_features.items() 185 | if feature in features 186 | ] 187 | 188 | if info: 189 | e.add_field(name="Features", value="\n".join(info)) 190 | 191 | e.add_field(name="Channels", value="\n".join(channel_info)) 192 | 193 | if guild.premium_tier != 0: 194 | boosts = f"Level {guild.premium_tier}\n{guild.premium_subscription_count} boosts" 195 | last_boost = max(guild.members, key=lambda m: m.premium_since or guild.created_at) 196 | if last_boost.premium_since is not None: 197 | boosts = f"{boosts}\nLast Boost: {last_boost} ({time.human_timedelta(last_boost.premium_since, accuracy=2)})" 198 | e.add_field(name="Boosts", value=boosts, inline=False) 199 | 200 | bots = sum(m.bot for m in guild.members) 201 | # fmt = f'<:online:316856575413321728> {member_by_status["online"]} ' \ 202 | # f'<:idle:316856575098880002> {member_by_status["idle"]} ' \ 203 | # f'<:dnd:316856574868193281> {member_by_status["dnd"]} ' \ 204 | # f'<:offline:316856575501402112> {member_by_status["offline"]}\n' \ 205 | fmt = f"Total: {guild.member_count} ({formats.plural(bots):bot})" 206 | 207 | e.add_field(name="Members", value=fmt, inline=False) 208 | e.add_field( 209 | name="Roles", value=", ".join(roles) if len(roles) < 10 else f"{len(roles)} roles" 210 | ) 211 | 212 | emoji_stats = Counter() 213 | for emoji in guild.emojis: 214 | if emoji.animated: 215 | emoji_stats["animated"] += 1 216 | emoji_stats["animated_disabled"] += not emoji.available 217 | else: 218 | emoji_stats["regular"] += 1 219 | emoji_stats["disabled"] += not emoji.available 220 | 221 | fmt = ( 222 | f'Regular: {emoji_stats["regular"]}/{guild.emoji_limit}\n' 223 | f'Animated: {emoji_stats["animated"]}/{guild.emoji_limit}\n' 224 | ) 225 | if emoji_stats["disabled"] or emoji_stats["animated_disabled"]: 226 | fmt = f'{fmt}Disabled: {emoji_stats["disabled"]} regular, {emoji_stats["animated_disabled"]} animated\n' 227 | 228 | fmt = f"{fmt}Total Emoji: {len(guild.emojis)}/{guild.emoji_limit*2}" 229 | e.add_field(name="Emoji", value=fmt, inline=False) 230 | e.set_footer(text="Created").timestamp = guild.created_at 231 | await ctx.send(embed=e) 232 | 233 | async def say_permissions(self, ctx, member, channel): 234 | permissions = channel.permissions_for(member) 235 | e = discord.Embed(colour=member.colour) 236 | avatar = member.avatar_url_as(static_format="png") 237 | e.set_author(name=str(member), url=avatar) 238 | allowed, denied = [], [] 239 | for name, value in permissions: 240 | name = name.replace("_", " ").replace("guild", "server").title() 241 | if value: 242 | allowed.append(name) 243 | else: 244 | denied.append(name) 245 | 246 | e.add_field(name="Allowed", value="\n".join(allowed)) 247 | e.add_field(name="Denied", value="\n".join(denied)) 248 | await ctx.send(embed=e) 249 | 250 | @get_that.command(aliases=["up"]) 251 | @commands.cooldown(1, 5, commands.BucketType.user) 252 | @commands.guild_only() 253 | async def userperms( 254 | self, ctx, member: discord.Member = None, channel: discord.TextChannel = None 255 | ): 256 | """Shows a member's permissions in a specific channel. 257 | 258 | If no channel is given then it uses the current one. 259 | You cannot use this in private messages. If no member is given then 260 | the info returned will be yours. 261 | """ 262 | channel = channel or ctx.channel 263 | if member is None: 264 | member = ctx.author 265 | 266 | await self.say_permissions(ctx, member, channel) 267 | 268 | @get_that.command(aliases=["bp"]) 269 | @commands.guild_only() 270 | @commands.cooldown(1, 5, commands.BucketType.user) 271 | @checks.admin_or_permissions(manage_roles=True) 272 | async def botperms(self, ctx, *, channel: discord.TextChannel = None): 273 | """Shows the bot's permissions in a specific channel. 274 | 275 | If no channel is given then it uses the current one. 276 | This is a good way of checking if the bot has the permissions needed 277 | to execute the commands it wants to execute. 278 | To execute this command you must have Manage Roles permission. 279 | You cannot use this in private messages. 280 | """ 281 | channel = channel or ctx.channel 282 | member = ctx.guild.me 283 | await self.say_permissions(ctx, member, channel) 284 | 285 | @commands.command(aliases=["dxp"]) 286 | @commands.is_owner() 287 | async def debugperms(self, ctx, guild_id: int, channel_id: int, author_id: int = None): 288 | """Shows permission resolution for a channel and an optional author.""" 289 | 290 | guild = self.bot.get_guild(guild_id) 291 | if guild is None: 292 | return await ctx.send("Guild not found?") 293 | 294 | channel = guild.get_channel(channel_id) 295 | if channel is None: 296 | return await ctx.send("Channel not found?") 297 | 298 | member = guild.me if author_id is None else guild.get_member(author_id) 299 | if member is None: 300 | return await ctx.send("Member not found?") 301 | 302 | await self.say_permissions(ctx, member, channel) 303 | -------------------------------------------------------------------------------- /decancer/decancer.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import random 3 | import re 4 | import unicodedata 5 | from datetime import datetime, timedelta 6 | 7 | import discord 8 | import stringcase 9 | import unidecode 10 | from redbot.core import Config, checks, commands, modlog 11 | from redbot.core.utils.chat_formatting import box, humanize_timedelta 12 | from redbot.core.utils.menus import start_adding_reactions 13 | from redbot.core.utils.predicates import ReactionPredicate 14 | 15 | from .randomnames import adjectives, nouns, properNouns 16 | 17 | 18 | async def enabled_global(ctx: commands.Context): 19 | return ctx.bot.get_cog("Decancer").enabled_global 20 | 21 | 22 | # originally from https://github.com/PumPum7/PumCogs repo which has a en masse version of this 23 | class Decancer(commands.Cog): 24 | """ 25 | Decancer users names removing special and accented chars. 26 | 27 | `[p]decancerset` to get started if you're already using redbot core modlog 28 | """ 29 | 30 | def __init__(self, bot): 31 | self.bot = bot 32 | self.config = Config.get_conf( 33 | self, 34 | identifier=7778847744, 35 | force_registration=True, 36 | ) 37 | default_guild = {"modlogchannel": None, "new_custom_nick": "simp name", "auto": False} 38 | default_global = {"auto": True} 39 | self.config.register_guild(**default_guild) 40 | self.config.register_global(**default_global) 41 | 42 | self.enabled_global = None 43 | self.enabled_guilds = set() 44 | 45 | __author__ = ["KableKompany#0001", "PhenoM4n4n"] 46 | __version__ = "1.8.2" 47 | 48 | async def red_delete_data_for_user(self, **kwargs): 49 | """This cog does not store user data""" 50 | return 51 | 52 | async def initialize(self): 53 | self.enabled_global = await self.config.auto() 54 | for guild_id, guild_data in (await self.config.all_guilds()).items(): 55 | if guild_data["auto"]: 56 | self.enabled_guilds.add(guild_id) 57 | 58 | @staticmethod 59 | def is_cancerous(text: str) -> bool: 60 | for segment in text.split(): 61 | for char in segment: 62 | if not (char.isascii() and char.isalnum()): 63 | return True 64 | return False 65 | 66 | # the magic 67 | @staticmethod 68 | def strip_accs(text): 69 | try: 70 | text = unicodedata.normalize("NFKC", text) 71 | text = unicodedata.normalize("NFD", text) 72 | text = unidecode.unidecode(text) 73 | text = text.encode("ascii", "ignore") 74 | text = text.decode("utf-8") 75 | except Exception as e: 76 | print(e) 77 | return str(text) 78 | 79 | # the magician 80 | async def nick_maker(self, guild: discord.Guild, old_shit_nick): 81 | old_shit_nick = self.strip_accs(old_shit_nick) 82 | new_cool_nick = re.sub("[^a-zA-Z0-9 \n.]", "", old_shit_nick) 83 | new_cool_nick = " ".join(new_cool_nick.split()) 84 | new_cool_nick = stringcase.lowercase(new_cool_nick) 85 | new_cool_nick = stringcase.titlecase(new_cool_nick) 86 | default_name = await self.config.guild(guild).new_custom_nick() 87 | if len(new_cool_nick.replace(" ", "")) <= 1 or len(new_cool_nick) > 32: 88 | if default_name == "random": 89 | new_cool_nick = await self.get_random_nick(2) 90 | elif default_name: 91 | new_cool_nick = default_name 92 | else: 93 | new_cool_nick = "simp name" 94 | return new_cool_nick 95 | 96 | async def decancer_log( 97 | self, 98 | guild: discord.Guild, 99 | member: discord.Member, 100 | moderator: discord.Member, 101 | old_nick: str, 102 | new_nick: str, 103 | dc_type: str, 104 | ): 105 | channel = guild.get_channel(await self.config.guild(guild).modlogchannel()) 106 | if not channel or not ( 107 | channel.permissions_for(guild.me).send_messages 108 | and channel.permissions_for(guild.me).embed_links 109 | ): 110 | await self.config.guild(guild).modlogchannel.clear() 111 | return 112 | color = 0x2FFFFF 113 | description = [ 114 | f"**Offender:** {member} {member.mention}", 115 | f"**Reason:** Remove cancerous characters from previous name", 116 | f"**New Nickname:** {new_nick}", 117 | f"**Responsible Moderator:** {moderator} {moderator.mention}", 118 | ] 119 | embed = discord.Embed( 120 | color=discord.Color(color), 121 | title=dc_type, 122 | description="\n".join(description), 123 | timestamp=datetime.utcnow(), 124 | ) 125 | embed.set_footer(text=f"ID: {member.id}") 126 | await channel.send(embed=embed) 127 | 128 | async def get_random_nick(self, nickType: int): 129 | if nickType == 1: 130 | new_nick = random.choice(properNouns) 131 | elif nickType == 2: 132 | adjective = random.choice(adjectives) 133 | noun = random.choice(nouns) 134 | new_nick = adjective + noun 135 | elif nickType == 3: 136 | adjective = random.choice(adjectives) 137 | new_nick = adjective.lower() 138 | if nickType == 4: 139 | nounNicks = nouns, properNouns 140 | new_nick = random.choice(random.choices(nounNicks, weights=map(len, nounNicks))[0]) 141 | return new_nick 142 | 143 | @commands.group() 144 | @checks.mod_or_permissions(manage_channels=True) 145 | @commands.guild_only() 146 | async def decancerset(self, ctx): 147 | """ 148 | Set up the modlog channel for decancer'd users, 149 | and set your default name if decancer is unsuccessful. 150 | """ 151 | if ctx.invoked_subcommand: 152 | return 153 | data = await self.config.guild(ctx.guild).all() 154 | channel = ctx.guild.get_channel(data["modlogchannel"]) 155 | name = data["new_custom_nick"] 156 | auto = data["auto"] 157 | if channel is None: 158 | try: 159 | check_modlog_exists = await modlog.get_modlog_channel(ctx.guild) 160 | await self.config.guild(ctx.guild).modlogchannel.set(check_modlog_exists.id) 161 | await ctx.send( 162 | f"I set {check_modlog_exists.mention} as the decancer log channel. You can change this by running ``{ctx.prefix}decancerset modlog [--override]`" 163 | ) 164 | channel = check_modlog_exists.mention 165 | except RuntimeError: 166 | channel = "**NOT SET**" 167 | else: 168 | channel = channel.mention 169 | values = [f"**Modlog Destination:** {channel}", f"**Default Name:** `{name}`"] 170 | if await self.config.auto(): 171 | values.append(f"**Auto-Decancer:** `{auto}`") 172 | e = discord.Embed(colour=await ctx.embed_colour()) 173 | e.add_field( 174 | name=f"{ctx.guild.name} Settings", 175 | value="\n".join(values), 176 | ) 177 | e.set_footer(text="To change these, pass [p]decancerset modlog|defaultname") 178 | e.set_image(url=ctx.guild.icon_url) 179 | try: 180 | await ctx.send(embed=e) 181 | except Exception: 182 | pass 183 | 184 | @decancerset.command(aliases=["ml"]) 185 | async def modlog(self, ctx, channel: discord.TextChannel, override: str = None): 186 | """ 187 | Set a decancer entry to your modlog channel. 188 | If you've set one up with `[p]modlogset channel` 189 | it will default to using that 190 | """ 191 | channel_check = await self.config.guild(ctx.guild).modlogchannel() 192 | 193 | if override != "-override" and channel_check: 194 | await ctx.send( 195 | f"Your current channel is <#{channel_check}>. Pass `-override` to change this." 196 | ) 197 | return 198 | 199 | if not ( 200 | channel.permissions_for(ctx.guild.me).send_messages 201 | and channel.permissions_for(ctx.guild.me).embed_links 202 | ): 203 | await ctx.send("Kind of need permissions to post in that channel LMFAO") 204 | return 205 | 206 | await self.config.guild(ctx.guild).modlogchannel.set(channel.id) 207 | await ctx.send(f"Channel has been set to {channel.mention}") 208 | await ctx.tick() 209 | 210 | @decancerset.command(aliases=["name"]) 211 | async def defaultname(self, ctx, *, name): 212 | """ 213 | If you don't want a server of simps, change this 214 | to whatever you'd like, simp. 215 | 216 | 217 | Example: `[p]decancerset name kable is coolaf` 218 | Changing the default to "random" might do something cool.. 219 | """ 220 | if len(name) > 32 or len(name) < 3: 221 | await ctx.send("Let's keep that nickname within reasonable range, scrub") 222 | return 223 | 224 | await self.config.guild(ctx.guild).new_custom_nick.set(name) 225 | await ctx.send( 226 | f"Your fallback name, should the cancer be too gd high for me to fix, is `{name}`" 227 | ) 228 | 229 | @commands.check(enabled_global) 230 | @decancerset.command() 231 | async def auto(self, ctx, true_or_false: bool = None): 232 | """Toggle automatically decancering new users.""" 233 | if not await self.config.guild(ctx.guild).modlogchannel(): 234 | return await ctx.send( 235 | f"Set up a modlog for this server using `{ctx.prefix}decancerset modlog #channel`" 236 | ) 237 | target_state = ( 238 | true_or_false 239 | if true_or_false is not None 240 | else not (await self.config.guild(ctx.guild).auto()) 241 | ) 242 | await self.config.guild(ctx.guild).auto.set(target_state) 243 | if target_state: 244 | self.enabled_guilds.add(ctx.guild.id) 245 | await ctx.send("I will now decancer new users.") 246 | else: 247 | self.enabled_guilds.remove(ctx.guild.id) 248 | await ctx.send("I will no longer decancer new users.") 249 | 250 | @checks.is_owner() 251 | @decancerset.command(name="autoglobal") 252 | async def global_auto(self, ctx, true_or_false: bool = None): 253 | """Enable/disable auto-decancering globally.""" 254 | target_state = ( 255 | true_or_false if true_or_false is not None else not (await self.config.auto()) 256 | ) 257 | await self.config.auto.set(target_state) 258 | self.enabled_global = target_state 259 | if target_state: 260 | await ctx.send("Automatic decancering has been re-enabled globally.") 261 | else: 262 | await ctx.send("Automatic decancering has been disabled globally.") 263 | 264 | @commands.command(name="decancer") 265 | @checks.mod_or_permissions(manage_nicknames=True) 266 | @checks.bot_has_permissions(manage_nicknames=True) 267 | @commands.guild_only() 268 | async def nick_checker( 269 | self, ctx: commands.Context, user: discord.Member, freeze: bool = False 270 | ): 271 | """ 272 | Remove special/cancerous characters from user nicknames 273 | 274 | Change username glyphs (i.e 乇乂, 黑, etc) 275 | special font chars (zalgo, latin letters, accents, etc) 276 | to their unicode counterpart. If the former, expect the "english" 277 | equivalent to other language based glyphs. 278 | """ 279 | if not await self.config.guild(ctx.guild).modlogchannel(): 280 | return await ctx.send( 281 | f"Set up a modlog for this server using `{ctx.prefix}decancerset modlog #channel`" 282 | ) 283 | 284 | if user.top_role >= ctx.me.top_role: 285 | return await ctx.send( 286 | f"I can't decancer that user since they are higher than me in heirarchy." 287 | ) 288 | m_nick = user.display_name 289 | new_cool_nick = await self.nick_maker(ctx.guild, m_nick) 290 | if m_nick != new_cool_nick: 291 | try: 292 | await user.edit( 293 | reason=f"Old name ({m_nick}): contained special characters", 294 | nick=new_cool_nick, 295 | ) 296 | if ( 297 | freeze 298 | ): # thanks for this badass cog from Dav@https://github.com/Dav-Git/Dav-Cogs 299 | cog_checking = self.bot.get_cog("NickNamer") 300 | freeze_it = self.bot.get_command("freezenick") 301 | await ctx.invoke( 302 | freeze_it, 303 | user=user, 304 | nickname=new_cool_nick, 305 | reason="Decancer'd and frozen", 306 | ) 307 | except Exception as e: 308 | await ctx.send( 309 | f"Double check my order in heirarchy buddy, got an error\n```diff\n- {e}\n```" 310 | ) 311 | return 312 | await ctx.send(f"({m_nick}) was changed to {new_cool_nick}") 313 | 314 | guild = ctx.guild 315 | await self.decancer_log(guild, user, ctx.author, m_nick, new_cool_nick, "decancer") 316 | try: 317 | await ctx.tick() 318 | except discord.NotFound: 319 | pass 320 | 321 | else: 322 | await ctx.send(f"{user.display_name} was already decancer'd") 323 | try: 324 | await ctx.message.add_reaction("\N{CROSS MARK}") 325 | except Exception: 326 | return 327 | 328 | @commands.max_concurrency(1, commands.BucketType.guild) 329 | @commands.cooldown(1, 36000, commands.BucketType.guild) 330 | @checks.mod_or_permissions(manage_nicknames=True) 331 | @checks.bot_has_permissions(manage_nicknames=True) 332 | @commands.guild_only() 333 | @commands.command(cooldown_after_parsing=True) 334 | async def dehoist(self, ctx: commands.Context, *, role: discord.Role = None): 335 | """Decancer all members of the targeted role. 336 | 337 | Role defaults to all members of the server.""" 338 | if not await self.config.guild(ctx.guild).modlogchannel(): 339 | await ctx.send( 340 | f"Set up a modlog for this server using `{ctx.prefix}decancerset modlog #channel`" 341 | ) 342 | ctx.command.reset_cooldown(ctx) 343 | return 344 | 345 | role = role or ctx.guild.default_role 346 | guild = ctx.guild 347 | cancerous_list = [ 348 | member 349 | for member in role.members 350 | if not member.bot 351 | and self.is_cancerous(member.display_name) 352 | and ctx.me.top_role > member.top_role 353 | ] 354 | if not cancerous_list: 355 | await ctx.send(f"There's no one I can decancer in **`{role}`**.") 356 | ctx.command.reset_cooldown(ctx) 357 | return 358 | if len(cancerous_list) > 5000: 359 | await ctx.send( 360 | "There are too many members to decancer in the targeted role. " 361 | "Please select a role with less than 5000 members." 362 | ) 363 | ctx.command.reset_cooldown(ctx) 364 | return 365 | member_preview = "\n".join( 366 | f"{member} - {member.id}" 367 | for index, member in enumerate(cancerous_list, 1) 368 | if index <= 10 369 | ) + ( 370 | f"\nand {len(cancerous_list) - 10} other members.." if len(cancerous_list) > 10 else "" 371 | ) 372 | 373 | case = "" if len(cancerous_list) == 1 else "s" 374 | msg = await ctx.send( 375 | f"Are you sure you want me to decancer the following {len(cancerous_list)} member{case}?\n" 376 | + box(member_preview, "py") 377 | ) 378 | start_adding_reactions(msg, ReactionPredicate.YES_OR_NO_EMOJIS) 379 | pred = ReactionPredicate.yes_or_no(msg, ctx.author) 380 | try: 381 | await self.bot.wait_for("reaction_add", check=pred, timeout=60) 382 | except asyncio.TimeoutError: 383 | await ctx.send("Action cancelled.") 384 | ctx.command.reset_cooldown(ctx) 385 | return 386 | 387 | if pred.result is True: 388 | await ctx.send( 389 | f"Ok. This will take around **{humanize_timedelta(timedelta=timedelta(seconds=len(cancerous_list) * 1.5))}**." 390 | ) 391 | async with ctx.typing(): 392 | for member in cancerous_list: 393 | await asyncio.sleep(1) 394 | old_nick = member.display_name 395 | new_cool_nick = await self.nick_maker(guild, member.display_name) 396 | if old_nick.lower() != new_cool_nick.lower(): 397 | try: 398 | await member.edit( 399 | reason=f"Dehoist | Old name ({old_nick}): contained special characters", 400 | nick=new_cool_nick, 401 | ) 402 | except discord.Forbidden: 403 | await ctx.send("Dehoist failed due to invalid permissions.") 404 | return 405 | except discord.NotFound: 406 | continue 407 | # else: 408 | # await self.decancer_log( 409 | # guild, member, guild.me, old_nick, new_cool_nick, "dehoist" 410 | # ) 411 | try: 412 | await ctx.send("Dehoist completed.") 413 | except (discord.NotFound, discord.Forbidden): 414 | pass 415 | else: 416 | await ctx.send("Action cancelled.") 417 | ctx.command.reset_cooldown(ctx) 418 | return 419 | 420 | @commands.Cog.listener() 421 | async def on_member_join(self, member: discord.Member): 422 | if self.enabled_global is False or member.bot: 423 | return 424 | 425 | guild: discord.Guild = member.guild 426 | if guild.id not in self.enabled_guilds: 427 | return 428 | 429 | data = await self.config.guild(guild).all() 430 | if not ( 431 | data["auto"] and data["modlogchannel"] and guild.me.guild_permissions.manage_nicknames 432 | ): 433 | return 434 | 435 | old_nick = member.display_name 436 | if not self.is_cancerous(old_nick): 437 | return 438 | 439 | await asyncio.sleep( 440 | 5 441 | ) # waiting for auto mod actions to take place to prevent discord from fucking up the nickname edit 442 | member = guild.get_member(member.id) 443 | if not member: 444 | return 445 | if member.top_role >= guild.me.top_role: 446 | return 447 | new_cool_nick = await self.nick_maker(guild, old_nick) 448 | if old_nick.lower() != new_cool_nick.lower(): 449 | try: 450 | await member.edit( 451 | reason=f"Auto Decancer | Old name ({old_nick}): contained special characters", 452 | nick=new_cool_nick, 453 | ) 454 | except discord.NotFound: 455 | pass 456 | except discord.Forbidden: 457 | await self.config.guild(guild).auto.set(False) 458 | else: 459 | await self.decancer_log( 460 | guild, member, guild.me, old_nick, new_cool_nick, "auto-decancer" 461 | ) 462 | -------------------------------------------------------------------------------- /.tools/generate_info.py: -------------------------------------------------------------------------------- 1 | """Script to automatically generate info.json files 2 | and generate class docstrings from single info.yaml file for whole repo. 3 | 4 | DISCLAIMER: While this script works, it uses some hacks and I don't recommend using it 5 | if you don't understand how it does some stuff and why it does it like this. 6 | 7 | --- 8 | 9 | Copyright 2018-2020 Jakub Kuczys (https://github.com/jack1142) 10 | 11 | Licensed under the Apache License, Version 2.0 (the "License"); 12 | you may not use this file except in compliance with the License. 13 | You may obtain a copy of the License at 14 | 15 | https://www.apache.org/licenses/LICENSE-2.0 16 | 17 | Unless required by applicable law or agreed to in writing, software 18 | distributed under the License is distributed on an "AS IS" BASIS, 19 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | See the License for the specific language governing permissions and 21 | limitations under the License. 22 | """ 23 | 24 | import json 25 | import re 26 | import string 27 | import subprocess 28 | import sys 29 | import typing 30 | from collections.abc import Sequence 31 | from pathlib import Path 32 | from types import SimpleNamespace 33 | 34 | import parso 35 | from redbot import VersionInfo 36 | from strictyaml import ( 37 | Bool, 38 | EmptyDict, 39 | EmptyList, 40 | Enum, 41 | Map, 42 | MapPattern, 43 | Optional, 44 | Regex, 45 | ScalarValidator, 46 | Seq, 47 | Str, 48 | Url, 49 | ) 50 | from strictyaml import load as yaml_load 51 | from strictyaml.exceptions import YAMLSerializationError, YAMLValidationError 52 | from strictyaml.utils import is_string 53 | from strictyaml.yamllocation import YAMLChunk 54 | 55 | ROOT_PATH = Path(__file__).absolute().parent.parent 56 | 57 | # `FormatPlaceholder`, `FormatDict` and `safe_format_alt` taken from 58 | # https://stackoverflow.com/posts/comments/100958805 59 | 60 | 61 | class FormatPlaceholder: 62 | def __init__(self, key): 63 | self.key = key 64 | 65 | def __format__(self, spec): 66 | result = self.key 67 | if spec: 68 | result += ":" + spec 69 | return "{" + result + "}" 70 | 71 | def __getitem__(self, index): 72 | self.key = f"{self.key}[{index}]" 73 | return self 74 | 75 | def __getattr__(self, attr): 76 | self.key = f"{self.key}.{attr}" 77 | return self 78 | 79 | 80 | class FormatDict(dict): 81 | def __missing__(self, key): 82 | return FormatPlaceholder(key) 83 | 84 | 85 | def safe_format_alt(text, source): 86 | formatter = string.Formatter() 87 | return formatter.vformat(text, (), FormatDict(source)) 88 | 89 | 90 | class PythonVersion(ScalarValidator): 91 | REGEX = re.compile(r"(\d+)\.(\d+)\.(\d+)") 92 | 93 | def __init__(self) -> None: 94 | self._matching_message = "when expecting Python version (MAJOR.MINOR.MICRO)" 95 | 96 | def validate_scalar(self, chunk: YAMLChunk) -> typing.List[int]: 97 | match = self.REGEX.fullmatch(chunk.contents) 98 | if match is None: 99 | raise YAMLValidationError(self._matching_message, "found non-matching string", chunk) 100 | return [int(group) for group in match.group(1, 2, 3)] 101 | 102 | def to_yaml(self, data: typing.Any) -> str: 103 | if isinstance(data, Sequence): 104 | if len(data) != 3: 105 | raise YAMLSerializationError( 106 | f"expected a sequence of 3 elements, got {len(data)} elements" 107 | ) 108 | for item in data: 109 | if not isinstance(item, int): 110 | raise YAMLSerializationError( 111 | f"expected int, got '{item}' of type '{type(item).__name__}'" 112 | ) 113 | if item < 0: 114 | raise YAMLSerializationError(f"expected non-negative int, got {item}") 115 | return ".".join(str(segment) for segment in data) 116 | if is_string(data): 117 | # we just validated that it's a string 118 | version_string = typing.cast(str, data) 119 | if self.REGEX.fullmatch(version_string) is None: 120 | raise YAMLSerializationError( 121 | "expected Python version (MAJOR.MINOR.MICRO)," f" got '{version_string}'" 122 | ) 123 | return version_string 124 | raise YAMLSerializationError( 125 | "expected string or sequence," f" got '{data}' of type '{type(data).__name__}'" 126 | ) 127 | 128 | 129 | # TODO: allow author in COG_KEYS and merge them with repo/shared fields lists 130 | REPO_KEYS = { 131 | "name": Str(), # Downloader doesn't use this but I can set friendlier name 132 | "short": Str(), 133 | "description": Str(), 134 | "install_msg": Str(), 135 | "author": Seq(Str()), 136 | } 137 | COMMON_KEYS = { 138 | Optional("min_bot_version"): Regex(VersionInfo._VERSION_STR_PATTERN), 139 | Optional("max_bot_version"): Regex(VersionInfo._VERSION_STR_PATTERN), 140 | Optional("min_python_version"): PythonVersion(), 141 | Optional("hidden", False): Bool(), 142 | Optional("disabled", False): Bool(), 143 | Optional("type", "COG"): Enum(["COG", "SHARED_LIBRARY"]), 144 | } 145 | SHARED_FIELDS_KEYS = { 146 | "install_msg": Str(), 147 | "author": Seq(Str()), 148 | **COMMON_KEYS, 149 | } 150 | COG_KEYS = { 151 | "name": Str(), # Downloader doesn't use this but I can set friendlier name 152 | "short": Str(), 153 | "description": Str(), 154 | "end_user_data_statement": Str(), 155 | Optional("class_docstring"): Str(), 156 | Optional("install_msg"): Str(), 157 | Optional("author"): Seq(Str()), 158 | Optional("required_cogs", {}): EmptyDict() | MapPattern(Str(), Url()), 159 | Optional("requirements", []): EmptyList() | Seq(Str()), 160 | Optional("tags", []): EmptyList() | Seq(Str()), 161 | **COMMON_KEYS, 162 | } 163 | SCHEMA = Map( 164 | { 165 | "repo": Map(REPO_KEYS), 166 | "shared_fields": Map(SHARED_FIELDS_KEYS), 167 | "cogs": MapPattern(Str(), Map(COG_KEYS)), 168 | } 169 | ) 170 | SKIP_COG_KEYS_INFO_JSON = {"class_docstring"} 171 | # TODO: auto-format to proper key order 172 | AUTOLINT_REPO_KEYS_ORDER = list(REPO_KEYS.keys()) 173 | AUTOLINT_SHARED_FIELDS_KEYS_ORDER = [getattr(key, "key", key) for key in SHARED_FIELDS_KEYS] 174 | 175 | AUTOLINT_COG_KEYS_ORDER = [getattr(key, "key", key) for key in COG_KEYS] 176 | 177 | 178 | def check_order(data: dict) -> int: 179 | """Temporary order checking, until strictyaml adds proper support for sorting.""" 180 | to_check = { 181 | "repo": AUTOLINT_REPO_KEYS_ORDER, 182 | "shared_fields": AUTOLINT_SHARED_FIELDS_KEYS_ORDER, 183 | } 184 | exit_code = 0 185 | for key, order in to_check.items(): 186 | section = data[key] 187 | original_keys = list(section.keys()) 188 | sorted_keys = sorted(section.keys(), key=order.index) 189 | if original_keys != sorted_keys: 190 | print( 191 | "\033[93m\033[1mWARNING:\033[0m " 192 | f"Keys in `{key}` section have wrong order - use this order: " 193 | f"{', '.join(sorted_keys)}" 194 | ) 195 | exit_code = 1 196 | 197 | original_cog_names = list(data["cogs"].keys()) 198 | sorted_cog_names = sorted(data["cogs"].keys()) 199 | if original_cog_names != sorted_cog_names: 200 | print( 201 | "\033[93m\033[1mWARNING:\033[0m " 202 | "Cog names in `cogs` section aren't sorted. Use alphabetical order." 203 | ) 204 | exit_code = 1 205 | 206 | for pkg_name, cog_info in data["cogs"].items(): 207 | # strictyaml breaks ordering of keys for some reason 208 | original_keys = [k for k, v in cog_info.items() if v] 209 | sorted_keys = sorted( 210 | (k for k, v in cog_info.items() if v), key=AUTOLINT_COG_KEYS_ORDER.index 211 | ) 212 | if original_keys != sorted_keys: 213 | print( 214 | "\033[93m\033[1mWARNING:\033[0m " 215 | f"Keys in `cogs->{pkg_name}` section have wrong order" 216 | f" - use this order: {', '.join(sorted_keys)}" 217 | ) 218 | print(original_keys) 219 | print(sorted_keys) 220 | exit_code = 1 221 | for key in ("required_cogs", "requirements", "tags"): 222 | list_or_dict = cog_info[key] 223 | if hasattr(list_or_dict, "keys"): 224 | original_list = list(list_or_dict.keys()) 225 | else: 226 | original_list = list_or_dict 227 | sorted_list = sorted(original_list) 228 | if original_list != sorted_list: 229 | friendly_name = key.capitalize().replace("_", " ") 230 | print( 231 | "\033[93m\033[1mWARNING:\033[0m " 232 | f"{friendly_name} for `{pkg_name}` cog aren't sorted." 233 | " Use alphabetical order." 234 | ) 235 | print(original_list) 236 | print(sorted_list) 237 | exit_code = 1 238 | 239 | return exit_code 240 | 241 | 242 | def update_class_docstrings(cogs: dict, repo_info: dict) -> int: 243 | """Update class docstrings with descriptions from info.yaml 244 | 245 | This is created with few assumptions: 246 | - name of cog's class is under "name" key in `cogs` dictionary 247 | - following imports until we find class definition is enough to find it 248 | - class name is imported directly: 249 | `from .rlstats import RLStats` not `from . import rlstats` 250 | - import is relative 251 | - star imports are ignored 252 | """ 253 | for pkg_name, cog_info in cogs.items(): 254 | class_name = cog_info["name"] 255 | path = ROOT_PATH / pkg_name / "__init__.py" 256 | if not path.is_file(): 257 | raise RuntimeError("Folder `{pkg_name}` isn't a valid package.") 258 | while True: 259 | with path.open(encoding="utf-8") as fp: 260 | source = fp.read() 261 | tree = parso.parse(source) 262 | class_node = next( 263 | (node for node in tree.iter_classdefs() if node.name.value == class_name), 264 | None, 265 | ) 266 | if class_node is not None: 267 | break 268 | 269 | for import_node in tree.iter_imports(): 270 | if import_node.is_star_import(): 271 | # we're ignoring star imports 272 | continue 273 | for import_path in import_node.get_paths(): 274 | if import_path[-1].value == class_name: 275 | break 276 | else: 277 | continue 278 | 279 | if import_node.level == 0: 280 | raise RuntimeError("Script expected relative import of cog's class.") 281 | if import_node.level > 1: 282 | raise RuntimeError("Attempted relative import beyond top-level package.") 283 | path = ROOT_PATH / pkg_name 284 | for part in import_path[:-1]: 285 | path /= part.value 286 | path = path.with_suffix(".py") 287 | if not path.is_file(): 288 | raise RuntimeError( 289 | f"Path `{path}` isn't a valid file. Finding cog's class failed." 290 | ) 291 | break 292 | 293 | doc_node = class_node.get_doc_node() 294 | new_docstring = cog_info.get("class_docstring") or cog_info["short"] 295 | replacements = { 296 | "repo_name": repo_info["name"], 297 | "cog_name": cog_info["name"], 298 | } 299 | new_docstring = new_docstring.format_map(replacements) 300 | if doc_node is not None: 301 | doc_node.value = f'"""{new_docstring}"""' 302 | else: 303 | first_leaf = class_node.children[-1].get_first_leaf() 304 | # gosh, this is horrible 305 | first_leaf.prefix = f'\n """{new_docstring}"""\n' 306 | 307 | new_code = tree.get_code() 308 | if source != new_code: 309 | print(f"Updated class docstring for {class_name}") 310 | with path.open("w", encoding="utf-8") as fp: 311 | fp.write(tree.get_code()) 312 | 313 | return 0 314 | 315 | 316 | def check_cog_data_path_use(cogs: dict) -> int: 317 | for pkg_name in cogs: 318 | p = subprocess.run( 319 | ("git", "grep", "-q", "cog_data_path", "--", f"{pkg_name}/"), 320 | cwd=ROOT_PATH, 321 | check=False, 322 | ) 323 | if p.returncode == 0: 324 | print( 325 | "\033[94m\033[1mINFO:\033[0m " 326 | f"{pkg_name} uses cog_data_path, make sure" 327 | " that you notify the user about it in install message." 328 | ) 329 | elif p.returncode != 1: 330 | raise RuntimeError("git grep command failed") 331 | return 0 332 | 333 | 334 | CONTAINERS = parso.python.tree._FUNC_CONTAINERS | { 335 | "async_funcdef", 336 | "funcdef", 337 | "classdef", 338 | } 339 | 340 | SMALL_STMT_LIST = { 341 | "expr_stmt", 342 | "del_stmt", 343 | "pass_stmt", 344 | "flow_stmt", 345 | "import_stmt", 346 | "global_stmt", 347 | "nonlocal_stmt", 348 | "assert_stmt", 349 | } 350 | CONTAINERS_WITHOUT_LOCAL_SCOPE = ( 351 | parso.python.tree._RETURN_STMT_CONTAINERS 352 | | {"with_item"} 353 | | parso.python.tree._IMPORTS 354 | | SMALL_STMT_LIST 355 | ) 356 | 357 | 358 | def _scan_recursively( 359 | children: typing.List[parso.tree.NodeOrLeaf], name: str, containers: typing.Set[str] 360 | ): 361 | for element in children: 362 | if element.type == name: 363 | yield element 364 | if element.type in containers: 365 | yield from _scan_recursively(element.children, name, containers) 366 | 367 | 368 | def check_command_docstrings(cogs: dict) -> int: 369 | ret = 0 370 | for pkg_name in cogs: 371 | pkg_folder = ROOT_PATH / pkg_name 372 | for file in pkg_folder.glob("**/*.py"): 373 | with file.open() as fp: 374 | tree = parso.parse(fp.read()) 375 | for node in _scan_recursively(tree.children, "async_funcdef", CONTAINERS): 376 | funcdef = node.children[-1] 377 | decorators = funcdef.get_decorators() 378 | ignore = any( 379 | ( 380 | prefix_part.type == "comment" 381 | and prefix_part.value == "# geninfo-ignore: missing-docstring" 382 | ) 383 | for prefix_part in decorators[0].children[0]._split_prefix() 384 | ) 385 | for deco in decorators: 386 | maybe_name = deco.children[1] 387 | if maybe_name.type == "dotted_name": 388 | it = (n.value for n in maybe_name.children) 389 | # ignore first item (can either be `commands` or `groupname`) 390 | next(it, None) 391 | deco_name = "".join(it) 392 | elif maybe_name.type == "name": 393 | deco_name = maybe_name.value 394 | else: 395 | raise RuntimeError("Unexpected type of decorator name.") 396 | if deco_name in {".command", ".group"}: 397 | break 398 | else: 399 | continue 400 | if funcdef.get_doc_node() is None: 401 | if not ignore: 402 | print( 403 | "\033[93m\033[1mWARNING:\033[0m " 404 | f"command `{funcdef.name.value}` misses help docstring!" 405 | ) 406 | ret = 1 407 | elif ignore: 408 | print( 409 | "\033[93m\033[1mWARNING:\033[0m " 410 | f"command `{funcdef.name.value}` has unused" 411 | " missing-docstring ignore comment!" 412 | ) 413 | ret = 1 414 | return ret 415 | 416 | 417 | def check_for_end_user_data_statement(cogs: dict) -> int: 418 | ret = 0 419 | for pkg_name, cog_info in cogs.items(): 420 | path = ROOT_PATH / pkg_name / "__init__.py" 421 | if not path.is_file(): 422 | raise RuntimeError("Folder `{pkg_name}` isn't a valid package.") 423 | with path.open(encoding="utf-8") as fp: 424 | source = fp.read() 425 | tree = parso.parse(source) 426 | for node in _scan_recursively(tree.children, "name", CONTAINERS_WITHOUT_LOCAL_SCOPE): 427 | if node.value == "__red_end_user_data_statement__": 428 | break 429 | else: 430 | print( 431 | "\033[93m\033[1mWARNING:\033[0m " 432 | f"cog package `{pkg_name}` is missing end user data statement!" 433 | ) 434 | ret = 1 435 | 436 | return ret 437 | 438 | 439 | MAX_RED_VERSIONS = { 440 | (3, 8): None, 441 | } 442 | MAX_PYTHON_VERSION = next(reversed(MAX_RED_VERSIONS)) 443 | 444 | 445 | def main() -> int: 446 | print("Loading info.yaml...") 447 | with open(ROOT_PATH / "info.yaml", encoding="utf-8") as fp: 448 | data = yaml_load(fp.read(), SCHEMA).data 449 | 450 | print("Checking order in sections...") 451 | exit_code = check_order(data) 452 | 453 | print("Preparing repo's info.json...") 454 | repo_info = data["repo"] 455 | repo_info["install_msg"] = repo_info["install_msg"].format_map( 456 | {"repo_name": repo_info["name"]} 457 | ) 458 | with open(ROOT_PATH / "info.json", "w", encoding="utf-8") as fp: 459 | json.dump(repo_info, fp, indent=4) 460 | 461 | all_requirements: typing.Set[str] = set() 462 | requirements: typing.Dict[typing.Tuple[int, int], typing.Set[str]] = { 463 | (3, 8): set(), 464 | } 465 | black_file_list: typing.Dict[typing.Tuple[int, int], typing.List[str]] = { 466 | (3, 8): [".ci"], 467 | } 468 | compileall_file_list: typing.Dict[typing.Tuple[int, int], typing.List[str]] = { 469 | (3, 8): ["."], 470 | } 471 | print("Preparing info.json files for cogs...") 472 | shared_fields = data["shared_fields"] 473 | global_min_bot_version = shared_fields.get("min_bot_version") 474 | global_min_python_version = shared_fields.get("min_python_version") 475 | cogs = data["cogs"] 476 | for pkg_name, cog_info in cogs.items(): 477 | all_requirements.update(cog_info["requirements"]) 478 | min_bot_version = cog_info.get("min_bot_version", global_min_bot_version) 479 | min_python_version = (3, 8) 480 | if min_bot_version is not None: 481 | red_version_info = VersionInfo.from_str(min_bot_version) 482 | for python_version, max_red_version in MAX_RED_VERSIONS.items(): 483 | if max_red_version is None: 484 | min_python_version = python_version 485 | break 486 | if red_version_info >= max_red_version: 487 | continue 488 | min_python_version = python_version 489 | break 490 | python_version = cog_info.get("min_python_version", global_min_python_version) 491 | if python_version is not None and min_python_version < python_version: 492 | min_python_version = python_version 493 | for python_version, reqs in requirements.items(): 494 | if python_version >= min_python_version: 495 | reqs.update(cog_info["requirements"]) 496 | for python_version, file_list in compileall_file_list.items(): 497 | if python_version is MAX_PYTHON_VERSION: 498 | continue 499 | if python_version >= min_python_version: 500 | file_list.append(pkg_name) 501 | black_file_list[min_python_version].append(pkg_name) 502 | 503 | print(f"Preparing info.json for {pkg_name} cog...") 504 | output = {} 505 | for key in AUTOLINT_COG_KEYS_ORDER: 506 | if key in SKIP_COG_KEYS_INFO_JSON: 507 | continue 508 | value = cog_info.get(key) 509 | if value is None: 510 | value = shared_fields.get(key) 511 | if value is None: 512 | continue 513 | output[key] = value 514 | replacements = { 515 | "repo_name": repo_info["name"], 516 | "cog_name": output["name"], 517 | } 518 | shared_fields_namespace = SimpleNamespace(**shared_fields) 519 | maybe_bundled_data = ROOT_PATH / pkg_name / "data" 520 | if maybe_bundled_data.is_dir(): 521 | new_msg = f"{output['install_msg']}\n\nThis cog comes with bundled data." 522 | output["install_msg"] = new_msg 523 | for to_replace in ("short", "description", "install_msg"): 524 | output[to_replace] = safe_format_alt( 525 | output[to_replace], {"shared_fields": shared_fields_namespace} 526 | ) 527 | if to_replace == "description": 528 | output[to_replace] = output[to_replace].format_map( 529 | {**replacements, "short": output["short"]} 530 | ) 531 | else: 532 | output[to_replace] = output[to_replace].format_map(replacements) 533 | 534 | with open(ROOT_PATH / pkg_name / "info.json", "w", encoding="utf-8") as fp: 535 | json.dump(output, fp, indent=4) 536 | 537 | print("Preparing requirements file for CI...") 538 | with open(ROOT_PATH / ".ci/requirements/all_cogs.txt", "w", encoding="utf-8") as fp: 539 | fp.write("Red-DiscordBot\n") 540 | for requirement in sorted(all_requirements): 541 | fp.write(f"{requirement}\n") 542 | for python_version, reqs in requirements.items(): 543 | folder_name = f"py{''.join(map(str, python_version))}" 544 | with open( 545 | ROOT_PATH / f".ci/{folder_name}/requirements/all_cogs.txt", 546 | "w", 547 | encoding="utf-8", 548 | ) as fp: 549 | fp.write("Red-DiscordBot\n") 550 | for req in sorted(reqs): 551 | fp.write(f"{req}\n") 552 | with open( 553 | ROOT_PATH / f".ci/{folder_name}/black_file_list.txt", "w", encoding="utf-8" 554 | ) as fp: 555 | fp.write(" ".join(sorted(black_file_list[python_version]))) 556 | with open( 557 | ROOT_PATH / f".ci/{folder_name}/compileall_file_list.txt", 558 | "w", 559 | encoding="utf-8", 560 | ) as fp: 561 | fp.write(" ".join(sorted(compileall_file_list[python_version]))) 562 | 563 | print("Preparing all cogs list in README.md...") 564 | with open(ROOT_PATH / "README.md", "r+", encoding="utf-8") as fp: 565 | text = fp.read() 566 | match = re.search(r"## Cog Menu\n{2}(.+)\n{2}## Contributing", text, flags=re.DOTALL) 567 | if match is None: 568 | print("\033[91m\033[1mERROR:\033[0m Couldn't find cogs sections in README.md!") 569 | return 1 570 | start, end = match.span(1) 571 | lines = ["---\n| Name | Description |\n| --- | --- |"] 572 | for pkg_name, cog_info in cogs.items(): 573 | replacements = { 574 | "repo_name": repo_info["name"], 575 | "cog_name": cog_info["name"], 576 | } 577 | desc = cog_info["short"].format_map(replacements) 578 | lines.append(f"| {pkg_name} | {desc} |") 579 | cogs_section = "\n".join(lines) 580 | fp.seek(0) 581 | fp.truncate() 582 | fp.write(f"{text[:start]}{cogs_section}{text[end:]}") 583 | 584 | print("Updating class docstrings...") 585 | update_class_docstrings(cogs, repo_info) 586 | print("Checking for cog_data_path usage...") 587 | check_cog_data_path_use(cogs) 588 | print("Checking for missing help docstrings...") 589 | check_command_docstrings(cogs) 590 | print("Checking for missing end user data statements...") 591 | check_for_end_user_data_statement(cogs) 592 | 593 | print("Done!") 594 | return exit_code 595 | 596 | 597 | if __name__ == "__main__": 598 | sys.exit(main()) 599 | -------------------------------------------------------------------------------- /customapps/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import datetime as dt # lgtm [py/import-and-import-from] 3 | 4 | # silencing this flag will rework at final 5 | import logging 6 | from datetime import datetime, timedelta 7 | from typing import Any, Literal 8 | 9 | import discord 10 | from discord.utils import get 11 | from redbot.core import Config, checks, commands 12 | from redbot.core.utils.antispam import AntiSpam 13 | from redbot.core.utils.predicates import MessagePredicate 14 | 15 | Cog: Any = getattr(commands, "Cog", object) 16 | 17 | log = logging.getLogger("red.kablekogs.customapps") 18 | 19 | 20 | RequestType = Literal["discord_deleted_user", "owner", "user", "user_strict"] 21 | # thanks phen 22 | 23 | default = { 24 | "app_check": False, 25 | "name": [], 26 | "timezone": [], 27 | "age": [], 28 | "days": [], 29 | "hours": [], 30 | "position": [], 31 | "experience": [], 32 | "reasonforinterest": [], 33 | "answer8": [], 34 | "answer9": [], 35 | "answer10": [], 36 | "answer11": [], 37 | "answer12": [], 38 | "finalcomments": [], 39 | "raw_app": {}, 40 | } 41 | 42 | guild_defaults = { 43 | "app_questions": { 44 | "name": "What name do you prefer to be called?", 45 | "timezone": "What timezone are you located in? (Use google if you don't know)", 46 | "age": "What year were you born? RESPOND ONLY THE 4 DIGIT YEAR", 47 | "days": "What days can you be active in the server?", 48 | "hours": "How many hours per day can you be active?", 49 | "experience": "Do you have any previous experience? If so, please describe.", 50 | "reasonforinterest": "Why do you want to be a member of our staff?", 51 | "question8": None, 52 | "question9": None, 53 | "question10": None, 54 | "question11": None, 55 | "question12": None, 56 | "finalcomments": "Do you have any final comments for the admins?", 57 | }, 58 | "applicant_id": None, 59 | "accepter_id": None, 60 | "channel_id": None, 61 | "positions_available": None, 62 | } # for the sake of saving time for now. add agnostic before merge 63 | # TODO- 64 | 65 | # Originally from https://github.com/elijabesu/SauriCogs 66 | class CustomApps(Cog): 67 | """Customize Staff apps for your server""" 68 | 69 | async def red_delete_data_for_user(self, *, requester: RequestType, user_id: int): 70 | # pylint: disable=E1120 71 | await self.config.member_from_ids(user_id).clear() 72 | 73 | def __init__(self, bot): 74 | self.bot = bot 75 | self.config = Config.get_conf( 76 | self, 77 | identifier=73837383738, 78 | force_registration=True, 79 | ) 80 | self.config.register_member(**default) 81 | self.config.register_guild(**guild_defaults) 82 | self.antispam = {} 83 | self.spam_control = commands.CooldownMapping.from_cooldown( 84 | 1, 300, commands.BucketType.user 85 | ) 86 | 87 | async def save_application(self, embed: discord.Embed, applicant: discord.Member): 88 | e = embed 89 | await self.config.member(applicant).raw_app.set(e.to_dict()) 90 | 91 | @commands.Cog.listener() 92 | async def on_command_error(self, ctx, error): 93 | if not isinstance(error, commands.MaxConcurrencyReached): 94 | return # don't care about other errors 95 | 96 | bucket = self.spam_control.get_bucket(ctx.message) 97 | current = ctx.message.created_at.replace(tzinfo=dt.timezone.utc).timestamp() 98 | retry_after = bucket.update_rate_limit(current) 99 | author_id = ctx.message.author.id 100 | if retry_after and author_id != self.bot.owner_ids: 101 | return # don't care about users spamming the shit 102 | 103 | await ctx.send( 104 | f"{ctx.author.mention} this command is at it's max allowed processing queue.", 105 | delete_after=20, 106 | ) 107 | # insight 108 | 109 | @commands.command(cooldown_after_parsing=True) 110 | @commands.guild_only() 111 | @checks.bot_has_permissions(manage_roles=True, manage_channels=True, manage_webhooks=True) 112 | @commands.max_concurrency(10, per=commands.BucketType.guild, wait=False) 113 | async def apply(self, ctx: commands.Context): 114 | """Apply to be a staff member.""" 115 | role_add = get(ctx.guild.roles, name="Staff Applicant") 116 | app_data = await self.config.guild(ctx.guild).app_questions.all() 117 | user_data = self.config.member(ctx.author) 118 | 119 | channel = get(ctx.guild.text_channels, name="staff-applications") 120 | if ctx.guild not in self.antispam: 121 | self.antispam[ctx.guild] = {} 122 | if ctx.author not in self.antispam[ctx.guild]: 123 | self.antispam[ctx.guild][ctx.author] = AntiSpam([(timedelta(hours=6), 1)]) 124 | if self.antispam[ctx.guild][ctx.author].spammy: 125 | return await ctx.send( 126 | f"{ctx.author.mention} uh you're doing this way too frequently, and we don't need more than one application from you. Don't call us, we will maybe call you...LOL", 127 | delete_after=10, 128 | ) 129 | if role_add is None: 130 | return await ctx.send("Uh oh. Looks like your Admins haven't added the required role.") 131 | if role_add.position > ctx.guild.me.top_role.position: 132 | return await ctx.send( 133 | "The staff applicant role is above me, and I need it below me if I am to assign it on completion. Tell your admins" 134 | ) 135 | 136 | if channel is None: 137 | return await ctx.send( 138 | "Uh oh. Looks like your Admins haven't added the required channel." 139 | ) 140 | available_positions = await self.config.guild(ctx.guild).positions_available() 141 | if available_positions is None: 142 | fill_this = "Reply with the position you are applying for to continue." 143 | else: 144 | list_positions = "\n".join(available_positions) 145 | fill_this = "Reply with the desired position from this list to continue\n`{}`".format( 146 | list_positions 147 | ) 148 | try: 149 | await ctx.author.send( 150 | f"Let's do this! You have maximum of __5 minutes__ for each question.\n{fill_this}\n\n*To cancel at anytime respond with `cancel`*\n\n*DISCLAIMER: Your responses are stored for proper function of this feature*" 151 | ) 152 | except discord.Forbidden: 153 | return await ctx.send( 154 | f"{ctx.author.mention} I can't DM you. Do you have them closed?", delete_after=10 155 | ) 156 | await ctx.send(f"Okay, {ctx.author.mention}, I've sent you a DM.", delete_after=7) 157 | 158 | def check(m): 159 | return m.author == ctx.author and m.channel == ctx.author.dm_channel 160 | 161 | try: 162 | position = await self.bot.wait_for("message", timeout=300, check=check) 163 | if position.content.lower() == "cancel": 164 | return await ctx.author.send("Application has been canceled.") 165 | await user_data.position.set(position.content) 166 | except asyncio.TimeoutError: 167 | try: 168 | await ctx.author.send("You took too long. Try again, please.") 169 | except discord.HTTPException: 170 | return await ctx.send(f"Thanks for nothing, {ctx.author.mention}") 171 | return 172 | await ctx.author.send(app_data["name"]) 173 | try: 174 | name = await self.bot.wait_for("message", timeout=300, check=check) 175 | if name.content.lower() == "cancel": 176 | return await ctx.author.send("Application has been canceled.") 177 | await user_data.name.set(name.content) 178 | except asyncio.TimeoutError: 179 | try: 180 | await ctx.author.send("You took too long. Try again, please.") 181 | except discord.HTTPException: 182 | return await ctx.send(f"Thanks for nothing, {ctx.author.mention}") 183 | return 184 | await ctx.author.send(app_data["timezone"]) 185 | try: 186 | timezone = await self.bot.wait_for("message", timeout=300, check=check) 187 | if timezone.content.lower() == "cancel": 188 | return await ctx.author.send("Application has been canceled.") 189 | await user_data.timezone.set(timezone.content) 190 | except asyncio.TimeoutError: 191 | try: 192 | await ctx.author.send("You took too long. Try again, please.") 193 | except discord.HTTPException: 194 | return await ctx.send(f"Thanks for nothing, {ctx.author.mention}") 195 | return 196 | await ctx.author.send(app_data["age"]) 197 | try: 198 | age = await self.bot.wait_for("message", timeout=300, check=check) 199 | if age.content.lower() == "cancel": 200 | return await ctx.author.send("Application has been canceled.") 201 | a = age.content 202 | b = str(datetime.today()) 203 | c = b[:4] 204 | d = int(c) 205 | try: 206 | e = int(a) 207 | yearmath = d - e 208 | total_age = f"YOB: {a}\n{yearmath} years old" 209 | except Exception: 210 | total_age = f"Recorded response of `{a}`. Could not calculate age." 211 | 212 | await user_data.age.set(total_age) 213 | 214 | except asyncio.TimeoutError: 215 | try: 216 | await ctx.author.send("You took too long. Try again, please.") 217 | except discord.HTTPException: 218 | return await ctx.send(f"Thanks for nothing, {ctx.author.mention}") 219 | return 220 | 221 | await ctx.author.send(app_data["days"]) 222 | try: 223 | days = await self.bot.wait_for("message", timeout=300, check=check) 224 | if days.content.lower() == "cancel": 225 | return await ctx.author.send("Application has been canceled.") 226 | await user_data.days.set(days.content) 227 | except asyncio.TimeoutError: 228 | try: 229 | await ctx.author.send("You took too long. Try again, please.") 230 | except discord.HTTPException: 231 | return await ctx.send(f"Thanks for nothing, {ctx.author.mention}") 232 | return 233 | await ctx.author.send(app_data["hours"]) 234 | try: 235 | hours = await self.bot.wait_for("message", timeout=300, check=check) 236 | if hours.content.lower() == "cancel": 237 | return await ctx.author.send("Application has been canceled.") 238 | await user_data.hours.set(hours.content) 239 | except asyncio.TimeoutError: 240 | try: 241 | await ctx.author.send("You took too long. Try again, please.") 242 | except discord.HTTPException: 243 | return await ctx.send(f"Thanks for nothing, {ctx.author.mention}") 244 | return 245 | await ctx.author.send(app_data["experience"]) 246 | try: 247 | experience = await self.bot.wait_for("message", timeout=300, check=check) 248 | if experience.content.lower() == "cancel": 249 | return await ctx.author.send("Application has been canceled.") 250 | await user_data.experience.set(experience.content) 251 | except asyncio.TimeoutError: 252 | try: 253 | await ctx.author.send("You took too long. Try again, please.") 254 | except discord.HTTPException: 255 | return await ctx.send(f"Thanks for nothing, {ctx.author.mention}") 256 | return 257 | await ctx.author.send(app_data["reasonforinterest"]) 258 | try: 259 | reasonforinterest = await self.bot.wait_for("message", timeout=300, check=check) 260 | if reasonforinterest.content.lower() == "cancel": 261 | return await ctx.author.send("Application has been canceled.") 262 | await user_data.reasonforinterest.set(reasonforinterest.content) 263 | except asyncio.TimeoutError: 264 | try: 265 | await ctx.author.send("You took too long. Try again, please.") 266 | except discord.HTTPException: 267 | return await ctx.send(f"Thanks for nothing, {ctx.author.mention}") 268 | return 269 | check_8 = app_data["question8"] 270 | if check_8 is not None: 271 | await ctx.author.send(app_data["question8"]) 272 | try: 273 | answer8 = await self.bot.wait_for("message", timeout=300, check=check) 274 | if answer8.content.lower() == "cancel": 275 | return await ctx.author.send("Application has been canceled.") 276 | await user_data.answer8.set(answer8.content) 277 | except asyncio.TimeoutError: 278 | try: 279 | await ctx.author.send("You took too long. Try again, please.") 280 | except discord.HTTPException: 281 | return await ctx.send(f"Thanks for nothing, {ctx.author.mention}") 282 | return 283 | check_9 = app_data["question9"] 284 | if check_9 is not None: 285 | await ctx.author.send(app_data["question9"]) 286 | try: 287 | answer9 = await self.bot.wait_for("message", timeout=300, check=check) 288 | if answer9.content.lower() == "cancel": 289 | return await ctx.author.send("Application has been canceled.") 290 | await user_data.answer9.set(answer9.content) 291 | except asyncio.TimeoutError: 292 | try: 293 | await ctx.author.send("You took too long. Try again, please.") 294 | except discord.HTTPException: 295 | return await ctx.send(f"Thanks for nothing, {ctx.author.mention}") 296 | return 297 | check_10 = app_data["question10"] 298 | if check_10 is not None: 299 | await ctx.author.send(app_data["question10"]) 300 | try: 301 | answer10 = await self.bot.wait_for("message", timeout=300, check=check) 302 | if answer10.content.lower() == "cancel": 303 | return await ctx.author.send("Application has been canceled.") 304 | await user_data.answer10.set(answer10.content) 305 | except asyncio.TimeoutError: 306 | try: 307 | await ctx.author.send("You took too long. Try again, please.") 308 | except discord.HTTPException: 309 | return await ctx.send(f"Thanks for nothing, {ctx.author.mention}") 310 | return 311 | check_11 = app_data["question11"] 312 | if check_11 is not None: 313 | await ctx.author.send(app_data["question11"]) 314 | try: 315 | answer11 = await self.bot.wait_for("message", timeout=300, check=check) 316 | if answer11.content.lower() == "cancel": 317 | return await ctx.author.send("Application has been canceled.") 318 | await user_data.answer11.set(answer11.content) 319 | except asyncio.TimeoutError: 320 | try: 321 | await ctx.author.send("You took too long. Try again, please.") 322 | except discord.HTTPException: 323 | return await ctx.send(f"Thanks for nothing, {ctx.author.mention}") 324 | return 325 | check_12 = app_data["question12"] 326 | if check_12 is not None: 327 | await ctx.author.send(app_data["question12"]) 328 | try: 329 | answer12 = await self.bot.wait_for("message", timeout=300, check=check) 330 | if answer12.content.lower() == "cancel": 331 | return await ctx.author.send("Application has been canceled.") 332 | await user_data.answer12.set(answer12.content) 333 | except asyncio.TimeoutError: 334 | try: 335 | await ctx.author.send("You took too long. Try again, please.") 336 | except discord.HTTPException: 337 | return await ctx.send(f"Thanks for nothing, {ctx.author.mention}") 338 | return 339 | 340 | await ctx.author.send(app_data["finalcomments"]) 341 | try: 342 | finalcomments = await self.bot.wait_for("message", timeout=300, check=check) 343 | if finalcomments.content.lower() == "cancel": 344 | return await ctx.author.send("Application has been canceled.") 345 | await user_data.finalcomments.set(finalcomments.content) 346 | except asyncio.TimeoutError: 347 | return await ctx.author.send("You took too long. Try again, please.") 348 | 349 | embed = discord.Embed(color=await ctx.embed_colour(), timestamp=datetime.utcnow()) 350 | embed.set_author( 351 | name=f"Applicant: {ctx.author.name} | ID: {ctx.author.id}", 352 | icon_url=ctx.author.avatar_url, 353 | ) 354 | embed.set_footer( 355 | text=f"{ctx.author.name}#{ctx.author.discriminator} UserID: {ctx.author.id}" 356 | ) 357 | embed.title = f"Application for {position.content}" 358 | embed.add_field( 359 | name="Applicant Name:", 360 | value=f"Mention: {ctx.author.mention}\nPreferred: " + name.content, 361 | inline=True, 362 | ) 363 | embed.add_field( 364 | name="Age", 365 | value=total_age, 366 | inline=True, 367 | ) 368 | embed.add_field(name="Timezone:", value=timezone.content, inline=True) 369 | embed.add_field(name="Desired position:", value=position.content, inline=True) 370 | embed.add_field(name="Active days/week:", value=days.content, inline=True) 371 | embed.add_field(name="Active hours/day:", value=hours.content, inline=True) 372 | embed.add_field( 373 | name="{}...".format(app_data["reasonforinterest"][:197]).replace("$", "\\$"), 374 | value=reasonforinterest.content, 375 | inline=False, 376 | ) 377 | embed.add_field(name="Previous experience:", value=experience.content, inline=False) 378 | 379 | if check_8 is not None: 380 | embed.add_field( 381 | name="{}...".format(app_data["question8"][:197]).replace("$", "\\$"), 382 | value=answer8.content, 383 | inline=False, 384 | ) 385 | if check_9 is not None: 386 | embed.add_field( 387 | name="{}...".format(app_data["question9"][:197]).replace("$", "\\$"), 388 | value=answer9.content, 389 | inline=False, 390 | ) 391 | if check_10 is not None: 392 | embed.add_field( 393 | name="{}...".format(app_data["question10"][:197]).replace("$", "\\$"), 394 | value=answer10.content, 395 | inline=False, 396 | ) 397 | if check_11 is not None: 398 | embed.add_field( 399 | name="{}...".format(app_data["question11"][:197]).replace("$", "\\$"), 400 | value=answer11.content, 401 | inline=False, 402 | ) 403 | if check_12 is not None: 404 | embed.add_field( 405 | name="{}...".format(app_data["question12"][:197]).replace("$", "\\$"), 406 | value=answer12.content, 407 | inline=False, 408 | ) 409 | embed.add_field(name="Final Comments", value=finalcomments.content, inline=False) 410 | try: 411 | webhook = None 412 | for hook in await channel.webhooks(): 413 | if hook.name == ctx.guild.me.name: 414 | webhook = hook 415 | if webhook is None: 416 | webhook = await channel.create_webhook(name=ctx.guild.me.name) 417 | 418 | await webhook.send( 419 | embed=embed, username=ctx.guild.me.display_name, avatar_url=ctx.guild.me.avatar_url 420 | ) 421 | except Exception as e: 422 | log.info(f"{e} occurred in {ctx.author.name} | {ctx.author.id} application") 423 | try: 424 | return await ctx.author.send( 425 | "Seems your responses were too verbose. Let's try again, but without the life stories." 426 | ) 427 | except Exception: 428 | return 429 | # except discord.HTTPException: 430 | # return await ctx.author.send( 431 | # "Your final application was too long to resolve as an embed. Give this another shot, keeping answers a bit shorter." 432 | # ) 433 | # except commands.CommandInvokeError: 434 | # return await ctx.author.send( 435 | # "You need to start over but this time when it asks for year of birth, respond only with a 4 digit year i.e `1999`" 436 | # ) 437 | await ctx.author.add_roles(role_add) 438 | 439 | try: 440 | await ctx.author.send( 441 | f"Your application has been sent to {ctx.guild.name} Admins! Thanks for your interest!" 442 | ) 443 | except commands.CommandInvokeError: 444 | return await ctx.send( 445 | f"{ctx.author.mention} I sent your app to the admins. Thanks for closing dms early tho rude ass" 446 | ) 447 | self.antispam[ctx.guild][ctx.author].stamp() 448 | # lets save the embed instead of calling on it again 449 | await self.save_application(embed=embed, applicant=ctx.author) 450 | 451 | await self.config.member(ctx.author).app_check.set(True) 452 | 453 | @checks.admin_or_permissions(manage_guild=True) 454 | @commands.group(name="appq", aliases=["appquestions"]) 455 | @commands.guild_only() 456 | async def app_questions(self, ctx: commands.Context): 457 | """Set/see the custom questions for the applications in your server""" 458 | app_questions = await self.config.guild(ctx.guild).app_questions.get_raw() 459 | question_1 = app_questions["name"] 460 | question_2 = app_questions["timezone"] 461 | question_3 = app_questions["age"] 462 | question_4 = app_questions["days"] 463 | question_5 = app_questions["hours"] 464 | question_6 = app_questions["experience"] 465 | question_7 = app_questions["reasonforinterest"] 466 | question_8 = app_questions["question8"] 467 | question_9 = app_questions["question9"] 468 | question_10 = app_questions["question10"] 469 | question_11 = app_questions["question11"] 470 | question_12 = app_questions["question12"] 471 | question_13 = app_questions["finalcomments"] 472 | 473 | await ctx.send( 474 | "There are 13 questions in this application feature, with a few preloaded already for you.\nHere is the current configuration:" 475 | ) 476 | e = discord.Embed(colour=await ctx.embed_colour()) 477 | e.add_field( 478 | name="Question 1", value=f"{question_1}" if question_1 else "Not Set", inline=False 479 | ) 480 | e.add_field( 481 | name="Question 2", value=f"{question_2}" if question_2 else "Not Set", inline=False 482 | ) 483 | e.add_field( 484 | name="Question 3", value=f"{question_3}" if question_3 else "Not Set", inline=False 485 | ) 486 | e.add_field( 487 | name="Question 4", value=f"{question_4}" if question_4 else "Not Set", inline=False 488 | ) 489 | e.add_field( 490 | name="Question 5", value=f"{question_5}" if question_5 else "Not Set", inline=False 491 | ) 492 | e.add_field( 493 | name="Question 6", value=f"{question_6}" if question_6 else "Not Set", inline=False 494 | ) 495 | e.add_field( 496 | name="Question 7", value=f"{question_7}" if question_7 else "Not Set", inline=False 497 | ) 498 | e.add_field( 499 | name="Question 8", value=f"{question_8}" if question_8 else "Not Set", inline=False 500 | ) 501 | e.add_field( 502 | name="Question 9", value=f"{question_9}" if question_9 else "Not Set", inline=False 503 | ) 504 | e.add_field( 505 | name="Question 10", value=f"{question_10}" if question_10 else "Not Set", inline=False 506 | ) 507 | e.add_field( 508 | name="Question 11", value=f"{question_11}" if question_11 else "Not Set", inline=False 509 | ) 510 | e.add_field( 511 | name="Question 12", value=f"{question_12}" if question_12 else "Not Set", inline=False 512 | ) 513 | e.add_field( 514 | name="Question 13", value=f"{question_13}" if question_13 else "Not Set", inline=False 515 | ) 516 | await ctx.send(embed=e) 517 | 518 | @app_questions.command(name="set") 519 | async def set_questions(self, ctx: commands.Context): 520 | """Set up custom questions for your server""" 521 | 522 | def check(m): 523 | return m.author == ctx.author and m.channel == ctx.channel 524 | 525 | await ctx.send( 526 | "Let's set up those questions we've not pre-filled:\nYou will be setting questions 8-12. You can view the preloaded questions by passing `{}appq`. To begin, reply with `admin abuse` *spelled exact*".format( 527 | ctx.prefix 528 | ) 529 | ) 530 | try: 531 | confirmation = await ctx.bot.wait_for("message", check=check, timeout=20) 532 | if confirmation.content.lower() != "admin abuse": 533 | return await ctx.send("Alright, let's do these later then") 534 | except asyncio.TimeoutError: 535 | return await ctx.send( 536 | "Took to long to respond, gotta be smarter than the users you're hiring for sure." 537 | ) 538 | 539 | app_questions = await self.config.guild(ctx.guild).app_questions.get_raw() 540 | question_8 = app_questions["question8"] 541 | question_9 = app_questions["question9"] 542 | question_10 = app_questions["question10"] 543 | question_11 = app_questions["question11"] 544 | question_12 = app_questions["question12"] 545 | await ctx.send( 546 | "Alright, let's start with question 8: You have 5min to decide and respond with question you'd like, or respond with cancel to do this later" 547 | ) 548 | 549 | if question_8 is not None: 550 | await ctx.send( 551 | f"Looks like question 8 is currently `{question_8}`:\n Do you want to change this? Type `no` to skip or the question you wish to change to if you want to change." 552 | ) 553 | try: 554 | submit_8 = await ctx.bot.wait_for("message", check=check, timeout=300) 555 | if submit_8.content.lower() != "no": 556 | if len(submit_8.content) > 750: 557 | return await ctx.send( 558 | "Talkitive are we? Too many characters to fit in final embed, shorten the question some" 559 | ) 560 | await self.config.guild(ctx.guild).app_questions.question8.set( 561 | submit_8.content 562 | ) 563 | except asyncio.TimeoutError: 564 | return await ctx.send( 565 | "Took too long bud. Let's be coherent for this and try again." 566 | ) 567 | 568 | if question_8 is None: 569 | try: 570 | submit_8 = await ctx.bot.wait_for("message", check=check, timeout=300) 571 | if submit_8.content.lower() != "cancel": 572 | if len(submit_8.content) > 750: 573 | return await ctx.send( 574 | "Talkitive are we? Too many characters to fit in final embed, shorten the question some" 575 | ) 576 | await self.config.guild(ctx.guild).app_questions.question8.set( 577 | submit_8.content 578 | ) 579 | except asyncio.TimeoutError: 580 | return await ctx.send( 581 | "Took too long bud. Let's be coherent for this and try again." 582 | ) 583 | await ctx.send("Moving to question 9: Please respond with your next app question") 584 | 585 | if question_9 is not None: 586 | await ctx.send( 587 | f"Looks like question 9 is currently `{question_9}`:\n Do you want to change this? Type `no` to skip or the question you wish to change to if you want to change." 588 | ) 589 | try: 590 | submit_9 = await ctx.bot.wait_for("message", check=check, timeout=300) 591 | if submit_9.content.lower() != "no": 592 | if len(submit_9.content) > 750: 593 | return await ctx.send( 594 | "Talkitive are we? Too many characters to fit in final embed, shorten the question some" 595 | ) 596 | await self.config.guild(ctx.guild).app_questions.question9.set( 597 | submit_9.content 598 | ) 599 | except asyncio.TimeoutError: 600 | return await ctx.send( 601 | "Took too long bud. Let's be coherent for this and try again." 602 | ) 603 | await ctx.send("Moving to question 10: Please respond with your next app question") 604 | 605 | if question_9 is None: 606 | try: 607 | submit_9 = await ctx.bot.wait_for("message", check=check, timeout=300) 608 | if submit_9.content.lower() != "cancel": 609 | if len(submit_9.content) > 750: 610 | return await ctx.send( 611 | "Talkitive are we? Too many characters to fit in final embed, shorten the question some" 612 | ) 613 | await self.config.guild(ctx.guild).app_questions.question9.set( 614 | submit_9.content 615 | ) 616 | except asyncio.TimeoutError: 617 | return await ctx.send( 618 | "Took too long bud. Let's be coherent for this and try again." 619 | ) 620 | await ctx.send("Moving to question 10: Please respond with your next app question") 621 | 622 | if question_10 is not None: 623 | await ctx.send( 624 | f"Looks like question 10 is currently `{question_10}`:\n Do you want to change this? Type `no` to skip or the question you wish to change to if you want to change." 625 | ) 626 | try: 627 | submit_10 = await ctx.bot.wait_for("message", check=check, timeout=300) 628 | if submit_10.content.lower() != "no": 629 | if len(submit_10.content) > 750: 630 | return await ctx.send( 631 | "Talkitive are we? Too many characters to fit in final embed, shorten the question some" 632 | ) 633 | await self.config.guild(ctx.guild).app_questions.question10.set( 634 | submit_10.content 635 | ) 636 | except asyncio.TimeoutError: 637 | return await ctx.send( 638 | "Took too long bud. Let's be coherent for this and try again." 639 | ) 640 | await ctx.send("Moving to question 11: Please respond with your next app question") 641 | 642 | if question_10 is None: 643 | try: 644 | submit_10 = await ctx.bot.wait_for("message", check=check, timeout=300) 645 | if submit_10.content.lower() != "cancel": 646 | if len(submit_10.content) > 750: 647 | return await ctx.send( 648 | "Talkitive are we? Too many characters to fit in final embed, shorten the question some" 649 | ) 650 | await self.config.guild(ctx.guild).app_questions.question10.set( 651 | submit_10.content 652 | ) 653 | except asyncio.TimeoutError: 654 | return await ctx.send( 655 | "Took too long bud. Let's be coherent for this and try again." 656 | ) 657 | await ctx.send("Moving to question 11: Please respond with your next app question") 658 | 659 | if question_11 is not None: 660 | await ctx.send( 661 | f"Looks like question 11 is currently `{question_11}`:\n Do you want to change this? Type `no` to skip or the question you wish to change to if you want to change." 662 | ) 663 | try: 664 | submit_11 = await ctx.bot.wait_for("message", check=check, timeout=300) 665 | if submit_11.content.lower() != "no": 666 | if len(submit_11.content) > 750: 667 | return await ctx.send( 668 | "Talkitive are we? Too many characters to fit in final embed, shorten the question some" 669 | ) 670 | await self.config.guild(ctx.guild).app_questions.question11.set( 671 | submit_11.content 672 | ) 673 | except asyncio.TimeoutError: 674 | return await ctx.send( 675 | "Took too long bud. Let's be coherent for this and try again." 676 | ) 677 | await ctx.send("Moving to question 12: Please respond with your next app question") 678 | 679 | if question_11 is None: 680 | try: 681 | submit_11 = await ctx.bot.wait_for("message", check=check, timeout=300) 682 | if submit_11.content.lower() != "cancel": 683 | if len(submit_11.content) > 750: 684 | return await ctx.send( 685 | "Talkitive are we? Too many characters to fit in final embed, shorten the question some" 686 | ) 687 | await self.config.guild(ctx.guild).app_questions.question11.set( 688 | submit_11.content 689 | ) 690 | except asyncio.TimeoutError: 691 | return await ctx.send( 692 | "Took too long bud. Let's be coherent for this and try again." 693 | ) 694 | await ctx.send("Moving to question 12: Please respond with your next app question") 695 | 696 | if question_12 is not None: 697 | await ctx.send( 698 | f"Looks like question 12 is currently `{question_12}`:\n Do you want to change this? Type `no` to skip or the question you wish to change to if you want to change." 699 | ) 700 | try: 701 | submit_12 = await ctx.bot.wait_for("message", check=check, timeout=300) 702 | if submit_12.content.lower() != "no": 703 | if len(submit_12.content) > 750: 704 | return await ctx.send( 705 | "Talkitive are we? Too many characters to fit in final embed, shorten the question some" 706 | ) 707 | await self.config.guild(ctx.guild).app_questions.question12.set( 708 | submit_12.content 709 | ) 710 | except asyncio.TimeoutError: 711 | return await ctx.send( 712 | "Took too long bud. Let's be coherent for this and try again." 713 | ) 714 | 715 | if question_12 is None: 716 | try: 717 | submit_12 = await ctx.bot.wait_for("message", check=check, timeout=300) 718 | if submit_12.content.lower() != "cancel": 719 | if len(submit_12.content) > 750: 720 | return await ctx.send( 721 | "Talkitive are we? Too many characters to fit in final embed, shorten the question some" 722 | ) 723 | await self.config.guild(ctx.guild).app_questions.question12.set( 724 | submit_12.content 725 | ) 726 | except asyncio.TimeoutError: 727 | return await ctx.send( 728 | "Took too long bud. Let's be coherent for this and try again." 729 | ) 730 | 731 | await ctx.send( 732 | "That's all the questions and your apps are set *maybe, if you answered, anyway*. Check this with `{}appq`".format( 733 | ctx.prefix 734 | ) 735 | ) 736 | 737 | @checks.mod_or_permissions(manage_roles=True) 738 | @commands.command() 739 | @commands.guild_only() 740 | @checks.bot_has_permissions(embed_links=True) 741 | async def appcheck(self, ctx: commands.Context, user_id: discord.Member): 742 | """ 743 | *Not Functioning* 744 | Pull an application that was completed by a user 745 | """ 746 | return await ctx.send( 747 | "This command is currently being reworked, follow updates in The Kompound" 748 | ) 749 | 750 | @checks.admin_or_permissions(administrator=True) 751 | @commands.command() 752 | @commands.guild_only() 753 | @checks.bot_has_permissions(manage_channels=True, manage_roles=True) 754 | async def applysetup(self, ctx: commands.Context): 755 | """Go through the initial setup process.""" 756 | pred = MessagePredicate.yes_or_no(ctx) 757 | role = MessagePredicate.valid_role(ctx) 758 | 759 | applicant = get(ctx.guild.roles, name="Staff Applicant") 760 | channel = get(ctx.guild.text_channels, name="staff-applications") 761 | 762 | await ctx.send( 763 | "This will create required channel and role. Do you wish to continue? (yes/no)" 764 | ) 765 | try: 766 | await self.bot.wait_for("message", timeout=30, check=pred) 767 | except asyncio.TimeoutError: 768 | return await ctx.send("You took too long. Try again, please.") 769 | if not pred.result: 770 | return await ctx.send("Setup cancelled.") 771 | if not applicant: 772 | try: 773 | applicant = await ctx.guild.create_role( 774 | name="Staff Applicant", reason="Application cog setup" 775 | ) 776 | except discord.Forbidden: 777 | return await ctx.send( 778 | "Uh oh. Looks like I don't have permissions to manage roles." 779 | ) 780 | if not channel: 781 | await ctx.send("Do you want everyone to see the applications channel? (yes/no)") 782 | try: 783 | await self.bot.wait_for("message", timeout=30, check=pred) 784 | except asyncio.TimeoutError: 785 | return await ctx.send("You took too long. Try again, please.") 786 | if pred.result: 787 | overwrites = { 788 | ctx.guild.default_role: discord.PermissionOverwrite(send_messages=False), 789 | ctx.guild.me: discord.PermissionOverwrite(send_messages=True), 790 | } 791 | else: 792 | overwrites = { 793 | ctx.guild.default_role: discord.PermissionOverwrite(read_messages=False), 794 | ctx.guild.me: discord.PermissionOverwrite(read_messages=True), 795 | } 796 | try: 797 | channel = await ctx.guild.create_text_channel( 798 | "staff-applications", 799 | overwrites=overwrites, 800 | reason="Application cog setup", 801 | ) 802 | except discord.Forbidden: 803 | return await ctx.send( 804 | "Uh oh. Looks like I don't have permissions to manage channels." 805 | ) 806 | await ctx.send(f"What role can accept or reject applicants?") 807 | try: 808 | await self.bot.wait_for("message", timeout=30, check=role) 809 | except asyncio.TimeoutError: 810 | return await ctx.send("You took too long. Try again, please.") 811 | accepter = role.result 812 | await self.config.guild(ctx.guild).applicant_id.set(applicant.id) 813 | await self.config.guild(ctx.guild).channel_id.set(channel.id) 814 | await self.config.guild(ctx.guild).accepter_id.set(accepter.id) 815 | await ctx.send( 816 | "You have finished the setup! Please, move your new channel to the category you want it in." 817 | ) 818 | 819 | @commands.command() 820 | @commands.guild_only() 821 | @checks.bot_has_permissions(manage_roles=True) 822 | async def accept(self, ctx: commands.Context, target: discord.Member): 823 | """Accept a staff applicant. 824 | can be a mention or an ID.""" 825 | try: 826 | accepter = get(ctx.guild.roles, id=await self.config.guild(ctx.guild).accepter_id()) 827 | except TypeError: 828 | accepter = None 829 | if ( 830 | not accepter 831 | and not ctx.author.guild_permissions.administrator 832 | or accepter 833 | and accepter not in ctx.author.roles 834 | ): 835 | return await ctx.send("Uh oh, you cannot use this command.") 836 | try: 837 | applicant = get(ctx.guild.roles, id=await self.config.guild(ctx.guild).applicant_id()) 838 | except TypeError: 839 | applicant = None 840 | if not applicant: 841 | applicant = get(ctx.guild.roles, name="Staff Applicant") 842 | if not applicant: 843 | return await ctx.send( 844 | "Uh oh, the configuration is not correct. Ask the Admins to set it." 845 | ) 846 | role = MessagePredicate.valid_role(ctx) 847 | if applicant in target.roles: 848 | await ctx.send(f"What role do you want to accept {target.name} as?") 849 | try: 850 | await self.bot.wait_for("message", timeout=30, check=role) 851 | except asyncio.TimeoutError: 852 | return await ctx.send("You took too long. Try again, please.") 853 | role_add = role.result 854 | try: 855 | await target.add_roles(role_add) 856 | except discord.Forbidden: 857 | return await ctx.send( 858 | "Uh oh, I cannot give them the role. It might be above all of my roles." 859 | ) 860 | await target.remove_roles(applicant) 861 | await ctx.send(f"Accepted {target.mention} as {role_add}.") 862 | await target.send(f"You have been accepted as {role_add} in {ctx.guild.name}.") 863 | else: 864 | await ctx.send(f"Uh oh. Looks like {target.mention} hasn't applied for anything.") 865 | 866 | @commands.command() 867 | @commands.guild_only() 868 | @checks.bot_has_permissions(manage_roles=True) 869 | async def deny(self, ctx: commands.Context, target: discord.Member): 870 | """Deny a staff applicant. 871 | can be a mention or an ID""" 872 | try: 873 | accepter = get(ctx.guild.roles, id=await self.config.guild(ctx.guild).accepter_id()) 874 | except TypeError: 875 | accepter = None 876 | if not accepter: 877 | if not ctx.author.guild_permissions.administrator: 878 | return await ctx.send("Uh oh, you cannot use this command.") 879 | else: 880 | if accepter not in ctx.author.roles: 881 | return await ctx.send("Uh oh, you cannot use this command.") 882 | try: 883 | applicant = get(ctx.guild.roles, id=await self.config.guild(ctx.guild).applicant_id()) 884 | except TypeError: 885 | applicant = None 886 | if not applicant: 887 | applicant = get(ctx.guild.roles, name="Staff Applicant") 888 | if not applicant: 889 | return await ctx.send( 890 | "Uh oh, the configuration is not correct. Ask the Admins to set it." 891 | ) 892 | if applicant in target.roles: 893 | await ctx.send("Would you like to specify a reason? (yes/no)") 894 | pred = MessagePredicate.yes_or_no(ctx) 895 | try: 896 | await self.bot.wait_for("message", timeout=30, check=pred) 897 | except asyncio.TimeoutError: 898 | return await ctx.send("You took too long. Try again, please.") 899 | if pred.result: 900 | await ctx.send("Please, specify your reason now.") 901 | 902 | def check(m): 903 | return m.author == ctx.author 904 | 905 | try: 906 | reason = await self.bot.wait_for("message", timeout=120, check=check) 907 | except asyncio.TimeoutError: 908 | return await ctx.send("You took too long. Try again, please.") 909 | try: 910 | await target.send( 911 | f"Your application in {ctx.guild.name} has been denied.\n*Reason:* {reason.content}" 912 | ) 913 | except Exception as e: 914 | await ctx.send( 915 | f"Getting the following error when trying to send this user a message:\n```\n{e}\n```" 916 | ) 917 | else: 918 | await target.send(f"Your application in {ctx.guild.name} has been denied.") 919 | await target.remove_roles(applicant) 920 | await ctx.send(f"Denied {target.mention}'s application.") 921 | else: 922 | await ctx.send(f"Uh oh. Looks like {target.mention} hasn't applied for anything.") 923 | 924 | @app_questions.command(name="reset") 925 | async def clear_config(self, ctx: commands.Context): 926 | """ 927 | Fully resets server configuation to default, and clears all custom app questions 928 | """ 929 | 930 | def check(m): 931 | return m.author == ctx.author and m.channel == ctx.channel 932 | 933 | await ctx.send( 934 | "Are you certain about this? This will wipe all settings/custom questions in your server's configuration\nType: `RESET THIS GUILD` to continue (must be typed exact)" 935 | ) 936 | try: 937 | confirm_reset = await ctx.bot.wait_for("message", check=check, timeout=30) 938 | if confirm_reset.content != "RESET THIS GUILD": 939 | return await ctx.send("Okay, not resetting today") 940 | except asyncio.TimeoutError: 941 | return await ctx.send("You took too long to reply") 942 | await self.config.guild(ctx.guild).app_questions.clear_raw() 943 | await ctx.send("Guild Reset, goodluck") 944 | -------------------------------------------------------------------------------- /lockitup/lockitup.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from typing import Union 4 | 5 | import discord 6 | from redbot.core import Config, checks, commands 7 | from redbot.core.commands import Greedy 8 | from redbot.core.utils.chat_formatting import pagify 9 | from redbot.core.utils.menus import DEFAULT_CONTROLS, menu 10 | 11 | 12 | # core functioning from Sharky-Cogs @https://github.com/SharkyTheKing/Sharky 13 | class LockItUp(commands.Cog): 14 | """Lockdown a list of channels, a channel, or the whole server.""" 15 | 16 | def __init__(self, bot): 17 | self.bot = bot 18 | self.config = Config.get_conf(self, identifier=3734879387937497) 19 | 20 | default_guild = { 21 | "channels": [], 22 | "roles": [], 23 | "lockdown_message": None, 24 | "unlockdown_message": None, 25 | "locked": False, 26 | "vc_channels": [], 27 | "music_channels": [], 28 | "send_alert": True, 29 | "nondefault": False, 30 | "secondary_role": None, 31 | "secondary_channels": [], 32 | "lock_role": None, 33 | "logging_channel": None, 34 | } 35 | 36 | self.config.register_guild(**default_guild) 37 | self.log = logging.getLogger("red.kko-cogs.lockitup") 38 | 39 | async def red_delete_data_for_user(self, **kwargs): 40 | """This cog does not store user data""" 41 | return 42 | 43 | async def ack_lockdown(self, ctx: commands.Context, guild: discord.Guild): 44 | msg = await self.config.guild(guild).lockdown_message() 45 | color1 = 0xF50A0A 46 | e = discord.Embed( 47 | color=discord.Color(color1), 48 | title=f"Server Lockdown :lock:", 49 | description=msg, 50 | timestamp=ctx.message.created_at, 51 | ) 52 | e.set_footer(text=f"{guild.name}") 53 | channel_ids = await self.config.guild(guild).channels() 54 | spec_check = await self.config.guild(guild).secondary_channels() 55 | if spec_check: 56 | channel_ids += spec_check 57 | for guild_channel in guild.channels: 58 | if guild_channel.id in channel_ids: 59 | try: 60 | await guild_channel.send(embed=e) 61 | await asyncio.sleep(0.3) 62 | except discord.Forbidden: 63 | self.log.info("Could not send message to {}".format(guild_channel.name)) 64 | await self.loggerhook( 65 | guild, 66 | error=f"Can't send messages in {guild_channel.mention} after lock down. Check bot perms.", 67 | ) 68 | 69 | async def reign_lockdown(self, ctx: commands.Context, guild: discord.Guild): 70 | bot_override = self.bot.user 71 | author = ctx.author 72 | channel_ids = await self.config.guild(guild).channels() 73 | role = guild.default_role 74 | for guild_channel in guild.channels: 75 | if guild_channel.id in channel_ids: 76 | overwrite1 = guild_channel.overwrites_for(bot_override) 77 | overwrite1.update(send_messages=True, embed_links=True) 78 | try: 79 | await guild_channel.set_permissions( 80 | bot_override, 81 | overwrite=overwrite1, 82 | reason="Securing bot overrides for lockdown", 83 | ) 84 | await asyncio.sleep(0.3) 85 | except Exception: 86 | return await ctx.send( 87 | "You'll need to give me permissions to send messages in the channels I am locking down, so I can manage that permissions for others. I failed trying to secure my own overrides. This lockdown will not resume. Best way to fix this is ensure my role in the server settings has send messages permission turned on." 88 | ) 89 | 90 | overwrite = guild_channel.overwrites_for(role) 91 | overwrite.update(send_messages=False) 92 | try: 93 | await guild_channel.set_permissions( 94 | role, 95 | overwrite=overwrite, 96 | reason="Lockdown in effect. Requested by {} ({})".format( 97 | author.name, author.id 98 | ), 99 | ) 100 | await asyncio.sleep(0.2) 101 | except discord.Forbidden: 102 | self.log.info("Could not lockdown {}".format(guild_channel.name)) 103 | await self.loggerhook( 104 | guild, 105 | error=f"Could not lockdown {guild_channel.name}. Check my permissions and make sure I can manage the channel", 106 | ) 107 | 108 | msg = await self.config.guild(guild).lockdown_message() 109 | if msg: 110 | notifier = await self.config.guild(guild).send_alert() 111 | if notifier is True: 112 | await self.ack_lockdown(ctx, guild) 113 | 114 | async def secondary_lockdown(self, ctx: commands.Context, guild: discord.Guild): 115 | bot_override = self.bot.user 116 | author = ctx.author 117 | special_chans = await self.config.guild(guild).secondary_channels() 118 | spec_role = await self.config.guild(guild).secondary_role() 119 | for guild_channel in guild.channels: 120 | if guild_channel.id in special_chans: 121 | overwrite1 = guild_channel.overwrites_for(bot_override) 122 | overwrite1.update(send_messages=True, embed_links=True) 123 | try: 124 | if not overwrite1.send_messages: 125 | await guild_channel.set_permissions( 126 | bot_override, 127 | overwrite=overwrite1, 128 | reason="Securing bot overrides for lockdown", 129 | ) 130 | await asyncio.sleep(0.5) 131 | except Exception as er: 132 | return await self.loggerhook( 133 | guild, 134 | error=f"Error on lock for {guild_channel.mention} in securing bot overrides. Make sure I have the ability to send messages in these channels so I can manage this permission for others. ERROR: {er}\nLockdown will not resume", 135 | ) 136 | 137 | role = discord.utils.get(guild.roles, id=spec_role) 138 | spec_overwrite = guild_channel.overwrites_for(role) 139 | spec_overwrite.update(send_messages=False) 140 | try: 141 | await guild_channel.set_permissions( 142 | role, 143 | overwrite=spec_overwrite, 144 | reason="Lockdown in effect. Requested by {} ({})".format( 145 | author.name, author.id 146 | ), 147 | ) 148 | await asyncio.sleep(0.5) 149 | except discord.Forbidden as er: 150 | self.log.info("In {}, could not lock {}".format(guild.id, guild_channel.name)) 151 | await self.loggerhook( 152 | guild, 153 | error=f"Error on lockdown for {guild_channel.mention}\n```diff\n+ ERROR:\n- {er}\n```", 154 | ) 155 | 156 | @commands.command() 157 | @commands.guild_only() 158 | @commands.max_concurrency(1, commands.BucketType.guild) 159 | @checks.mod_or_permissions(manage_channels=True) 160 | @checks.bot_has_permissions(manage_channels=True, manage_roles=True) 161 | async def lockdown(self, ctx: commands.Context, lockrole: bool = False): 162 | """ 163 | Lockdown a server 164 | 165 | If you pass true, your @everyone role will also be denied permissions from within the role menu 166 | """ 167 | guild = ctx.guild 168 | config_check = await self.config.guild(guild).channels() 169 | if not config_check: 170 | await ctx.send( 171 | "You need to set this up by running `{}lockdownset`, first and stepping through those configuration subcommands".format( 172 | ctx.prefix 173 | ) 174 | ) 175 | return 176 | 177 | def check(m): 178 | return m.author == ctx.author and m.channel == ctx.channel 179 | 180 | lock_check = await self.config.guild(ctx.guild).locked() 181 | if lock_check is True: 182 | return await ctx.send("You're already locked") 183 | 184 | await ctx.send(f"Proceed with locking down {guild.name}?\n`[yes|no]`") 185 | try: 186 | confirm_lock = await ctx.bot.wait_for("message", check=check, timeout=30) 187 | if confirm_lock.content.lower() != "yes": 188 | return await ctx.send( 189 | "Good thing I was made for my time to be wasted - canceling lockdown..." 190 | ) 191 | except asyncio.TimeoutError: 192 | return await ctx.send("You took too long to reply!") 193 | 194 | await ctx.trigger_typing() 195 | nondefault_lock = await self.config.guild(guild).nondefault() 196 | if nondefault_lock is True: 197 | await self.secondary_lockdown(ctx, guild) 198 | 199 | # proceed to default lockdown 200 | await self.reign_lockdown(ctx, guild) 201 | 202 | if lockrole: 203 | perms = ctx.guild.get_role(ctx.guild.id).permissions 204 | perms.send_messages = False 205 | if not ctx.me.guild_permissions.manage_roles: 206 | await ctx.send( 207 | "I'm missing the ability to manage roles so we will skip making changes to roles in the server settings" 208 | ) 209 | try: 210 | await ctx.guild.default_role.edit( 211 | permissions=perms, reason=f"Role Lockdown requested by {ctx.author.name}" 212 | ) 213 | await self.config.guild(ctx.guild).lock_role.set(True) 214 | except Exception as e: 215 | await ctx.send( 216 | f"Getting an error when attempting to edit role permissions in server settings:\n{e}\nSkipping..." 217 | ) 218 | 219 | # finalize 220 | try: 221 | await ctx.send( 222 | "Server locked down. Revert this by running `{}unlockdown`".format(ctx.prefix) 223 | ) 224 | except Exception as er: 225 | self.log.info( 226 | f"Couldn't secure overrides in Guild {ctx.guild.name} ({ctx.guild.id}): Locked as requested." 227 | ) 228 | await self.loggerhook( 229 | guild, 230 | error=f"Unable to send messages on lockdown to your channels due to the following error\n```diff\n+ ERROR:\n- {er}\n```", 231 | ) 232 | 233 | await self.config.guild(guild).locked.set(True) # write it to configs 234 | 235 | async def ack_unlockdown(self, ctx: commands.Context, guild: discord.Guild): 236 | msg = await self.config.guild(guild).unlockdown_message() 237 | color2 = 0x2FFFFF 238 | e = discord.Embed( 239 | color=discord.Color(color2), 240 | title=f"Server Unlock :unlock:", 241 | description=msg, 242 | timestamp=ctx.message.created_at, 243 | ) 244 | e.set_footer(text=f"{guild.name}") 245 | 246 | channel_ids = await self.config.guild(guild).channels() 247 | spec_check = await self.config.guild(guild).secondary_channels() 248 | if spec_check: 249 | channel_ids += spec_check 250 | for guild_channel in guild.channels: 251 | if guild_channel.id in channel_ids: 252 | try: 253 | await guild_channel.send(embed=e) 254 | await asyncio.sleep(0.3) 255 | except discord.Forbidden: 256 | self.log.info("Could not send message to {}".format(guild_channel.name)) 257 | await self.loggerhook( 258 | guild, 259 | error=f"Can't send messages in {guild_channel.mention} after lock down. Check bot perms.", 260 | ) 261 | 262 | async def reign_unlockdown(self, ctx: commands.Context, guild: discord.Guild): 263 | author = ctx.author 264 | channel_ids = await self.config.guild(guild).channels() 265 | role = guild.default_role 266 | for guild_channel in guild.channels: 267 | if guild_channel.id in channel_ids: 268 | overwrite = guild_channel.overwrites_for(role) 269 | overwrite.update(send_messages=None) 270 | try: 271 | await guild_channel.set_permissions( 272 | role, 273 | overwrite=overwrite, 274 | reason="Lockdown rescinded. Requested by {} ({})".format( 275 | author.name, author.id 276 | ), 277 | ) 278 | await asyncio.sleep(0.2) 279 | except discord.Forbidden as er: 280 | self.log.info("Could not unlock {}".format(guild_channel.name)) 281 | await self.loggerhook( 282 | guild, 283 | error=f"Error on unlock for {guild_channel.mention}\n```diff\n+ ERROR:\n- {er}\n```", 284 | ) 285 | 286 | msg = await self.config.guild(guild).unlockdown_message() 287 | if msg: 288 | notifier = await self.config.guild(guild).send_alert() 289 | if notifier is True: 290 | await self.ack_unlockdown(ctx, guild) 291 | 292 | async def secondary_unlockdown(self, ctx: commands.Context, guild: discord.Guild): 293 | author = ctx.author 294 | special_chans = await self.config.guild(guild).secondary_channels() 295 | spec_role = await self.config.guild(guild).secondary_role() 296 | for guild_channel in guild.channels: 297 | if guild_channel.id in special_chans: 298 | role = discord.utils.get(guild.roles, id=spec_role) 299 | spec_overwrite = guild_channel.overwrites_for(role) 300 | spec_overwrite.update(send_messages=True) 301 | try: 302 | await guild_channel.set_permissions( 303 | role, 304 | overwrite=spec_overwrite, 305 | reason="Lockdown rescinded. Requested by {} ({})".format( 306 | author.name, author.id 307 | ), 308 | ) 309 | await asyncio.sleep(0.5) 310 | except discord.Forbidden as er: 311 | self.log.info( 312 | "In {}, could not unlock {}".format(guild.id, guild_channel.name) 313 | ) 314 | await self.loggerhook( 315 | guild, 316 | error=f"Error on unlock for {guild_channel.mention}\n```diff\n+ ERROR:\n- {er}\n```", 317 | ) 318 | 319 | @commands.command() 320 | @commands.guild_only() 321 | @commands.max_concurrency(1, commands.BucketType.guild) 322 | @checks.mod_or_permissions(manage_channels=True) 323 | @checks.bot_has_permissions(manage_channels=True, manage_roles=True) 324 | async def unlockdown(self, ctx: commands.Context, lockrole: bool = False): 325 | """ 326 | Unlock the server 327 | 328 | If you pass true, your @everyone role will also be allowed permissions from within the role menu to send messages 329 | """ 330 | guild = ctx.guild 331 | 332 | def check(m): 333 | return m.author == ctx.author and m.channel == ctx.channel 334 | 335 | unlock_check = await self.config.guild(ctx.guild).locked() 336 | if unlock_check is False: 337 | return await ctx.send("You're not locked") 338 | 339 | await ctx.send("R U Sure About That? `[yes|no]`") 340 | try: 341 | confirm_unlock = await ctx.bot.wait_for("message", check=check, timeout=30) 342 | if confirm_unlock.content.lower() != "yes": 343 | return await ctx.send("Looks like we aren't unlocking this thing today") 344 | except asyncio.TimeoutError: 345 | return await ctx.send("You took too long to reply!") 346 | 347 | await ctx.trigger_typing() 348 | nondefault_lock = await self.config.guild(guild).nondefault() 349 | if nondefault_lock is True: 350 | await self.secondary_unlockdown(ctx, guild) 351 | 352 | # proceed to default unlockdown 353 | await self.reign_unlockdown(ctx, guild) 354 | 355 | if lockrole: 356 | perms = ctx.guild.get_role(ctx.guild.id).permissions 357 | perms.send_messages = True 358 | if not ctx.me.guild_permissions.manage_roles: 359 | await ctx.send( 360 | "I'm missing the ability to manage roles so we will skip making changes to roles in the server settings" 361 | ) 362 | try: 363 | await ctx.guild.default_role.edit( 364 | permissions=perms, reason=f"Role Unlock requested by {ctx.author.name}" 365 | ) 366 | await self.config.guild(ctx.guild).lock_role.set(True) 367 | except Exception as e: 368 | await ctx.send( 369 | f"Getting an error when attempting to edit role permissions in server settings:\n{e}\nSkipping..." 370 | ) 371 | 372 | # finalize 373 | try: 374 | await ctx.send("Server is unlocked") 375 | except discord.Forbidden: 376 | await self.loggerhook( 377 | guild, 378 | error="I lack perms to successfully unlock this server — please verify I have the send messagees permissions myself in the role menu", 379 | ) 380 | return 381 | 382 | await self.config.guild(guild).locked.set(False) # write it to configs 383 | 384 | @commands.group(aliases=["lds"]) 385 | @commands.guild_only() 386 | @checks.admin_or_permissions(manage_guild=True) 387 | @checks.bot_has_permissions(embed_links=True) 388 | async def lockdownset(self, ctx: commands.Context): 389 | """ 390 | Settings for lockdown 391 | """ 392 | 393 | @lockdownset.command(name="logchan") 394 | @checks.admin_or_permissions(manage_guild=True) 395 | @checks.bot_has_permissions(manage_channels=True, manage_webhooks=True) 396 | async def logging_channel(self, ctx: commands.Context, logchannel: discord.TextChannel): 397 | """ 398 | Set up logging channel to record what channels the bot couldn't successfully lock/unlock 399 | """ 400 | guild = ctx.guild 401 | 402 | await self.config.guild(guild).logging_channel.set(logchannel.id) 403 | try: 404 | await self.loggerhook( 405 | guild, 406 | error="THIS IS A TEST\n```diff\n+ If you are seeing this, you have correctly set up error log for lock/unlock features\n```", 407 | ) 408 | except Exception: 409 | self.log.info(f"Error'd on setup in {guild.id} for webhook logging") 410 | 411 | try: 412 | await ctx.send( 413 | f"Set up the logging — will send webhooks to {logchannel.mention} when there is a permissions error on lock/unlocks" 414 | ) 415 | except Exception as e: 416 | self.log.info(f"Error setting up guild logs in {ctx.guild.id}: Error {e}") 417 | 418 | async def loggerhook(self, guild: discord.Guild, error: str): 419 | channel = guild.get_channel(await self.config.guild(guild).logging_channel()) 420 | if not channel or not (channel.permissions_for(guild.me).manage_webhooks): 421 | await self.config.guild(guild).logging_channel.clear() 422 | return 423 | 424 | webhook = None 425 | for hook in await channel.webhooks(): 426 | if hook.name == self.bot.user.name: 427 | webhook = hook 428 | if webhook is None: 429 | webhook = await channel.create_webhook(name=self.bot.user.name) 430 | 431 | await webhook.send( 432 | content=f"**LockItUp Error Log**\n{error}", 433 | username=self.bot.user.name, 434 | avatar_url=self.bot.user.avatar_url, 435 | ) 436 | 437 | @lockdownset.command(name="showsettings") 438 | async def show_settings(self, ctx: commands.Context): 439 | """See Guild Configuration""" 440 | guild = ctx.guild 441 | fetch_all = await self.config.guild(guild).get_raw() 442 | get_channel = fetch_all["channels"] 443 | get_lock = fetch_all["lockdown_message"] 444 | get_unlock = fetch_all["unlockdown_message"] 445 | get_sec_role = fetch_all["secondary_role"] 446 | get_sec_chans = fetch_all["secondary_channels"] 447 | check_silent = fetch_all["send_alert"] 448 | get_vc = fetch_all["vc_channels"] 449 | get_music = fetch_all["music_channels"] 450 | 451 | chan_count = len(get_channel) 452 | if not get_channel: 453 | e = discord.Embed( 454 | color=await ctx.embed_color(), 455 | title="Lockdown Settings:", 456 | description="No channels added", 457 | ) 458 | check_specs = fetch_all["nondefault"] 459 | if check_specs: 460 | e.add_field( 461 | name="Special Role", 462 | value=f"<@&{get_sec_role}> — `{get_sec_role}`" if get_sec_role else "**None**", 463 | inline=False, 464 | ) 465 | spec_msg = "" 466 | for chan_id in get_sec_chans: 467 | channel_name = f"<#{chan_id}>" 468 | spec_msg += f"`{chan_id}` — {channel_name}\n" 469 | e.add_field( 470 | name="Special Channels", 471 | value=f"{spec_msg}" if get_sec_chans else "**None**", 472 | inline=False, 473 | ) 474 | e.add_field(name="Lock Message:", value=get_lock or "**None**") 475 | e.add_field(name="Unlock Message:", value=get_unlock or "**None**") 476 | e.add_field( 477 | name="Channel Notification:", 478 | value="**Enabled**" if check_silent else "**Disabled**", 479 | ) 480 | return await ctx.send(embed=e) 481 | else: 482 | msg = "" 483 | for chan_id in get_channel: 484 | channel_name = f"<#{chan_id}>" 485 | msg += f"`{chan_id}` — {channel_name}\n" 486 | 487 | e_list = [] 488 | for page in pagify(msg, shorten_by=1000): 489 | e = discord.Embed( 490 | color=await ctx.embed_color(), 491 | title="Lockdown Settings:", 492 | description="Channels: {}\n{}".format(chan_count, page), 493 | ) 494 | e.add_field(name="Lock Message:", value=get_lock or "**None**", inline=False) 495 | e.add_field( 496 | name="Unlock Message:", 497 | value=get_unlock or "**None**", 498 | inline=False, 499 | ) 500 | 501 | check_specs = fetch_all["nondefault"] 502 | if check_specs: 503 | e.add_field( 504 | name="Special Role", 505 | value=f"<@&{get_sec_role}> — `{get_sec_role}`" if get_sec_role else "**None**", 506 | inline=False, 507 | ) 508 | spec_msg = "" 509 | for chan_id in get_sec_chans: 510 | channel_name = f"<#{chan_id}>" 511 | spec_msg += f"`{chan_id}` — {channel_name}\n" 512 | e.add_field( 513 | name="Special Channels", 514 | value=f"{spec_msg}" if get_sec_chans else "**None**", 515 | inline=False, 516 | ) 517 | if get_vc: 518 | vc_list = "" 519 | for c in get_vc: 520 | c_name = f"<#{c}>" 521 | vc_list += f"`{c}` — {c_name}\n" 522 | e.add_field(name="Voice Channels", value=f"{vc_list}", inline=False) 523 | 524 | if get_music: 525 | music_list = "" 526 | for mc in get_music: 527 | mc_name = f"<#{mc}>" # ctx.guild.get_channel(mc) 528 | music_list += f"`{mc}` — {mc_name}\n" 529 | e.add_field(name="Music Channels", value=f"{music_list}", inline=False) 530 | 531 | e.add_field( 532 | name="Channel Notification:", 533 | value="**Enabled**" if check_silent else "**Disabled**", 534 | inline=False, 535 | ) 536 | e.set_author(name=ctx.guild.name, icon_url=guild.icon_url) 537 | e.set_footer(text="Lockdown Configuration") 538 | e_list.append(e) 539 | 540 | await menu(ctx, e_list, DEFAULT_CONTROLS) 541 | 542 | @lockdownset.command() 543 | async def addchan(self, ctx: commands.Context, channels: Greedy[discord.TextChannel]): 544 | """ 545 | Adds channel to list of channels to lock/unlock 546 | 547 | IDs are also accepted. 548 | """ 549 | if not channels: 550 | raise commands.BadArgument 551 | guild = ctx.guild 552 | chans = await self.config.guild(guild).channels() 553 | if len(chans) > 35: 554 | return await ctx.send("Think you've added enough. Keep it under 35 please") 555 | for chan in channels: 556 | if chan.id not in chans: 557 | chans.append(chan.id) 558 | await self.config.guild(guild).channels.set(chans) 559 | else: 560 | continue 561 | chan_count = len(chans) 562 | msg = "" 563 | for chan_id in chans: 564 | channel_name = f"<#{chan_id}>" 565 | msg += f"`{chan_id}` - {channel_name}\n" 566 | 567 | e_list = [] 568 | for page in pagify(msg, shorten_by=1000): 569 | 570 | embed = discord.Embed( 571 | description="Channel List: {}\n{}".format(chan_count, page), 572 | colour=await ctx.embed_color(), 573 | ) 574 | embed.set_author(name=guild.name, icon_url=guild.icon_url) 575 | embed.set_footer(text="Lockdown Channel Settings") 576 | e_list.append(embed) 577 | await ctx.send("Added to the list, here's your current channels") 578 | await menu(ctx, e_list, DEFAULT_CONTROLS) 579 | 580 | @lockdownset.command(name="addspecialchannel", aliases=["asc"]) 581 | async def add_special_channel( 582 | self, ctx: commands.Context, channels: Greedy[discord.TextChannel] 583 | ): 584 | """ 585 | Adds channel to list of channels to lock/unlock for special role 586 | """ 587 | if not channels: 588 | raise commands.BadArgument 589 | guild = ctx.guild 590 | chans = await self.config.guild(guild).secondary_channels() 591 | if len(chans) > 25: 592 | return await ctx.send("Think you've added enough. Keep it under 25 please") 593 | for chan in channels: 594 | if chan.id in chans: 595 | continue 596 | chans.append(chan.id) 597 | await self.config.guild(guild).secondary_channels.set(chans) 598 | chan_count = len(chans) 599 | msg = "" 600 | for chan_id in chans: 601 | channel_name = f"<#{chan_id}>" 602 | msg += f"`{chan_id}` - {channel_name}\n" 603 | 604 | e_list = [] 605 | for page in pagify(msg, shorten_by=1000): 606 | 607 | embed = discord.Embed( 608 | description="Channel List: {}\n{}".format(chan_count, page), 609 | colour=await ctx.embed_color(), 610 | ) 611 | embed.set_author(name=guild.name, icon_url=guild.icon_url) 612 | embed.set_footer(text="Secondary Channels") 613 | e_list.append(embed) 614 | 615 | nondefault = await self.config.guild(guild).nondefault() 616 | check_role = await self.config.guild(guild).secondary_role() 617 | if nondefault is False: 618 | if check_role is not None: 619 | await self.config.guild(guild).nondefault.set(value=True) 620 | else: 621 | await ctx.send( 622 | "Make sure you add the role for this using `{}lds specrole `".format( 623 | ctx.prefix 624 | ) 625 | ) 626 | 627 | await menu(ctx, e_list, DEFAULT_CONTROLS) 628 | 629 | @lockdownset.command(name="rmspecialchannel", aliases=["rsc"]) 630 | async def remove_special_channel(self, ctx: commands.Context, channels: Greedy[int]): 631 | """ 632 | Remove a channel to list of channels to lock/unlock for special roles 633 | 634 | Accepts only channel IDs 635 | """ 636 | if not channels: 637 | raise commands.BadArgument 638 | guild = ctx.guild 639 | chans = await self.config.guild(guild).secondary_channels() 640 | for chan in channels: 641 | if chan in chans: 642 | chans.remove(chan) 643 | await self.config.guild(guild).secondary_channels.set(chans) 644 | 645 | chan_count = len(chans) 646 | if not chan_count: 647 | return await ctx.send( 648 | "After removing that, no more special channels exist in this server's configuration." 649 | ) 650 | msg = "" 651 | for chan_id in chans: 652 | channel_name = f"<#{chan_id}>" 653 | msg += f"`{chan_id}` - {channel_name}\n" 654 | 655 | e_list = [] 656 | for page in pagify(msg, shorten_by=1000): 657 | 658 | embed = discord.Embed( 659 | description="Channel List: {}\n{}".format(chan_count, page), 660 | colour=await ctx.embed_color(), 661 | ) 662 | embed.set_author(name=guild.name, icon_url=guild.icon_url) 663 | embed.set_footer(text="Lockdown Channel Sttings") 664 | e_list.append(embed) 665 | await ctx.send("Removed from the list, here's your current channels") 666 | await menu(ctx, e_list, DEFAULT_CONTROLS) 667 | nondefault = await self.config.guild(guild).nondefault() 668 | check_role = await self.config.guild(guild).secondary_role() 669 | if not chans and nondefault is True and not check_role: 670 | await self.config.guild(guild).nondefault.set(value=False) 671 | await ctx.send( 672 | "Removed secondary configurations from this guild as there is no role or channels assigned" 673 | ) 674 | 675 | @lockdownset.command() 676 | async def rmchan(self, ctx: commands.Context, channels: Greedy[int]): 677 | """ 678 | Remove a channel to list of channels to lock/unlock 679 | 680 | Accepts only channel IDs 681 | """ 682 | if not channels: 683 | raise commands.BadArgument 684 | guild = ctx.guild 685 | chans = await self.config.guild(guild).channels() 686 | for chan in channels: 687 | if chan in chans: 688 | chans.remove(chan) 689 | await self.config.guild(guild).channels.set(chans) 690 | 691 | chan_count = len(chans) 692 | if not chan_count: 693 | return await ctx.send( 694 | "After removing that, no more channels are left in this server's configuration" 695 | ) 696 | msg = "" 697 | for chan_id in chans: 698 | channel_name = f"<#{chan_id}>" 699 | msg += f"`{chan_id}` - {channel_name}\n" 700 | 701 | e_list = [] 702 | for page in pagify(msg, shorten_by=1000): 703 | 704 | embed = discord.Embed( 705 | description="Channel List: {}\n{}".format(chan_count, page), 706 | colour=await ctx.embed_color(), 707 | ) 708 | embed.set_author(name=guild.name, icon_url=guild.icon_url) 709 | embed.set_footer(text="Lockdown Channel Sttings") 710 | e_list.append(embed) 711 | await ctx.send("Removed from the list, here's your current channels") 712 | await menu(ctx, e_list, DEFAULT_CONTROLS) 713 | 714 | @lockdownset.command(name="reset") 715 | async def clear_config(self, ctx: commands.Context): 716 | """ 717 | Fully resets server configuation to default, and clears all channels from list 718 | """ 719 | 720 | def check(m): 721 | return m.author == ctx.author and m.channel == ctx.channel 722 | 723 | await ctx.send( 724 | "Are you certain about this? This will wipe all settings/messages/channels in your servers configuration Type: `RESET THIS GUILD` to continue (must be typed exact)" 725 | ) 726 | try: 727 | confirm_reset = await ctx.bot.wait_for("message", check=check, timeout=30) 728 | if confirm_reset.content != "RESET THIS GUILD": 729 | return await ctx.send("Okay, not resetting today") 730 | except asyncio.TimeoutError: 731 | return await ctx.send("You took too long to reply") 732 | await self.config.guild(ctx.guild).clear_raw() 733 | await ctx.send("Guild Reset, goodluck") 734 | 735 | @lockdownset.command() 736 | async def lockmsg(self, ctx: commands.Context, *, str=None): 737 | """ 738 | Sets the lock message for your server 739 | """ 740 | guild = ctx.guild 741 | msg = await self.config.guild(guild).lockdown_message() 742 | if msg is not None: 743 | await ctx.send(f"Your current message is {msg}") 744 | await self.config.guild(guild).lockdown_message.set(value=str) 745 | await ctx.send(f"Your lockdown message has been changed to:\n `{str}`") 746 | 747 | @lockdownset.command(name="unlockmsg") 748 | async def unlockmsg(self, ctx: commands.Context, *, str=None): 749 | """ 750 | Sets the unlock message for your server 751 | """ 752 | guild = ctx.guild 753 | msg = await self.config.guild(guild).unlockdown_message() 754 | if msg is not None: 755 | await ctx.send(f"Your current message is {msg}") 756 | await self.config.guild(guild).unlockdown_message.set(value=str) 757 | await ctx.send(f"Your unlock message has been changed to:\n `{str}`") 758 | 759 | @lockdownset.command(name="setvc") 760 | async def vc_setter( 761 | self, 762 | ctx: commands.Context, 763 | vc_channel: Greedy[discord.VoiceChannel], 764 | ): 765 | """ 766 | Adds channel to list of voice chats to lock/unlock 767 | """ 768 | if not vc_channel: 769 | return await ctx.send_help() 770 | guild = ctx.guild 771 | vc_chans = await self.config.guild(guild).vc_channels() 772 | for chan in vc_channel: 773 | if chan.id not in vc_chans: 774 | vc_chans.append(chan.id) 775 | await self.config.guild(guild).vc_channels.set(vc_chans) 776 | 777 | await ctx.send(f"Added to the list") 778 | 779 | @lockdownset.command(name="setmusic") 780 | async def music_setter( 781 | self, 782 | ctx: commands.Context, 783 | vc_channel: Greedy[discord.VoiceChannel], 784 | ): 785 | """ 786 | Adds channel to list of Music channels to lock/unlock 787 | 788 | Music channels are treated with different perms on unlock (forcing negative overrides for @everyone role to speak) 789 | """ 790 | if not vc_channel: 791 | return await ctx.send_help() 792 | guild = ctx.guild 793 | music_chans = await self.config.guild(guild).music_channels() 794 | for chan in vc_channel: 795 | if chan.id not in music_chans: 796 | music_chans.append(chan.id) 797 | await self.config.guild(guild).music_channels.set(music_chans) 798 | 799 | await ctx.send(f"Added to the list") 800 | 801 | @lockdownset.command(name="rmvc") 802 | async def vc_remove( 803 | self, 804 | ctx: commands.Context, 805 | vc_channel: Greedy[int], 806 | ): 807 | """ 808 | Remove chat channel from list of voice chats 809 | 810 | Must use ID 811 | """ 812 | if not vc_channel: 813 | return await ctx.send_help() 814 | guild = ctx.guild 815 | vc_chans = await self.config.guild(guild).vc_channels() 816 | for chan in vc_channel: 817 | if chan in vc_chans: 818 | vc_chans.remove(chan) 819 | await self.config.guild(guild).vc_channels.set(vc_chans) 820 | await ctx.send(f"Removed from the list") 821 | 822 | @lockdownset.command(name="remusic") 823 | async def music_remove( 824 | self, 825 | ctx: commands.Context, 826 | vc_channel: Greedy[int], 827 | ): 828 | """ 829 | Remove channel from list of music chats 830 | 831 | Music channels are treated with different perms on unlock (forcing negative overrides for @everyone role to speak) 832 | """ 833 | if not vc_channel: 834 | return await ctx.send_help() 835 | guild = ctx.guild 836 | music_chans = await self.config.guild(guild).music_channels() 837 | for chan in vc_channel: 838 | if chan in music_chans: 839 | music_chans.remove(chan) 840 | await self.config.guild(guild).music_channels.set(music_chans) 841 | 842 | await ctx.send(f"Removed from the list") 843 | 844 | @lockdownset.command(name="notify") 845 | async def notify_channels(self, ctx: commands.Context, *, option: bool = True): 846 | """ 847 | Set whether to send channel notifications on lockdown and unlockdown to each effected channel 848 | """ 849 | guild = ctx.guild 850 | confirm = await self.config.guild(guild).send_alert() 851 | if not option: 852 | await self.config.guild(guild).send_alert.set(value=False) 853 | await ctx.send("Will silence the channel notifications on lockdown/unlockdown") 854 | return 855 | if confirm is True: 856 | await ctx.send( 857 | f"Currently you're set to send notification in channels that are locked/unlocked if there are messages set. To change this, run `{ctx.prefix}lockdownset notify false`" 858 | ) 859 | return 860 | await self.config.guild(guild).send_alert.set(value=True) 861 | await ctx.send( 862 | "Will now send notification in each channel effected for lockdown/unlockdown" 863 | ) 864 | 865 | @lockdownset.command(name="specrole") 866 | async def add_role(self, ctx: commands.Context, *, role: discord.Role): 867 | """ 868 | Add a role to lock from sending messages instead of the @everyone role 869 | Make sure to add the channels that are applicable 870 | """ 871 | if not role: 872 | return await ctx.send_help() 873 | 874 | def check(m): 875 | return m.author == ctx.author and m.channel == ctx.channel 876 | 877 | guild = ctx.guild 878 | nondefault = await self.config.guild(guild).nondefault() 879 | get_role = await self.config.guild(guild).secondary_role() 880 | if get_role: 881 | await ctx.send(f"You want to change <@&{get_role}> to {role.mention}? `[yes|no]`") 882 | try: 883 | confirm_change = await ctx.bot.wait_for("message", check=check, timeout=30) 884 | if confirm_change.content.lower() != "yes": 885 | return await ctx.send( 886 | f"Looks like we will keep <@&{get_role}> as your secondary role." 887 | ) 888 | except asyncio.TimeoutError: 889 | return await ctx.send("You took too long to reply!") 890 | 891 | await self.config.guild(guild).secondary_role.set(role.id) 892 | await ctx.send(f"Added {role.mention} to your configuration") 893 | spec_chans = await self.config.guild(guild).secondary_channels() 894 | if nondefault is False: 895 | if spec_chans: 896 | await self.config.guild(guild).nondefault.set(value=True) 897 | else: 898 | return await ctx.send( 899 | "Make sure you set up your channels for this role by doing `{}lds asc <..channels..>`".format( 900 | ctx.prefix 901 | ) 902 | ) 903 | 904 | async def voice_channel_lock( 905 | self, ctx: commands.Context, author: discord.Member, guild: discord.Guild 906 | ): 907 | """Lock function for voice/music channels""" 908 | voice_channels = await self.config.guild(guild).vc_channels() 909 | music_channels = await self.config.guild(guild).music_channels() 910 | if not voice_channels and not music_channels: 911 | return await ctx.send( 912 | f"You need to add some channels to your configuration using `{ctx.prefix}lds setvc|setmusic` to use this" 913 | ) 914 | role = guild.default_role 915 | if voice_channels is not None: 916 | for voice_channel in guild.channels: 917 | if voice_channel.id in voice_channels: 918 | overwrite = voice_channel.overwrites_for(role) 919 | overwrite.update(read_messages=True, connect=False, speak=False, stream=False) 920 | try: 921 | await voice_channel.set_permissions( 922 | role, 923 | overwrite=overwrite, 924 | reason="Locked down Voice Chats at request of {} ({})".format( 925 | author.name, author.id 926 | ), 927 | ) 928 | except discord.Forbidden: 929 | await self.loggerhook.send( 930 | guild=guild, 931 | error="You gotta give me permissions to manage {} so I can lock it properly".format( 932 | voice_channel.mention 933 | ), 934 | ) 935 | await ctx.send("Voice channels locked :mute:") 936 | 937 | # roll on with music channels for lock down 938 | def check(m): 939 | return m.author == ctx.author and m.channel == ctx.channel 940 | 941 | if music_channels: 942 | message = await ctx.send( 943 | "Detected music channels in your configuration, do you want to lock those too?" 944 | ) 945 | try: 946 | confirm_music_too = await ctx.bot.wait_for("message", check=check, timeout=30) 947 | if confirm_music_too.content.lower() != "yes": 948 | return await ctx.send("Okay, won't bother locking those") 949 | except asyncio.TimeoutError: 950 | return await ctx.send( 951 | "You took too long to reply, won't lock your music channels. You can lock those independently. Not sure why they're in your configuration in this case though." 952 | ) 953 | for voice_channel in guild.channels: 954 | if voice_channel.id in music_channels: 955 | overwrite = voice_channel.overwrites_for(role) 956 | overwrite.update(read_messages=True, connect=False, speak=False, stream=False) 957 | try: 958 | await voice_channel.set_permissions( 959 | role, 960 | overwrite=overwrite, 961 | reason="Locked down Music Channels at request of {} ({})".format( 962 | author.name, author.id 963 | ), 964 | ) 965 | except discord.Forbidden: 966 | await self.loggerhook.send( 967 | guild=guild, 968 | error="You gotta give me permissions to manage {} so I can lock it properly".format( 969 | voice_channel.mention 970 | ), 971 | ) 972 | await message.edit(content="Music Channels are locked, too.") 973 | 974 | async def voice_channel_unlock( 975 | self, ctx: commands.Context, author: discord.Member, guild: discord.Guild 976 | ): 977 | """Unlock function for voice/music channels""" 978 | voice_channels = await self.config.guild(guild).vc_channels() 979 | music_channels = await self.config.guild(guild).music_channels() 980 | if not voice_channels and not music_channels: 981 | return await ctx.send( 982 | f"You need to add some channels to your configuration using `{ctx.prefix}lds setvc|setmusic` to use this" 983 | ) 984 | role = guild.default_role 985 | if voice_channels is not None: 986 | for voice_channel in guild.channels: 987 | if voice_channel.id in voice_channels: 988 | overwrite = voice_channel.overwrites_for(role) 989 | overwrite.update(read_messages=None, connect=None, speak=None, stream=None) 990 | try: 991 | await voice_channel.set_permissions( 992 | role, 993 | overwrite=overwrite, 994 | reason="Unlocked Voice Chats at request of {} ({})".format( 995 | author.name, author.id 996 | ), 997 | ) 998 | except discord.Forbidden: 999 | await self.loggerhook.send( 1000 | guild=guild, 1001 | error="You gotta give me permissions to manage {} so I can lock it properly".format( 1002 | voice_channel.mention 1003 | ), 1004 | ) 1005 | await ctx.send("Voice channels unlocked :speaker:") 1006 | 1007 | # roll on with music channels for lock down 1008 | def check(m): 1009 | return m.author == ctx.author and m.channel == ctx.channel 1010 | 1011 | if music_channels: 1012 | message = await ctx.send( 1013 | "Detected music channels in your configuration, do you want to unlock those too?" 1014 | ) 1015 | try: 1016 | confirm_music_too = await ctx.bot.wait_for("message", check=check, timeout=30) 1017 | if confirm_music_too.content.lower() != "yes": 1018 | return await ctx.send("Okay, won't bother unlocking those") 1019 | except asyncio.TimeoutError: 1020 | return await ctx.send( 1021 | "You took too long to reply, won't unlock your music channels. You can unlock those independently. Not sure why they're in your configuration in this case though." 1022 | ) 1023 | for voice_channel in guild.channels: 1024 | if voice_channel.id in music_channels: 1025 | overwrite = voice_channel.overwrites_for(role) 1026 | overwrite.update(read_messages=None, connect=None, speak=False, stream=False) 1027 | try: 1028 | await voice_channel.set_permissions( 1029 | role, 1030 | overwrite=overwrite, 1031 | reason="Unlocked Music Channels at request of {} ({})".format( 1032 | author.name, author.id 1033 | ), 1034 | ) 1035 | except discord.Forbidden: 1036 | await self.loggerhook.send( 1037 | guild=guild, 1038 | error="You gotta give me permissions to manage {} so I can unlock it properly".format( 1039 | voice_channel.mention 1040 | ), 1041 | ) 1042 | await message.edit(content="Music Channels are unlocked, too.") 1043 | 1044 | @checks.mod_or_permissions(manage_channels=True) 1045 | @commands.command() 1046 | @commands.guild_only() 1047 | async def lockvc(self, ctx: commands.Context): 1048 | """ 1049 | Locks all voice channels 1050 | """ 1051 | set_check = await self.config.guild(ctx.guild).vc_channels() 1052 | if not set_check: 1053 | return await ctx.send( 1054 | "You need to set the channels using `{}lds setvc `".format(ctx.prefix) 1055 | ) 1056 | guild = ctx.guild 1057 | author = ctx.author 1058 | await self.voice_channel_lock(ctx=ctx, guild=guild, author=author) 1059 | 1060 | @checks.mod_or_permissions(manage_channels=True) 1061 | @commands.command() 1062 | @commands.guild_only() 1063 | async def unlockvc(self, ctx: commands.Context): 1064 | """ 1065 | Unlocks all voice channels 1066 | """ 1067 | set_check = await self.config.guild(ctx.guild).vc_channels() 1068 | if not set_check: 1069 | return await ctx.send( 1070 | "You need to set the channels using `{}lds setvc `".format(ctx.prefix) 1071 | ) 1072 | guild = ctx.guild 1073 | author = ctx.author 1074 | 1075 | await self.voice_channel_unlock(ctx=ctx, guild=guild, author=author) 1076 | 1077 | @commands.command(name="lockit", aliases=["lockchan"]) 1078 | @checks.mod_or_permissions(manage_messages=True) 1079 | @checks.bot_has_permissions(manage_channels=True) 1080 | @commands.guild_only() 1081 | async def channellock( 1082 | self, 1083 | ctx: commands.Context, 1084 | channel: Union[discord.TextChannel, discord.VoiceChannel] = None, 1085 | ): 1086 | """Lock selected text/voice channel""" 1087 | author = ctx.author 1088 | role = ctx.guild.default_role 1089 | if channel is None: 1090 | channel = ctx.channel 1091 | 1092 | overwrite = channel.overwrites_for(role) 1093 | bot_overwrite = channel.overwrites_for(ctx.bot.user) 1094 | # Checking channel type 1095 | 1096 | if channel.type == discord.ChannelType.text: 1097 | if overwrite.send_messages is False: 1098 | return await ctx.send( 1099 | "{} is already locked. To unlock, please use `{}unlockit {}`".format( 1100 | channel.mention, ctx.prefix, channel.id 1101 | ) 1102 | ) 1103 | if not bot_overwrite.send_messages: 1104 | bot_overwrite.update(send_messages=True) 1105 | overwrite.update(send_messages=False) 1106 | elif channel.type == discord.ChannelType.voice: 1107 | if overwrite.connect is False: 1108 | return await ctx.send( 1109 | "{} is already locked. To unlock, please use `{}channelunlock {}`".format( 1110 | channel.mention, ctx.prefix, channel.id 1111 | ) 1112 | ) 1113 | overwrite.update(connect=False) 1114 | try: 1115 | await channel.set_permissions( 1116 | ctx.bot.user, 1117 | overwrite=bot_overwrite, 1118 | reason=f"Securing overrides for {ctx.bot.user.name}", 1119 | ) 1120 | await channel.set_permissions( 1121 | role, 1122 | overwrite=overwrite, 1123 | reason="Lockdown in effect. Requested by {} ({})".format(author.name, author.id), 1124 | ) 1125 | except discord.Forbidden: 1126 | return await ctx.send("Error: Bot doesn't have perms to adjust that channel.") 1127 | await ctx.send("Done. Locked {}".format(channel.mention)) 1128 | 1129 | @commands.command(name="unlockit", aliases=["ulockchan"]) 1130 | @checks.mod_or_permissions(manage_messages=True) 1131 | @checks.bot_has_permissions(manage_channels=True) 1132 | @commands.guild_only() 1133 | async def channelunlock( 1134 | self, 1135 | ctx: commands.Context, 1136 | channel: Union[discord.TextChannel, discord.VoiceChannel] = None, 1137 | ): 1138 | """Unlock selected text/voice channel""" 1139 | author = ctx.author 1140 | role = ctx.guild.default_role 1141 | if channel is None: 1142 | channel = ctx.channel 1143 | 1144 | overwrite = channel.overwrites_for(role) 1145 | 1146 | # Checking channel type 1147 | if channel.type == discord.ChannelType.text: 1148 | if overwrite.send_messages is None: 1149 | return await ctx.send( 1150 | "{} is already unlocked. To lock, please use `{}lockit {}`".format( 1151 | channel.mention, ctx.prefix, channel.id 1152 | ) 1153 | ) 1154 | overwrite.update(send_messages=None) 1155 | elif channel.type == discord.ChannelType.voice: 1156 | if overwrite.connect is None: 1157 | return await ctx.send( 1158 | "{} is already unlocked. To lock, please use `{}unlockit {}`".format( 1159 | channel.mention, ctx.prefix, channel.id 1160 | ) 1161 | ) 1162 | overwrite.update(connect=None) 1163 | 1164 | try: 1165 | await channel.set_permissions( 1166 | role, 1167 | overwrite=overwrite, 1168 | reason="Lockdown over. Requested by {} ({})".format(author.name, author.id), 1169 | ) 1170 | except discord.Forbidden: 1171 | return await ctx.send("Error: Bot doesn't have perms to adjust that channel.") 1172 | await ctx.send("Unlocked {}".format(channel.mention)) 1173 | --------------------------------------------------------------------------------