├── .flake8 ├── .gitattributes ├── .github └── workflows │ ├── codeql.yml │ ├── matchers │ └── check-json.json │ └── run-precommit.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── bible ├── __init__.py ├── bible.py ├── info.json └── utils.py ├── customhelp ├── __init__.py ├── abc.py ├── core │ ├── __init__.py │ ├── base_help.py │ ├── category.py │ ├── dpy_menus.py │ ├── utils.py │ └── views.py ├── customhelp.py ├── info.json ├── locales │ └── messages.pot └── themes │ ├── __init__.py │ ├── blocks.py │ ├── dank.py │ ├── danny.py │ ├── justcore.py │ ├── minimal.py │ ├── mix.py │ ├── nadeko.py │ └── twin.py ├── docs ├── Makefile ├── make.bat └── source │ ├── conf.py │ ├── customhelp.rst │ ├── customhelp.txt │ ├── images │ ├── chelp_create.png │ ├── chelp_show.png │ ├── edits.png │ ├── final_help.png │ ├── listthemes.png │ ├── myhelp.png │ └── raw_help.png │ └── index.rst ├── google ├── __init__.py ├── google.py ├── info.json ├── utils.py └── yandex.py ├── info.json ├── menubuttons ├── menu_new.py ├── menubuttons.py └── utils.py ├── noreplyping ├── __init__.py ├── info.json └── noreplyping.py ├── pyproject.toml ├── simpleweb ├── __init__.py ├── data │ ├── static │ │ └── main.css │ └── templates │ │ └── commands.jinja2 ├── info.json └── simpleweb.py ├── snake ├── __init__.py ├── game.py ├── info.json ├── snake.py └── utils.py ├── snipe ├── __init__.py ├── info.json └── snipe.py ├── speak ├── __init__.py ├── data │ ├── insult.txt │ └── sadme.txt ├── info.json └── speak.py ├── todo ├── __init__.py ├── info.json └── todo.py ├── typeracer ├── __init__.py ├── data │ ├── filtered.txt │ └── lorem.txt ├── info.json ├── single.py ├── speedevent.py ├── typerace.py └── utils.py └── weeb ├── __init__.py ├── data ├── owo.txt ├── uwu.txt └── xwx.txt ├── info.json └── weeb.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 99 3 | select = C,E,F,W 4 | # E203 whitespace before ':' 5 | # incompatible with Black 6 | # E501 line too long 7 | # This would be good to have to automatically check string length as Black doesn't do it. 8 | # However, this includes docstrings and Red's handling of command docstrings 9 | # sometimes requires long lines to be shown properly in help output. 10 | # E731 do not assign a lambda expression, use a def - while 11 | # while it's generally a bad practice, we do use these a bit with i18n stuff 12 | # W503 line break before binary operator 13 | # incompatible with Black 14 | ignore = E203, E501, E731, W503 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | 3 | # binary file excludsions 4 | *.png binary 5 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: "3 12 * * 5" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ python ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /.github/workflows/matchers/check-json.json: -------------------------------------------------------------------------------- 1 | { 2 | "__comment": "Credits to: https://github.com/home-assistant/core/blob/d32c364d7f9e138e0dd9363b34b3cb39f4afcd06/.github/workflows/matchers/check-json.json", 3 | "problemMatcher": [ 4 | { 5 | "owner": "check-json", 6 | "pattern": [ 7 | { 8 | "regexp": "^(.+):\\s(Failed to json decode\\s.+\\sline\\s(\\d+)\\scolumn\\s(\\d+).+)$", 9 | "file": 1, 10 | "message": 2, 11 | "line": 3, 12 | "column": 4 13 | } 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/run-precommit.yml: -------------------------------------------------------------------------------- 1 | name: Run pre-commit 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | run_precommit: 11 | name: Run pre-commit 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | # Checkout repository 16 | - uses: actions/checkout@v2 17 | 18 | # Setup Python and install pre-commit 19 | - uses: actions/setup-python@v2 20 | with: 21 | python-version: "3.11" 22 | - name: Install pre-commit 23 | run: | 24 | pip install -U pre-commit 25 | # Load cached pre-commit environment 26 | - name: set PY 27 | run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV 28 | - uses: actions/cache@v2 29 | with: 30 | path: ~/.cache/pre-commit 31 | key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} 32 | 33 | # Register problem matchers 34 | - name: Register problem matchers 35 | run: | 36 | echo "::add-matcher::.github/workflows/matchers/check-json.json" 37 | 38 | # Run pre-commit 39 | - name: Run pre-commit 40 | run: | 41 | pre-commit run --show-diff-on-failure --color=never --all-files --verbose 42 | -------------------------------------------------------------------------------- /.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 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 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 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3 3 | exclude: ^.stubs/ 4 | repos: 5 | - repo: https://github.com/psf/black 6 | rev: '24.4.2' 7 | hooks: 8 | - id: black 9 | 10 | - repo: https://github.com/pycqa/isort 11 | rev: '5.13.2' 12 | hooks: 13 | - id: isort 14 | 15 | - repo: https://github.com/pre-commit/pre-commit-hooks 16 | rev: v4.6.0 17 | hooks: 18 | # all files should end with an empty line (for one, it minimizes the diffs) 19 | - id: end-of-file-fixer 20 | # `.gitattributes` should technically already handle this 21 | # but autocrlf can result in local files keeping the CRLF 22 | # which is problematic for codespell 23 | - id: mixed-line-ending 24 | args: 25 | - "--fix=lf" 26 | 27 | # Trailing whitespace is evil 28 | - id: trailing-whitespace 29 | 30 | # Ensure that links to code on GitHub use the permalinks 31 | - id: check-vcs-permalinks 32 | 33 | # Syntax validation 34 | - id: check-ast 35 | - id: check-json 36 | - id: check-toml 37 | # can be switched to yamllint when this issue gets resolved: 38 | # https://github.com/adrienverge/yamllint/issues/238 39 | - id: check-yaml 40 | 41 | # JSON auto-formatter 42 | - id: pretty-format-json 43 | args: 44 | - "--autofix" 45 | - "--indent=4" 46 | - "--no-sort-keys" 47 | 48 | # Checks for git-related issues 49 | - id: check-case-conflict 50 | - id: check-merge-conflict 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 npc203 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Npc-Cogs V3 2 | 3 | [![Red-DiscordBot](https://img.shields.io/badge/Red--DiscordBot-V3-red.svg)](https://github.com/Cog-Creators/Red-DiscordBot) 4 | [![Discord.py](https://img.shields.io/badge/Discord.py-rewrite-blue.svg)](https://github.com/Rapptz/discord.py/tree/rewrite) 5 | [![Code Style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) 6 | 7 | A fun oriented list of Red-Cogs made for fun and stonks. 8 | Discord User: epic guy#0715 9 | Docs: https://npc-cogs.readthedocs.io/en/latest 10 | 11 | # Installation 12 | 13 | To add cogs from this repo to your instance, do these steps: 14 | 15 | - `[p]repo add npc-cogs https://github.com/npc203/npc-cogs` 16 | - `[p]cog install npc-cogs ` 17 | - `[p]load ` 18 | 19 | ## About Cogs 20 | 21 | | Cog | Status | Description | 22 | | ----------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 23 | | Bible | Alpha |
Get bible verses or get references for wordsPowered by biblegateway, this cog can get bible verses and also can reverse search by getting the references for the searched word
| 24 | | CustomHelp | Alpha |
A category themed custom helpKindly read https://npc-cogs.readthedocs.io/en/latest/customhelp.html on how to setup
| 25 | | Google | Alpha |
A google search cog with tons of functionsThis cog scrapes google to get results/reverse image search, cards, books, images, etc.. (siu3334 did a lotta work in this cog as well)
| 26 | | NoReplyPing | Beta |
Notifies in dms if a person replies to you but turned their ping off Made for the servers with extra modesty who turn their pings off and you miss their message
| 27 | | Speak | Alpha |
Speak as others or for yourselfThis uses webhooks to mimic the person's identity and speak what you type, it also can speak stuff for you (insults and sadme)
| 28 | | Todo | Alpha |
A todo cogA simple todo cog to remember your tasks
| 29 | | TypeRacer | Alpha |
Typing speed testTest your typing skills with this cog
| 30 | | Weeb | Alpha |
Bunch of Otaku emoticonsExpwess youw weebness using the bunch of wandom weeb emoticons UwU
| 31 | | Snipe | Alpha |
Multi Snipe for fun and non-profitBulk sniping to stab back those anti-sniping smart ass users
| 32 | | Snake | Beta |
A simple Snake GameThis is a classical snake game, uses dpy menus. Be fully aware of this cog spamming the channel ratelimit buckets
| 33 | 34 | ## Credits 35 | 36 | - Everyone who tested my cogs and helped me with the code. <3 37 | - Everyone who contributed to make this better. 38 | - Thank you Red community, you guys are awesome. 39 | 40 | # Contributing 41 | 42 | - Haven't set up pre-commit hooks yet, so if you want to contribute, please do it yourself. 43 | - Kindly follow the format of black with line-length = 99 and isort 44 | - This can be done by `pip install -U black isort` 45 | - Then run the below commands to auto format your code 46 | 47 | ```py 48 | black . 49 | isort . 50 | ``` 51 | -------------------------------------------------------------------------------- /bible/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | from redbot.core.bot import Red 5 | 6 | from .bible import Bible 7 | 8 | with open(Path(__file__).parent / "info.json") as fp: 9 | __red_end_user_data_statement__ = json.load(fp)["end_user_data_statement"] 10 | 11 | 12 | async def setup(bot: Red) -> None: 13 | await bot.add_cog(Bible(bot)) 14 | -------------------------------------------------------------------------------- /bible/bible.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import aiohttp 4 | import bs4 5 | import discord 6 | from html2text import html2text as h2t 7 | from redbot.core import commands 8 | from redbot.core.bot import Red 9 | from redbot.core.utils.chat_formatting import pagify 10 | from redbot.vendored.discord.ext import menus 11 | from redbot_ext_menus import ViewMenuPages 12 | 13 | from .utils import EmbedField, group_embed_fields 14 | 15 | 16 | class Bible(commands.Cog): 17 | """ 18 | Pull up biblical verses fast 19 | """ 20 | 21 | def __init__(self, bot: Red) -> None: 22 | self.bot = bot 23 | self.BASE_URL = "https://www.biblegateway.com" 24 | self.ver_re = re.compile(r"--?(?:V|v|ver|version)(?:=| )(\w+)") 25 | 26 | def parse_search(self, text, title, version, emb_color): 27 | fields = [] 28 | pages = [] 29 | for result in text.findAll("li", {"class": "bible-item"}): 30 | ref = result.find("a", {"class": "bible-item-title"}) 31 | name = ref.text 32 | value = result.find("div", {"class": "bible-item-text"}) 33 | value.find("div").decompose() 34 | # Change headers to markdown 35 | for h3 in value.find_all("h3"): 36 | h3.name = "b" 37 | fields.append( 38 | EmbedField( 39 | name, f"[{h2t(str(value))}]({self.BASE_URL+ref.get('href')})"[:1000], False 40 | ) 41 | ) 42 | 43 | raw = group_embed_fields(fields) 44 | size = len(raw) 45 | for i, group in enumerate(raw, 1): 46 | emb = discord.Embed(title="Search Results for " + title, colour=emb_color) 47 | emb.set_footer( 48 | text=f"Version: {version} | Powered by Biblegateway.com | Page {i}/{size}" 49 | ) 50 | for field in group: 51 | emb.add_field(**field._asdict()) 52 | pages.append(emb) 53 | 54 | return pages 55 | 56 | def parse_reference(self, text, full_chap, title, version, emb_color): 57 | # Remove cross references 58 | for sup in text.find_all("sup", {"class": "crossreference"}): 59 | sup.decompose() 60 | 61 | # Remove other hidden junk 62 | for div_class in ("footnotes", "crossrefs", "passage-other-trans", "full-chap-link"): 63 | if elements := text.find_all(class_=div_class): 64 | for ele in elements: 65 | if isinstance(ele, bs4.Tag): 66 | ele.decompose() 67 | 68 | # Change headers to markdown 69 | for h3 in text.find_all("h3"): 70 | h3.name = "b" 71 | for h4 in text.find_all("h4"): 72 | h4.name = "b" 73 | 74 | text = h2t(str(text)) 75 | pages = [] 76 | raw = list(pagify(text, page_length=4000)) 77 | size = len(raw) 78 | 79 | for i, page in enumerate(raw, 1): 80 | emb = discord.Embed(title=title, description=page, colour=emb_color) 81 | emb.url = full_chap 82 | emb.set_footer( 83 | text=f"Version: {version} | Powered by Biblegateway.com | Page {i}/{size}" 84 | ) 85 | pages.append(emb) 86 | return pages 87 | 88 | @commands.command() 89 | async def bible(self, ctx, *, query): 90 | """ 91 | Pull up bible verses or reverse search by querying a word and get all it's references 92 | 93 | 94 | Now supports version as well, look up to this site for available versions: https://www.biblegateway.com/versions 95 | 96 | Example: 97 | [p]bible revelation 1:1 98 | [p]bible gen 1:1 -v KJV 99 | [p]bible gen1:5 --version NKJV 100 | [p]bible test 101 | """ 102 | version = "NIV" 103 | if ver_match := self.ver_re.search(query): 104 | version = ver_match.group(1) 105 | query = self.ver_re.sub("", query).strip() 106 | 107 | if re.match(r"\w+(?: ?)\d+:\d+", query): 108 | url = "/passage/?search=" 109 | else: 110 | url = "/quicksearch/?quicksearch=" 111 | 112 | async with ctx.typing(): 113 | async with aiohttp.ClientSession() as session: 114 | async with session.get( 115 | self.BASE_URL + url + query + f"&version={version}" 116 | ) as resp: 117 | soup = bs4.BeautifulSoup(await resp.text(), "html.parser") 118 | 119 | # Reference search 120 | if text := soup.find("div", {"class": "passage-text"}): 121 | full_chap = soup.find("a", {"class": "full-chap-link"}) 122 | title = soup.find("div", {"class": "dropdown-display-text"}).text 123 | pages = self.parse_reference( 124 | text, 125 | (self.BASE_URL + full_chap.get("href")) if full_chap else None, 126 | title, 127 | version, 128 | emb_color=await ctx.embed_color(), 129 | ) 130 | 131 | # Word Search 132 | elif text := soup.find("div", {"class": "search-result-list"}): 133 | pages = self.parse_search( 134 | text, query, version, emb_color=await ctx.embed_color() 135 | ) 136 | # No result checks 137 | else: 138 | return await ctx.send( 139 | "**No results found**\n" 140 | "1) Kindly make sure the verse exists\n" 141 | "2) Use the format of `book chapter:verse-range`" 142 | ) 143 | 144 | menu = ViewMenuPages(Source(pages, per_page=1), clear_reactions_after=True) 145 | await menu.start(ctx) 146 | 147 | async def red_delete_data_for_user(self, *, requester, user_id: int) -> None: 148 | return 149 | 150 | 151 | class Source(menus.ListPageSource): 152 | async def format_page(self, menu, embeds): 153 | return embeds 154 | -------------------------------------------------------------------------------- /bible/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Bible", 3 | "short": "Pull up biblical verses fast", 4 | "description": "Powered by biblegateway, this cog can get bible verses", 5 | "end_user_data_statement": "This cog does not persistently store any data or metadata about users.", 6 | "install_msg": "OwO, You probably are a pious person, thanks for installing", 7 | "author": [ 8 | "epic guy" 9 | ], 10 | "required_cogs": {}, 11 | "requirements": [ 12 | "html2text", 13 | "beautifulsoup4", 14 | "git+https://github.com/npc203/redbot-ext-menus-views" 15 | ], 16 | "tags": [ 17 | "utility", 18 | "fun" 19 | ], 20 | "min_bot_version": "3.3.10", 21 | "hidden": false, 22 | "disabled": false, 23 | "type": "COG" 24 | } 25 | -------------------------------------------------------------------------------- /bible/utils.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from typing import List 3 | 4 | EmbedField = namedtuple("EmbedField", "name value inline") 5 | 6 | 7 | # Yoinked from the RedHelpFormatter https://github.com/Cog-Creators/Red-DiscordBot/blob/1fa76bf43f0df9eecf264c0f21dd3d3505d89d60/redbot/core/commands/help.py#L438-#L460 8 | def group_embed_fields(fields: List[EmbedField], max_chars=1000): 9 | curr_group = [] 10 | ret = [] 11 | current_count = 0 12 | 13 | for i, f in enumerate(fields): 14 | f_len = len(f.value) + len(f.name) 15 | 16 | # Commands start at the 1st index of fields, i < 2 is a hacky workaround for now 17 | if not current_count or f_len + current_count < max_chars or i < 2: 18 | current_count += f_len 19 | curr_group.append(f) 20 | elif curr_group: 21 | ret.append(curr_group) 22 | current_count = f_len 23 | curr_group = [f] 24 | else: 25 | if curr_group: 26 | ret.append(curr_group) 27 | 28 | return ret 29 | -------------------------------------------------------------------------------- /customhelp/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | from redbot.core.bot import Red 5 | 6 | from .customhelp import CustomHelp 7 | 8 | with open(Path(__file__).parent / "info.json") as fp: 9 | __red_end_user_data_statement__ = json.load(fp)["end_user_data_statement"] 10 | 11 | 12 | async def setup(bot: Red) -> None: 13 | cog = CustomHelp(bot) 14 | await bot.add_cog(cog) 15 | -------------------------------------------------------------------------------- /customhelp/abc.py: -------------------------------------------------------------------------------- 1 | from types import FunctionType 2 | 3 | 4 | class ThemesMeta: 5 | """This is the skeletal structure of any theme""" 6 | 7 | make_embeds: FunctionType 8 | get_default_tagline: FunctionType 9 | embed_template: FunctionType 10 | filter_categories: FunctionType 11 | send_pages: FunctionType 12 | 13 | help_filter_func: FunctionType 14 | 15 | get_category_help_mapping: FunctionType 16 | get_cog_help_mapping: FunctionType 17 | get_group_help_mapping: FunctionType 18 | 19 | # Would need to work on this, to make the class look like an ABC ie:ABC but incomplete interfaces. 20 | # https://stackoverflow.com/questions/61328355/prohibit-addition-of-new-methods-to-a-python-child-class 21 | # No themes can have helper methods cause "self" changes during monkey-patch, making them obselete 22 | def __init_subclass__(cls, *args, **kw): 23 | super().__init_subclass__(*args, **kw) 24 | ALL_FEATURES = ( 25 | "format_cog_help", 26 | "format_category_help", 27 | "format_bot_help", 28 | "format_command_help", 29 | ) 30 | # By inspecting `cls.__dict__` we pick all methods declared directly on the class 31 | for name, attr in cls.__dict__.items(): 32 | attr = getattr(cls, name) 33 | if not callable(attr) or name in ALL_FEATURES: 34 | continue 35 | else: 36 | # method not found in superclasses: 37 | raise TypeError( 38 | f"Method {name} defined in {cls.__name__} does not exist in superclasses" 39 | ) 40 | 41 | 42 | """ 43 | async def format_cog_help(self, ctx: Context, obj: commands.Cog, help_settings: HelpSettings): 44 | pass 45 | 46 | async def format_category_help(self, ctx: Context, obj: Category, help_settings: HelpSettings): 47 | pass 48 | 49 | async def format_bot_help(self, ctx: Context, help_settings: HelpSettings): 50 | pass 51 | 52 | async def format_command_help( 53 | self, ctx: Context, obj: commands.Command, help_settings: HelpSettings 54 | ): 55 | pass 56 | """ 57 | -------------------------------------------------------------------------------- /customhelp/core/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, List 2 | 3 | if TYPE_CHECKING: 4 | from customhelp.core.category import Arrow, Category 5 | 6 | 7 | class ArrowManager: 8 | def __init__(self): 9 | self.arrows: List[Arrow] = [] 10 | 11 | def append(self, arrow): 12 | self.arrows.append(arrow) 13 | 14 | def clear(self): 15 | self.arrows.clear() 16 | 17 | def __getitem__(self, name: str): 18 | for arrow in self.arrows: 19 | if arrow.name == name: 20 | return arrow 21 | raise RuntimeError(f"No arrow with name {name}") 22 | 23 | def __iter__(self): 24 | return iter(self.arrows) 25 | 26 | 27 | class CategoryManager: 28 | def __init__(self) -> None: 29 | self._list: List[Category] = [] 30 | 31 | @property 32 | def uncategorised(self): 33 | for category in self._list: 34 | if category.is_uncat: 35 | return category 36 | raise RuntimeError("Uncategorised category not set!") 37 | 38 | def get(self, name): 39 | return self._list[self.index(name)] 40 | 41 | # TODO remove redundant methods 42 | def clear(self): 43 | self._list.clear() 44 | 45 | def index(self, name): 46 | return self._list.index(name) 47 | 48 | def append(self, value): 49 | self._list.append(value) 50 | 51 | def __len__(self): 52 | return len(self._list) 53 | 54 | def __bool__(self): 55 | return bool(self._list) 56 | 57 | def __iter__(self): 58 | return iter(self._list) 59 | 60 | 61 | # Keeping all global vars in one place 62 | GLOBAL_CATEGORIES = CategoryManager() 63 | ARROWS = ArrowManager() 64 | -------------------------------------------------------------------------------- /customhelp/core/category.py: -------------------------------------------------------------------------------- 1 | from dataclasses import asdict, dataclass 2 | from typing import Optional 3 | 4 | import discord 5 | from redbot.core import commands 6 | 7 | from . import GLOBAL_CATEGORIES 8 | 9 | 10 | @dataclass 11 | class Category: 12 | name: str 13 | desc: str 14 | cogs: list 15 | is_uncat: bool = False 16 | reaction: Optional[str] = None 17 | long_desc: Optional[str] = None 18 | thumbnail: Optional[str] = None 19 | label: str = "" 20 | style: str = "primary" 21 | 22 | def __eq__(self, item): 23 | return item == self.name 24 | 25 | def __hash__(self) -> int: 26 | return hash(self.name) 27 | 28 | def to_dict(self) -> dict: 29 | return asdict(self) 30 | 31 | 32 | @dataclass(frozen=True) 33 | class Arrow: 34 | name: str 35 | emoji: str 36 | label: str 37 | style: discord.ButtonStyle 38 | 39 | def __eq__(self, item): 40 | return item == self.name 41 | 42 | def __getitem__(self, item): 43 | return getattr(self, item, None) 44 | 45 | def keys(self): 46 | return ("emoji", "label", "style") 47 | 48 | def items(self): 49 | return {key: getattr(self, key) for key in self.keys()} 50 | 51 | 52 | # Helpers 53 | def get_category(category: Optional[str]) -> Optional[Category]: 54 | if not category: 55 | return 56 | 57 | for x in GLOBAL_CATEGORIES: 58 | if x.name == category: 59 | return x 60 | 61 | 62 | class CategoryConvert(commands.Converter): 63 | async def convert(self, ctx, value: str): 64 | category = get_category(value) 65 | if category is not None: 66 | return category 67 | raise commands.BadArgument() 68 | -------------------------------------------------------------------------------- /customhelp/core/dpy_menus.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | from typing import Any, List, Optional, Union 5 | 6 | import discord 7 | from redbot.core.bot import Red 8 | from redbot.vendored.discord.ext import menus 9 | 10 | import customhelp.core.base_help as base_help 11 | 12 | from . import ARROWS, GLOBAL_CATEGORIES 13 | 14 | 15 | class BaseMenu(menus.Menu): 16 | def __init__( 17 | self, 18 | message: Optional[discord.Message] = None, 19 | *, 20 | hmenu: base_help.HybridMenus, 21 | ) -> None: 22 | super().__init__(message=message, timeout=hmenu.settings["timeout"]) 23 | self.use_reply = hmenu.settings["replies"] 24 | self.hmenu = hmenu 25 | 26 | self.message: discord.Message 27 | self.bot: Red 28 | 29 | async def send_initial_message(self, ctx, channel): 30 | page = self.hmenu.pages[0] 31 | kwargs = self.hmenu._get_kwargs_from_page(page) 32 | if self.use_reply: 33 | kwargs["reference"] = ctx.message.to_reference( 34 | fail_if_not_exists=False 35 | ) # sends message silently when message is deleted 36 | return await ctx.send(**kwargs, view=self.hmenu.menus[1]) 37 | 38 | async def start(self, ctx, channel=None, wait=False): 39 | await super().start(ctx, channel=channel, wait=wait) 40 | return self.message 41 | 42 | def reaction_check(self, payload): 43 | """Just extends the default reaction_check to use owner_ids""" 44 | if payload.message_id != self.message.id: 45 | return False 46 | if self.bot.owner_ids and payload.user_id not in (*self.bot.owner_ids, self._author_id): 47 | return False 48 | return payload.emoji in self.buttons 49 | 50 | 51 | async def react_page(category_obj, pages): 52 | async def action(menu: BaseMenu, payload): 53 | await menu.hmenu.category_react_action(menu.ctx, menu.message, category_obj.name) 54 | 55 | return menus.Button(category_obj.reaction, action) 56 | 57 | 58 | async def arrow_react(arrow_obj): 59 | async def action(menu: BaseMenu, payload): 60 | await menu.hmenu.arrow_emoji_button[arrow_obj.name](menu.message) 61 | 62 | return menus.Button(arrow_obj.emoji, action) 63 | 64 | 65 | async def home_react(home_emoji): 66 | async def action(menu: BaseMenu, payload): 67 | await menu.hmenu.home_page(menu.ctx, menu.message) 68 | 69 | return menus.Button(home_emoji, action) 70 | -------------------------------------------------------------------------------- /customhelp/core/utils.py: -------------------------------------------------------------------------------- 1 | # This contains a bunch of utils 2 | 3 | import re 4 | from typing import Optional 5 | 6 | from redbot.core.utils.chat_formatting import humanize_timedelta 7 | 8 | # From dpy server >.< 9 | EMOJI_REGEX = r"<(?Pa?):(?P[a-zA-Z0-9_]{2,32}):(?P[0-9]{18,22})>" 10 | # https://www.w3resource.com/python-exercises/re/python-re-exercise-42.php 11 | LINK_REGEX = r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+" 12 | 13 | 14 | def emoji_converter(bot, emoji) -> Optional[str]: 15 | """General emoji converter""" 16 | # TODO find a way to detect unicode emojis properly 17 | if not emoji: 18 | return 19 | if isinstance(emoji, int) or emoji.isdigit(): 20 | return bot.get_emoji(int(emoji)) 21 | emoji = emoji.strip() 22 | return emoji 23 | 24 | 25 | # Taken from the core help as well :) 26 | def shorten_line(a_line: str, thbnail=False) -> str: 27 | # TODO for now if thumbnail is present, 28 | # we'll just return the line as is 29 | accepted_len = 140 if thbnail else 70 30 | if len(a_line) < accepted_len: # embed max width needs to be lower 31 | return a_line 32 | 33 | final_word = "" 34 | for word in re.split("(?<=\\S) ", a_line): 35 | if len(final_word) + len(word) < accepted_len: 36 | final_word += word + " " 37 | else: 38 | break 39 | return final_word.rstrip() + " …" 40 | 41 | 42 | # Add permissions 43 | def get_perms(command): 44 | final_perms = "" 45 | neat_format = lambda x: " ".join(i.capitalize() for i in x.replace("_", " ").split()) 46 | 47 | user_perms = [] 48 | if perms := getattr(command.requires, "user_perms"): 49 | user_perms.extend(neat_format(i) for i, j in perms if j) 50 | if perms := command.requires.privilege_level: 51 | if perms.name != "NONE": 52 | user_perms.append(neat_format(perms.name)) 53 | 54 | if user_perms: 55 | final_perms += "User Permission(s): " + ", ".join(user_perms) + "\n" 56 | 57 | if perms := getattr(command.requires, "bot_perms"): 58 | if perms_list := ", ".join(neat_format(i) for i, j in perms if j): 59 | final_perms += "Bot Permission(s): " + perms_list 60 | 61 | return final_perms 62 | 63 | 64 | # Add cooldowns 65 | def get_cooldowns(command): 66 | cooldowns = [] 67 | if s := command._buckets._cooldown: 68 | txt = f"{s.rate} time{'s' if s.rate>1 else ''} in {humanize_timedelta(seconds=s.per)}" 69 | try: 70 | txt += f" per {s.type.name.capitalize()}" 71 | # This is to avoid custom bucketype erroring out stuff (eg:licenseinfo) 72 | except AttributeError: 73 | pass 74 | cooldowns.append(txt) 75 | 76 | if s := command._max_concurrency: 77 | cooldowns.append(f"Max concurrent uses: {s.number} per {s.per.name.capitalize()}") 78 | 79 | return cooldowns 80 | 81 | 82 | # Add aliases 83 | def get_aliases(command, original): 84 | if alias := list(command.aliases): 85 | if original in alias: 86 | alias.remove(original) 87 | alias.append(command.name) 88 | return alias 89 | 90 | 91 | async def get_category_page_mapper_chunk( 92 | formatter, get_pages, ctx, cat, help_settings, page_mapping 93 | ): 94 | if not get_pages: 95 | if cat_page := await formatter.format_category_help( 96 | ctx, cat, help_settings=help_settings, get_pages=True 97 | ): 98 | page_mapping[cat] = cat_page 99 | else: 100 | return False 101 | return True 102 | -------------------------------------------------------------------------------- /customhelp/core/views.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import logging 3 | from typing import TYPE_CHECKING, List, Optional 4 | 5 | import discord 6 | from redbot.core import commands 7 | 8 | if TYPE_CHECKING: 9 | import customhelp.core.base_help as base_help 10 | 11 | LOG = logging.getLogger("red.customhelp.core.views") 12 | 13 | 14 | class ComponentType(enum.IntEnum): 15 | MENU = 0 16 | ARROW = 1 17 | 18 | 19 | # PICKER MENUS (Stuff for selecting buttons, select etc) 20 | class MenuView(discord.ui.View): 21 | def __init__(self, uid, config, callback): 22 | super().__init__(timeout=120) 23 | self.uid = uid 24 | self.message: discord.Message 25 | self.update_callback = callback 26 | self.config = config 27 | self.values: List[Optional[str]] = [None, None] # menutype value,arrowtype value 28 | 29 | async def interaction_check(self, interaction: discord.Interaction) -> bool: 30 | if interaction.user.id == self.uid: # type:ignore 31 | return True 32 | else: 33 | await interaction.response.send_message( 34 | "You are not allowed to interact with this menu.", ephemeral=True 35 | ) 36 | return False 37 | 38 | @discord.ui.button(label="Accept", emoji="✅", style=discord.ButtonStyle.success, row=2) 39 | async def accept(self, interaction, button): 40 | if self.values.count(None) == len(self.values): 41 | return await self.message.edit(content="No value selected.") 42 | 43 | final_message = "" 44 | for ind, val in enumerate(self.values): 45 | name = ComponentType(ind).name 46 | if val: 47 | final_message += f"Selected {name.lower()}type: {val}\n" 48 | await getattr(self.config, name.lower() + "type").set(val.lower()) 49 | self.update_callback("settings", name.lower() + "type", val.lower()) 50 | 51 | await interaction.message.edit(content=final_message, view=None) 52 | 53 | self.stop() 54 | 55 | @discord.ui.button(label="Cancel", emoji="✖", style=discord.ButtonStyle.danger, row=2) 56 | async def cancel(self, interaction, button): 57 | await self.message.edit(content="Selection cancelled.", view=None) 58 | self.stop() 59 | 60 | async def on_timeout(self) -> None: 61 | await self.message.edit(content="Selection timed out.", view=None) 62 | 63 | 64 | class MenuPicker(discord.ui.Select): 65 | view: MenuView 66 | 67 | def __init__(self, menutype, options): 68 | self.menutype: ComponentType = menutype 69 | super().__init__( 70 | placeholder=f"Select {self.menutype.name.lower()}type", 71 | min_values=1, 72 | max_values=1, 73 | options=options, 74 | row=self.menutype, # HACKS 75 | ) 76 | 77 | async def callback(self, interaction: discord.Interaction): 78 | self.view.values[self.menutype] = self.values[0] 79 | await interaction.response.defer() 80 | 81 | 82 | # HELP MENU Interaction items 83 | class BaseInteractionMenu(discord.ui.View): 84 | def __init__(self, *, hmenu): 85 | self.hmenu: base_help.HybridMenus = hmenu 86 | super().__init__(timeout=hmenu.settings["timeout"]) 87 | 88 | def update_buttons(self): 89 | pass 90 | 91 | def _get_kwargs_from_page(self, value): 92 | if isinstance(value, dict): 93 | return value 94 | elif isinstance(value, str): 95 | return {"content": value, "embed": None} 96 | elif isinstance(value, discord.Embed): 97 | return {"embed": value, "content": None} 98 | return {} 99 | 100 | async def on_timeout(self): 101 | children = [] 102 | # Filter select bars and disable them 103 | for child in self.children: 104 | if isinstance(child, discord.ui.Select): 105 | child.disabled = True 106 | children.append(child) 107 | 108 | self.clear_items() 109 | for child in children: 110 | self.add_item(child) 111 | 112 | try: 113 | await self.message.edit(view=self) 114 | except discord.NotFound: # User unloaded the cog 115 | pass 116 | 117 | async def start( 118 | self, 119 | ctx: commands.Context, 120 | message: Optional[discord.Message] = None, 121 | ): 122 | if message is None: 123 | if self.hmenu.settings["replies"]: 124 | self.message = await ctx.send( 125 | **self._get_kwargs_from_page(self.hmenu.pages[0]), 126 | view=self, 127 | mention_author=False, 128 | reference=ctx.message.to_reference(fail_if_not_exists=False), 129 | ) 130 | else: 131 | self.message = await ctx.send( 132 | **self._get_kwargs_from_page(self.hmenu.pages[0]), view=self 133 | ) 134 | else: 135 | self.message = message 136 | self.ctx = ctx 137 | self.valid_ids = list(ctx.bot.owner_ids) # type: ignore 138 | self.valid_ids.append(ctx.author.id) 139 | 140 | async def interaction_check(self, interaction: discord.Interaction) -> bool: 141 | if interaction.user.id in self.valid_ids: 142 | return True 143 | else: 144 | await interaction.response.send_message( 145 | "You cannot use this help menu.", ephemeral=True 146 | ) 147 | return False 148 | 149 | 150 | class ReactButton(discord.ui.Button): 151 | view: BaseInteractionMenu 152 | 153 | def __init__(self, **kwargs): 154 | super().__init__(**kwargs) 155 | self.custom_id: str 156 | 157 | async def callback(self, interaction: discord.Interaction): 158 | await self.view.hmenu.category_react_action(self.view.ctx, interaction, self.custom_id) 159 | 160 | 161 | # Selection Bar 162 | class SelectMenuHelpBar(discord.ui.Select): 163 | view: BaseInteractionMenu 164 | 165 | def __init__(self, categories: List[discord.SelectOption]): 166 | super().__init__( 167 | placeholder="Select a category...", 168 | min_values=1, 169 | max_values=1, 170 | options=categories, 171 | row=0, 172 | ) 173 | 174 | async def callback(self, interaction: discord.Interaction): 175 | await self.view.hmenu.category_react_action(self.view.ctx, interaction, self.values[0]) 176 | 177 | 178 | class SelectArrowHelpBar(discord.ui.Select): 179 | view: BaseInteractionMenu 180 | 181 | def __init__(self, arrows: List[discord.SelectOption]): 182 | super().__init__( 183 | placeholder="Select an arrow...", 184 | min_values=1, 185 | max_values=1, 186 | options=arrows, 187 | ) 188 | 189 | async def callback(self, interaction): 190 | if self.values: 191 | if self.values[0] == "Home": 192 | await self.view.hmenu.category_react_action(self.view.ctx, interaction, "home") 193 | await self.view.hmenu.arrow_emoji_button[self.values[0]](interaction) 194 | -------------------------------------------------------------------------------- /customhelp/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CustomHelp", 3 | "short": "A custom help", 4 | "description": "This is customisable help, kindly read the readme of this cog, to set it up", 5 | "end_user_data_statement": "This cog does not persistently store any data or metadata about users.", 6 | "author": [ 7 | "epic guy", 8 | "PhenoM4n4n" 9 | ], 10 | "install_msg": "This is a BETA cog, so expect bugs and kindly report them to me in the cog support server\n The docs for this cog can be found here: https://npc-cogs.readthedocs.io/en/latest/", 11 | "requirements": [ 12 | "tabulate", 13 | "packaging" 14 | ], 15 | "tags": [ 16 | "help", 17 | "utility" 18 | ], 19 | "min_bot_version": "3.3.10", 20 | "type": "COG" 21 | } 22 | -------------------------------------------------------------------------------- /customhelp/locales/messages.pot: -------------------------------------------------------------------------------- 1 | # 2 | msgid "" 3 | msgstr "" 4 | "Project-Id-Version: PACKAGE VERSION\n" 5 | "POT-Creation-Date: 2021-02-15 00:33+0530\n" 6 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 7 | "Last-Translator: FULL NAME \n" 8 | "Language-Team: LANGUAGE \n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "Generated-By: redgettext 3.3\n" 13 | 14 | #: customhelp\customhelp.py:58 15 | #, docstring 16 | msgid "" 17 | "\n" 18 | " A custom customisable help for fun and profit\n" 19 | " " 20 | msgstr "" 21 | 22 | #: customhelp\customhelp.py:173 23 | #, docstring 24 | msgid "Configure your custom help" 25 | msgstr "" 26 | 27 | #: customhelp\customhelp.py:177 28 | #, docstring 29 | msgid "Short info about various themes" 30 | msgstr "" 31 | 32 | #: customhelp\customhelp.py:185 33 | #, docstring 34 | msgid "Auto categorise cogs based on it's tags" 35 | msgstr "" 36 | 37 | #: customhelp\customhelp.py:222 38 | #, docstring 39 | msgid "Show the current help settings" 40 | msgstr "" 41 | 42 | #: customhelp\customhelp.py:264 43 | #, docstring 44 | msgid "" 45 | "Set to toggle custom formatter or the default help formatter\n" 46 | "`[p]chelp set 0` to turn custom off \n" 47 | "`[p]chelp set 1` to turn it on" 48 | msgstr "" 49 | 50 | #: customhelp\customhelp.py:281 51 | #, docstring 52 | msgid "Create a new category to add cogs to it using yaml" 53 | msgstr "" 54 | 55 | #: customhelp\customhelp.py:375 56 | #, docstring 57 | msgid "Add reactions and descriptions to the category" 58 | msgstr "" 59 | 60 | #: customhelp\customhelp.py:473 61 | #, docstring 62 | msgid "Show the list of categories and the cogs in them" 63 | msgstr "" 64 | 65 | #: customhelp\customhelp.py:482 66 | msgid "Set Categories:\n" 67 | msgstr "" 68 | 69 | #: customhelp\customhelp.py:482 70 | msgid "Set Category:\n" 71 | msgstr "" 72 | 73 | #: customhelp\customhelp.py:498 74 | #, docstring 75 | msgid "" 76 | "Load another preset theme.\n" 77 | "Use `[p]chelp load all` to load everything from that theme" 78 | msgstr "" 79 | 80 | #: customhelp\customhelp.py:537 81 | #, docstring 82 | msgid "" 83 | "Resets all settings to default **custom** help \n" 84 | " use `[p]chelp set 0` to revert back to the old help" 85 | msgstr "" 86 | 87 | #: customhelp\customhelp.py:555 88 | #, docstring 89 | msgid "Hard reset, clear everything" 90 | msgstr "" 91 | 92 | #: customhelp\customhelp.py:578 93 | #, docstring 94 | msgid "Unloads the given feature, this will reset to default" 95 | msgstr "" 96 | 97 | #: customhelp\customhelp.py:600 98 | #, docstring 99 | msgid "Remove categories/cogs or everything" 100 | msgstr "" 101 | 102 | #: customhelp\customhelp.py:604 103 | #, docstring 104 | msgid "This will delete all the categories" 105 | msgstr "" 106 | 107 | #: customhelp\customhelp.py:628 108 | #, docstring 109 | msgid "Remove a single category" 110 | msgstr "" 111 | 112 | #: customhelp\customhelp.py:647 113 | #, docstring 114 | msgid "Remove a cog from a category" 115 | msgstr "" 116 | 117 | #: customhelp\customhelp.py:668 118 | #, docstring 119 | msgid "Change various help settings" 120 | msgstr "" 121 | 122 | #: customhelp\customhelp.py:672 123 | #, docstring 124 | msgid "Toggles adding reaction for navigation." 125 | msgstr "" 126 | 127 | #: customhelp\customhelp.py:679 128 | #, docstring 129 | msgid "" 130 | "Set your thumbnail image here.\n" 131 | " use `[p]chelp settings thumbnail` to reset this" 132 | msgstr "" 133 | 134 | #: customhelp\customhelp.py:694 135 | #, docstring 136 | msgid "Add categories to nsfw, only displayed in nsfw channels" 137 | msgstr "" 138 | 139 | #: customhelp\customhelp.py:698 140 | #, docstring 141 | msgid "Add categories to the nsfw list" 142 | msgstr "" 143 | 144 | #: customhelp\customhelp.py:716 145 | #, docstring 146 | msgid "Remove categories from the nsfw list" 147 | msgstr "" 148 | 149 | #: customhelp\customhelp.py:732 150 | #, docstring 151 | msgid "Add categories to dev, only displayed to the bot owner(s)" 152 | msgstr "" 153 | 154 | #: customhelp\customhelp.py:736 155 | #, docstring 156 | msgid "Add categories to the dev list" 157 | msgstr "" 158 | 159 | #: customhelp\customhelp.py:754 160 | #, docstring 161 | msgid "Remove categories from the dev list" 162 | msgstr "" 163 | 164 | #: customhelp\customhelp.py:770 165 | #, docstring 166 | msgid "List the themes and available features" 167 | msgstr "" 168 | 169 | #: customhelp\customhelp.py:789 170 | #, docstring 171 | msgid "Get the category where the command is present" 172 | msgstr "" 173 | -------------------------------------------------------------------------------- /customhelp/themes/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from importlib import import_module 3 | from inspect import isclass 4 | from pkgutil import iter_modules 5 | 6 | from ..abc import ThemesMeta 7 | 8 | list = {} 9 | 10 | # This auto populates the list with the themes present in this folder 11 | pkg_dir = os.path.dirname(__file__) 12 | for module_loader, name, ispkg in iter_modules([pkg_dir]): 13 | theme_module = import_module(f"{__name__}.{name}") 14 | for attribute in dir(theme_module): 15 | attr = getattr(theme_module, attribute) 16 | if isclass(attr) and issubclass(attr, ThemesMeta) and attr is not ThemesMeta: 17 | list[name] = attr 18 | -------------------------------------------------------------------------------- /customhelp/themes/blocks.py: -------------------------------------------------------------------------------- 1 | from redbot.core.utils.chat_formatting import box 2 | from tabulate import tabulate 3 | 4 | from ..abc import ThemesMeta 5 | from ..core.base_help import ( 6 | EMPTY_STRING, 7 | Category, 8 | Context, 9 | EmbedField, 10 | HelpSettings, 11 | commands, 12 | pagify, 13 | ) 14 | 15 | grouper = lambda a, n: [a[k : k + n] for k in range(0, len(a), n)] 16 | 17 | 18 | class Blocks(ThemesMeta): 19 | """Max's Suggestion to add something new I believe >_>""" 20 | 21 | async def format_category_help( 22 | self, 23 | ctx: Context, 24 | obj: Category, 25 | help_settings: HelpSettings, 26 | get_pages: bool = False, 27 | **kwargs, 28 | ): 29 | coms = await self.get_category_help_mapping( 30 | ctx, obj, help_settings=help_settings, **kwargs 31 | ) 32 | if not coms: 33 | return 34 | all_cog_text = [] 35 | 36 | for cog_name, data in coms: 37 | all_cog_text.extend(map(lambda x: ctx.clean_prefix + x, data.keys())) 38 | 39 | all_cog_str = tabulate( 40 | grouper(all_cog_text, 3), 41 | tablefmt="plain", 42 | ) 43 | 44 | if await ctx.embed_requested(): 45 | emb = await self.embed_template(help_settings, ctx) 46 | emb["thumbnail"] = obj.thumbnail 47 | emb["embed"]["title"] = ( 48 | (str(obj.reaction) if obj.reaction else "") + " " + obj.name.capitalize() 49 | ) 50 | 51 | if description := obj.long_desc: 52 | emb["embed"]["description"] = f"{description[:250]}" 53 | 54 | for page in pagify(all_cog_str, page_length=998, shorten_by=0): 55 | field = EmbedField( 56 | EMPTY_STRING, 57 | box(page), 58 | False, 59 | ) 60 | emb["fields"].append(field) 61 | 62 | pages = await self.make_embeds(ctx, emb, help_settings=help_settings) 63 | if get_pages: 64 | return pages 65 | else: 66 | await self.send_pages( 67 | ctx, 68 | pages, 69 | embed=True, 70 | help_settings=help_settings, 71 | ) 72 | else: 73 | await self.send_pages( 74 | ctx, 75 | list(map(box, pagify(all_cog_str, shorten_by=0, page_length=2042))), 76 | embed=False, 77 | help_settings=help_settings, 78 | ) 79 | 80 | async def format_cog_help(self, ctx: Context, obj: commands.Cog, help_settings: HelpSettings): 81 | coms = await self.get_cog_help_mapping(ctx, obj, help_settings=help_settings) 82 | if not (coms or help_settings.verify_exists): 83 | return 84 | 85 | description = obj.format_help_for_context(ctx) 86 | 87 | cmd_list = tabulate( 88 | grouper(list(map(lambda x: ctx.clean_prefix + x, sorted(coms.keys()))), 3), 89 | tablefmt="plain", 90 | ) 91 | 92 | if await ctx.embed_requested(): 93 | emb = await self.embed_template(help_settings, ctx) 94 | if description: 95 | emb["embed"]["description"] = "**" + description + "**" 96 | if coms: 97 | for page in pagify(cmd_list, page_length=998, shorten_by=0): 98 | emb["fields"].append(EmbedField(EMPTY_STRING, box(page), False)) 99 | 100 | pages = await self.make_embeds(ctx, emb, help_settings=help_settings) 101 | await self.send_pages( 102 | ctx, 103 | pages, 104 | embed=True, 105 | help_settings=help_settings, 106 | ) 107 | else: 108 | await self.send_pages( 109 | ctx, 110 | list(map(box, pagify(cmd_list, shorten_by=0, page_length=2042))), 111 | embed=False, 112 | help_settings=help_settings, 113 | ) 114 | -------------------------------------------------------------------------------- /customhelp/themes/dank.py: -------------------------------------------------------------------------------- 1 | from ..abc import ThemesMeta 2 | from ..core.base_help import ( 3 | EMPTY_STRING, 4 | GLOBAL_CATEGORIES, 5 | Category, 6 | Context, 7 | EmbedField, 8 | HelpSettings, 9 | _, 10 | cast, 11 | commands, 12 | get_aliases, 13 | get_category_page_mapper_chunk, 14 | get_cooldowns, 15 | get_perms, 16 | pagify, 17 | ) 18 | 19 | 20 | class DankHelp(ThemesMeta): 21 | """Inspired from Dankmemer's help menu""" 22 | 23 | async def format_bot_help( 24 | self, ctx: Context, help_settings: HelpSettings, get_pages: bool = False 25 | ): 26 | if await ctx.embed_requested(): 27 | emb = await self.embed_template(help_settings, ctx) 28 | description = ctx.bot.description or "" 29 | emb["embed"]["description"] = description 30 | 31 | filtered_categories = await self.filter_categories(ctx, GLOBAL_CATEGORIES) 32 | page_mapping = {} 33 | 34 | # Maybe add category desc with long_desc somewhere? 35 | for cat in filtered_categories: 36 | if cat.cogs: 37 | if not await get_category_page_mapper_chunk( 38 | self, get_pages, ctx, cat, help_settings, page_mapping 39 | ): 40 | continue 41 | 42 | title = ( 43 | str(cat.reaction) + " " if cat.reaction else "" 44 | ) + cat.name.capitalize() 45 | 46 | emb["fields"].append( 47 | EmbedField( 48 | title, 49 | f"`{ctx.clean_prefix}help {cat.name}`\n{cat.long_desc if cat.long_desc else ''}", 50 | True, 51 | ) 52 | ) 53 | pages = await self.make_embeds(ctx, emb, help_settings=help_settings) 54 | if get_pages: 55 | return pages 56 | else: 57 | await self.send_pages( 58 | ctx, 59 | pages, 60 | embed=True, 61 | help_settings=help_settings, 62 | page_mapping=page_mapping, 63 | ) 64 | else: 65 | await ctx.send(_("You need to enable embeds to use the help menu")) 66 | 67 | async def format_category_help( 68 | self, 69 | ctx: Context, 70 | obj: Category, 71 | help_settings: HelpSettings, 72 | get_pages: bool = False, 73 | **kwargs, 74 | ): 75 | coms = await self.get_category_help_mapping( 76 | ctx, obj, help_settings=help_settings, **kwargs 77 | ) 78 | if not coms: 79 | return 80 | 81 | if await ctx.embed_requested(): 82 | emb = await self.embed_template(help_settings, ctx) 83 | emb["thumbnail"] = obj.thumbnail 84 | emb["embed"]["title"] = ( 85 | (str(obj.reaction) if obj.reaction else "") + " " + obj.name.capitalize() 86 | ) 87 | if description := obj.long_desc: 88 | emb["embed"]["description"] = f"{description[:250]}" 89 | 90 | all_cog_text = [ 91 | ", ".join(f"`{name}`" for name, command in sorted(data.items())) 92 | for cog_name, data in coms 93 | ] 94 | 95 | all_cog_text = ", ".join(all_cog_text) 96 | for i, page in enumerate( 97 | pagify(all_cog_text, page_length=1000, delims=[","], shorten_by=0) 98 | ): 99 | field = EmbedField( 100 | EMPTY_STRING, 101 | page[1:] if page.startswith(",") else page, 102 | False, 103 | ) 104 | emb["fields"].append(field) 105 | 106 | pages = await self.make_embeds(ctx, emb, help_settings=help_settings) 107 | if get_pages: 108 | return pages 109 | else: 110 | await self.send_pages( 111 | ctx, 112 | pages, 113 | embed=True, 114 | help_settings=help_settings, 115 | ) 116 | else: 117 | await ctx.send(_("You need to enable embeds to use the help menu")) 118 | 119 | async def format_command_help( 120 | self, ctx: Context, obj: commands.Command, help_settings: HelpSettings 121 | ): 122 | send = help_settings.verify_exists 123 | if not send: 124 | async for __ in self.help_filter_func( 125 | ctx, (obj,), bypass_hidden=True, help_settings=help_settings 126 | ): 127 | send = True 128 | 129 | if not send: 130 | return 131 | 132 | command = obj 133 | signature = _("`{ctx.clean_prefix}{command.qualified_name} {command.signature}`").format( 134 | ctx=ctx, command=command 135 | ) 136 | subcommands = None 137 | 138 | if hasattr(command, "all_commands"): 139 | grp = cast(commands.Group, command) 140 | subcommands = await self.get_group_help_mapping(ctx, grp, help_settings=help_settings) 141 | 142 | if await ctx.embed_requested(): 143 | emb = await self.embed_template(help_settings, ctx) 144 | if description := command.description: 145 | emb["embed"]["title"] = f"{description[:250]}" 146 | 147 | command_help = command.format_help_for_context(ctx) 148 | if command_help: 149 | splitted = command_help.split("\n\n") 150 | name = splitted[0] 151 | value = "\n\n".join(splitted[1:]) 152 | emb["fields"].append(EmbedField("Description:", name[:250], False)) 153 | else: 154 | value = "" 155 | emb["fields"].append(EmbedField("Usage:", signature, False)) 156 | 157 | if aliases := get_aliases(command, ctx.invoked_with): 158 | emb["fields"].append(EmbedField("Aliases", ", ".join(aliases), False)) 159 | 160 | if final_perms := get_perms(command): 161 | emb["fields"].append(EmbedField("Permissions", final_perms, False)) 162 | 163 | if cooldowns := get_cooldowns(command): 164 | emb["fields"].append(EmbedField("Cooldowns:", "\n".join(cooldowns), False)) 165 | 166 | if value: 167 | emb["fields"].append(EmbedField("Full description:", value[:1024], False)) 168 | 169 | if subcommands: 170 | 171 | def shorten_line(a_line: str) -> str: 172 | if len(a_line) < 70: # embed max width needs to be lower 173 | return a_line 174 | return a_line[:67] + ".." 175 | 176 | subtext = "\n" + "\n".join( 177 | shorten_line(f"`{name:<15}:`{command.format_shortdoc_for_context(ctx)}") 178 | for name, command in sorted(subcommands.items()) 179 | ) 180 | for i, page in enumerate(pagify(subtext, page_length=500, shorten_by=0)): 181 | if i == 0: 182 | title = _("**__Subcommands:__**") 183 | else: 184 | title = _(EMPTY_STRING) 185 | field = EmbedField(title, page, False) 186 | emb["fields"].append(field) 187 | 188 | pages = await self.make_embeds(ctx, emb, help_settings=help_settings) 189 | await self.send_pages( 190 | ctx, 191 | pages, 192 | embed=True, 193 | help_settings=help_settings, 194 | ) 195 | else: 196 | await ctx.send(_("You need to enable embeds to use the help menu")) 197 | -------------------------------------------------------------------------------- /customhelp/themes/danny.py: -------------------------------------------------------------------------------- 1 | from ..abc import ThemesMeta 2 | from ..core.base_help import ( 3 | EMPTY_STRING, 4 | GLOBAL_CATEGORIES, 5 | Category, 6 | Context, 7 | EmbedField, 8 | HelpSettings, 9 | _, 10 | get_category_page_mapper_chunk, 11 | pagify, 12 | ) 13 | 14 | 15 | class DannyHelp(ThemesMeta): 16 | """Inspired from R.danny's help menu""" 17 | 18 | async def format_bot_help( 19 | self, ctx: Context, help_settings: HelpSettings, get_pages: bool = False 20 | ): 21 | if await ctx.embed_requested(): # Maybe redirect to non-embed minimal format 22 | emb = await self.embed_template(help_settings, ctx, ctx.bot.description) 23 | filtered_categories = await self.filter_categories(ctx, GLOBAL_CATEGORIES) 24 | page_mapping = {} 25 | for cat in filtered_categories: 26 | if cat.cogs: 27 | if not await get_category_page_mapper_chunk( 28 | self, get_pages, ctx, cat, help_settings, page_mapping 29 | ): 30 | continue 31 | 32 | cog_names = "`" + "` `".join(cat.cogs) + "`" if cat.cogs else "" 33 | for i, page in enumerate(pagify(cog_names, page_length=1000, shorten_by=0)): 34 | if i == 0: 35 | title = ( 36 | str(cat.reaction) if cat.reaction else "" 37 | ) + f"**{cat.name.capitalize()}:**" 38 | else: 39 | title = EMPTY_STRING 40 | emb["fields"].append(EmbedField(title, cog_names, True)) 41 | pages = await self.make_embeds(ctx, emb, help_settings=help_settings) 42 | if get_pages: 43 | return pages 44 | else: 45 | await self.send_pages( 46 | ctx, 47 | pages, 48 | embed=True, 49 | help_settings=help_settings, 50 | page_mapping=page_mapping, 51 | ) 52 | else: 53 | await ctx.send(_("You need to enable embeds to use the help menu")) 54 | 55 | async def format_category_help( 56 | self, 57 | ctx: Context, 58 | obj: Category, 59 | help_settings: HelpSettings, 60 | get_pages: bool = False, 61 | **kwargs, 62 | ): 63 | coms = await self.get_category_help_mapping( 64 | ctx, obj, help_settings=help_settings, **kwargs 65 | ) 66 | if not coms: 67 | return 68 | 69 | if await ctx.embed_requested(): 70 | emb = await self.embed_template(help_settings, ctx) 71 | emb["thumbnail"] = obj.thumbnail 72 | 73 | if description := obj.long_desc: 74 | emb["embed"]["title"] = f"{description[:250]}" 75 | for cog_name, data in coms: 76 | title = f"**{cog_name}**" if cog_name else _("**No Category:**") 77 | cog_text = " ".join((f"`{name}`") for name, command in sorted(data.items())) 78 | 79 | for page in pagify(cog_text, page_length=256, delims=[" "], shorten_by=0): 80 | field = EmbedField(title, page, True) 81 | emb["fields"].append(field) 82 | 83 | pages = await self.make_embeds(ctx, emb, help_settings=help_settings) 84 | if get_pages: 85 | return pages 86 | else: 87 | await self.send_pages( 88 | ctx, 89 | pages, 90 | embed=True, 91 | help_settings=help_settings, 92 | ) 93 | 94 | else: 95 | await ctx.send(_("You need to enable embeds to use the help menu")) 96 | -------------------------------------------------------------------------------- /customhelp/themes/justcore.py: -------------------------------------------------------------------------------- 1 | from packaging import version 2 | from redbot import __version__ 3 | from redbot.core.utils.chat_formatting import box, humanize_list, humanize_number 4 | 5 | from ..abc import ThemesMeta 6 | from ..core.base_help import ( 7 | Category, 8 | Context, 9 | EmbedField, 10 | HelpSettings, 11 | _, 12 | cast, 13 | commands, 14 | get_cooldowns, 15 | get_perms, 16 | pagify, 17 | shorten_line, 18 | ) 19 | 20 | 21 | class JustCore(ThemesMeta): 22 | """This is the raw core help, but with categories""" 23 | 24 | async def format_category_help( 25 | self, 26 | ctx: Context, 27 | obj: Category, 28 | help_settings: HelpSettings, 29 | get_pages: bool = False, 30 | **kwargs, 31 | ): 32 | coms = await self.get_category_help_mapping( 33 | ctx, obj, help_settings=help_settings, **kwargs 34 | ) 35 | if not coms: 36 | return 37 | 38 | if await ctx.embed_requested(): 39 | emb = await self.embed_template(help_settings, ctx) 40 | if description := obj.long_desc: 41 | emb["embed"]["title"] = f"{description[:250]}" 42 | 43 | for cog_name, data in coms: 44 | title = f"**__{cog_name}:__**" 45 | cog_text = "\n".join( 46 | shorten_line(f"**{name}** {command.format_shortdoc_for_context(ctx)}") 47 | for name, command in sorted(data.items()) 48 | ) 49 | 50 | for i, page in enumerate(pagify(cog_text, page_length=1000, shorten_by=0)): 51 | title = title if i < 1 else _("{title} (continued)").format(title=title) 52 | field = EmbedField(title, page, False) 53 | emb["fields"].append(field) 54 | 55 | pages = await self.make_embeds(ctx, emb, help_settings=help_settings) 56 | if get_pages: 57 | return pages 58 | else: 59 | await self.send_pages( 60 | ctx, 61 | pages, 62 | embed=True, 63 | help_settings=help_settings, 64 | ) 65 | 66 | else: 67 | await ctx.send(_("You need to enable embeds to use the help menu")) 68 | 69 | async def format_cog_help(self, ctx: Context, obj: commands.Cog, help_settings: HelpSettings): 70 | coms = await self.get_cog_help_mapping(ctx, obj, help_settings=help_settings) 71 | if not (coms or help_settings.verify_exists): 72 | return 73 | 74 | if await ctx.embed_requested(): 75 | emb = await self.embed_template(help_settings, ctx, obj.format_help_for_context(ctx)) 76 | 77 | if coms: 78 | command_text = "\n".join( 79 | shorten_line(f"**{name}** {command.format_shortdoc_for_context(ctx)}") 80 | for name, command in sorted(coms.items()) 81 | ) 82 | for i, page in enumerate(pagify(command_text, page_length=500, shorten_by=0)): 83 | if i == 0: 84 | title = _("**__Commands:__**") 85 | else: 86 | title = _("**__Commands:__** (continued)") 87 | field = EmbedField(title, page, False) 88 | emb["fields"].append(field) 89 | pages = await self.make_embeds(ctx, emb, help_settings=help_settings) 90 | await self.send_pages( 91 | ctx, 92 | pages, 93 | embed=True, 94 | help_settings=help_settings, 95 | ) 96 | else: 97 | await ctx.send(_("You need to enable embeds to use the help menu")) 98 | 99 | async def format_command_help( 100 | self, ctx: Context, obj: commands.Command, help_settings: HelpSettings 101 | ): 102 | send = help_settings.verify_exists 103 | if not send: 104 | async for __ in self.help_filter_func( 105 | ctx, (obj,), bypass_hidden=True, help_settings=help_settings 106 | ): 107 | send = True 108 | 109 | if not send: 110 | return 111 | 112 | command = obj 113 | 114 | signature = _( 115 | "Syntax: {ctx.clean_prefix}{command.qualified_name} {command.signature}" 116 | ).format(ctx=ctx, command=command) 117 | 118 | # Backward compatible. 119 | if version.parse(__version__) >= version.parse("3.4.6"): 120 | aliases = command.aliases 121 | if help_settings.show_aliases and aliases: 122 | alias_fmt = _("Aliases") if len(command.aliases) > 1 else _("Alias") 123 | aliases = sorted(aliases, key=len) 124 | 125 | a_counter = 0 126 | valid_alias_list = [] 127 | for alias in aliases: 128 | if (a_counter := a_counter + len(alias)) < 500: 129 | valid_alias_list.append(alias) 130 | else: 131 | break 132 | 133 | a_diff = len(aliases) - len(valid_alias_list) 134 | aliases_list = [ 135 | f"{ctx.clean_prefix}{command.parent.qualified_name + ' ' if command.parent else ''}{alias}" 136 | for alias in valid_alias_list 137 | ] 138 | if len(valid_alias_list) < 10: 139 | aliases_content = humanize_list(aliases_list) 140 | else: 141 | aliases_formatted_list = ", ".join(aliases_list) 142 | if a_diff > 1: 143 | aliases_content = _("{aliases} and {number} more aliases.").format( 144 | aliases=aliases_formatted_list, number=humanize_number(a_diff) 145 | ) 146 | else: 147 | aliases_content = _("{aliases} and one more alias.").format( 148 | aliases=aliases_formatted_list 149 | ) 150 | signature += f"\n{alias_fmt}: {aliases_content}" 151 | 152 | subcommands = None 153 | if hasattr(command, "all_commands"): 154 | grp = cast(commands.Group, command) 155 | subcommands = await self.get_group_help_mapping(ctx, grp, help_settings=help_settings) 156 | 157 | if await ctx.embed_requested(): 158 | emb = await self.embed_template( 159 | help_settings, ctx, command.format_help_for_context(ctx) 160 | ) 161 | if description := command.description: 162 | emb["embed"]["title"] = f"{description[:250]}" 163 | 164 | emb["embed"]["description"] = box(signature, "yml") 165 | 166 | if final_perms := get_perms(command): 167 | emb["fields"].append(EmbedField("Permissions", final_perms, False)) 168 | 169 | if cooldowns := get_cooldowns(command): 170 | emb["fields"].append(EmbedField("Cooldowns:", "\n".join(cooldowns), False)) 171 | 172 | if subcommands: 173 | 174 | def shorten_line(a_line: str) -> str: 175 | if len(a_line) < 70: # embed max width needs to be lower 176 | return a_line 177 | return a_line[:67] + "..." 178 | 179 | subtext = "\n".join( 180 | shorten_line(f"**{name}** {command.format_shortdoc_for_context(ctx)}") 181 | for name, command in sorted(subcommands.items()) 182 | ) 183 | for i, page in enumerate(pagify(subtext, page_length=500, shorten_by=0)): 184 | if i == 0: 185 | title = _("**__Subcommands:__**") 186 | else: 187 | title = _("**__Subcommands:__** (continued)") 188 | field = EmbedField(title, page, False) 189 | emb["fields"].append(field) 190 | pages = await self.make_embeds(ctx, emb, help_settings=help_settings) 191 | await self.send_pages( 192 | ctx, 193 | pages, 194 | embed=True, 195 | help_settings=help_settings, 196 | ) 197 | else: 198 | await ctx.send(_("You need to enable embeds to use the help menu")) 199 | -------------------------------------------------------------------------------- /customhelp/themes/minimal.py: -------------------------------------------------------------------------------- 1 | from ..abc import ThemesMeta 2 | from ..core.base_help import ( 3 | GLOBAL_CATEGORIES, 4 | Category, 5 | Context, 6 | HelpSettings, 7 | _, 8 | cast, 9 | chain, 10 | commands, 11 | get_aliases, 12 | get_category_page_mapper_chunk, 13 | get_cooldowns, 14 | get_perms, 15 | pagify, 16 | ) 17 | 18 | 19 | class MinimalHelp(ThemesMeta): 20 | """This is a no embed minimal theme for the simplistic people.\nThis won't use reactions.\nThanks OwO for design advices""" 21 | 22 | async def format_bot_help( 23 | self, 24 | ctx: Context, 25 | help_settings: HelpSettings, 26 | get_pages: bool = False, 27 | ): 28 | description = ctx.bot.description or "" 29 | tagline = (help_settings.tagline) or self.get_default_tagline(ctx) 30 | full_text = f"{description}\n\n{tagline}" 31 | 32 | filtered_categories = await self.filter_categories(ctx, GLOBAL_CATEGORIES) 33 | page_mapping = {} 34 | # Maybe add category desc somewhere? 35 | for cat in filtered_categories: 36 | if cat.cogs: 37 | if not await get_category_page_mapper_chunk( 38 | self, get_pages, ctx, cat, help_settings, page_mapping 39 | ): 40 | continue 41 | # TODO getting categories twice, remove sometime! 42 | coms = await self.get_category_help_mapping(ctx, cat, help_settings=help_settings) 43 | all_cog_text = [" · ".join(f"{name}" for name in data) for cogname, data in coms] 44 | all_cog_text = " · ".join(all_cog_text) 45 | full_text += f"\n\n__**{cat.name}**__: {all_cog_text}" 46 | text_no = list(pagify(full_text)) 47 | if get_pages: 48 | return text_no 49 | await self.send_pages( 50 | ctx, 51 | text_no, 52 | embed=False, 53 | help_settings=help_settings, 54 | page_mapping=page_mapping, 55 | ) 56 | 57 | async def format_category_help( 58 | self, 59 | ctx: Context, 60 | obj: Category, 61 | help_settings: HelpSettings, 62 | get_pages: bool = False, 63 | **kwargs, 64 | ): 65 | coms = await self.get_category_help_mapping( 66 | ctx, obj, help_settings=help_settings, **kwargs 67 | ) 68 | if not coms: 69 | return 70 | 71 | description = ctx.bot.description or "" 72 | tagline = (help_settings.tagline) or self.get_default_tagline(ctx) 73 | full_text = f"{description}\n\n{tagline}\n\n" 74 | 75 | spacer_list = chain(*(i[1].keys() for i in coms)) 76 | spacing = len(max(spacer_list, key=len)) 77 | for cogname, data in coms: 78 | full_text += "\n".join( 79 | f"`{name:<{spacing}}`:{command.format_shortdoc_for_context(ctx)}" 80 | for name, command in data.items() 81 | ) 82 | full_text += "\n" 83 | text_no = list(pagify(full_text)) 84 | if get_pages: 85 | return text_no 86 | await self.send_pages( 87 | ctx, 88 | text_no, 89 | embed=False, 90 | help_settings=help_settings, 91 | ) 92 | 93 | async def format_cog_help(self, ctx: Context, obj: commands.Cog, help_settings: HelpSettings): 94 | coms = await self.get_cog_help_mapping(ctx, obj, help_settings=help_settings) 95 | if not (coms or help_settings.verify_exists): 96 | return 97 | description = obj.format_help_for_context(ctx) or "" 98 | tagline = (help_settings.tagline) or self.get_default_tagline(ctx) 99 | full_text = f"{description}\n\n{tagline}\n\n" 100 | 101 | spacing = len(max(coms.keys(), key=len)) 102 | full_text += "\n".join( 103 | f"`{name:<{spacing}}:`{command.format_shortdoc_for_context(ctx)}" 104 | for name, command in sorted(coms.items()) 105 | ) 106 | pages = list(pagify(full_text)) 107 | await self.send_pages(ctx, pages, embed=False, help_settings=help_settings) 108 | 109 | async def format_command_help( 110 | self, ctx: Context, obj: commands.Command, help_settings: HelpSettings 111 | ): 112 | send = help_settings.verify_exists 113 | if not send: 114 | async for __ in self.help_filter_func( 115 | ctx, (obj,), bypass_hidden=True, help_settings=help_settings 116 | ): 117 | send = True 118 | 119 | if not send: 120 | return 121 | 122 | command = obj 123 | 124 | signature = _("`{ctx.clean_prefix}{command.qualified_name} {command.signature}`").format( 125 | ctx=ctx, command=command 126 | ) 127 | subcommands = None 128 | 129 | if hasattr(command, "all_commands"): 130 | grp = cast(commands.Group, command) 131 | subcommands = await self.get_group_help_mapping(ctx, grp, help_settings=help_settings) 132 | 133 | full_text = "" 134 | command_help = command.format_help_for_context(ctx) 135 | if command_help: 136 | splitted = command_help.split("\n\n") 137 | name = splitted[0] 138 | value = "\n".join(splitted[1:]) 139 | full_text += "**Usage:** `" + signature + "`\n" 140 | 141 | if aliases := get_aliases(command, ctx.invoked_with): 142 | full_text += "**Aliases:** " + ",".join(aliases) + "\n" 143 | 144 | if cooldowns := get_cooldowns(command): 145 | full_text += "**Cooldowns:** " + "\n".join(cooldowns) + "\n" 146 | 147 | if final_perms := get_perms(command): 148 | full_text += "**Permissions:**\n" + final_perms + "\n" 149 | 150 | if command_help: 151 | full_text += ( 152 | "**Description:**\n" + name + "\n" + (value + "\n" if value else "") + "\n" 153 | ) 154 | 155 | if subcommands: 156 | spacing = len(max(subcommands.keys(), key=len)) 157 | subtext = "\n" + "\n".join( 158 | f"`{name:<{spacing}}`:{command.format_shortdoc_for_context(ctx)}" 159 | for name, command in sorted(subcommands.items()) 160 | ) 161 | for i, page in enumerate(pagify(subtext, shorten_by=0)): 162 | if i == 0: 163 | title = _("**__Subcommands:__**") 164 | else: 165 | title = "" 166 | full_text += f"{title}{page}" 167 | text_no = list(pagify(full_text)) 168 | await self.send_pages( 169 | ctx, 170 | text_no, 171 | embed=False, 172 | help_settings=help_settings, 173 | ) 174 | -------------------------------------------------------------------------------- /customhelp/themes/mix.py: -------------------------------------------------------------------------------- 1 | from ..abc import ThemesMeta 2 | from ..core.base_help import ( 3 | EMPTY_STRING, 4 | GLOBAL_CATEGORIES, 5 | Category, 6 | Context, 7 | EmbedField, 8 | HelpSettings, 9 | _, 10 | chain, 11 | commands, 12 | get_category_page_mapper_chunk, 13 | pagify, 14 | shorten_line, 15 | ) 16 | 17 | 18 | class Mixture(ThemesMeta): 19 | """This is a mixture of other themes, a variant filling the lacking features of others""" 20 | 21 | async def format_bot_help( 22 | self, ctx: Context, help_settings: HelpSettings, get_pages: bool = False 23 | ): 24 | if await ctx.embed_requested(): 25 | emb = await self.embed_template(help_settings, ctx, ctx.bot.description) 26 | filtered_categories = await self.filter_categories(ctx, GLOBAL_CATEGORIES) 27 | page_mapping = {} 28 | for cat in filtered_categories: 29 | if cat.cogs: 30 | if not await get_category_page_mapper_chunk( 31 | self, get_pages, ctx, cat, help_settings, page_mapping 32 | ): 33 | continue 34 | coms = await self.get_category_help_mapping( 35 | ctx, cat, help_settings=help_settings 36 | ) 37 | commands_list = ", ".join( 38 | ", ".join(f"{name}" for name in data) for _, data in coms 39 | ) 40 | for i, page in enumerate( 41 | pagify(commands_list, page_length=1000, delims=[","], shorten_by=0) 42 | ): 43 | if i == 0: 44 | title = ( 45 | f"{cat.reaction} " if cat.reaction else "" 46 | ) + f"**{cat.name.capitalize()}:**" 47 | else: 48 | title = EMPTY_STRING 49 | emb["fields"].append(EmbedField(title, "> " + page, False)) 50 | pages = await self.make_embeds(ctx, emb, help_settings=help_settings) 51 | if get_pages: 52 | return pages 53 | else: 54 | await self.send_pages( 55 | ctx, 56 | pages, 57 | embed=True, 58 | help_settings=help_settings, 59 | page_mapping=page_mapping, 60 | ) 61 | else: 62 | await ctx.send(_("You need to enable embeds to use the help menu")) 63 | 64 | async def format_category_help( 65 | self, 66 | ctx: Context, 67 | obj: Category, 68 | help_settings: HelpSettings, 69 | get_pages: bool = False, 70 | **kwargs, 71 | ): 72 | coms = await self.get_category_help_mapping( 73 | ctx, obj, help_settings=help_settings, **kwargs 74 | ) 75 | if not coms: 76 | return 77 | if await ctx.embed_requested(): 78 | emb = await self.embed_template(help_settings, ctx) 79 | emb["thumbnail"] = obj.thumbnail 80 | 81 | if description := obj.long_desc: 82 | emb["embed"]["description"] = f"{description[:250]}" 83 | 84 | spacer_list = chain(*(i[1].keys() for i in coms)) 85 | spacing = len(max(spacer_list, key=len)) 86 | 87 | for cog_name, data in coms: 88 | title = f"**__{cog_name}:__**" 89 | 90 | cog_text = "\n" + "\n".join( 91 | shorten_line(f"`{name:<{spacing}}:`{command.format_shortdoc_for_context(ctx)}") 92 | for name, command in sorted(data.items()) 93 | ) 94 | for i, page in enumerate(pagify(cog_text, page_length=1000, shorten_by=0)): 95 | title = title if i < 1 else _("{title} (continued)").format(title=title) 96 | field = EmbedField(title, page, False) 97 | emb["fields"].append(field) 98 | 99 | pages = await self.make_embeds(ctx, emb, help_settings=help_settings) 100 | if get_pages: 101 | return pages 102 | else: 103 | await self.send_pages(ctx, pages, embed=True, help_settings=help_settings) 104 | else: 105 | await ctx.send(_("You need to enable embeds to use the help menu")) 106 | 107 | async def format_cog_help(self, ctx: Context, obj: commands.Cog, help_settings: HelpSettings): 108 | coms = await self.get_cog_help_mapping(ctx, obj, help_settings=help_settings) 109 | if not (coms or help_settings.verify_exists): 110 | return 111 | 112 | if await ctx.embed_requested(): 113 | emb = await self.embed_template(help_settings, ctx) 114 | 115 | if description := obj.format_help_for_context(ctx): 116 | emb["embed"]["description"] = "**" + description + "**" 117 | 118 | for name, command in sorted(coms.items()): 119 | emb["fields"].append( 120 | EmbedField(name, command.format_shortdoc_for_context(ctx) or "\N{ZWSP}", False) 121 | ) 122 | 123 | pages = await self.make_embeds(ctx, emb, help_settings=help_settings) 124 | await self.send_pages( 125 | ctx, 126 | pages, 127 | embed=True, 128 | help_settings=help_settings, 129 | ) 130 | else: 131 | await ctx.send(_("You need to enable embeds to use the help menu")) 132 | -------------------------------------------------------------------------------- /customhelp/themes/nadeko.py: -------------------------------------------------------------------------------- 1 | from redbot.core.utils.chat_formatting import box 2 | 3 | from ..abc import ThemesMeta 4 | from ..core.base_help import ( 5 | EMPTY_STRING, 6 | GLOBAL_CATEGORIES, 7 | Category, 8 | Context, 9 | EmbedField, 10 | HelpSettings, 11 | _, 12 | get_category_page_mapper_chunk, 13 | pagify, 14 | ) 15 | 16 | 17 | class NadekoHelp(ThemesMeta): 18 | """Inspired from Nadeko's help menu""" 19 | 20 | async def format_bot_help( 21 | self, ctx: Context, help_settings: HelpSettings, get_pages: bool = False 22 | ): 23 | if await ctx.embed_requested(): 24 | emb = await self.embed_template(help_settings, ctx, ctx.bot.description) 25 | filtered_categories = await self.filter_categories(ctx, GLOBAL_CATEGORIES) 26 | page_mapping = {} 27 | cat_titles = "" 28 | for cat in filtered_categories: 29 | if cat.cogs: 30 | if not await get_category_page_mapper_chunk( 31 | self, get_pages, ctx, cat, help_settings, page_mapping 32 | ): 33 | continue 34 | cat_titles += f"• {cat.name}\n" 35 | 36 | # TODO Dont be a moron trying to pagify this or do we? yes we do, lmao. 37 | for i, vals in enumerate(pagify(cat_titles, page_length=1000)): 38 | emb["fields"].append( 39 | EmbedField( 40 | (_("List of Categories") if i < 1 else EMPTY_STRING + " "), vals, False 41 | ) 42 | ) 43 | pages = await self.make_embeds(ctx, emb, help_settings=help_settings) 44 | if get_pages: 45 | return pages 46 | else: 47 | await self.send_pages( 48 | ctx, 49 | pages, 50 | embed=True, 51 | help_settings=help_settings, 52 | page_mapping=page_mapping, 53 | ) 54 | else: 55 | await ctx.send(_("You need to enable embeds to use the help menu")) 56 | 57 | async def format_category_help( 58 | self, 59 | ctx: Context, 60 | obj: Category, 61 | help_settings: HelpSettings, 62 | get_pages: bool = False, 63 | **kwargs, 64 | ): 65 | coms = await self.get_category_help_mapping( 66 | ctx, obj, help_settings=help_settings, **kwargs 67 | ) 68 | if not coms: 69 | return 70 | 71 | if await ctx.embed_requested(): 72 | emb = await self.embed_template(help_settings, ctx) 73 | emb["thumbnail"] = obj.thumbnail 74 | if description := obj.long_desc: 75 | emb["embed"]["description"] = f"{description[:250]}" 76 | 77 | for cog_name, data in coms: 78 | title = f"**{cog_name}**" if cog_name else _("**No Category:**") 79 | cog_text = [ 80 | f"{ctx.clean_prefix}{name:<15}{command.aliases}" 81 | for name, command in sorted(data.items()) 82 | ] 83 | # need to customize this 84 | for i, page in enumerate( 85 | (cog_text[n : n + 7] for n in range(0, len(cog_text), 7)) 86 | ): 87 | field = EmbedField( 88 | title if i < 1 else _("{title} (continued)").format(title=title), 89 | box("\n".join(page), lang="css"), 90 | True, 91 | ) 92 | emb["fields"].append(field) 93 | 94 | pages = await self.make_embeds(ctx, emb, help_settings=help_settings) 95 | if get_pages: 96 | return pages 97 | else: 98 | await self.send_pages(ctx, pages, embed=True, help_settings=help_settings) 99 | else: 100 | await ctx.send(_("You need to enable embeds to use the help menu")) 101 | -------------------------------------------------------------------------------- /customhelp/themes/twin.py: -------------------------------------------------------------------------------- 1 | from ..abc import ThemesMeta 2 | from ..core.base_help import ( 3 | EMPTY_STRING, 4 | GLOBAL_CATEGORIES, 5 | Category, 6 | Context, 7 | EmbedField, 8 | HelpSettings, 9 | _, 10 | get_category_page_mapper_chunk, 11 | pagify, 12 | ) 13 | 14 | 15 | class TwinHelp(ThemesMeta): 16 | """This help is designed by TwinShadow a.k.a TwinShadow#0666""" 17 | 18 | async def format_bot_help( 19 | self, ctx: Context, help_settings: HelpSettings, get_pages: bool = False 20 | ): 21 | if await ctx.embed_requested(): 22 | emb = await self.embed_template(help_settings, ctx, ctx.bot.description) 23 | filtered_categories = await self.filter_categories(ctx, GLOBAL_CATEGORIES) 24 | page_mapping = {} 25 | for cat in filtered_categories: 26 | if cat.cogs: 27 | if not await get_category_page_mapper_chunk( 28 | self, get_pages, ctx, cat, help_settings, page_mapping 29 | ): 30 | continue 31 | cog_names = "`" + "` `".join(cat.cogs) + "`" if cat.cogs else "" 32 | for i, page in enumerate(pagify(cog_names, page_length=1000, shorten_by=0)): 33 | if i == 0: 34 | title = ( 35 | cat.reaction and str(cat.reaction) or "" 36 | ) + f" __{cat.name.capitalize()}:__ " 37 | else: 38 | title = EMPTY_STRING 39 | emb["fields"].append(EmbedField(title, cog_names, False)) 40 | pages = await self.make_embeds(ctx, emb, help_settings=help_settings) 41 | if get_pages: 42 | return pages 43 | else: 44 | await self.send_pages( 45 | ctx, 46 | pages, 47 | embed=True, 48 | help_settings=help_settings, 49 | page_mapping=page_mapping, 50 | ) 51 | else: 52 | await ctx.send(_("You need to enable embeds to use the help menu")) 53 | 54 | async def format_category_help( 55 | self, 56 | ctx: Context, 57 | obj: Category, 58 | help_settings: HelpSettings, 59 | get_pages: bool = False, 60 | **kwargs, 61 | ): 62 | coms = await self.get_category_help_mapping( 63 | ctx, obj, help_settings=help_settings, **kwargs 64 | ) 65 | if not coms: 66 | return 67 | if await ctx.embed_requested(): 68 | emb = await self.embed_template(help_settings, ctx, obj.long_desc) 69 | emb["thumbnail"] = obj.thumbnail 70 | 71 | for cog_name, data in coms: 72 | title = f"__**{cog_name}**__" if cog_name else _("**No Category:**") 73 | cog_text = ", ".join(f"`{name}`" for name, command in sorted(data.items())) 74 | for i, page in enumerate( 75 | pagify(cog_text, page_length=1000, delims=[","], shorten_by=0) 76 | ): 77 | if i > 0: 78 | title = f"**{cog_name} (continued):**" 79 | field = EmbedField( 80 | title, page[1:] if page.startswith(",") else page, False 81 | ) # precision matters xd 82 | emb["fields"].append(field) 83 | 84 | pages = await self.make_embeds(ctx, emb, help_settings=help_settings) 85 | if get_pages: 86 | return pages 87 | else: 88 | await self.send_pages(ctx, pages, embed=True, help_settings=help_settings) 89 | else: 90 | await ctx.send(_("You need to enable embeds to use the help menu")) 91 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = "npc-cogs" 21 | copyright = "2021, npc203" 22 | author = "npc203" 23 | 24 | 25 | # -- General configuration --------------------------------------------------- 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be 28 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 29 | # ones. 30 | extensions = [] 31 | 32 | # Add any paths that contain templates here, relative to this directory. 33 | templates_path = ["_templates"] 34 | 35 | # List of patterns, relative to source directory, that match files and 36 | # directories to ignore when looking for source files. 37 | # This pattern also affects html_static_path and html_extra_path. 38 | exclude_patterns = [] 39 | 40 | 41 | # -- Options for HTML output ------------------------------------------------- 42 | 43 | # The theme to use for HTML and HTML Help pages. See the documentation for 44 | # a list of builtin themes. 45 | # 46 | html_theme = "default" 47 | 48 | # Add any paths that contain custom static files (such as style sheets) here, 49 | # relative to this directory. They are copied after the builtin static files, 50 | # so a file named "default.css" will overwrite the builtin "default.css". 51 | html_static_path = ["_static"] 52 | -------------------------------------------------------------------------------- /docs/source/customhelp.rst: -------------------------------------------------------------------------------- 1 | A Customisable Custom Help Cog For Red: 2 | ======================================= 3 | 4 | 5 | | Couldn't get a shorter title. Anyways, the cog introduces categories, meaning you can now bunch up cogs into one blob and give it a name,description and reaction. 6 | | 7 | | This cog is made cause I didn't like 30 help pages for my bot and I wanted to bunch my cogs. 8 | | 9 | | Use ``[p]chelp`` to see what can be customised and ``[p]chelp set`` for even more customisations. 10 | | 11 | | As an additional bonus, if you have the alias cog loaded, those aliases will also be retrieved. 12 | 13 | Setup 14 | ----- 15 | *Note: Use* ``[p]chelp toggle 1`` *to set your help to the custom help, else it'll remain as the normal one* 16 | 17 | 1. | Start by doing ``[p]chelp list`` to list all your cogs 18 | 19 | 2. | Pick the cogs you need to group into a category. 20 | | Then use ``[p]chelp create``, now add the catergorized cogs as shown below. 21 | 22 | .. note:: 23 | This command can be run as many times as needed and can load up cogs into existing categories as well. 24 | 25 | | This is the yaml syntax. 26 | 27 | .. code:: yaml 28 | 29 | category1: 30 | - Cog1 31 | - Cog2 32 | category2: 33 | - Cog3 34 | - Cog4 35 | 36 | | |create categories| 37 | 38 | 3. Congrats, you just bunched up cogs into categories. Now you can do 39 | ``[p]help `` to load the help of all those cogs in the category. 40 | 41 | 4. | Now ``[p]help`` should show those categories to be that much cooler! 42 | | |raw help| 43 | 44 | 5. | Yay! *But wait*, we need to fill in the blanks. 45 | | Use ``[p]chelp edit`` to add the everything you need to customise a category. 46 | | The format is simply: 47 | 48 | .. code:: yaml 49 | 50 | category: 51 | - name: new name (ONLY use this to rename! else this isn't necessary) 52 | - desc: new description 53 | - long_desc: long description 54 | - reaction: reaction emoji 55 | - thumbnail: url to thumbnail for the category 56 | - label: Label for category (For category and buttons) 57 | - style: ButtonStyle for category button (primary, secondary, success, danger) 58 | 59 | | Feel free to mix and match to your liking, as in example below: 60 | | |editz| 61 | 62 | 6. | Once you've edited everything, your help menu will look even more impressive! 63 | | |Default command| 64 | 65 | 7. *But wait, there's even more.* 66 | 67 | Themes 68 | ------ 69 | 70 | Introducing themes that were shamesslessly ripped off from other bots cause I'm bad at designing. 71 | 72 | 1. | ``[p]chelp listthemes`` 73 | | This will get all the themes and the features available in each of them. 74 | | |list themes| 75 | 76 | 2. | ``[p]chelp load `` 77 | | This will load the respective theme for a particular feature. 78 | 79 | .. note:: 80 | | You can use ``[p]chelp load all`` to load all the available feature in that theme. 81 | | You can also mix and match any theme. (You will not lose configured categories. <_<) 82 | 83 | | An example of ``[p]chelp load dank main`` is shown below: 84 | | |image5| 85 | 86 | 3. | ``[p]chelp show`` 87 | | This will show what themes are loaded, along with your current settings. 88 | | |image6| 89 | 90 | 4. | ``[p]chelp unload `` 91 | | Run this command to reset the given feature back to default. 92 | 93 | 5. | ``[p]chelp reset`` 94 | | This command will reset the themes to default. 95 | 96 | .. note:: 97 | This won't revert to the regular Red help menu, to do so use ``[p]chelp set 0`` 98 | 99 | 6. Whew, wait you thought we were done? *Or are we...* 100 | 101 | Category Configuration 102 | ---------------------- 103 | 104 | | Custom Help also has additional configuration available to hide categories in 105 | | certain circumstances, such as ``developer`` or even ``NSFW`` from public 106 | | view. 107 | 108 | 1. | ``[p]chelp dev`` 109 | | This will hide categories and only be visible by the bot owner. 110 | 111 | 2. | ``[p]chelp nsfw`` 112 | | This will hide categories to only be visible within NSFW-marked channels. 113 | 114 | 3. | ``[p]chelp auto`` 115 | | To make a pre-formatted list of categories, this will take tags from your installed cogs 116 | | and auto-generate a list for you to use in ``[p]chelp create``. 117 | 118 | 4. | ``[p]chelp info`` 119 | | This will provide a description of themes available. 120 | 121 | Custom Help Settings 122 | -------------------- 123 | 124 | | Additional settings in Custom Help can be configured via ``[p]chelp set``, including but not limited to 125 | | custom navigation, thumbnails, using replies, and more. 126 | 127 | 1. | ``[p]chelp set arrows`` 128 | | Custom Navigation, YAY! 129 | | If you feel the default arrow icons are boring and plain, and you want to spice up navigation, you're probably looking for this. (Supports custom emotes.) 130 | | When using custom emotes from servers, your bot must also have access to them to be used. 131 | | You can use the emote ID (``:some_emote:123456789123``), or use the emote itself in the following format: 132 | 133 | .. note:: 134 | The valid arrows are ``left``, ``right``, ``cross``, ``home``, ``force_left`` and ``force_right`` 135 | 136 | .. code-block:: yaml 137 | 138 | Example: 139 | left : 140 | - emoji: ↖️ 141 | - style: success (primary, secondary, success, danger) 142 | - label: 'text is cool' 143 | 144 | 2. | ``[p]chelp set thumbnail`` 145 | | If you ever wanted to add a little image on the top right of the embed, you can set the image 146 | | for the thumbnail with a valid link. 147 | 148 | .. note:: 149 | The link must be a direct image link, ending in GIF, JPG, or PNG. 150 | 151 | 3. | ``[p]chelp set timeout`` 152 | | This will change how long the reaction menu stays (in seconds) before being removed. 153 | 154 | 4. | ``[p]chelp set type`` 155 | | This command enables the owner to change the menutype/arrowtype to buttons, dropdowns or just reactions 156 | 157 | 5. | ``[p]chelp set usereply`` 158 | | This will have the bot reply to your message after using commands. 159 | 160 | 6. | ``[p]chelp set nav`` 161 | | This command allows to remove the arrows completely. Without the arrows, the user cannot navigate. 162 | | This setting was made cause of multiple user requests, use it at will. 163 | 164 | Additional Notes 165 | ---------------- 166 | 167 | - Don't be a moron trying to mix minimal theme (non-embed) with the other embed-based themes. 168 | 169 | - Use `[p]helpset pagecharlimit` to increase or decrease your page size, so as to add/subract more categories per page. 170 | 171 | - For my sanity, kindly disable menus if you are using the minimal theme. 172 | 173 | - A **Good Practice** is to have the category names all **lowercased** and the category description as **Camelcase**. 174 | 175 | - All the reactions and arrow emojis can be **custom** and even **animated**, you can even put the emoji ID (if you don't have nitro). 176 | 177 | - | Feel free to suggest new themes which you might want to see. Let me know if you think any part of the theme can be made better. 178 | | I'm available in the `Cog Support server `__. 179 | 180 | - If the owner of any bot feels that their theme needs to be removed from this cog, please inform me, I'll remove it. 181 | 182 | FAQ 183 | ---- 184 | 185 | 1. Reactions are not working, why?! 186 | 187 | 1. Your bot should have the react perms 188 | 2. ``[p]helpset usemenus 1`` (menus must be enabled) 189 | 190 | 2. Can I make my own theme in your cog? 191 | 192 | | Well you can just learn about the help formatter api. 193 | | If you really need categories as well then you can fork my repo, 194 | navigate to the themes folder, see how the themes are made and make a 195 | new file in that folder with your custom coded theme and load the cog. 196 | | Your theme should magically appear in the ``[p]chelp listthemes`` 197 | 198 | 3. Some of my reactions are vanishing? 199 | 200 | You probably have more than 14 categories. A message can only have 14 reactions from a bot at max (I think). 201 | This is a discord limitation and it's unhandled by the cog. 202 | 203 | Credits 204 | -------- 205 | - My heartfelt thanks to `OofChair `__ and `TwinShadow `__. 206 | Both of these amazing people did some major testing and contribution to the cog. 207 | - To everyone who patiently answered my noob coding questions. 208 | - To the other bots ``R.Danny``, ``Dankmemer``, ``Nadeko`` from which the theme designs were taken. 209 | - ``Pikachu's help menu`` from `Flare `__ 210 | which was the spark, that the idea of this cog isn't too far fetched 211 | - The whole Red community cause redbot is epic and the help\_formatter 212 | is God sent. 213 | - Special thanks to `Jackenmen `__ who 214 | solved most of the doubts that came during the development. 215 | 216 | .. |create categories| image:: images/chelp_create.png 217 | .. |raw help| image:: images/raw_help.png 218 | :width: 300 219 | .. |editz| image:: images/edits.png 220 | :width: 400 221 | .. |Default command| image:: images/final_help.png 222 | :width: 400 223 | .. |list themes| image:: images/listthemes.png 224 | .. |image5| image:: images/myhelp.png 225 | .. |image6| image:: images/chelp_show.png 226 | 227 | Changelogs 228 | ========== 229 | 230 | v1.1.0 231 | ------ 232 | 233 | ------------- 234 | Major Changes 235 | ------------- 236 | - Revamped the Uncategorised cog architecture, uncategorised is no longer a "special" config entry 237 | - Improved chelp create and edit 238 | - Better cog setup, loads/reload handling 239 | - Some QoL improvements on display 240 | 241 | ------------- 242 | Bug Fixes 243 | ------------- 244 | - Fixed [p]chelp nsfw add not working 245 | - Fixed [p]chelp reorder 246 | - Properly show up arrows when the page needs it 247 | 248 | v1.0.1 249 | ------ 250 | 251 | ------------- 252 | Major Changes 253 | ------------- 254 | - Removed slashtags and it's dependencies from the cog 255 | - Uncategorised can now be reordered to be placed anywhere using ``[p]chelp reorder`` 256 | - Support for dpy2 257 | - Addition of dropdowns, buttons, and reactions for both menus and arrows 258 | - Added support for custom thumbnails per category 259 | - Zero config read calls when [p]help is called, Additional internal caches for optimised page processing 260 | 261 | --------------- 262 | Command Changes 263 | --------------- 264 | - Changed ``chelp set`` to ``chelp toggle`` 265 | - ``chelp set`` is used as an alias for ``chelp settings`` 266 | - ``chelp set`` now has a new subcommand ``nav`` to toggle the navigation arrows 267 | - ``chelp show`` shows more info now 268 | - ``chelp set type`` for setting the menu and arrow type 269 | - chelp create and edit now have more arguments for button/dropdown customisability 270 | -------------------------------------------------------------------------------- /docs/source/customhelp.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/npc203/npc-cogs/4aceb14659cb145f7040ca357a39c09d9b01805e/docs/source/customhelp.txt -------------------------------------------------------------------------------- /docs/source/images/chelp_create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/npc203/npc-cogs/4aceb14659cb145f7040ca357a39c09d9b01805e/docs/source/images/chelp_create.png -------------------------------------------------------------------------------- /docs/source/images/chelp_show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/npc203/npc-cogs/4aceb14659cb145f7040ca357a39c09d9b01805e/docs/source/images/chelp_show.png -------------------------------------------------------------------------------- /docs/source/images/edits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/npc203/npc-cogs/4aceb14659cb145f7040ca357a39c09d9b01805e/docs/source/images/edits.png -------------------------------------------------------------------------------- /docs/source/images/final_help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/npc203/npc-cogs/4aceb14659cb145f7040ca357a39c09d9b01805e/docs/source/images/final_help.png -------------------------------------------------------------------------------- /docs/source/images/listthemes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/npc203/npc-cogs/4aceb14659cb145f7040ca357a39c09d9b01805e/docs/source/images/listthemes.png -------------------------------------------------------------------------------- /docs/source/images/myhelp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/npc203/npc-cogs/4aceb14659cb145f7040ca357a39c09d9b01805e/docs/source/images/myhelp.png -------------------------------------------------------------------------------- /docs/source/images/raw_help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/npc203/npc-cogs/4aceb14659cb145f7040ca357a39c09d9b01805e/docs/source/images/raw_help.png -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to npc-cogs's documentation! 2 | ==================================== 3 | 4 | These are cogs meant to be used with Red-Bot. The docs serve as a quick reference and a guide to setup/configure my cogs in your redbot instance 5 | 6 | Index 7 | ^^^^^ 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | customhelp 14 | 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`search` 21 | -------------------------------------------------------------------------------- /google/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | from redbot.core.bot import Red 5 | 6 | from .google import Google 7 | 8 | with open(Path(__file__).parent / "info.json") as fp: 9 | __red_end_user_data_statement__ = json.load(fp)["end_user_data_statement"] 10 | 11 | 12 | async def setup(bot: Red) -> None: 13 | await bot.add_cog(Google(bot)) 14 | -------------------------------------------------------------------------------- /google/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Google", 3 | "short": "A google search cog, with rich card results", 4 | "description": "This searches google and fetches results using a custom scraper. The reverse search supports replies as well", 5 | "end_user_data_statement": "This cog does not persistently store any data or metadata about users.", 6 | "install_msg": "Warning: This might get your ip 429'ed from google.com for a day or so at max. (Haven't gotten to that despite testing hard)\n Shoutout to siu3334 for making half the commands (autofill, doodle, books) lulz\nFixator helped me so much, with the query url and in many many other things.", 7 | "author": [ 8 | "epic guy", 9 | "ow0x", 10 | "fixator10" 11 | ], 12 | "required_cogs": {}, 13 | "requirements": [ 14 | "html2text", 15 | "beautifulsoup4", 16 | "js2py", 17 | "git+https://github.com/npc203/redbot-ext-menus-views" 18 | ], 19 | "tags": [ 20 | "fun", 21 | "utility", 22 | "internet" 23 | ], 24 | "min_bot_version": "3.3.10", 25 | "hidden": false, 26 | "disabled": false, 27 | "type": "COG" 28 | } 29 | -------------------------------------------------------------------------------- /google/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | import textwrap 3 | from collections import namedtuple 4 | 5 | import discord 6 | from html2text import html2text as h2t 7 | from redbot.core.utils.chat_formatting import pagify 8 | from redbot.vendored.discord.ext import menus 9 | from redbot_ext_menus import ViewMenuPages 10 | 11 | nsfwcheck = lambda ctx: (not ctx.guild) or ctx.channel.is_nsfw() 12 | 13 | s = namedtuple("searchres", "url title desc") 14 | 15 | 16 | def reply(ctx): 17 | # Helper reply grabber 18 | if hasattr(ctx.message, "reference") and ctx.message.reference is not None: 19 | msg = ctx.message.reference.resolved 20 | if isinstance(msg, discord.Message): 21 | return msg 22 | 23 | 24 | def get_url(msg_obj, check=False): 25 | # Helper get potential url, if check is True then returns none if nothing is found in embeds 26 | if msg_obj.embeds: 27 | emb = msg_obj.embeds[0].to_dict() 28 | if "image" in emb: 29 | return emb["image"]["url"] 30 | elif "thumbnail" in emb: 31 | return emb["thumbnail"]["url"] 32 | if msg_obj.attachments: 33 | return msg_obj.attachments[0].url 34 | else: 35 | return None if check else msg_obj.content.lstrip("<").rstrip(">") 36 | 37 | 38 | # TODO improve this? 39 | def check_url(url: str): 40 | # Helper function to check if valid url or not 41 | return url.startswith("http") and " " not in url 42 | 43 | 44 | def get_query(ctx, url): 45 | query = None 46 | if resp := reply(ctx): 47 | query = get_url(resp) 48 | 49 | # TODO More work on this to shorten code. 50 | if not query or not check_url(query): 51 | if query := get_url(ctx.message, check=True): 52 | pass 53 | elif url is None: 54 | return 55 | else: 56 | query = url.lstrip("<").rstrip(">") 57 | 58 | # Big brain url parsing 59 | if not check_url(query): 60 | return 61 | if not query or not query.startswith("http") or " " in query: 62 | return 63 | 64 | return query 65 | 66 | 67 | def get_card(soup, final, kwargs): 68 | """Getting cards if present, here started the pain""" 69 | # common card 70 | if card := soup.select_one("div.g.mnr-c.g-blk"): 71 | if desc := card.find("span", class_="hgKElc"): 72 | final.append(s(None, "Google Info Card:", h2t(str(desc)))) 73 | return 74 | # another webpull card: what is the language JetBrains made? TODO fix this, depends on too many classes as of now 75 | if card := soup.select("div.kp-blk.c2xzTb"): 76 | if head := card.select("div.Z0LcW.XcVN5d.AZCkJd"): 77 | if desc := card.find("div", class_="iKJnec"): 78 | final.append(s(None, f"Answer: {head.text}", h2t(str(desc)))) 79 | return 80 | 81 | # calculator card 82 | if card := soup.find("div", class_="tyYmIf"): 83 | if question := card.find("span", class_="vUGUtc"): 84 | if answer := card.find("span", class_="qv3Wpe"): 85 | tmp = h2t(str(question)).strip("\n") 86 | final.append(s(None, "Google Calculator:", f"**{tmp}** {h2t(str(answer))}")) 87 | return 88 | 89 | # sidepage card 90 | if card := soup.find("div", class_="osrp-blk"): 91 | if thumbnail := card.find("g-img", attrs={"data-lpage": True}): 92 | kwargs["thumbnail"] = thumbnail["data-lpage"] 93 | if title := card.find("div", class_=re.compile("ZxoDOe")): 94 | if desc := soup.find("div", class_=re.compile("qDOt0b|kno-rdesc")): 95 | if remove := desc.find(class_=re.compile("Uo8X3b")): 96 | remove.decompose() 97 | 98 | desc = textwrap.shorten(h2t(str(desc.span)), 1024, placeholder="...") + "\n" 99 | 100 | if more_info := soup.findAll("div", class_="Z1hOCe"): 101 | for thing in more_info: 102 | tmp = thing.findAll("span") 103 | if len(tmp) >= 2: 104 | desc2 = f"\n **{tmp[0].text}**`{tmp[1].text.lstrip(':')}`" 105 | # More jack advises :D 106 | MAX = 1024 107 | MAX_LEN = MAX - len(desc2) 108 | if len(desc) > MAX_LEN: 109 | desc = ( 110 | next( 111 | pagify( 112 | desc, 113 | delims=[" ", "\n"], 114 | page_length=MAX_LEN - 1, 115 | shorten_by=0, 116 | ) 117 | ) 118 | + "\N{HORIZONTAL ELLIPSIS}" 119 | ) 120 | desc = desc + desc2 121 | final.append( 122 | s( 123 | None, 124 | "Google Featured Card: " 125 | + h2t(str(title)).replace("\n\n", "\n").replace("#", ""), 126 | desc, 127 | ) 128 | ) 129 | return 130 | 131 | # time cards and unit conversions and moar-_- WORK ON THIS, THIS IS BAD STUFF 100 132 | if card := soup.find("div", class_="vk_c"): 133 | if conversion := card.findAll("div", class_="rpnBye"): 134 | if len(conversion) != 2: 135 | return 136 | tmp = tuple( 137 | map( 138 | lambda thing: ( 139 | thing.input["value"], 140 | thing.findAll("option", selected=True)[0].text, 141 | ), 142 | conversion, 143 | ) 144 | ) 145 | final.append( 146 | s( 147 | None, 148 | "Unit Conversion v1:", 149 | "`" + " ".join(tmp[0]) + " is equal to " + " ".join(tmp[1]) + "`", 150 | ) 151 | ) 152 | return 153 | elif card.find("div", "lu_map_section"): 154 | if img := re.search(r"\((.*)\)", h2t(str(card)).replace("\n", "")): 155 | kwargs["image"] = "https://www.google.com" + img[1] 156 | return 157 | else: 158 | # time card 159 | if tail := card.find("table", class_="d8WIHd"): 160 | tail.decompose() 161 | tmp = h2t(str(card)).replace("\n\n", "\n").split("\n") 162 | final.append(s(None, tmp[0], "\n".join(tmp[1:]))) 163 | return 164 | 165 | # translator cards 166 | if card := soup.find("div", class_="tw-src-ltr"): 167 | langs = soup.find("div", class_="pcCUmf") 168 | src_lang = "**" + langs.find("span", class_="source-language").text + "**" 169 | dest_lang = "**" + langs.find("span", class_="target-language").text + "**" 170 | final_text = "" 171 | if source := card.find("div", id="KnM9nf"): 172 | final_text += (src_lang + "\n`" + source.find("pre").text) + "`\n" 173 | if dest := card.find("div", id="kAz1tf"): 174 | final_text += dest_lang + "\n`" + dest.find("pre").text.strip("\n") + "`" 175 | final.append(s(None, "Google Translator", final_text)) 176 | return 177 | 178 | # Unit conversions 179 | if card := soup.find("div", class_="nRbRnb"): 180 | final_text = "\N{ZWSP}\n**" 181 | if source := card.find("div", class_="vk_sh c8Zgcf"): 182 | final_text += "`" + h2t(str(source)).strip("\n") 183 | if dest := card.find("div", class_="dDoNo ikb4Bb gsrt gzfeS"): 184 | final_text += " " + h2t(str(dest)).strip("\n") + "`**" 185 | if time := card.find("div", class_="hqAUc"): 186 | if remove := time.find("select"): 187 | remove.decompose() 188 | tmp = h2t(str(time)).replace("\n", " ").split("·") 189 | final_text += ( 190 | "\n" 191 | + (f"`{tmp[0].strip()}` ·{tmp[1]}" if len(tmp) == 2 else "·".join(tmp)) 192 | + "\n\N{ZWSP}" 193 | ) 194 | final.append(s(None, "Unit Conversion", final_text)) 195 | return 196 | 197 | # Definition cards - 198 | if card := soup.find("div", class_="KIy09e"): 199 | final_text = "" 200 | if word := card.find("div", class_="ya2TWb"): 201 | if sup := word.find("sup"): 202 | sup.decompose() 203 | final_text += "`" + word.text + "`" 204 | 205 | if pronounciate := card.find("div", class_="S23sjd"): 206 | final_text += " | " + pronounciate.text 207 | 208 | if type_ := card.find("span", class_="YrbPuc"): 209 | final_text += " | " + type_.text + "\n\n" 210 | 211 | if definition := card.find("div", class_="LTKOO sY7ric"): 212 | if remove_flex_row := definition.find(class_="bqVbBf jfFgAc CqMNyc"): 213 | remove_flex_row.decompose() 214 | 215 | for text in definition.findAll("span"): 216 | tmp = h2t(str(text)) 217 | if tmp.count("\n") < 5: 218 | final_text += "`" + tmp.strip("\n").replace("\n", " ") + "`" + "\n" 219 | 220 | final.append(s(None, "Definition", final_text)) 221 | return 222 | 223 | # single answer card 224 | if card := soup.find("div", class_="ayRjaf"): 225 | final.append( 226 | s( 227 | None, 228 | h2t(str(card.find("div", class_="zCubwf"))).replace("\n", ""), 229 | h2t(str(card.find("span").find("span"))).strip("\n") + "\n\N{ZWSP}", 230 | ) 231 | ) 232 | return 233 | # another single card? 234 | if card := soup.find("div", class_="sXLaOe"): 235 | final.append(s(None, "Single Answer Card:", card.text)) 236 | return 237 | 238 | 239 | # Dpy menus 240 | class Source(menus.ListPageSource): 241 | async def format_page(self, menu, embeds): 242 | return embeds 243 | 244 | 245 | # Thanks fixator https://github.com/fixator10/Fixator10-Cogs/blob/V3.leveler_abc/leveler/menus/top.py 246 | class ResultMenu(ViewMenuPages, inherit_buttons=False): 247 | def __init__(self, **kwargs): 248 | super().__init__( 249 | **kwargs, 250 | timeout=60, 251 | clear_reactions_after=True, 252 | delete_message_after=True, 253 | ) 254 | 255 | def _skip_double_triangle_buttons(self): 256 | return super()._skip_double_triangle_buttons() 257 | 258 | async def finalize(self, timed_out): 259 | if timed_out and self.delete_message_after: 260 | self.delete_message_after = False 261 | 262 | @menus.button( 263 | "\u23ee\ufe0f", 264 | position=menus.First(0), 265 | skip_if=_skip_double_triangle_buttons, 266 | ) 267 | async def go_to_first_page(self, payload): 268 | """go to the first page""" 269 | await self.show_page(0) 270 | 271 | @menus.button("\u2b05\ufe0f", position=menus.First(1)) 272 | async def go_to_previous_page(self, payload): 273 | """go to the previous page""" 274 | if self.current_page == 0: 275 | await self.show_page(self._source.get_max_pages() - 1) 276 | else: 277 | await self.show_checked_page(self.current_page - 1) 278 | 279 | @menus.button("\u27a1\ufe0f", position=menus.Last(0)) 280 | async def go_to_next_page(self, payload): 281 | """go to the next page""" 282 | if self.current_page == self._source.get_max_pages() - 1: 283 | await self.show_page(0) 284 | else: 285 | await self.show_checked_page(self.current_page + 1) 286 | 287 | @menus.button( 288 | "\u23ed\ufe0f", 289 | position=menus.Last(1), 290 | skip_if=_skip_double_triangle_buttons, 291 | ) 292 | async def go_to_last_page(self, payload): 293 | """go to the last page""" 294 | # The call here is safe because it's guarded by skip_if 295 | await self.show_page(self._source.get_max_pages() - 1) 296 | 297 | @menus.button("\N{CROSS MARK}", position=menus.First(2)) 298 | async def stop_pages(self, payload) -> None: 299 | self.stop() 300 | -------------------------------------------------------------------------------- /google/yandex.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import json 3 | import urllib 4 | 5 | import discord 6 | from bs4 import BeautifulSoup 7 | from redbot.core import commands 8 | 9 | from .utils import get_query 10 | 11 | 12 | class Yandex: 13 | @commands.group() 14 | async def yandex(self, ctx): 15 | """Yandex related search commands""" 16 | 17 | @yandex.command(name="reverse", aliases=["rev"]) 18 | async def yandex_reverse(self, ctx, *, url: str = None): 19 | """Attach or paste the url of an image to reverse search, or reply to a message which has the image/embed with the image""" 20 | 21 | if query := get_query(ctx, url): 22 | pass 23 | else: 24 | return await ctx.send_help() 25 | 26 | encoded = { 27 | "rpt": "imageview", 28 | "url": query, 29 | "format": "json", 30 | "request": { 31 | "blocks": [ 32 | {"block": "extra-content", "params": {}, "version": 2}, 33 | {"block": "i-global__params:ajax", "params": {}, "version": 2}, 34 | {"block": "suggest2-history", "params": {}, "version": 2}, 35 | {"block": "cbir-intent__image-link", "params": {}, "version": 2}, 36 | {"block": "content_type_search-by-image", "params": {}, "version": 2}, 37 | {"block": "serp-controller", "params": {}, "version": 2}, 38 | {"block": "cookies_ajax", "params": {}, "version": 2}, 39 | {"block": "advanced-search-block", "params": {}, "version": 2}, 40 | ], 41 | "metadata": { 42 | "bundles": {"lb": "n?O/G?b*G$"}, 43 | "assets": { 44 | "las": "justifier-height=1;thumb-underlay=1;justifier-setheight=1;fitimages-height=1;justifier-fitincuts=1;react-with-dom=1;720.0=1;616.0=1;6022a8.0=1;0e3c2c.0=1;464.0=1;da4144.0=1" 45 | }, 46 | "version": "0x32f8444edac", 47 | "extraContent": {"names": ["i-react-ajax-adapter"]}, 48 | }, 49 | }, 50 | } 51 | 52 | async with ctx.typing(): 53 | async with self.session.get( 54 | "https://yandex.com/images/search?" + urllib.parse.urlencode(encoded), 55 | headers=self.options, 56 | ) as resp: 57 | text = await resp.read() 58 | await ctx.send(text) 59 | redir_url = resp.url 60 | prep = functools.partial(self.yandex_reverse_search, text) 61 | result = await self.bot.loop.run_in_executor(None, prep) 62 | if result: 63 | result = json.loads(result)["tags"] 64 | emb = discord.Embed( 65 | title="Yandex Reverse Image Search", 66 | description=f"[`Cliek here to View in Browser`]({redir_url})\n", 67 | color=await ctx.embed_color(), 68 | ) 69 | emb.add_field( 70 | name="Results", 71 | value="\n".join( 72 | map(lambda x: f"[{x['text']}]({'https://yandex.com'+x['url']})", result) 73 | ), 74 | ) 75 | emb.set_footer(text="Powered by yandex") 76 | emb.set_thumbnail(url=query) 77 | await ctx.send(embed=emb) 78 | else: 79 | await ctx.send( 80 | embed=discord.Embed( 81 | title="Yandex Reverse Image Search", 82 | description="[`" + ("Nothing relevant found") + f"`]({redir_url})", 83 | color=await ctx.embed_color(), 84 | ).set_thumbnail(url=query) 85 | ) 86 | 87 | def yandex_reverse_search(self, text): 88 | soup = BeautifulSoup(text, features="html.parser") 89 | if sidebar := soup.find( 90 | "div", 91 | class_="cbir-search-by-image-page__section cbir-search-by-image-page__section_name_tags", 92 | ): 93 | if check := sidebar.find("div", {"data-state": True}): 94 | return check["data-state"] 95 | -------------------------------------------------------------------------------- /info.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": [ 3 | "epic guy#0715" 4 | ], 5 | "install_msg": "Thank you for installing my repo. If you need support, ping me in the Cog Support server. Have fun", 6 | "name": "npc-cogs", 7 | "short": "A decent bunch of cogs aimed for having fun", 8 | "description": "A simple set of cogs that I wrote for people to enjoy", 9 | "tags": [ 10 | "fun", 11 | "utility", 12 | "webhooks" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /menubuttons/menu_new.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import functools 3 | from typing import List, Union 4 | 5 | import discord 6 | from redbot.core import commands 7 | from redbot.core.utils.menus import start_adding_reactions 8 | 9 | 10 | class MenuMixin: 11 | def _get_emoji(self, button): # InteractionButton, can't type hint this ;c 12 | emoji_string = button.custom_id[len(self.custom_id) + 1 :] 13 | 14 | def send_with_buttons(self, button): 15 | pass 16 | 17 | def create_proper_controls(self, controls): 18 | """Returns a Dict[Arrow,function]""" 19 | 20 | async def new_button_menu( 21 | self, 22 | ctx: commands.Context, 23 | pages: Union[List[str], List[discord.Embed]], 24 | controls: dict, 25 | message: discord.Message = None, 26 | page: int = 0, 27 | timeout: float = 30.0, 28 | ): 29 | """Same red menu but supports buttons""" 30 | if not isinstance(pages[0], (discord.Embed, str)): 31 | raise RuntimeError("Pages must be of type discord.Embed or str") 32 | if not all(isinstance(x, discord.Embed) for x in pages) and not all( 33 | isinstance(x, str) for x in pages 34 | ): 35 | raise RuntimeError("All pages must be of the same type") 36 | for key, value in controls.items(): 37 | maybe_coro = value 38 | if isinstance(value, functools.partial): 39 | maybe_coro = value.func 40 | if not asyncio.iscoroutinefunction(maybe_coro): 41 | raise RuntimeError("Function must be a coroutine") 42 | current_page = pages[page] 43 | 44 | if not message: 45 | if isinstance(current_page, discord.Embed): 46 | message = await ctx.send(embed=current_page) 47 | else: 48 | message = await ctx.send(current_page) 49 | else: 50 | try: 51 | if isinstance(current_page, discord.Embed): 52 | await message.edit(embed=current_page) 53 | else: 54 | await message.edit(content=current_page) 55 | except discord.NotFound: 56 | return 57 | 58 | try: 59 | predicate = ( 60 | lambda payload: not ctx.author.bot 61 | and message.id == payload.message.id 62 | and payload.custom_id in tuple(controls.keys()) # TODO 63 | ) 64 | payload = await ctx.bot.wait_for( 65 | "button_interaction", check=predicate, timeout=timeout 66 | ) 67 | except asyncio.TimeoutError: 68 | if not ctx.me: 69 | return 70 | try: 71 | if message.channel.permissions_for(ctx.me).manage_messages: 72 | await message.clear_reactions() 73 | else: 74 | raise RuntimeError 75 | except (discord.Forbidden, RuntimeError): # cannot remove all reactions 76 | for key in controls.keys(): 77 | try: 78 | await message.remove_reaction(key, ctx.bot.user) 79 | except discord.Forbidden: 80 | return 81 | except discord.HTTPException: 82 | pass 83 | except discord.NotFound: 84 | return 85 | else: 86 | return await controls[payload.custom_id]( 87 | ctx, pages, controls, message, page, timeout, react.emoji 88 | ) 89 | -------------------------------------------------------------------------------- /menubuttons/menubuttons.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | import discord 5 | from redbot.core import commands 6 | from redbot.core.bot import Red 7 | from redbot.core.config import Config 8 | from redbot.core.utils import menus 9 | from redbot.core.utils.chat_formatting import pagify 10 | 11 | from .menu_new import MenuMixin 12 | from .utils import emoji_converter, parse_yaml, quick_emoji_converter 13 | 14 | log = logging.getLogger("red.npc-cogs.menubuttons") 15 | 16 | 17 | class MenuButtons(MenuMixin, commands.Cog): 18 | """ 19 | Red menus to buttons + support for custom menu emojis 20 | """ 21 | 22 | def __init__(self, bot: Red) -> None: 23 | self.bot = bot 24 | 25 | # Config things 26 | self.config: Config = Config.get_conf( 27 | self, 28 | identifier=32674893924237, 29 | force_registration=True, 30 | ) 31 | self.default_arrows = { 32 | "left": "\N{LEFTWARDS BLACK ARROW}\N{VARIATION SELECTOR-16}", 33 | "right": "\N{BLACK RIGHTWARDS ARROW}\N{VARIATION SELECTOR-16}", 34 | "cross": "\N{CROSS MARK}", 35 | } 36 | self.config.register_global( 37 | arrows=self.default_arrows, 38 | toggle=False, 39 | ) 40 | self.map_conf_arrows = { 41 | "left": menus.prev_page, 42 | "right": menus.next_page, 43 | "cross": menus.close_menu, 44 | } 45 | 46 | # Cache 47 | # self.conf_toggle = False 48 | 49 | # old menu stuff backup 50 | self.old_controls = menus.DEFAULT_CONTROLS.copy() 51 | self.old_menu = menus.menu 52 | 53 | # proper init stuff 54 | self._ready = asyncio.Event() 55 | self._init_task = None 56 | self._ready_raised = False 57 | 58 | # Oh jack, sweet jackenmen https://discord.com/channels/133049272517001216/160386989819035648/666985042136006670 59 | def create_init_task(self): 60 | def _done_callback(task): 61 | exc = task.exception() 62 | if exc is not None: 63 | log.error( 64 | "An unexpected error occurred during CogName's initialization.", exc_info=exc 65 | ) 66 | self._ready_raised = True 67 | self._ready.set() 68 | 69 | self._init_task = asyncio.create_task(self.initialize()) 70 | self._init_task.add_done_callback(_done_callback) 71 | 72 | async def initialize(self): 73 | # alternatively use wait_until_red_ready() if you need some stuff that happens in our post-connection startup 74 | await self.bot.wait_until_ready() 75 | toggle = await self.config.toggle() 76 | if toggle: 77 | menus.menu = self.new_button_menu 78 | await self.refresh_arrows() 79 | 80 | async def refresh_arrows(self): 81 | raw_arrows = await self.config.arrows() 82 | def_ctrls = menus.DEFAULT_CONTROLS 83 | def_ctrls.clear() 84 | 85 | for name, emoji in raw_arrows: 86 | if valid_emoji := quick_emoji_converter(self.bot, emoji): 87 | def_ctrls[valid_emoji] = self.map_conf_arrows[name] 88 | else: 89 | def_ctrls[self.default_arrows[name]] = self.map_conf_arrows[name] 90 | log.warn("The {} arrow emoji {} is not found by the bot".format(name, emoji)) 91 | 92 | def cog_unload(self): 93 | if self._init_task is not None: 94 | self._init_task.cancel() 95 | 96 | menus.menu = self.old_menu 97 | menus.DEFAULT_CONTROLS = self.old_controls 98 | 99 | async def cog_before_invoke(self, ctx): 100 | # use if commands need initialize() to finish 101 | async with ctx.typing(): 102 | await self._ready.wait() 103 | if self._ready_raised: 104 | await ctx.send( 105 | "There was an error during CogName's initialization. Check logs for more information." 106 | ) 107 | raise commands.CheckFailure() 108 | 109 | @commands.is_owner() 110 | @commands.group() 111 | async def buttons(self, ctx): 112 | """Base menubuttons command""" 113 | 114 | @buttons.command() 115 | async def toggle(self, ctx, toggle: bool): 116 | """Toggle between button menus and normal red ones""" 117 | await self.config.toggle.set(toggle) 118 | if toggle: 119 | menus.menu = self.new_button_menu 120 | else: 121 | menus.menu = self.old_menu 122 | await ctx.send(f"Sucessfully {'en' if toggle else 'dis'}abled button menus") 123 | 124 | @buttons.command() 125 | async def refresh(self, ctx): 126 | """Refresh the menu buttons, incase they don't show up""" 127 | await self.refresh_arrows() 128 | await ctx.tick() 129 | 130 | @buttons.command() 131 | async def show(self, ctx): 132 | """Show your current menu configuration""" 133 | emb = discord.Embed(title="Current red menu settings") 134 | await ctx.send(embed=emb) 135 | 136 | @buttons.command(aliases=["arrow"]) 137 | async def arrows(self, ctx, *, correct_txt=None): 138 | """Add custom arrows for fun and profit""" 139 | if correct_txt: 140 | content = correct_txt 141 | else: 142 | await ctx.send( 143 | "Your next message should be with the specfied format as follows(see docs for more info).\n" 144 | "**If you enter an invalid emoji your help will break.**\n" 145 | "Example:\n" 146 | "left :\n" 147 | " - emoji: ↖️\n" 148 | " - style: success\n" 149 | " - label: 'text is cool'\n" 150 | "Note: The other arrows are `right`,`cross`, `home`, `force_left` and `force_right`" 151 | ) 152 | try: 153 | msg = await self.bot.wait_for( 154 | "message", 155 | timeout=180, 156 | check=lambda m: m.author == ctx.author and m.channel == ctx.channel, 157 | ) 158 | content = msg.content 159 | except asyncio.TimeoutError: 160 | return await ctx.send("Timed out, please try again.") 161 | 162 | if not (yaml_data := await parse_yaml(ctx, content)): 163 | return 164 | 165 | already_present_emojis = [ 166 | quick_emoji_converter(self.bot, arrow) 167 | for arrow in menus.DEFAULT_CONTROLS.keys() 168 | if arrow 169 | ] 170 | 171 | parsed = {} 172 | failed = [] # [(reason for failure,arrow_name)] 173 | check = ("emoji", "label", "style") 174 | check_name = ( 175 | "left", 176 | "right", 177 | "cross", 178 | "home", 179 | ) # Maybe later add "force_right", "force_left" 180 | check_style = ["primary", "secondary", "success", "danger"] 181 | 182 | parsed_data = {} 183 | for k, v in yaml_data.items(): 184 | tmp = {} 185 | for val in v: 186 | final_key, final_val = val.popitem() 187 | tmp[final_key] = final_val 188 | parsed_data[k] = tmp 189 | 190 | for arrow, details in parsed_data.items(): 191 | if arrow not in check_name: 192 | failed.append(("Invalid arrow name", arrow)) 193 | else: 194 | parsed[arrow] = details 195 | 196 | # Junk 197 | remove_key = [] 198 | for key in details: 199 | if key not in check: 200 | failed.append(((key, "Invalid key"), arrow)) 201 | remove_key.append(key) 202 | for key in remove_key: 203 | details.pop(key) 204 | 205 | # Emoji verify 206 | if emoji := details.pop("emoji", None): 207 | converted = await emoji_converter(ctx, emoji) 208 | if converted: 209 | if emoji in already_present_emojis: 210 | failed.append((("emoji", "Emoji already present as arrow"), arrow)) 211 | else: 212 | parsed[arrow]["emoji"] = converted 213 | else: 214 | failed.append((("emoji", "Bot can't react this arrow"), arrow)) 215 | 216 | # ButtonStyle verify 217 | if style := details.pop("style", None): 218 | if style in check_style: 219 | parsed[arrow]["style"] = style 220 | else: 221 | failed.append((("button", "Invalid button style"), arrow)) 222 | 223 | async with self.config.arrows() as conf: 224 | for name, modified_values in parsed.items(): 225 | for arrow in conf: 226 | if arrow["name"] == name: 227 | arrow.update(modified_values) 228 | break 229 | 230 | for page in pagify( 231 | "Successfully added the edits" 232 | if not failed 233 | else "The following things failed:\n" 234 | + "\n".join( 235 | [ 236 | f"`{reason[0]}` failed in `{arrow}`, `Reason: {reason[1]}`" 237 | for reason, arrow in failed 238 | ] 239 | ) 240 | ): 241 | await ctx.send(page) 242 | await self.refresh_arrows() 243 | 244 | async def red_delete_data_for_user(self, *, requester, user_id: int) -> None: 245 | return 246 | -------------------------------------------------------------------------------- /menubuttons/utils.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | import discord 4 | import yaml 5 | from redbot.core.utils.chat_formatting import box 6 | 7 | 8 | @dataclass 9 | class Arrow: 10 | emoji: discord.Emoji 11 | label: str 12 | style: str 13 | 14 | 15 | async def emoji_converter(bot, emoji: discord.Emoji): 16 | pass 17 | 18 | 19 | def quick_emoji_converter(bot, emoji: str): 20 | pass 21 | 22 | 23 | async def parse_yaml(ctx, content): 24 | """Parse the yaml with basic structure checks""" 25 | # TODO make this as an util function? 26 | try: 27 | parsed_data = yaml.safe_load(content) 28 | except (yaml.parser.ParserError, yaml.constructor.ConstructorError): # type: ignore 29 | await ctx.send("Wrongly formatted") 30 | return 31 | except yaml.scanner.ScannerError as e: # type: ignore 32 | await ctx.send(box(str(e).replace("`", "\N{ZWSP}`"))) 33 | return 34 | if type(parsed_data) != dict: 35 | await ctx.send("Invalid Format, Missed a colon probably") 36 | return 37 | 38 | # TODO pls get a better type checking method 39 | for i in parsed_data: 40 | if type(parsed_data[i]) != list: 41 | await ctx.send("Invalid Format, Likely added unwanted spaces") 42 | return 43 | return parsed_data 44 | -------------------------------------------------------------------------------- /noreplyping/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | from redbot.core.bot import Red 5 | 6 | from .noreplyping import NoReplyPing 7 | 8 | with open(Path(__file__).parent / "info.json") as fp: 9 | __red_end_user_data_statement__ = json.load(fp)["end_user_data_statement"] 10 | 11 | 12 | async def setup(bot: Red) -> None: 13 | await bot.add_cog(NoReplyPing(bot)) 14 | -------------------------------------------------------------------------------- /noreplyping/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NoReplyPing", 3 | "short": "Track the people who reply but turned off their ping", 4 | "description": "Cog's meant to jump to message where you are replied with a ping off so that you might not have noticed ", 5 | "end_user_data_statement": "This cog stores uids for a toggle to notify people of reply ping or not", 6 | "install_msg": "Small utility to help people find things they need aha. It does wait for 15 seconds before dm'ing so as to not annoy inbetween chats", 7 | "author": [ 8 | "epic guy" 9 | ], 10 | "required_cogs": {}, 11 | "requirements": [], 12 | "tags": [ 13 | "utility" 14 | ], 15 | "min_bot_version": "3.3.10", 16 | "hidden": false, 17 | "disabled": false, 18 | "type": "COG" 19 | } 20 | -------------------------------------------------------------------------------- /noreplyping/noreplyping.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import datetime 3 | from collections import namedtuple 4 | 5 | import discord 6 | from redbot.core import commands 7 | from redbot.core.bot import Red 8 | from redbot.core.config import Config 9 | 10 | 11 | class NoReplyPing(commands.Cog): 12 | """ 13 | Track the people who reply but turned off their ping 14 | """ 15 | 16 | def __init__(self, bot: Red) -> None: 17 | self.bot = bot 18 | self.config = Config.get_conf( 19 | self, 20 | identifier=3423123, 21 | force_registration=True, 22 | ) 23 | self.fake_obj = namedtuple("FakeMessage", "guild") 24 | self.config.register_member(send_dms=False) 25 | 26 | @commands.Cog.listener() 27 | async def on_message_without_command(self, message: discord.Message): 28 | # Don't bother dms or bots 29 | if not message.guild or message.author.bot: 30 | return 31 | 32 | # Phen from AC https://discord.com/channels/133049272517001216/718148684629540905/825041973919744010 33 | if ref := message.reference: 34 | ref_message = ref.cached_message or ( 35 | ref.resolved 36 | if ref.resolved and isinstance(ref.resolved, discord.Message) 37 | else None 38 | ) 39 | if not ref_message and ref.message_id: 40 | ref_chan = message.guild.get_channel(ref.channel_id) 41 | if isinstance(ref_chan, discord.TextChannel): 42 | try: 43 | ref_message = await ref_chan.fetch_message(ref.message_id) 44 | except (discord.Forbidden, discord.NotFound): 45 | pass 46 | 47 | # Valid reply 48 | if ref_message: 49 | if any(member.id == ref_message.author.id for member in message.mentions): 50 | # User pinged them 51 | return 52 | else: 53 | if ref_message.author and ref_message.author.id != message.author.id: 54 | if await self.config.member_from_ids( 55 | message.guild.id, ref_message.author.id 56 | ).send_dms(): 57 | # wait for 60 seconds before sending dm, so as to not annoy when chatting 58 | try: 59 | await self.bot.wait_for( 60 | "message", 61 | timeout=60, 62 | check=lambda msg: msg.author.id == ref_message.author.id 63 | and msg.channel.id == message.channel.id, 64 | ) 65 | except asyncio.TimeoutError: 66 | emb = discord.Embed( 67 | title=f"Reply from {message.author}", 68 | color=await self.bot.get_embed_color( 69 | self.fake_obj(message.guild) # type:ignore 70 | ), 71 | ) 72 | emb.description = message.content 73 | emb.add_field( 74 | name="Your message", 75 | value=ref_message.content[:1024], 76 | inline=False, 77 | ) 78 | emb.add_field( 79 | name="Reply message Link", 80 | value=f"[Click Here]({message.jump_url})", 81 | ) 82 | emb.timestamp = datetime.datetime.utcnow() 83 | await ref_message.author.send(embed=emb) 84 | 85 | @commands.guild_only() # type:ignore 86 | @commands.group(invoke_without_command=True, aliases=["nrp"]) 87 | async def noreplyping(self, ctx, toggle: bool): 88 | """ 89 | Track the people who reply but turned off their ping for this channel. 90 | bots are ignored by default. It also checks for 15 seconds on inactivity before dm'ing 91 | """ 92 | await self.config.member_from_ids(ctx.guild.id, ctx.author.id).send_dms.set(toggle) 93 | await ctx.send( 94 | f"You will {'now' if toggle else 'NOT'} be dm'ed when someone replies to your message without pinging you, for this guild" 95 | ) 96 | 97 | @commands.is_owner() 98 | @noreplyping.command(name="stats") 99 | async def replying_stats(self, ctx): 100 | """See how many people enabled this command""" 101 | total = sum( 102 | sum(1 for i in conf.values() if i["send_dms"]) 103 | for conf in (await self.config.all_members()).values() 104 | ) 105 | await ctx.send(f"A total of {total:,} member(s) have opted for noreplyping") 106 | 107 | async def red_delete_data_for_user(self, *, requester, user_id: int) -> None: 108 | for g_id, guild in (await self.config.all_members()).items(): 109 | if user_id in guild: 110 | await self.config.member_from_ids(g_id, user_id).clear() 111 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 99 3 | target-version = ['py311'] 4 | 5 | [tool.isort] 6 | profile = "black" 7 | line_length = 99 8 | combine_as_imports = true 9 | filter_files = true 10 | -------------------------------------------------------------------------------- /simpleweb/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | from redbot.core.bot import Red 5 | from redbot.core.errors import CogLoadError 6 | 7 | from .simpleweb import SimpleWeb 8 | 9 | with open(Path(__file__).parent / "info.json") as fp: 10 | __red_end_user_data_statement__ = json.load(fp)["end_user_data_statement"] 11 | 12 | 13 | async def setup(bot: Red) -> None: 14 | if not bot.rpc_enabled: 15 | raise CogLoadError("RPC is not enabled.") 16 | cog = SimpleWeb(bot) 17 | await cog.cog_load() 18 | bot.add_cog(cog) 19 | -------------------------------------------------------------------------------- /simpleweb/data/static/main.css: -------------------------------------------------------------------------------- 1 | h1,p,.card { 2 | color:white; 3 | } 4 | 5 | /* Background styles */ 6 | body { 7 | background-color: #000000; 8 | background-position: center; 9 | } 10 | 11 | #results { 12 | background-color: rgb(19, 19, 19); 13 | min-height: 100vh; 14 | padding: 1em; 15 | border-radius: 5px; 16 | 17 | } 18 | 19 | /* Glassmorphism card effect */ 20 | .card { 21 | backdrop-filter: blur(15px) saturate(100%); 22 | -webkit-backdrop-filter: blur(15px) saturate(100%); 23 | background-color: rgba(17, 25, 40, 0.38); 24 | border-radius: 12px; 25 | border: 1px solid rgba(255, 255, 255, 0.125); 26 | min-height: 100px; 27 | } 28 | 29 | .container { 30 | padding: 0px 16px 16px 16px; 31 | } 32 | 33 | body 34 | { 35 | width: 70%; 36 | margin-left:auto; 37 | margin-right:auto; 38 | } 39 | 40 | input 41 | { 42 | width: 100%; 43 | padding: 8px 8px; 44 | margin: 8px 0px; 45 | box-sizing: border-box; 46 | } 47 | -------------------------------------------------------------------------------- /simpleweb/data/templates/commands.jinja2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{app_info.name}}'s Dashboard 5 | 6 | 7 | 8 | 9 |

{{app_info.name}}'s Commands

10 | 11 |

Search Results: 0

12 |
13 |
14 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /simpleweb/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/Cog-Creators/Red-DiscordBot/V3/develop/schema/red_cog.schema.json", 3 | "name": "SimpleWeb", 4 | "short": "A simple webpage dashboard", 5 | "description": "Simple Dashboard for a quick overview of the bot", 6 | "end_user_data_statement": "This cog does not persistently store any data or metadata about users.", 7 | "install_msg": "Very much BETA WIP stuff, you need to have the --rpc flag to use it, check out [p]routes for the available routes. Example: 127.0.0.1:6133/ \n The port 6133 needs to be exposed if you are accessing it remotely (which usually isnt a good idea)", 8 | "author": [ 9 | "epic guy" 10 | ], 11 | "required_cogs": {}, 12 | "requirements": [ 13 | "aiohttp_jinja2" 14 | ], 15 | "tags": [ 16 | "fun", 17 | "utility", 18 | "web" 19 | ], 20 | "min_bot_version": "3.3.10", 21 | "hidden": true, 22 | "disabled": false, 23 | "type": "COG" 24 | } 25 | -------------------------------------------------------------------------------- /simpleweb/simpleweb.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | import aiohttp_jinja2 4 | import discord 5 | import jinja2 6 | import redbot 7 | from aiohttp import web 8 | from aiohttp.web_response import json_response 9 | from aiohttp.web_urldispatcher import StaticResource 10 | from redbot.core import commands 11 | from redbot.core.bot import Red 12 | from redbot.core.config import Config 13 | 14 | RequestType = Literal["discord_deleted_user", "owner", "user", "user_strict"] 15 | 16 | 17 | # RPC PORT: 6133 (default) 18 | class SimpleWeb(commands.Cog): 19 | """ 20 | A simple webpage dashboard 21 | """ 22 | 23 | def __init__(self, bot: Red) -> None: 24 | self.bot = bot 25 | self.config = Config.get_conf( 26 | self, 27 | identifier=3820423, 28 | force_registration=True, 29 | ) 30 | self.data_path = redbot.core.data_manager.bundled_data_path(self) 31 | aiohttp_jinja2.setup( 32 | bot.rpc.app, loader=jinja2.FileSystemLoader(self.data_path / "templates") 33 | ) 34 | self.cache = {} 35 | 36 | async def cog_load(self): 37 | await self.refresh_cache() 38 | self.routes = { 39 | "/ping": ("get", self.hello), 40 | "/": ("get", self.help_commands), 41 | "/api/cmds": ("get", self.cmd_json), 42 | "/static": ("static", self.data_path / "static"), 43 | } 44 | 45 | # Remove older routes 46 | for path in self.routes: 47 | for index, resource in enumerate(self.bot.rpc.app.router._resources): 48 | if isinstance(resource, StaticResource): 49 | if resource._prefix == path: 50 | self.bot.rpc.app.router._resources.pop(index) 51 | break 52 | elif resource._path == path: 53 | self.bot.rpc.app.router._resources.pop(index) 54 | break 55 | 56 | self.bot.rpc.app.router._frozen = False 57 | self.bot.rpc.app.add_routes( 58 | [getattr(web, obj[0])(k, obj[1]) for k, obj in self.routes.items()] 59 | ) 60 | self.bot.rpc.app.router._frozen = True 61 | 62 | async def refresh_cache(self): 63 | self.cache["app_info"] = await self.bot.application_info() 64 | self.cache["cmds"] = [(c.qualified_name, c.help) for c in self.bot.walk_commands()] 65 | 66 | ### Commands Section ### 67 | @commands.command("routes") 68 | async def show_routes(self, ctx): 69 | await ctx.send( 70 | embed=discord.Embed(title="Available Routes", description="\n".join(self.routes)) 71 | ) 72 | 73 | @commands.command() 74 | async def refresh_routes(self, ctx): 75 | await self.refresh_cache() 76 | await ctx.tick() 77 | 78 | #### ROUTES SECTION #### 79 | async def help_commands(self, request): 80 | response = aiohttp_jinja2.render_template("commands.jinja2", request, self.cache) 81 | return response 82 | 83 | async def hello(self, request) -> web.Response: 84 | return web.Response(text="Pong! Site working") 85 | 86 | async def cmd_json(self, request): 87 | return json_response(self.cache["cmds"]) 88 | 89 | async def red_delete_data_for_user(self, *, requester: RequestType, user_id: int) -> None: 90 | # TODO: Replace this with the proper end user data removal handling. 91 | return 92 | -------------------------------------------------------------------------------- /snake/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | from redbot.core.bot import Red 5 | 6 | from .snake import Snake 7 | 8 | with open(Path(__file__).parent / "info.json") as fp: 9 | __red_end_user_data_statement__ = json.load(fp)["end_user_data_statement"] 10 | 11 | 12 | async def setup(bot: Red) -> None: 13 | await bot.add_cog(Snake(bot)) 14 | -------------------------------------------------------------------------------- /snake/game.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | """ 4 | none = 0 5 | fruit = 1 6 | head = 2 7 | body = 3 8 | """ 9 | 10 | 11 | def get_point(size, board): 12 | """Returns x,y in board which is empty,else None""" 13 | i = 0 14 | # A max try of 20 chances 15 | for _ in range(20): 16 | x = random.randint(0, size - 1) 17 | y = random.randint(0, size - 1) 18 | if board[x][y] == 0: 19 | return x, y 20 | # At the worst case, try to traverse the board linearlly 21 | for i in range(size): 22 | for j in range(size): 23 | if board[i][j] == 0: 24 | return i, j 25 | 26 | 27 | class Game: 28 | def __init__(self, size): 29 | self.size = size 30 | self.board = [[0 for i in range(size)] for j in range(size)] 31 | self.snake = [get_point(size - 2, self.board)] 32 | self.snake.append(self.snake[0]) 33 | self.snake[1] = (self.snake[1][0], self.snake[1][1] - 1) 34 | self.board[self.snake[0][0]][self.snake[0][1]] = 2 35 | self.score = 1 36 | self.stop = False 37 | self.prev_fruit = None 38 | self.make_fruit() 39 | 40 | def move(self, m): 41 | mapper = { 42 | "w": (True, self.snake[0][0] - 1), 43 | "s": (True, self.snake[0][0] + 1), 44 | "a": (False, self.snake[0][1] - 1), 45 | "d": (False, self.snake[0][1] + 1), 46 | } 47 | if self.process_move(*mapper[m]): 48 | self.board[self.snake[0][0]][self.snake[0][1]] = 2 49 | self.board[self.snake[1][0]][self.snake[1][1]] = 3 50 | return True 51 | 52 | def make_fruit(self): 53 | if fruit := get_point(self.size, self.board): 54 | self.board[fruit[0]][fruit[1]] = 1 55 | if self.prev_fruit: 56 | self.board[self.prev_fruit[0]][self.prev_fruit[1]] = 0 57 | self.prev_fruit = fruit 58 | return True 59 | 60 | def process_move(self, x_or_y: bool, new: int): 61 | """Check death, increment score, return True or False 62 | If x_or_y is true then the player moved x, else y""" 63 | 64 | # TODO support going through walls a lil later 65 | if new >= self.size or new < 0: 66 | return False 67 | if x_or_y: 68 | # Empty block ahead 69 | if self.board[new][self.snake[0][1]] == 0: 70 | self.snake.insert(0, [new, self.snake[0][1]]) 71 | rm = self.snake.pop(-1) 72 | self.board[rm[0]][rm[1]] = 0 73 | return True 74 | # User eats the fruit 75 | elif self.board[new][self.snake[0][1]] == 1: 76 | self.snake.insert(0, [new, self.snake[0][1]]) 77 | self.score += 1 78 | if self.make_fruit(): 79 | return True 80 | else: 81 | if self.board[self.snake[0][0]][new] == 0: 82 | self.snake.insert(0, [self.snake[0][0], new]) 83 | rm = self.snake.pop(-1) 84 | self.board[rm[0]][rm[1]] = 0 85 | return True 86 | elif self.board[self.snake[0][0]][new] == 1: 87 | self.snake.insert(0, [self.snake[0][0], new]) 88 | self.score += 1 89 | if self.make_fruit(): 90 | return True 91 | return False 92 | -------------------------------------------------------------------------------- /snake/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Snake", 3 | "short": "A simple Snake Game on discord", 4 | "description": "Ratelimit hell based classic snake game on discord", 5 | "end_user_data_statement": "This cog does not persistently store any data or metadata about users.", 6 | "install_msg": "This is a classical snake game, uses dpy menus. Be fully aware of this cog spamming the channel ratelimit buckets.\n Meant for fun, not for scale", 7 | "author": [ 8 | "epic guy" 9 | ], 10 | "required_cogs": {}, 11 | "requirements": [ 12 | "git+https://github.com/npc203/redbot-ext-menus-views" 13 | ], 14 | "tags": [ 15 | "games", 16 | "fun" 17 | ], 18 | "min_bot_version": "3.3.10", 19 | "hidden": false, 20 | "disabled": false, 21 | "type": "COG" 22 | } 23 | -------------------------------------------------------------------------------- /snake/snake.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | from redbot.core import commands 4 | from redbot.core.bot import Red 5 | 6 | from .utils import BoardMenu 7 | 8 | RequestType = Literal["discord_deleted_user", "owner", "user", "user_strict"] 9 | 10 | # Notes: 11 | # If something doesn't edit the board, it goes into utils, else it's in the game class 12 | 13 | 14 | class Snake(commands.Cog): 15 | """ 16 | A simple Snake Game 17 | """ 18 | 19 | def __init__(self, bot: Red) -> None: 20 | self.bot = bot 21 | 22 | @commands.max_concurrency(1, commands.BucketType.guild) 23 | @commands.command() 24 | async def snake(self, ctx): 25 | menu = BoardMenu(ctx.author.display_name, clear_reactions_after=True) 26 | await menu.start(ctx, wait=True) 27 | -------------------------------------------------------------------------------- /snake/utils.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import discord 4 | from redbot.vendored.discord.ext import menus 5 | from redbot_ext_menus import ViewMenu 6 | 7 | from .game import Game 8 | 9 | TRANS = { 10 | 0: "\N{BLACK LARGE SQUARE}", 11 | 1: "\N{RED APPLE}", 12 | 2: "\N{LARGE GREEN CIRCLE}", 13 | 3: "\N{LARGE GREEN SQUARE}", 14 | } 15 | 16 | GET_DIR = { 17 | "w": "up", 18 | "s": "down", 19 | "a": "left", 20 | "d": "right", 21 | None: "Click on a reaction to start", 22 | } 23 | 24 | 25 | # The locks part to sync was inspired by some stackoverflow post which I forgot by now 26 | # Will add the credit if I find it again 27 | class BoardMenu(ViewMenu): 28 | def __init__(self, player_name, **kwargs): 29 | super().__init__(**kwargs) 30 | self.cur_dir = None 31 | self.player_name = player_name 32 | self.game = Game(12) 33 | # maybe use lock here instead of event? 34 | self.is_started = asyncio.Event() 35 | 36 | def edit_board(self, end=False): 37 | emb = discord.Embed(title="Snake", description=self.make_board()) 38 | emb.add_field(name="Score", value=self.game.score) 39 | emb.add_field(name="Player", value=self.player_name) 40 | if end: 41 | emb.add_field(name="Current Direction", value="Game Ended") 42 | else: 43 | emb.add_field(name="Current Direction", value=GET_DIR[self.cur_dir]) 44 | return emb 45 | 46 | def make_board(self): 47 | return "\n".join("".join(map(lambda x: TRANS[x], i)) for i in self.game.board) 48 | 49 | async def loop(self): 50 | await self.is_started.wait() 51 | while True: 52 | await asyncio.sleep(1.2) 53 | if not self.game.move(self.cur_dir): 54 | await self.message.edit(embed=self.edit_board(end=True)) 55 | break 56 | await self.message.edit(embed=self.edit_board()) 57 | self.stop() 58 | 59 | async def send_initial_message(self, ctx, channel): 60 | self.task = ctx.bot.loop.create_task(self.loop()) 61 | return await self.send_with_view(channel, embed=self.edit_board()) 62 | 63 | @menus.button("⬆️") 64 | async def up(self, payload): 65 | self.cur_dir = "w" 66 | self.is_started.set() 67 | 68 | @menus.button("⬇️") 69 | async def down(self, payload): 70 | self.cur_dir = "s" 71 | self.is_started.set() 72 | 73 | @menus.button("⬅️") 74 | async def left(self, payload): 75 | self.cur_dir = "a" 76 | self.is_started.set() 77 | 78 | @menus.button("➡️") 79 | async def right(self, payload): 80 | self.cur_dir = "d" 81 | self.is_started.set() 82 | 83 | @menus.button("⏹️") 84 | async def on_stop(self, payload): 85 | self.task.cancel() 86 | await self.message.edit(embed=self.edit_board(end=True)) 87 | self.stop() 88 | -------------------------------------------------------------------------------- /snipe/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | from redbot.core.bot import Red 5 | 6 | from .snipe import Snipe 7 | 8 | with open(Path(__file__).parent / "info.json") as fp: 9 | __red_end_user_data_statement__ = json.load(fp)["end_user_data_statement"] 10 | 11 | 12 | async def setup(bot: Red) -> None: 13 | await bot.add_cog(Snipe(bot)) 14 | -------------------------------------------------------------------------------- /snipe/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Snipe", 3 | "short": "Multi Snipe for fun and non-profit", 4 | "description": "Bulk sniping to stab back those anti-sniping smart ass users", 5 | "end_user_data_statement": "This cog does not persistently store any data or metadata about users.", 6 | "install_msg": "Have fun, this cog isn't extensively tested, so if you find any bugs, please report them to me in the cog server", 7 | "author": [ 8 | "epic guy" 9 | ], 10 | "required_cogs": {}, 11 | "requirements": [ 12 | "git+https://github.com/npc203/redbot-ext-menus-views" 13 | ], 14 | "tags": [ 15 | "fun", 16 | "utility" 17 | ], 18 | "min_bot_version": "3.3.10", 19 | "hidden": false, 20 | "disabled": false, 21 | "type": "COG" 22 | } 23 | -------------------------------------------------------------------------------- /speak/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | from .speak import Speak 5 | 6 | with open(Path(__file__).parent / "info.json") as fp: 7 | __red_end_user_data_statement__ = json.load(fp)["end_user_data_statement"] 8 | 9 | 10 | async def setup(bot): 11 | await bot.add_cog(Speak(bot)) 12 | -------------------------------------------------------------------------------- /speak/data/insult.txt: -------------------------------------------------------------------------------- 1 | My battery lasts longer than your relationships. 2 | I’m surprised at you level of stupidity. 3 | Life is good, you should get one. 4 | I may not be super smart but compared to you I am Albert Einstein. 5 | It is not insult from another that causes you pain. It is the part of your mind that agrees with the insult. Agree only with the truth about you, and you are free. 6 | Please don’t interupt me when I’m ignoring you. 7 | You, sir, are an oxygen thief. 8 | He is known as an idiot savant, minus the savant. 9 | I never insult any people I only tell them what they are. 10 | You can’t control what other people say about you, but you can control how you respond. 11 | The trash will get picked up tomorrow, be ready. 12 | Did you forget your brain in your mother’s womb? Cause I’m pretty sure you did. 13 | Allowing you to survive childbirth was medical malpractice. 14 | The degree of your stupidity is enough to boil water. 15 | Your stupidity is so high I would like to kill myself and to do that I would have to jump from your ego to your IQ. 16 | I would love to insult you, but I’m afraid I won’t do as well as nature did. 17 | You are not useless because you can still be used as a bad example. 18 | The only reason Wiz Khalifa made the song black and yellow was because of your teeth. 19 | If stupidity was a profession then you’d be a billionaire. 20 | Are you sure this is your territory? 21 | I’ve heard of being hit with the ugly stick, but you must have been beaten senseless. 22 | When you die you could leave your brain to medical science, I’ve heard they need a new doorstop. 23 | Your mama is so fat that when God said let there be light, he first has to shove her out of the way. 24 | Of course I talk like an idiot. How else could you understand me? 25 | Sorry what? I don’t understand idiot language. 26 | I’m sorry, but you must die young. For the good of the universe just die young. 27 | Your mom had a severe case of diarrhea when you were born. 28 | Your mom never knew she was pregnant that’s why you were born in the toilet. 29 | You’re so ugly when your mom took you to the hospital the first doctor she met told her please I’m not a veterinary doctor. 30 | Can you please wipe your mouth. You’re dribbling sh*t again! 31 | I can explain it to you but I can’t understand it for you. 32 | I’d love to stay and chat but I’d be lying. 33 | Jesus loves you.(Everyone else thinks you suck) 34 | You look like the failed first draft of a final fantasies character. 35 | Your head is just there to keep your ears apart. 36 | Fools are temporary. 37 | But Stupids like you are forever. 38 | Go back to your planet. Earth is full. 39 | If brains were gasoline you wouldn’t have enough to propel a flea’s motorcycle around a doughnut. 40 | Why did the chicken cross the road? To get the hell away from you. 41 | I didn’t know the trash from your head could come out of your mouth. 42 | Oh I’m sorry, were we supposed to dress stupid today? 43 | You say you worth all the diamonds in the world, but when I look at you, you look like you worth all the rubbish in the world. 44 | I am glad God gave us you, otherwise what else would we be comparing ugliness to? 45 | I respect those, who hate me by showing my middle finger. 46 | Can you please fake my absence in my presence please! 47 | Even your mom loves you only as a friend !!! 48 | I wouldn’t say you are ugly but rather facially challenged. 49 | Try rolling your eyes, maybe you could find a brain back there. 50 | You were born on the freeway, where accidents happen. 51 | You’re not ugly, you’re just not someone to look at. 52 | You’re so ugly that you wouldn’t even look at yourself either. 53 | You’re so ugly, a sniper wouldn’t take you out. 54 | If I wanted to kill myself I’ll have to climb your ego and then jump to your IQ. 55 | Hi-my name is beauty. Who are you? The beast? 56 | If you look up the definition of moron in the dictionary there will be a picture of you. 57 | What’d you do to piss off the person with the ugly stick? 58 | You are so fat, people jog around you for exercise. 59 | You actually sounded smarter when you didn’t say anything. 60 | I would tell you to go to hell but I don’t want to see you again. 61 | If you were on fire with a bucket of water near you, I’d drink the water. 62 | I’d slap you, but that’d be animal abuse. 63 | I asked God to punish me, next day I met you. 64 | If you were twice as wise you are now, you’d probably still be stupid! 65 | Whenever I see your face, I feel like I am having a bad dream. 66 | What would intelligence be without stupidity, see you are important. 67 | Some people have no shame in denying the truth and defending a lie! 68 | Insult is a monstrous scorpion, and compliment is a likeable nightingale; one stings mercilessly, and the other sings sweetly. – Mehmet Murat ildan 69 | If you’re so smart, why aren’t you rich? 70 | You want me to go to hell? I don’t think I am ready for a visit to your home yet. 71 | B*tch, please, your birth certificate is an apology letter from the condom factory. 72 | The only positive thing about you is your HIV status. 73 | You are so ugly that when you were born, God left this planet. 74 | -------------------------------------------------------------------------------- /speak/data/sadme.txt: -------------------------------------------------------------------------------- 1 | Have you ever felt like your entire life is just a big school exam? I sure did, and I’m quite certain that I didn’t study for it. 2 | Good day, this is your trashcan speaking. 3 | My life’s purpose is to be a cautionary tale for others. 4 | Yeah, I know. I hate me too. 5 | “Today is not my day”, I mutter to myself every single day. 6 | My teacher called me average. How mean! 7 | My entire life is a big joke. So, tell why exactly I need to celebrate April Fool’s Day again? 8 | I’m actually a very hardworking person. Almost everything becomes harder when I’m the one working on it. 9 | No one can possibly hope to compete with me when it comes to being hard on myself. So, if you’re only here to judge me, please keep it to yourself and just shove it up your arse! 10 | They say money talks. But, all mine says is goodbye. 11 | How do I moisturize my face? I use my own tears! 12 | I know I’m ugly, but at least I’m still trying. 13 | There’s no way I’m willing to learn new skills unless I’m instantly proficient at them. Yeah, I know that at this point, I’m pretty much just sabotaging my own life. 14 | I just realized that my life can’t fall apart if I never had it all together in the first place. 15 | I might be obnoxious, but at least I’m also annoying. 16 | Am I a good person? No. But do I try to make myself a better person each day? Also no. 17 | I’m like 113% tired. 18 | I wouldn’t even settle for me, so why would you? 19 | The only abs I have are abnormalities. 20 | I’m quite smart and intelligent. Most of the time, I don't even understand a single word of what I’m talking about. 21 | I may be trash, but I burn with a bright flame. 22 | Using the “y=mx+b” formula, calculate the slope at which my life is going downhill. 23 | I’m dropping it like it’s my hopes and dreams. 24 | Oh yes, I'm a hot mess! 25 | I put the ace in disgrace! 26 | I found a way to not get cheated on. Just have a self-destructive personality and a tendency to self-sabotage. Trust me, this will keep you single for the rest your days. 27 | To the powers that be, if it’s inevitable that something bad must happen to me, at least make it funny. 28 | Everyone’s so dope, and I’m so nope. 29 | My exercise routine includes running away from my problems, running late, and running my mouth non-stop. 30 | I don’t suffer from insanity. I actually derive excitement from every second of it. 31 | I can’t deny that I made a lot of mistakes when I was younger. I’m older now, so I can make different, yet more severe mistakes. 32 | Sorry, demons! There’s no room inside me because I’m self-possessed. 33 | I feel like I’m already tired tomorrow. 34 | In photos, I’m ugly. In real life, I’m also ugly! 35 | I can’t exactly shame myself into becoming a better person, right? 36 | You’re guessing that out of the 7 billion people here on Earth, I’m going to chase someone who doesn’t even like me? Well, watch me closely because that’s exactly what I’m going to do. 37 | It’s true that I’m CUTE: C(ringy), U(nattractive), T(rash), and E(asy to forget). 38 | Whenever I’m ready to sleep, I don’t bother checking if my foot is hanging off the end of my bed anymore. Come get me, demons, I’m already living in hell. 39 | Do you want to hear a funny joke? My life. *LOL* 40 | Worrying works! More than 90 percent of the things I worry about never happen. 41 | Whenever I look at the mirror, it shows me what I lack, not what I have. 42 | The only time I’m funny is when I insult myself. 43 | I’m probably going to regret everything in 3...2...1... 44 | Not to brag, but I haven’t had a mood swing in, like, 7 minutes. 45 | One thing that’s emptier than my wallet is my heart. 46 | I chuckle whenever people try to figure what’s going on in my head. Like, good luck, I can’t even figure myself out. 47 | I used to be indecisive. Now, I don’t think I’m quite sure anymore. 48 | There's no law that says you have to be dissatisfied or disappointed with who you are. 49 | I’m human garbage. At the very least, please dispose of me properly. 50 | They told me that I can become anything if I willed it. So, I became a disappointment. 51 | I find it amusing when people try to insult me. They have no idea that I roast myself on a daily basis. 52 | If the government can shut down, then why can’t I? 53 | Pokemon? It’s funny that I’m trying to catch them all, yet I can’t even find myself. 54 | What would have happened if you exterminated the ugliest guy and the dumbest guy in the world yesterday? Right, this post wouldn’t exist. 55 | How to look good while crying? 56 | My clear conscience is just a sign of bad memory. 57 | I actually have friends despite of myself. 58 | Like a garbage phoenix, I will rise from the dumps. 59 | *winks at my reflection in the mirror* *reflection walks away* 60 | I have a good heart, but I really should fix this mouth of mine. 61 | People say that I’m creative and I couldn’t agree more because I create most of my own problems. 62 | All my imaginary friends tell me that I need therapy. 63 | “You are what you eat.” I call BS! I don’t even remember eating. 64 | What a beautiful day to hate on myself. 65 | Having very low expectations is the secret to happiness. Should I drop it more? Okay, I’ll go even lower. There you go. 66 | Don’t mind me. I’m just having an existential crisis. Move along, folks. 67 | Deep inside, I’m already dead. Still, I want to make other people feel alive and good. 68 | I believe in my pet dog/cat more than I believe in myself. 69 | People call me an alcoholic whenever I drink alcohol. But, when I drink Fanta, people never call me fantastic. 70 | I’m at a really low point right now. But the good news is: the worst is just ahead! 71 | I’ve been single ever since I mingled. 72 | Every day is Friday when you’re unemployed. 73 | Life is like a box of chocolates. But for some reason, I got the gross dark chocolate with the orange flavor in the middle. 74 | Do mood swings count as exercise? 75 | The good news is: I’m pretty much who I say I am. The bad news is: I’m pretty much who I say I am. 76 | Will this outfit get me the romantic partner of my dreams? Tune in tomorrow for the next episode of “Nope.” 77 | So, I stumbled upon this question asking if I’m an early bird or a night owl? I’m neither! I’m some form of permanently-exhausted pigeon. 78 | Think of each day as another chance to f*ck everything up again. 79 | We're all trash, quite frankly. 80 | My fridge is as empty as me. 81 | Don't get me wrong, being naked feels awesome, and I wish I could do it more. Well, just without any of the visual consequences. 82 | How can I face my problem when my problem is my face? 83 | I said “hello” to darkness my old friend, and it told me that it doesn’t want to be my friend. 84 | -------------------------------------------------------------------------------- /speak/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": [ 3 | "epic guy#0715" 4 | ], 5 | "name": "Speak", 6 | "install_msg": "This cog uses webhooks and thus requires manage webhook permissions to work \nThis cog comes with bundled data", 7 | "short": "A cog that speaks for you or speaks what you type as others", 8 | "description": "Ever wanted to speak as someone else or other bots? or you didn't have the right words to say? This cog speaks for you! or mimics other's identity as well, except that it adds a bot tag. It uses webhooks", 9 | "tags": [ 10 | "speak", 11 | "tell", 12 | "fun" 13 | ], 14 | "end_user_data_statement": "This cog does not persistently store data or metadata about users." 15 | } 16 | -------------------------------------------------------------------------------- /speak/speak.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from random import choice 3 | 4 | import discord 5 | from redbot.core import checks, commands, data_manager 6 | 7 | 8 | class Speak(commands.Cog): 9 | """Set of commands to talk as others or 10 | Say stuff for you when you don't have the right words!""" 11 | 12 | def __init__(self, bot): 13 | self.bot = bot 14 | self.cache = {} 15 | with open(data_manager.bundled_data_path(self) / "insult.txt", encoding="utf8") as fp: 16 | self.insult_list = fp.read().splitlines() 17 | with open(data_manager.bundled_data_path(self) / "sadme.txt", encoding="utf8") as fp: 18 | self.sadme_list = fp.read().splitlines() 19 | 20 | @commands.command() 21 | async def tell(self, ctx, channel: typing.Optional[discord.TextChannel], *, sentence: str): 22 | """Tells the given text as the yourself but with a bot tag""" 23 | if not (channel := await self.invalid_permissions_message(ctx, channel)): 24 | return 25 | 26 | hook = await self.get_hook(channel) 27 | if channel == ctx.channel: 28 | await ctx.message.delete() 29 | await hook.send( 30 | username=ctx.author.display_name, 31 | avatar_url=ctx.author.avatar.url, 32 | content=sentence, 33 | ) 34 | 35 | @commands.command() 36 | async def tellas( 37 | self, 38 | ctx, 39 | channel: typing.Optional[discord.TextChannel], 40 | mention: discord.Member, 41 | *, 42 | sentence: str, 43 | ): 44 | """Tells the given text as the mentioned users""" 45 | if not (channel := await self.invalid_permissions_message(ctx, channel)): 46 | return 47 | 48 | hook = await self.get_hook(channel) 49 | if channel == ctx.channel: 50 | await ctx.message.delete() 51 | await hook.send( 52 | username=mention.display_name, 53 | avatar_url=mention.avatar.url, 54 | content=sentence, 55 | ) 56 | 57 | @checks.bot_has_permissions(manage_webhooks=True, manage_messages=True) 58 | @commands.command() 59 | async def telluser( 60 | self, 61 | ctx, 62 | channel: typing.Optional[discord.TextChannel], 63 | username: str, 64 | avatar: str, 65 | *, 66 | sentence: str, 67 | ): 68 | """Says the given text with the specified name and avatar""" 69 | if not (channel := await self.invalid_permissions_message(ctx, channel)): 70 | return 71 | 72 | hook = await self.get_hook(channel) 73 | if channel == ctx.channel: 74 | await ctx.message.delete() 75 | if avatar.startswith("http"): 76 | if 1 < len(username) <= 80: 77 | await hook.send( 78 | username=username, 79 | avatar_url=avatar, 80 | content=sentence, 81 | ) 82 | else: 83 | await ctx.send("You must include a username of less than 80 characters.") 84 | await ctx.send_help() 85 | else: 86 | await ctx.send("You must include a URL to define the webhook avatar.") 87 | await ctx.send_help() 88 | 89 | @checks.bot_has_permissions(manage_webhooks=True, manage_messages=True) 90 | @commands.group(invoke_without_command=False) 91 | async def says(self, ctx): 92 | """Says Stuff for the user""" 93 | if ctx.invoked_subcommand is not None: 94 | await ctx.message.delete() 95 | 96 | @says.command() 97 | async def insult(self, ctx): 98 | """says lame insults, use at your own precaution""" 99 | await self.print_it(ctx, choice(self.insult_list)) 100 | 101 | @says.command() 102 | async def sadme(self, ctx): 103 | """says depressing stuff about you""" 104 | await self.print_it(ctx, choice(self.sadme_list)) 105 | 106 | async def print_it(self, ctx, stuff: str, retried=False): 107 | hook = await self.get_hook(ctx.channel) 108 | try: 109 | await hook.send( 110 | username=ctx.message.author.display_name, 111 | avatar_url=ctx.message.author.avatar.url, 112 | content=stuff, 113 | ) 114 | except discord.NotFound: # Yup user deleted the hook, invalidate cache, retry 115 | if retried: # This is an edge case, just a hack to prevent infinite loops 116 | return await ctx.send("I can't find the webhook, sorry.") 117 | self.cache.pop(ctx.channel.id) 118 | await self.print_it(ctx, stuff, True) 119 | 120 | async def get_hook(self, channel: discord.TextChannel): 121 | try: 122 | if channel.id not in self.cache: 123 | for i in await channel.webhooks(): 124 | if i.user and i.user.id == self.bot.user.id: 125 | hook = i 126 | self.cache[channel.id] = hook 127 | break 128 | else: 129 | hook = await channel.create_webhook(name="red_bot_hook_" + str(channel.id)) 130 | else: 131 | hook = self.cache[channel.id] 132 | except discord.NotFound: # Probably user deleted the hook 133 | hook = await channel.create_webhook(name="red_bot_hook_" + str(channel.id)) 134 | return hook 135 | 136 | async def invalid_permissions_message( 137 | self, ctx: commands.Context, channel: typing.Optional[discord.TextChannel] 138 | ) -> typing.Optional[discord.TextChannel]: 139 | """Returns target channel if bot and user have valid permissions""" 140 | if channel is None: 141 | channel = ctx.channel 142 | 143 | permissions_bot = channel.permissions_for(ctx.guild.me) 144 | permissions_author = channel.permissions_for(ctx.author) 145 | if ( 146 | not permissions_bot.manage_webhooks 147 | or channel == ctx.channel 148 | and not permissions_bot.manage_messages 149 | ): 150 | await ctx.send( 151 | f"The bot does not have enough permissions to send a webhook in {channel.mention}." 152 | ) 153 | return 154 | if ( 155 | not permissions_author.send_messages 156 | and not permissions_author.read_messages 157 | and not permissions_author.read_message_history 158 | ): 159 | await ctx.send(f"You do not have enough permissions in {channel.mention}.") 160 | return 161 | return channel 162 | 163 | async def red_get_data_for_user(self, *, user_id: int): 164 | # this cog does not store any data 165 | return {} 166 | 167 | async def red_delete_data_for_user(self, *, requester, user_id: int) -> None: 168 | # this cog does not store any data 169 | pass 170 | -------------------------------------------------------------------------------- /todo/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | from redbot.core.bot import Red 5 | 6 | from .todo import Todo 7 | 8 | with open(Path(__file__).parent / "info.json") as fp: 9 | __red_end_user_data_statement__ = json.load(fp)["end_user_data_statement"] 10 | 11 | 12 | async def setup(bot: Red) -> None: 13 | await bot.add_cog(Todo(bot)) 14 | -------------------------------------------------------------------------------- /todo/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Todo", 3 | "short": "A simple todo list", 4 | "description": "A simple todo list cog with add remove and list todos", 5 | "end_user_data_statement": "This cog stores the todos associated with uids", 6 | "install_msg": "Thank you for installing. Type `[p]help todo` to get the list of commands", 7 | "author": [ 8 | "epic guy#0715" 9 | ], 10 | "required_cogs": {}, 11 | "requirements": [ 12 | "git+https://github.com/npc203/redbot-ext-menus-views" 13 | ], 14 | "tags": [ 15 | "utility" 16 | ], 17 | "min_bot_version": "3.3.10", 18 | "hidden": false, 19 | "disabled": false, 20 | "type": "COG" 21 | } 22 | -------------------------------------------------------------------------------- /typeracer/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | from .typerace import TypeRacer 5 | 6 | with open(Path(__file__).parent / "info.json") as fp: 7 | __red_end_user_data_statement__ = json.load(fp)["end_user_data_statement"] 8 | 9 | 10 | async def setup(bot): 11 | await bot.add_cog(TypeRacer(bot)) 12 | -------------------------------------------------------------------------------- /typeracer/data/lorem.txt: -------------------------------------------------------------------------------- 1 | blandit 2 | penatibus 3 | felis 4 | magnis 5 | imperdiet 6 | habitant 7 | fringilla 8 | sollicitudin 9 | vitae 10 | pulvinar 11 | ligula 12 | condimentum 13 | commodo 14 | mollis 15 | tempor 16 | sed 17 | Mauris 18 | Fusce 19 | dapibus 20 | libero 21 | parturient 22 | scelerisque 23 | Aliquam 24 | Duis 25 | porta 26 | Curabitur 27 | velit 28 | Integer 29 | tincidunt 30 | neque 31 | montes 32 | elit 33 | est 34 | morbi 35 | bibendum 36 | augue 37 | a 38 | vestibulum 39 | dolor 40 | consectetuer 41 | sodales 42 | mauris 43 | id 44 | erat 45 | sit 46 | tortor 47 | ad 48 | ultricies 49 | quam 50 | eu 51 | nascetur 52 | eleifend 53 | molestie 54 | ipsum 55 | Morbi 56 | odio 57 | vel 58 | quis 59 | volutpat 60 | ullamcorper 61 | In 62 | porttitor 63 | lobortis 64 | senectus 65 | per 66 | interdum 67 | litora 68 | accumsan 69 | tellus 70 | urna 71 | hendrerit 72 | ut 73 | vehicula 74 | varius 75 | Etiam 76 | congue 77 | Ut 78 | Cras 79 | Cum 80 | platea 81 | torquent 82 | euismod 83 | Sed 84 | taciti 85 | purus 86 | et 87 | , 88 | mi 89 | viverra 90 | amet 91 | ornare 92 | lacus 93 | faucibus 94 | non 95 | netus 96 | feugiat 97 | risus 98 | Nunc 99 | sociis 100 | leo 101 | fermentum 102 | dis 103 | gravida 104 | eros 105 | metus 106 | ultrices 107 | nec 108 | conubia 109 | mus 110 | dui 111 | tristique 112 | habitasse 113 | hac 114 | Vivamus 115 | pretium 116 | adipiscing 117 | nibh 118 | Nullam 119 | potenti 120 | ridiculus 121 | nonummy 122 | lacinia 123 | aliquam 124 | Pellentesque 125 | nunc 126 | nostra 127 | enim 128 | Donec 129 | sagittis 130 | orci 131 | malesuada 132 | Lorem 133 | sociosqu 134 | egestas 135 | pharetra 136 | at 137 | sapien 138 | lectus 139 | dictumst 140 | placerat 141 | lorem 142 | eget 143 | cursus 144 | Maecenas 145 | nisl 146 | turpis 147 | magna 148 | hymenaeos 149 | laoreet 150 | aliquet 151 | pede 152 | nulla 153 | venenatis 154 | inceptos 155 | fames 156 | Aenean 157 | Suspendisse 158 | auctor 159 | mattis 160 | posuere 161 | semper 162 | aptent 163 | wisi 164 | consequat 165 | Praesent 166 | vulputate 167 | Class 168 | in 169 | rhoncus 170 | Proin 171 | Quisque 172 | ac 173 | Vestibulum 174 | Nam 175 | justo 176 | . 177 | diam 178 | tempus 179 | arcu 180 | natoque 181 | iaculis 182 | sem 183 | pellentesque 184 | ante 185 | massa 186 | -------------------------------------------------------------------------------- /typeracer/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": [ 3 | "epic guy#0715" 4 | ], 5 | "name": "TypeRacer", 6 | "short": "A fun typing speed test cog", 7 | "install_msg": "This cog uses bundled data to make words out of thin air.", 8 | "description": "This cog can be used to check your typing speed, it isnt that accurate cause of ping issues, otherwise it's a decent one", 9 | "tags": [ 10 | "utility", 11 | "fun", 12 | "speedtest" 13 | ], 14 | "hidden": false, 15 | "requirements": [ 16 | "tabulate" 17 | ], 18 | "end_user_data_statement": "This cog does not persistently store data or metadata about users." 19 | } 20 | -------------------------------------------------------------------------------- /typeracer/single.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | 4 | from .utils import evaluate, get_text, nocheats 5 | 6 | 7 | class Single: 8 | """Single personal typing test stuff""" 9 | 10 | def __init__(self, ctx, settings): 11 | self.ctx = ctx 12 | self.settings = settings 13 | 14 | async def start(self): 15 | """Start the test, Display the question and get result""" 16 | async with self.ctx.typing(): 17 | a_string, status_code = await get_text(self.settings) 18 | if status_code: 19 | self.task = asyncio.create_task(self.task_personal_race(self.ctx, a_string)) 20 | else: 21 | return 22 | try: 23 | time_taken, b_string = await self.task 24 | await evaluate(self.ctx, a_string, b_string, time_taken, None) 25 | except asyncio.CancelledError: 26 | return await self.ctx.send("Cancelled typing test") 27 | 28 | async def cancel(self): 29 | # Maybe make this function not async? 30 | self.task.cancel() 31 | 32 | async def task_personal_race(self, ctx, a_string): 33 | """Personal Race""" 34 | msg = await ctx.send( 35 | f"{ctx.author.display_name} started a typing test: \n Let's Start in 3" 36 | ) 37 | for i in range(2, 0, -1): 38 | await asyncio.sleep(1) 39 | await msg.edit( 40 | content=f"{ctx.author.display_name} started a typing test: \n Let's Start in {i}" 41 | ) 42 | await asyncio.sleep(1) 43 | await msg.edit(content="```" + nocheats(a_string) + "```") 44 | start = time.perf_counter() 45 | try: 46 | b_string = ( 47 | await ctx.bot.wait_for( 48 | "message", 49 | timeout=180.0, 50 | check=lambda m: m.author.id == ctx.author.id 51 | and m.channel.id == ctx.channel.id, 52 | ) 53 | ).content.strip() 54 | end = time.perf_counter() 55 | except asyncio.TimeoutError: 56 | await msg.edit(content="Sorry you were way too slow, timed out") 57 | raise asyncio.CancelledError 58 | time_taken = end - start 59 | return time_taken, b_string 60 | -------------------------------------------------------------------------------- /typeracer/speedevent.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import contextlib 3 | import time 4 | 5 | from discord import NotFound 6 | from tabulate import tabulate 7 | 8 | from .utils import evaluate, get_text, nocheats 9 | 10 | 11 | class Speedevent: 12 | def __init__(self, ctx, countdown, settings, all=False): 13 | self.ctx = ctx 14 | self.countdown = countdown 15 | self.settings = settings 16 | self.joined = {ctx.author.id: ctx.author.display_name} if not all else dict() 17 | self.tasks = {} 18 | self.event_started = False 19 | self.leaderboard = [] 20 | self.all = all 21 | self.finished = 0 22 | self.bot_msg = None 23 | # if all means, everyone are enrolled in the speedevent 24 | self.check = ( 25 | (lambda msg: msg.author.id not in self.joined) 26 | if all 27 | else (lambda msg: msg.author.id in self.joined) 28 | ) 29 | 30 | async def start(self): 31 | self.a_string, status_code = await get_text(self.settings) 32 | if not status_code: 33 | await self.ctx.send("Something went wrong while getting the text") 34 | return 35 | self.tasks["event"] = asyncio.create_task(self.task_event_race()) 36 | 37 | iscancelled = False 38 | try: 39 | await self.tasks["event"] 40 | except asyncio.CancelledError: 41 | iscancelled = True 42 | 43 | if self.leaderboard: 44 | await self.ctx.send( 45 | "```Event results:\n{}```".format( 46 | tabulate( 47 | sorted(self.leaderboard, key=lambda x: x[2], reverse=True), 48 | headers=("Name", "Time taken", "WPM", "Mistakes"), 49 | tablefmt="fancy_grid", 50 | ) 51 | ) 52 | ) 53 | elif not iscancelled: 54 | await self.ctx.send("No one was brave enough to complete the test 😦") 55 | 56 | async def stop(self, name): 57 | for task in self.tasks.values(): 58 | task.cancel() 59 | 60 | await self.ctx.send(f"{name} ended the speedevent") 61 | 62 | async def join(self, user_id, nick): 63 | if self.event_started: 64 | return await self.ctx.send("Sorry event already started and ongoing") 65 | 66 | if self.all: 67 | notify = await self.ctx.send("This event is open for all, You don't need to join in") 68 | elif user_id in self.joined: 69 | notify = await self.ctx.send("You already joined in") 70 | else: 71 | self.joined[user_id] = nick 72 | notify = await self.ctx.send(f"{nick} has joined in") 73 | 74 | perms = notify.channel.permissions_for(self.ctx.me) 75 | # Delete the message 76 | await asyncio.sleep(2) 77 | if perms.manage_messages: 78 | with contextlib.suppress(NotFound): 79 | await notify.delete() 80 | 81 | async def final_evaluate(self, msg_result, time_fin): 82 | if self.check(msg_result): 83 | if results := await evaluate( 84 | self.ctx, 85 | self.a_string, 86 | msg_result.content, 87 | time_fin, 88 | msg_result.author.id if self.settings["dm"] else None, 89 | author_name=msg_result.author.display_name, 90 | ): 91 | results.insert(0, str(msg_result.author)) 92 | self.leaderboard.append(results) 93 | if self.all: 94 | self.joined[msg_result.author.id] = True 95 | else: 96 | self.joined.pop(msg_result.author.id) 97 | 98 | async def task_event_race(self): 99 | """Event Race""" 100 | active = "\n".join( 101 | [f"{index}. {self.joined[user]}" for index, user in enumerate(self.joined, 1)] 102 | ) 103 | countdown = await self.ctx.send( 104 | f"A Typing speed test event will commence in {self.countdown} seconds\n" 105 | + ( 106 | f" Type `{self.ctx.prefix}speedevent join` to enter the race\n " 107 | + f"Joined Users:\n{active}" 108 | if not self.all 109 | else "" 110 | ) 111 | ) 112 | await asyncio.sleep(5) 113 | for i in range(self.countdown - 5, 5, -5): 114 | active = "\n".join( 115 | [f"{index}. {self.joined[user]}" for index, user in enumerate(self.joined, 1)] 116 | ) 117 | await countdown.edit( 118 | content=f"A Typing speed test event will commence in {i} seconds\n" 119 | + ( 120 | f" Type `{self.ctx.prefix}speedevent join` to enter the race\n " 121 | + f"Joined Users:\n{active}" 122 | if not self.all 123 | else "" 124 | ) 125 | ) 126 | await asyncio.sleep(5) 127 | await countdown.delete() 128 | 129 | if len(self.joined) <= 1 and not self.all: 130 | await self.ctx.send("Oh no, looks like nobody joined the speedevent") 131 | raise asyncio.CancelledError 132 | 133 | msg = await self.ctx.send("Speedevent Starts in 5") 134 | for i in range(4, 0, -1): 135 | await asyncio.sleep(1) 136 | await msg.edit(content=f"Speedevent Starts in {i}") 137 | 138 | self.tasks["sticky"] = asyncio.create_task( 139 | self.sticky(f"Write the given paragraph\n```{nocheats(self.a_string)}```") 140 | ) 141 | 142 | match_begin = time.perf_counter() 143 | self.event_started = True 144 | 145 | async def runner(): 146 | while True: 147 | msg_result = await self.ctx.bot.wait_for( 148 | "message", 149 | timeout=180.0, 150 | check=lambda msg: not msg.author.bot and msg.channel.id == self.ctx.channel.id, 151 | ) 152 | 153 | self.finished += 1 154 | asyncio.create_task( 155 | self.final_evaluate(msg_result, time.perf_counter() - match_begin) 156 | ) 157 | if not self.all and len(self.joined) == 0: 158 | break 159 | 160 | try: 161 | await asyncio.wait_for(runner(), timeout=180) 162 | except asyncio.TimeoutError: 163 | pass 164 | 165 | async def sticky(self, text): 166 | content = ("Remaing time: 180 seconds\n" if self.all else "") + text 167 | msg = await self.ctx.send(content) 168 | 169 | fin = 180 170 | cont = time.time() 171 | while fin > 0: 172 | if self.finished > 6: 173 | self.finished = 0 174 | await msg.delete() 175 | msg = await self.ctx.send(content) 176 | elif self.all and time.time() - cont > 5: 177 | fin -= 5 178 | content = (f"Remaing time: {fin} seconds\n" if self.all else "") + text 179 | await msg.edit(content=content) 180 | cont = time.time() 181 | 182 | await asyncio.sleep(1) 183 | -------------------------------------------------------------------------------- /typeracer/typerace.py: -------------------------------------------------------------------------------- 1 | from discord import Embed 2 | from redbot.core import Config, commands 3 | 4 | from .single import Single 5 | from .speedevent import Speedevent 6 | from .utils import typerset_check 7 | 8 | 9 | class TypeRacer(commands.Cog): 10 | """A Typing Speed test cog, to give test your typing skills""" 11 | 12 | def __init__(self, bot): 13 | self.bot = bot 14 | self.config = Config.get_conf(self, identifier=29834829369) 15 | default_guild = { 16 | "time_start": 60, 17 | "text_size": (10, 20), 18 | "type": "gibberish", 19 | "dm": True, 20 | } 21 | self.config.register_guild(**default_guild) 22 | default_user = { 23 | "text_size": (10, 20), 24 | "type": "gibberish", 25 | } 26 | self.config.register_user(**default_user) 27 | 28 | self.jobs = {"guilds": {}, "personal": {}} 29 | 30 | @commands.group() 31 | async def typer(self, ctx): 32 | """Commands to start and stop personal typing speed test""" 33 | 34 | @typer.command() 35 | async def settings(self, ctx): 36 | """Shows the current setting in the guild""" 37 | emb = Embed(color=await ctx.embed_color()) 38 | settings = ( 39 | await self.config.guild_from_id(ctx.guild.id).all() 40 | if ctx.guild 41 | else await self.config.user_from_id(ctx.author.id).all() 42 | ) 43 | 44 | val = ( 45 | f"`Type `:{settings['type']}\n" 46 | + ( 47 | (f"`Send dms `:{settings['dm']}\n" + f"`Start timer`:{settings['time_start']}\n") 48 | if ctx.guild 49 | else "" 50 | ) 51 | + f"`No of Words`:{settings['text_size'][0]} - {settings['text_size'][1]}\n" 52 | ) 53 | emb.add_field(name="TyperRacer settings", value=val) 54 | await ctx.send(embed=emb) 55 | 56 | @commands.is_owner() 57 | @typer.command() 58 | async def show(self, ctx): 59 | """Show the details of ongoing typer events globally""" 60 | emb = Embed(title="Ongoing Type racer stats", color=await ctx.embed_color()) 61 | if self.jobs["guilds"]: 62 | emb.add_field(name="Speedevents", value=len(self.jobs["guilds"])) 63 | if self.jobs["personal"]: 64 | emb.add_field(name="Personal typing tests", value=len(self.jobs["personal"])) 65 | await ctx.send(embed=emb) 66 | 67 | @typer.command(name="start") 68 | async def start_personal(self, ctx): 69 | """Start a personal typing speed test""" 70 | if ctx.author.id in self.jobs["personal"]: 71 | await ctx.send("You already are running a speedtest") 72 | else: 73 | test = Single( 74 | ctx, 75 | await ( 76 | self.config.guild_from_id(ctx.guild.id).all() 77 | if ctx.guild 78 | else self.config.user_from_id(ctx.author.id).all() 79 | ), 80 | ) 81 | self.jobs["personal"][ctx.author.id] = test 82 | await test.start() 83 | self.jobs["personal"].pop(ctx.author.id) 84 | 85 | @typer.command() 86 | async def stop(self, ctx): 87 | """Stop/Cancel taking the personal typing test""" 88 | if ctx.author.id in self.jobs["personal"]: 89 | await self.jobs["personal"][ctx.author.id].cancel() 90 | else: 91 | await ctx.send("You need to start the test.") 92 | 93 | @commands.guild_only() 94 | @commands.group() 95 | async def speedevent(self, ctx): 96 | """Play a speed test event with multiple players""" 97 | 98 | @commands.mod_or_permissions(manage_messages=True) 99 | @speedevent.command(name="start") 100 | async def start_event(self, ctx, countdown: int = None, *, args=""): 101 | """Start a typing speed test event 102 | Use `--all` for everyone to be added to the contest 103 | 104 | Takes an optional countdown argument to start the test 105 | (Be warned that cheating gets you disqualified) 106 | 107 | This lasts for 3 minutes at max, and stops if everyone completed 108 | 109 | Examples: 110 | `[p]speedevent start` 111 | `[p]speedevent start 20` 112 | `[p]speedevent start 30 --all` 113 | """ 114 | if ctx.guild.id in self.jobs["guilds"]: 115 | await ctx.send("There's already a speedtest event running in this guild") 116 | elif countdown and countdown > 300: 117 | await ctx.send("Exceeded time limit for countdown, Enter value less than 300 seconds") 118 | else: 119 | test = Speedevent( 120 | ctx, 121 | countdown or await self.config.guild_from_id(ctx.guild.id).time_start(), 122 | await self.config.guild_from_id(ctx.guild.id).all(), 123 | all=True if "--all" in args else False, 124 | ) 125 | self.jobs["guilds"][ctx.guild.id] = test 126 | await test.start() 127 | self.jobs["guilds"].pop(ctx.guild.id) 128 | 129 | @commands.mod_or_permissions(manage_messages=True) 130 | @speedevent.command(name="stop") 131 | async def stop_event(self, ctx): 132 | if ctx.guild.id in self.jobs["guilds"]: 133 | await self.jobs["guilds"][ctx.guild.id].stop(str(ctx.author)) 134 | else: 135 | await ctx.send("No speedevents found.") 136 | 137 | @speedevent.command() 138 | async def join(self, ctx): 139 | """Join the typing test speed event""" 140 | if ctx.guild.id in self.jobs["guilds"]: 141 | await self.jobs["guilds"][ctx.guild.id].join(ctx.author.id, ctx.author.display_name) 142 | else: 143 | await ctx.send("Event has not started yet") 144 | 145 | @typerset_check() 146 | @commands.group() 147 | async def typerset(self, ctx): 148 | """Settings for the typing speed test""" 149 | 150 | @commands.guild_only() 151 | @typerset.command() 152 | async def time(self, ctx, num: int): 153 | """Sets the time delay (in seconds) to start a speedtest event (max limit = 1000 seconds)""" 154 | if num <= 1000 and num >= 10: 155 | await self.config.guild_from_id(ctx.guild.id).time_start.set(num) 156 | await ctx.send(f"Changed delay to {num}") 157 | else: 158 | await ctx.send("The Min limit is 10 seconds\nThe Max limit is 1000 seconds") 159 | 160 | @typerset.command() 161 | async def words(self, ctx, min: int, max: int): 162 | """Sets the number of minimum and maximum number of words 163 | Range: min>0 and max<=100""" 164 | if min > 0 and max <= 100: 165 | await ( 166 | self.config.guild_from_id(ctx.guild.id).text_size.set((min, max)) 167 | if ctx.guild 168 | else self.config.user_from_id(ctx.author.id).text_size.set((min, max)) 169 | ) 170 | await ctx.send(f"The number of words are changed to\nMinimum:{min}\nMaximum:{max}") 171 | else: 172 | await ctx.send( 173 | "The minimum number of words must be greater than 0\nThe maxiumum number of words must be less than or equal to 100 " 174 | ) 175 | 176 | @commands.guild_only() 177 | @typerset.command() 178 | async def dm(self, ctx, toggle: bool): 179 | """Toggle whether the bot should send analytics in the dm or not 180 | Toggles available: false, true""" 181 | await self.config.guild_from_id(ctx.guild.id).dm.set(toggle) 182 | await ctx.send(f"I will {'' if toggle else 'not'} send the speedevent analytics in dms") 183 | 184 | @typerset.command(name="type") 185 | async def type_of_text(self, ctx, type_txt: str): 186 | """Set the type of text to generate. 187 | Types available: lorem, gibberish""" 188 | check = ("lorem", "gibberish") 189 | if type_txt in check: 190 | await ( 191 | self.config.guild_from_id(ctx.guild.id).type.set(type_txt) 192 | if ctx.guild 193 | else self.config.user_from_id(ctx.author.id).type.set(type_txt) 194 | ) 195 | await ctx.send(f"Changed type to {type_txt}") 196 | else: 197 | await ctx.send("Only two valid types available: gibberish,lorem") 198 | 199 | async def red_get_data_for_user(self, *, user_id: int): 200 | # this cog does not store any data 201 | return {} 202 | 203 | async def red_delete_data_for_user(self, *, requester, user_id: int) -> None: 204 | # this cog does not store any data 205 | pass 206 | -------------------------------------------------------------------------------- /typeracer/utils.py: -------------------------------------------------------------------------------- 1 | from difflib import ndiff 2 | from pathlib import Path 3 | from random import randint, sample 4 | 5 | from fuzzywuzzy import fuzz 6 | from redbot.core import commands 7 | from redbot.core.utils.mod import is_mod_or_superior 8 | from tabulate import tabulate 9 | 10 | # can't use bundled_data_path cause outside class 11 | path = Path(__file__).absolute().parent / "data" 12 | data = {} 13 | 14 | # https://www.mit.edu/~ecprice/wordlist.10000 15 | with open(path / "filtered.txt", "r", encoding="utf8") as f: 16 | data["gibberish"] = f.read().split() 17 | 18 | # https://raw.githubusercontent.com/ccpalettes/sublime-lorem-text/master/wordlist/word_list_fixed.txt 19 | with open(path / "lorem.txt", "r", encoding="utf8") as f: 20 | data["lorem"] = f.read().split() 21 | 22 | 23 | def typerset_check(): 24 | async def predicate(ctx): 25 | if ctx.guild is None: 26 | return True 27 | return ( 28 | # Owner check cause is_mod_or_superior doesn't respect it 29 | (ctx.author.id == ctx.bot.owner_id) 30 | # Mod or higher 31 | or (await is_mod_or_superior(ctx.bot, ctx.author)) 32 | # Guild Admin 33 | or ctx.channel.permissions_for(ctx.author).administrator 34 | ) 35 | 36 | return commands.check(predicate) 37 | 38 | 39 | async def evaluate(ctx, a_string: str, b_string: str, time_taken, dm_id, author_name=None): 40 | """Returns None on personal event, returns [time_taken,wpm,mistakes] on speedevents""" 41 | user_obj = ctx.guild.get_member(dm_id) if dm_id else ctx.author 42 | special_send = user_obj.send if dm_id else ctx.send 43 | if not author_name: 44 | author_name = ctx.author.display_name 45 | 46 | if "​" in b_string: 47 | if not dm_id: 48 | await special_send("Imagine cheating bruh, c'mon atleast be honest here.") 49 | else: 50 | await special_send("You cheated and hence you are disqualified.") 51 | return 52 | else: 53 | mistakes = 0 54 | for i, s in enumerate(ndiff(a_string, b_string)): 55 | if s[0] == " ": 56 | continue 57 | elif s[0] in ["-", "+"]: 58 | mistakes += 1 59 | # Analysis 60 | accuracy = fuzz.ratio(a_string, b_string) 61 | wpm = len(a_string) / 5 / (time_taken / 60) 62 | if accuracy > 66: # TODO add to config 63 | verdict = [ 64 | ( 65 | "WPM (Correct Words per minute)", 66 | wpm - (mistakes / (time_taken / 60)), 67 | ), 68 | ("Raw WPM (Without accounting mistakes)", wpm), 69 | ("Accuracy(Levenshtein)", accuracy), 70 | ("Words Given", len(a_string.split())), 71 | (f"Words from {author_name}", len(b_string.split())), 72 | ("Characters Given", len(a_string)), 73 | (f"Characters from {author_name}", len(b_string)), 74 | (f"Mistakes done by {author_name}", mistakes), 75 | ] 76 | await special_send(content="```" + tabulate(verdict, tablefmt="fancy_grid") + "```") 77 | return [time_taken, wpm - (mistakes / (time_taken / 60)), mistakes] 78 | else: 79 | await special_send( 80 | f"{'You' if dm_id else author_name} didn't want to complete the challenge." 81 | ) 82 | 83 | 84 | async def get_text(settings) -> tuple: 85 | """Gets the paragraph for the test""" 86 | length = randint(settings["text_size"][0], settings["text_size"][1]) 87 | a_string = " ".join(sample(data[settings["type"]], length)) + "." 88 | return (a_string, 1) 89 | 90 | 91 | def nocheats(text: str) -> str: 92 | """To catch Cheaters upto some extent""" 93 | text_list = list(text) 94 | size = len(text) 95 | for _ in range(size // 5): 96 | text_list.insert(randint(0, size), "​") 97 | return "".join(text_list) 98 | -------------------------------------------------------------------------------- /weeb/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | from .weeb import Weeb 5 | 6 | with open(Path(__file__).parent / "info.json") as fp: 7 | __red_end_user_data_statement__ = json.load(fp)["end_user_data_statement"] 8 | 9 | 10 | async def setup(bot): 11 | await bot.add_cog(Weeb(bot)) 12 | -------------------------------------------------------------------------------- /weeb/data/owo.txt: -------------------------------------------------------------------------------- 1 | OwO 2 | Owo 3 | owO 4 | ÓwÓ 5 | ÕwÕ 6 | @w@ 7 | ØwØ 8 | øwø 9 | uwu 10 | ☆w☆ 11 | ✧w✧ 12 | ♥w♥ 13 | ゜w゜ 14 | ◕w◕ 15 | ᅌwᅌ 16 | ◔w◔ 17 | ʘwʘ 18 | ⓪w⓪ ︠ 19 | ʘw ︠ʘ 20 | (owo) 21 | 𝕠𝕨𝕠 22 | 𝕆𝕨𝕆 23 | 𝔬𝔴𝔬 24 | 𝖔𝖜𝖔 25 | 𝓞𝔀𝓞 26 | 𝒪𝓌𝒪 27 | 𝐨𝐰𝐨 28 | 𝐎𝐰𝐎 29 | 𝘰𝘸𝘰 30 | 𝙤𝙬𝙤 31 | 𝙊𝙬𝙊 32 | 𝚘𝚠𝚘 33 | σωσ 34 | OɯO 35 | oʍo 36 | oᗯo 37 | ๏w๏ 38 | o̲wo̲ 39 | ᎧᏇᎧ 40 | օաօ 41 | (。O ω O。) 42 | (。O⁄ ⁄ω⁄ ⁄ O。) 43 | (O ᵕ O) 44 | (O꒳O) 45 | ღ(O꒳Oღ) 46 | ♥(。ᅌ ω ᅌ。) 47 | (ʘωʘ) 48 | ( ʘ ྌ ʘ ) 49 | (⁄ʘ⁄ ⁄ ω⁄ ⁄ ʘ⁄)♡ 50 | ( ͡o ω ͡o ) 51 | ( ͡o ᵕ ͡o ) 52 | ( ͡o ꒳ ͡o ) 53 | ( o͡ ꒳ o͡ ) 54 | ( °꒳° ) 55 | ( °ྌ° ) 56 | ( °ྌྏ° ) 57 | ( °ྌྏྏྏྏྏྏྏ° ) 58 | ( °ᵕ° ) 59 | ( ° ᳕ °) 60 | ( °﹏° ) 61 | ( °ω° ) ̷ 62 | (ⓞ̷ ̷꒳̷ ̷ⓞ̷) 63 | (⍟ ᳕ ⍟) 64 | (⦿ ᳕ ⦿) 65 | (⦾ ᳕ ⦾) 66 | ( ゜ω 。) 67 | ( 。ω ゜) 68 | OwO *𝘸𝘩𝘢𝘵’𝘴 𝘵𝘩𝘪𝘴* 69 | OwO *𝘯𝘰𝘵𝘪𝘤𝘦𝘴 𝘣𝘶𝘭𝘨𝘦* 70 | 𝐎𝐰𝐎 *𝘸𝘩𝘢𝘵’𝘴 𝘵𝘩𝘪𝘴* 71 | ๏w๏ *𝘯𝘰𝘵𝘪𝘤𝘦𝘴 𝘣𝘶𝘭𝘨𝘦* 72 | ( ͡o ꒳ ͡o )*𝔫𝔬𝔱𝔦𝔠𝔢𝔰 𝔟𝔲𝔩𝔤𝔢* 73 | *𝓌𝒶𝓉𝓈 𝒹𝒾𝓈?*ღ(O꒳Oღ) 74 | *𝓃𝓊𝓏𝓏𝓁𝑒𝓈 𝓎𝑜𝓊*(。O⁄ ⁄ω⁄ ⁄ O。) 75 | (𝐎𝐰𝐎)<𝕣𝕒𝕨𝕣𝕣𝕣)~ 76 | ‿︵*𝓇𝒶𝓌𝓇*‿︵ ʘwʘ 77 | ♥ ⑅ 𝒘𝒉𝒆𝒓𝒆 (⦿ ᳕ ⦿) 𝒓 𝒖 ? ⑅ ♥ 78 | ✼ ҉ (O꒳O) ҉ ✼ 79 | ✼ ҉♡ (。O⁄ ⁄ω⁄ ⁄ O。) ҉♡ ✼ 80 | ✧・゚: *✧・゚:*(OwO )*:・゚✧*:・゚✧ 81 | -------------------------------------------------------------------------------- /weeb/data/uwu.txt: -------------------------------------------------------------------------------- 1 | UwU 2 | Uwu 3 | uwU 4 | ÚwÚ 5 | uwu 6 | ☆w☆ 7 | ✧w✧ 8 | ♥w♥ 9 | ︠uw ︠u 10 | (uwu) 11 | (ᵘʷᵘ) 12 | (ᵘﻌᵘ) 13 | ˯˽˯ 14 | (◡ ω ◡) 15 | (◡ ꒳ ◡) 16 | (◡ w ◡) 17 | (◡ ሠ ◡) 18 | (˘ω˘) 19 | (⑅˘꒳˘) 20 | (˘ᵕ˘) 21 | (˘ሠ˘) 22 | (˘³˘) 23 | (˘ε˘) 24 | (´˘`) 25 | (´꒳`) 26 | (˘ ˘ ˘)⭜ 27 | ( ᴜ ω ᴜ ) 28 | ( ´ω` )۶ 29 | („ᵕᴗᵕ„) 30 | (*ฅ́˘ฅ̀*) 31 | (ㅅꈍ ˘ ꈍ) 32 | (⑅˘꒳˘) 33 | ( 。ᵘ ᵕ ᵘ 。) 34 | ( ᵘ ꒳ ᵘ ✼) 35 | ( ˘ᴗ˘ ) 36 | ˬ ͜ ˬ 37 | ᐡ꒳ᐡ 38 | (˯ ᵘ ꒳ ᵘ ˯) 39 | (ᵘᆸᵘ)⭜ 40 | (ᵕᴗ ᵕ⁎) 41 | *:・゚✧(ꈍᴗꈍ)✧・゚:* 42 | *˚*(ꈍ ω ꈍ).₊̣̇. 43 | (。U ω U。) 44 | (。U⁄ ⁄ω⁄ ⁄ U。) 45 | (U ᵕ U❁) 46 | (U ﹏ U) 47 | (◦ ᵕ ˘ ᵕ ◦) 48 | ღ(U꒳Uღ) 49 | ♥(。U ω U。) 50 | – ̗̀ (ᵕ꒳ᵕ) ̖́ – 51 | ಇ( ꈍᴗꈍ)ಇ 52 | ᕦ( ˘ᴗ˘ )ᕤ 53 | (⁄˘⁄ ⁄ ω⁄ ⁄ ˘⁄)♡ 54 | ( ͡U ω ͡U ) 55 | ( ͡o ᵕ ͡o ) 56 | ( ͡o ꒳ ͡o ) 57 | ( ˊ.ᴗˋ ) 58 | (灬´ᴗ`灬) 59 | [̲̅$̲̅(̲̅ ᵕ꒳ᵕ)̲̅$̲̅] 60 | ( ˶˘ ³˘(ᵕ꒳ᵕ)*₊˚♡ 61 | ⋆⛧*﹤ಇ( ᵕ꒳ᵕ)ಇ﹥*⛧⋆ 62 | ✧・゚: *✧・゚♡*(ᵘʷᵘ)*♡・゚✧*:・゚✧ 63 | ଘ(੭ ˘ ᵕ˘)━☆゚.*・。゚ᵕ꒳ᵕ~ 64 | -------------------------------------------------------------------------------- /weeb/data/xwx.txt: -------------------------------------------------------------------------------- 1 | ( ◕‿◕✿) 2 | (◕ᴗ◕✿) 3 | (◕ ﺮ ◕✿) 4 | (◕ ˬ ◕✿) 5 | (◕ˇ ◕✿) 6 | (◕◡◕✿) 7 | (◔◡◔✿) 8 | (◡‿◡✿) 9 | (◠‿◠✿) 10 | (◕ ω ◕✿) 11 | (◕ܫ◕✿) 12 | (◕▿◕✿) 13 | (◕ ワ ◕✿) 14 | (◕▽◕✿) 15 | (◕ ɔ ◕✿) 16 | (ʘ‿ʘ✿) 17 | (⓪__⓪ ✿) 18 | ♡ (ʘ ꒳ ʘ✿) 19 | (✧ᴗ✧✿) 20 | (✿◉‿◉)🗡 21 | (✿˶◉⚰︎◉˶)🗡 22 | (✿ ︣⓪ ‸ ︣⓪)っ🗡 23 | 🗡⊂(ʘ‿ʘ✿) 24 | ミ=͟͟͞͞(✿ʘ ᴗʘ)っ🗡 25 | =͟͟͞͞(✿⓪ ڡ ⓪ )🔪 26 | (✿ʘ‿ʘ)✂╰⋃╯ 27 | Huuu━━(◕言 ◕✿)━━uuh? 28 | (≖ ︿ ≖ ✿) 29 | (≖ ︿ ≖ ✿)ꐦꐦ 30 | (≖ ‸ ≖ ✿) 31 | (≖ ˆ ≖ ✿) 32 | (≖ Δ ≖ ✿) 33 | ꉂ `≖ o ≖´ ✿ ) 34 | (ಸ ︿ ಸ ✿) 35 | +゚*。:゚+凸(◕‿◕✿)+゚*。:゚+ 36 | (╯✿◕益◕)╯︵ ┻━┻ 37 | -------------------------------------------------------------------------------- /weeb/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": [ 3 | "epic guy#0715" 4 | ], 5 | "name": "Weeb", 6 | "short": "Bunch of weeb emoticons OwO", 7 | "description": "a random bunch of emoticons for the otakus", 8 | "install_msg": "This cog comes with bundled data", 9 | "tags": [ 10 | "emoticons", 11 | "emojis", 12 | "weeb", 13 | "fun" 14 | ], 15 | "end_user_data_statement": "This cog does not persistently store data or metadata about users." 16 | } 17 | -------------------------------------------------------------------------------- /weeb/weeb.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | from random import choice 3 | 4 | import discord 5 | from discord import NotFound 6 | from redbot.core import commands, data_manager 7 | 8 | 9 | class Weeb(commands.Cog): 10 | """Set of weeby commands to show your otaku-ness\n 11 | you can use 'c' as an additional argument for deleting your message 12 | Eg: `[p]uwu c`""" 13 | 14 | def __init__(self, bot): 15 | self.bot = bot 16 | with open(data_manager.bundled_data_path(self) / "owo.txt", "r", encoding="utf8") as f: 17 | self.owo = f.read().splitlines() 18 | with open(data_manager.bundled_data_path(self) / "uwu.txt", "r", encoding="utf8") as f: 19 | self.uwu = f.read().splitlines() 20 | with open(data_manager.bundled_data_path(self) / "xwx.txt", "r", encoding="utf8") as f: 21 | self.xwx = f.read().splitlines() 22 | 23 | @commands.command() 24 | async def uwu(self, ctx, option: str = None): 25 | """Replies with UwU variant emoticons\n 26 | `[p]uwu c` - deletes your message""" 27 | if option == "c": 28 | if ctx.channel.permissions_for(ctx.me).manage_messages: 29 | with contextlib.suppress(NotFound): 30 | await ctx.message.delete() 31 | else: 32 | raise commands.BotMissingPermissions(discord.Permissions(manage_messages=True)) 33 | await ctx.send(choice(self.uwu)) 34 | 35 | @commands.command() 36 | async def owo(self, ctx, option: str = None): 37 | """Replies with OwO variant emoticons 38 | `[p]owo c` - deletes your message""" 39 | if option == "c": 40 | if ctx.channel.permissions_for(ctx.me).manage_messages: 41 | with contextlib.suppress(NotFound): 42 | await ctx.message.delete() 43 | else: 44 | raise commands.BotMissingPermissions(discord.Permissions(manage_messages=True)) 45 | await ctx.send(choice(self.owo)) 46 | 47 | @commands.command() 48 | async def xwx(self, ctx, option: str = None): 49 | """Replies with flower girl/yandere girl 50 | `[p]xwx c` - deletes your message""" 51 | if option == "c": 52 | if ctx.channel.permissions_for(ctx.me).manage_messages: 53 | with contextlib.suppress(NotFound): 54 | await ctx.message.delete() 55 | else: 56 | raise commands.BotMissingPermissions(discord.Permissions(manage_messages=True)) 57 | await ctx.send(choice(self.xwx)) 58 | 59 | async def red_get_data_for_user(self, *, user_id: int): 60 | # this cog does not store any data 61 | return {} 62 | 63 | async def red_delete_data_for_user(self, *, requester, user_id: int) -> None: 64 | # this cog does not store any data 65 | pass 66 | --------------------------------------------------------------------------------