├── .build_docs.sh ├── .flake8 ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yaml └── workflows │ └── ci.yml ├── .gitignore ├── .linter.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── __init__.py ├── administration ├── __init__.py ├── permissions │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── permissions.py │ └── translations │ │ └── en.yml ├── roles │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── models.py │ ├── permissions.py │ └── translations │ │ └── en.yml ├── settings │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── permissions.py │ └── translations │ │ └── en.yml └── sudo │ ├── __init__.py │ ├── cog.py │ ├── documentation.md │ ├── permissions.py │ └── translations │ └── en.yml ├── contributor.py ├── general ├── __init__.py ├── betheprofessional │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── models.py │ ├── permissions.py │ └── translations │ │ └── en.yml ├── custom_commands │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── models.py │ ├── permissions.py │ └── translations │ │ └── en.yml ├── discord_bot_token_deleter │ ├── __init__.py │ ├── cog.py │ ├── documentation.md │ └── translations │ │ └── en.yml ├── news │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── models.py │ ├── permissions.py │ └── translations │ │ └── en.yml ├── polls │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── permissions.py │ └── translations │ │ └── en.yml ├── reactionpin │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── models.py │ ├── permissions.py │ ├── settings.py │ └── translations │ │ └── en.yml ├── reactionrole │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── models.py │ ├── permissions.py │ └── translations │ │ └── en.yml ├── remind_me │ ├── __init__.py │ ├── cog.py │ ├── documentation.md │ └── translations │ │ └── en.yml ├── utils │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── permissions.py │ └── translations │ │ └── en.yml └── voice_channel │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── models.py │ ├── names │ ├── computer_science_pioneers.txt │ ├── data_structures.txt │ ├── elements.txt │ ├── famous_mathematicians.txt │ ├── fields_medalists.txt │ ├── greek_alphabet.txt │ ├── greek_alphabet_names.txt │ ├── greek_gods_olymp.txt │ ├── hex.txt │ ├── intel_codenames.txt │ ├── nobelprize_physics.txt │ ├── np_complete_problems.txt │ ├── programming_languages.txt │ ├── roman_gods_olymp.txt │ ├── spaces.txt │ └── turing_award.txt │ ├── permissions.py │ └── translations │ └── en.yml ├── information ├── __init__.py ├── bot_info │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ └── translations │ │ └── en.yml ├── heartbeat │ ├── __init__.py │ ├── cog.py │ ├── documentation.md │ └── translations │ │ └── en.yml ├── help │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ └── translations │ │ └── en.yml ├── inactivity │ ├── __init__.py │ ├── cog.py │ ├── documentation.md │ ├── models.py │ ├── permissions.py │ ├── settings.py │ └── translations │ │ └── en.yml ├── server_info │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ └── translations │ │ └── en.yml └── user_info │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── models.py │ ├── permissions.py │ └── translations │ └── en.yml ├── integrations ├── __init__.py ├── adventofcode │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── models.py │ ├── permissions.py │ ├── settings.py │ └── translations │ │ └── en.yml ├── cleverbot │ ├── __init__.py │ ├── api.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── models.py │ ├── permissions.py │ └── translations │ │ └── en.yml ├── python_docs │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ └── translations │ │ └── en.yml ├── reddit │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── models.py │ ├── permissions.py │ ├── settings.py │ └── translations │ │ └── en.yml └── run_code │ ├── __init__.py │ ├── api.py │ ├── cog.py │ ├── documentation.md │ └── translations │ └── en.yml ├── moderation ├── __init__.py ├── autoclear │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── models.py │ ├── permissions.py │ └── translations │ │ └── en.yml ├── automod │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── permissions.py │ ├── settings.py │ └── translations │ │ └── en.yml ├── autorole │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── models.py │ ├── permissions.py │ └── translations │ │ └── en.yml ├── content_filter │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── models.py │ ├── permissions.py │ └── translations │ │ └── en.yml ├── invites │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── models.py │ ├── permissions.py │ └── translations │ │ └── en.yml ├── logging │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── models.py │ ├── permissions.py │ ├── settings.py │ └── translations │ │ └── en.yml ├── mediaonly │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── models.py │ ├── permissions.py │ └── translations │ │ └── en.yml ├── message │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── permissions.py │ └── translations │ │ └── en.yml ├── mod │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── models.py │ ├── permissions.py │ └── translations │ │ └── en.yml ├── role_notifications │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── models.py │ ├── permissions.py │ └── translations │ │ └── en.yml ├── spam_detection │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── permissions.py │ ├── settings.py │ └── translations │ │ └── en.yml ├── threads │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── permissions.py │ └── translations │ │ └── en.yml ├── user_notes │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── models.py │ ├── permissions.py │ └── translations │ │ └── en.yml └── verification │ ├── __init__.py │ ├── cog.py │ ├── colors.py │ ├── documentation.md │ ├── models.py │ ├── permissions.py │ ├── settings.py │ └── translations │ └── en.yml ├── poetry.lock ├── pre-commit.sh ├── pubsub.md ├── pubsub.py ├── pyproject.toml └── translations.py /.build_docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | repo=$(mktemp -d) 4 | git clone --recursive https://github.com/PyDrocsid/documentation.git $repo 5 | rm -rf $repo/cogs 6 | mkdir $repo/cogs 7 | cp -r * $repo/cogs/ 8 | pushd $repo 9 | 10 | ./pages_build.sh 11 | 12 | popd 13 | mv $repo/site . 14 | rm -rf $repo 15 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | ignore = C812,C813,C815,C816,Q000,RST210,RST213,RST215,RST304,W503 4 | extend-ignore = D,E800,S311,WPS 5 | extend-exclude = .venv 6 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @Defelo 2 | translations/ @PyDrocsid/code-reviewer @PyDrocsid/text-reviewer 3 | /custom_commands/ @PyDrocsid/code-reviewer @PyDrocsid/text-reviewer 4 | *.md @PyDrocsid/text-reviewer 5 | *.py @PyDrocsid/code-reviewer 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Your Configuration (please complete the following information):** 27 | - Bot Information (name, link to repository, exact version): 28 | - Hosting Method (with/without Pipenv/Docker): 29 | - Database (MySQL/MariaDB/PostreSQL): 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. E.g. "I am always frustrated when [...]" 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Description** 2 | Provide a description of what your changes do. 3 | 4 | **Issue** 5 | Closes #XX 6 | 7 | **Additional Notes** 8 | Add any other context about the pull request here. 9 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | 8 | updates: 9 | - package-ecosystem: "github-actions" 10 | # Workflow files stored in the default location of `.github/workflows` 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | reviewers: 15 | - "Defelo" 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 | 5 | [![CI](https://github.com/PyDrocsid/cogs/actions/workflows/ci.yml/badge.svg)](https://github.com/PyDrocsid/cogs/actions/workflows/ci.yml) 6 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 7 | [![Maintainability](https://api.codeclimate.com/v1/badges/58eddbda41411d6e4876/maintainability)](https://codeclimate.com/github/PyDrocsid/cogs/maintainability) 8 | [![Discord](https://img.shields.io/discord/637234990404599809.svg?label=Discord&logo=discord&logoColor=ffffff&color=7389D8)](https://pydrocsid.defelo.de/discord) 9 | [![Matrix](https://img.shields.io/matrix/pydrocsid:matrix.defelo.de.svg?label=Matrix&logo=matrix&logoColor=ffffff&color=4db798)](https://pydrocsid.defelo.de/matrix) 10 | 11 |

12 | 13 | 14 | # PyDrocsid Cog Library 15 | 16 | The official cog library used by PyDrocsid bots 17 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from . import translations as _ # noqa 2 | -------------------------------------------------------------------------------- /administration/__init__.py: -------------------------------------------------------------------------------- 1 | from .permissions import PermissionsCog 2 | from .roles import RolesCog 3 | from .settings import SettingsCog 4 | from .sudo import SudoCog 5 | 6 | 7 | __all__ = ["PermissionsCog", "RolesCog", "SettingsCog", "SudoCog"] 8 | -------------------------------------------------------------------------------- /administration/permissions/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import PermissionsCog 2 | 3 | 4 | __all__ = ["PermissionsCog"] 5 | -------------------------------------------------------------------------------- /administration/permissions/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | Permissions = MaterialColors.blue["a700"] 6 | -------------------------------------------------------------------------------- /administration/permissions/documentation.md: -------------------------------------------------------------------------------- 1 | # Permissions 2 | 3 | 4 | *Work in Progress* 5 | -------------------------------------------------------------------------------- /administration/permissions/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class PermissionsPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.permissions.permissions[self.name] 11 | 12 | view_own = auto() 13 | view_all = auto() 14 | manage = auto() 15 | -------------------------------------------------------------------------------- /administration/permissions/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | view_own: view own permissions 3 | view_all: view all permissions 4 | manage: manage permissions 5 | 6 | commands: 7 | permissions: manage bot permissions 8 | permissions_list: list all permissions 9 | permissions_my: list all permissions granted to the user 10 | permissions_set: configure bot permissions 11 | permissions_permission_levels: list all permission levels 12 | 13 | no_permissions: No priviliges granted. 14 | invalid_permission_level: Invalid permission level. 15 | invalid_permission: Invalid permission. 16 | cannot_manage_permission_level: You are not allowed to manage this permission level. 17 | permission_set: "Permission `{}` has been granted to `>= {}` :white_check_mark:" 18 | log_permission_set: "**Permission** `{}` has been **granted** to `>= {}` :white_check_mark:" 19 | permissions_title: Permissions 20 | my_permissions_title: My Permissions 21 | permission_levels: Permission Levels 22 | aliases: "Aliases: {}" 23 | level: "Level: `{}`" 24 | granted_by: "Granted by:" 25 | -------------------------------------------------------------------------------- /administration/roles/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import RolesCog 2 | 3 | 4 | __all__ = ["RolesCog"] 5 | -------------------------------------------------------------------------------- /administration/roles/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | Roles = MaterialColors.blue["a700"] 6 | MissingPermissions = MaterialColors.yellow["a700"] 7 | -------------------------------------------------------------------------------- /administration/roles/documentation.md: -------------------------------------------------------------------------------- 1 | # Roles 2 | 3 | 4 | *Work in Progress* 5 | -------------------------------------------------------------------------------- /administration/roles/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Union 4 | 5 | from sqlalchemy import BigInteger, Boolean, Column 6 | 7 | from PyDrocsid.database import Base, db, select 8 | 9 | 10 | class RoleAuth(Base): 11 | __tablename__ = "role_auth" 12 | 13 | source: Union[Column, int] = Column(BigInteger, primary_key=True) 14 | target: Union[Column, int] = Column(BigInteger, primary_key=True) 15 | perma_allowed: Union[Column, bool] = Column(Boolean()) 16 | 17 | @staticmethod 18 | async def add(source: int, target: int, perma_allowed: bool): 19 | await db.add(RoleAuth(source=source, target=target, perma_allowed=perma_allowed)) 20 | 21 | @staticmethod 22 | async def check(source: int, target: int) -> bool: 23 | return await db.exists(select(RoleAuth).filter_by(source=source, target=target)) 24 | 25 | 26 | class PermaRole(Base): 27 | __tablename__ = "perma_role" 28 | 29 | member_id: Union[Column, int] = Column(BigInteger, primary_key=True) 30 | role_id: Union[Column, int] = Column(BigInteger, primary_key=True) 31 | 32 | @staticmethod 33 | async def add(member_id: int, role_id: int): 34 | await db.add(PermaRole(member_id=member_id, role_id=role_id)) 35 | -------------------------------------------------------------------------------- /administration/roles/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class RolesPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.roles.permissions[self.name] 11 | 12 | config_read = auto() 13 | config_write = auto() 14 | auth_read = auto() 15 | auth_write = auto() 16 | list_members = auto() 17 | roles_clone = auto() 18 | -------------------------------------------------------------------------------- /administration/roles/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | config_read: read role configuration 3 | config_write: write role configuration 4 | auth_read: read role assignment permissions 5 | auth_write: write role assignment permissions 6 | list_members: list role members 7 | roles_clone: clones existing roles 8 | 9 | commands: 10 | roles: manage roles 11 | roles_config: role configuration 12 | roles_auth: configure role assignment authorizations 13 | roles_auth_add: add a new role assignment authorization 14 | roles_auth_remove: remove a role assignment authorization 15 | roles_add: assign a role to a member 16 | roles_clone: clone an existing role 17 | roles_remove: remove a role from a member 18 | roles_perma_add: assign a role permanently to a member 19 | roles_perma_unset: remove the perma flag from a member role 20 | roles_perma_remove: remove a perma role from a member 21 | roles_list: list all members with a specific role 22 | roles_perma_list: list all permanent role assignments 23 | 24 | roles: Roles 25 | configure_role: configure {} role 26 | role_not_set: ":x: Role not set." 27 | role_set: "Role has been set successfully. :white_check_mark:" 28 | log_role_set: "**{} role** has been **set to** {} ({})" 29 | 30 | role_auth: Role Assignment Authorizations 31 | no_role_auth: No role assignment authorizations have been created yet. 32 | role_auths: Role Authorizations 33 | user_auths: User Authorizations 34 | no_auth_for_bots: You cannot authorize bots to assign roles. 35 | role_auth_already_exists: Role assignment authorization already exists. 36 | role_auth_created: Role assignment authorization has been created successfully. 37 | log_role_auth_created: Role assignment authorization `@{}` -> `@{}` has been created. 38 | role_auth_not_found: Role assignment authorization does not exist. 39 | role_auth_removed: Role assignment authorization has been removed successfully. 40 | log_role_auth_removed: Role assignment authorization `@{}` -> `@{}` has been removed. 41 | role_not_authorized: You are not allowed to manage this role. 42 | member_list: Member List 43 | member_list_cnt: 44 | one: "Member List of @{} ({cnt} Member)" 45 | many: "Member List of @{} ({cnt} Members)" 46 | member_list_line: ":small_orange_diamond: {} ({})" 47 | no_members: No member was found with this role. 48 | role_already_assigned: Role has already been assigned to this member. 49 | role_not_assigned: Role has not yet been assigned to this member. 50 | added_perma_role: Role {} has been permanently assigned to {} ({}). 51 | removed_perma_role: Perma role {} has been removed from {} ({}). 52 | cannot_remove_perma: This role has been permanently assigned to this member and can therefore only be removed by using the `{}roles perma_remove` command. 53 | perma_reassigned: Perma role {} has been reassigned to {} ({}). If you want to remove this role, please use the `{}roles perma_remove` command. 54 | no_perma_roles: No permanent role assignments. 55 | perma_roles: Permanent Role Assignments 56 | could_not_reassign: Could not reassign perma role {} to {} ({}). 57 | clone_no_permission: I cannot clone this role because I do not have `manage_roles` permission on this server. 58 | failed_to_clone_role_permissions: ":warning: Could not clone following permissions:" 59 | -------------------------------------------------------------------------------- /administration/settings/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import SettingsCog 2 | 3 | 4 | __all__ = ["SettingsCog"] 5 | -------------------------------------------------------------------------------- /administration/settings/cog.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | from discord import Embed 4 | from discord.ext import commands 5 | from discord.ext.commands import CommandError, Context, guild_only 6 | 7 | from PyDrocsid.cog import Cog 8 | from PyDrocsid.command import docs, reply 9 | from PyDrocsid.prefix import set_prefix 10 | from PyDrocsid.translations import t 11 | 12 | from .colors import Colors 13 | from .permissions import SettingsPermission 14 | from ...contributor import Contributor 15 | from ...pubsub import send_to_changelog 16 | 17 | 18 | tg = t.g 19 | t = t.settings 20 | 21 | 22 | class SettingsCog(Cog, name="Settings"): 23 | CONTRIBUTORS = [Contributor.Defelo] 24 | 25 | @commands.command(name="prefix") 26 | @SettingsPermission.change_prefix.check 27 | @guild_only() 28 | @docs(t.commands.change_prefix) 29 | async def change_prefix(self, ctx: Context, *, new_prefix: str): 30 | if not 0 < len(new_prefix) <= 16: 31 | raise CommandError(t.invalid_prefix_length) 32 | 33 | valid_chars = set(string.ascii_letters + string.digits + string.punctuation) - {"`"} 34 | if any(c not in valid_chars for c in new_prefix): 35 | raise CommandError(t.prefix_invalid_chars) 36 | 37 | await set_prefix(new_prefix) 38 | embed = Embed(title=t.prefix, description=t.prefix_updated, colour=Colors.prefix) 39 | await reply(ctx, embed=embed) 40 | await send_to_changelog(ctx.guild, t.log_prefix_updated(new_prefix)) 41 | -------------------------------------------------------------------------------- /administration/settings/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | prefix = MaterialColors.indigo 6 | -------------------------------------------------------------------------------- /administration/settings/documentation.md: -------------------------------------------------------------------------------- 1 | # Settings 2 | 3 | Contains commands to change various settings of the bot. 4 | 5 | 6 | ## `prefix` 7 | 8 | Changes the bot prefix. Any message containing a command has to start with this prefix, directly followed by the command itself. When messaging the bot directly you can (but don't have to) omit the prefix. 9 | 10 | ```css 11 | .prefix 12 | ``` 13 | 14 | Arguments: 15 | 16 | | Argument | Required | Description | 17 | |:------------:|:-------------------------:|:---------------| 18 | | `new_prefix` | :fontawesome-solid-check: | The new prefix | 19 | 20 | Required Permissions: 21 | 22 | - `settings.change_prefix` 23 | 24 | !!! note 25 | - The prefix cannot contain more than 16 characters. 26 | - Only alphanumeric and punctuation characters are allowed. 27 | -------------------------------------------------------------------------------- /administration/settings/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class SettingsPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.settings.permissions[self.name] 11 | 12 | change_prefix = auto() 13 | -------------------------------------------------------------------------------- /administration/settings/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | change_prefix: change bot prefix 3 | 4 | commands: 5 | change_prefix: change the bot prefix 6 | 7 | prefix: Prefix 8 | invalid_prefix_length: Length of prefix must be between 1 and 16 9 | prefix_invalid_chars: Prefix contains invalid characters. 10 | prefix_updated: "Prefix has been updated. :white_check_mark:" 11 | log_prefix_updated: "**Bot prefix** has been **changed** to `{}`" 12 | -------------------------------------------------------------------------------- /administration/sudo/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import SudoCog 2 | 3 | 4 | __all__ = ["SudoCog"] 5 | -------------------------------------------------------------------------------- /administration/sudo/cog.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from discord import Message, TextChannel 4 | from discord.ext import commands 5 | from discord.ext.commands import CheckFailure, Context, check 6 | 7 | from PyDrocsid.cog import Cog 8 | from PyDrocsid.config import Config 9 | from PyDrocsid.emojis import name_to_emoji 10 | from PyDrocsid.environment import OWNER_ID 11 | from PyDrocsid.events import call_event_handlers 12 | from PyDrocsid.permission import permission_override 13 | from PyDrocsid.redis import redis 14 | from PyDrocsid.translations import t 15 | 16 | from .permissions import SudoPermission 17 | from ...contributor import Contributor 18 | 19 | 20 | tg = t.g 21 | t = t.sudo 22 | 23 | 24 | @check 25 | def is_sudoer(ctx: Context) -> bool: 26 | if ctx.author.id != OWNER_ID: 27 | raise CheckFailure(t.not_in_sudoers_file(ctx.author.mention)) 28 | 29 | return True 30 | 31 | 32 | class SudoCog(Cog, name="Sudo"): 33 | CONTRIBUTORS = [Contributor.Defelo, Contributor.TNT2k] 34 | 35 | def __init__(self): 36 | super().__init__() 37 | 38 | self.sudo_cache: dict[TextChannel, Message] = {} 39 | 40 | @staticmethod 41 | def prepare(): 42 | return bool(OWNER_ID) 43 | 44 | async def on_command_error(self, ctx: Context, _): 45 | if ctx.author.id == OWNER_ID: 46 | self.sudo_cache[ctx.channel] = ctx.message 47 | 48 | @commands.command(hidden=True) 49 | @is_sudoer 50 | async def sudo(self, ctx: Context, *, cmd: str): 51 | message: Message = ctx.message 52 | message.content = ctx.prefix + cmd 53 | 54 | if cmd == "!!" and ctx.channel in self.sudo_cache: 55 | message.content = self.sudo_cache.pop(ctx.channel).content 56 | 57 | permission_override.set(Config.PERMISSION_LEVELS.max()) 58 | await self.bot.process_commands(message) 59 | 60 | @commands.command() 61 | @SudoPermission.clear_cache.check 62 | async def clear_cache(self, ctx: Context): 63 | await redis.flushdb() 64 | await ctx.message.add_reaction(name_to_emoji["white_check_mark"]) 65 | 66 | @commands.command() 67 | @SudoPermission.reload.check 68 | async def reload(self, ctx: Context): 69 | await call_event_handlers("ready") 70 | await ctx.message.add_reaction(name_to_emoji["white_check_mark"]) 71 | 72 | @commands.command() 73 | @SudoPermission.stop.check 74 | async def stop(self, ctx: Context): 75 | await ctx.message.add_reaction(name_to_emoji["white_check_mark"]) 76 | await self.bot.close() 77 | 78 | @commands.command() 79 | @SudoPermission.kill.check 80 | async def kill(self, ctx: Context): 81 | await ctx.message.add_reaction(name_to_emoji["white_check_mark"]) 82 | sys.exit(1) 83 | -------------------------------------------------------------------------------- /administration/sudo/documentation.md: -------------------------------------------------------------------------------- 1 | # Sudo 2 | 3 | Contains the `.sudo` command, as well as some other commands used to maintain the bot instance. 4 | 5 | 6 | ## `sudo` 7 | 8 | Allows a specific user to execute any command even without having the necessary permission level by temporarily granting the user the highest permission level (similar to the `sudo` command on Linux). 9 | 10 | ```css 11 | .sudo 12 | ``` 13 | 14 | Arguments: 15 | 16 | | Argument | Required | Description | 17 | |:---------:|:-------------------------:|:--------------------------------------------:| 18 | | `command` | :fontawesome-solid-check: | The command to execute with owner privileges | 19 | 20 | !!! note 21 | To use this command your user ID has to match the value of the `OWNER_ID` environment variable. If this environment variable is not set, the Sudo cog is disabled. 22 | 23 | !!! Hint 24 | If you have run a command without having the required permission level, you can use `.sudo !!` to rerun this command with `owner` privileges. 25 | 26 | 27 | ## Maintenance Commands 28 | 29 | !!! note 30 | These commands do not necessarily have to be executed with the `.sudo` command. Theoretically, the required permission levels can be changed to any other permission level, so that users who are not allowed to execute the `.sudo` command can also use these maintenance commands. However, it is recommended to only allow trusted users to use these commands. 31 | 32 | 33 | ### `clear_cache` 34 | 35 | Clears the redis cache by executing the `FLUSHDB` command. 36 | 37 | ```css 38 | .clear_cache 39 | ``` 40 | 41 | Required Permissions: 42 | 43 | - `sudo.clear_cache` 44 | 45 | 46 | ### `reload` 47 | 48 | Reloads the bot by refiring all startup functions. 49 | 50 | ```css 51 | .reload 52 | ``` 53 | 54 | Required Permissions: 55 | 56 | - `sudo.reload` 57 | 58 | 59 | ### `stop` 60 | 61 | Stops the running bot instance gracefully. 62 | 63 | ```css 64 | .stop 65 | ``` 66 | 67 | Required Permissions: 68 | 69 | - `sudo.stop` 70 | 71 | 72 | ### `kill` 73 | 74 | Kills the running bot instance. 75 | 76 | ```css 77 | .kill 78 | ``` 79 | 80 | Required Permissions: 81 | 82 | - `sudo.kill` 83 | -------------------------------------------------------------------------------- /administration/sudo/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class SudoPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.sudo.permissions[self.name] 11 | 12 | clear_cache = auto() 13 | reload = auto() 14 | stop = auto() 15 | kill = auto() 16 | -------------------------------------------------------------------------------- /administration/sudo/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | clear_cache: clear redis cache 3 | reload: reload the bot 4 | stop: stop the bot 5 | kill: kill the bot 6 | 7 | not_in_sudoers_file: "{} is not in the sudoers file. This incident will be reported." 8 | -------------------------------------------------------------------------------- /contributor.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.config import Contributor as ContributorBase 2 | 3 | 4 | class Contributor(ContributorBase): 5 | Scriptim = (137906177487929344, "MDQ6VXNlcjEzMDAzOTI0") 6 | Florian = (541341790176018432, "MDQ6VXNlcjU0Njg5Mzc0") 7 | AdriBloober = (330148908531580928, "MDQ6VXNlcjQ1MzIxMTA3") 8 | pohlium = (None, "MDQ6VXNlcjI0NDgwNTc0") 9 | Anorak = (202370816384565248, "MDQ6VXNlcjMyNDg3NzYw") 10 | LoC = (519559136237715486, "MDQ6VXNlcjY0MDk1MjUz") 11 | Tristan = (277069925678317568, "MDQ6VXNlcjQ1MzMwNjY3") 12 | Tert0 = (621330363167539210, "MDQ6VXNlcjYyMDM2NDY0") 13 | MarcelCoding = (398895973490884608, "MDQ6VXNlcjM0ODE5NTI0") 14 | Infinity = (846009958062358548, "MDQ6VXNlcjgzODgzODQ5") 15 | Felux = (206815202375761920, "MDQ6VXNlcjMyNzQ5NzE0") 16 | hackandcode = (301823559737671680, "MDQ6VXNlcjI3Njc5Mjc0") 17 | -------------------------------------------------------------------------------- /general/__init__.py: -------------------------------------------------------------------------------- 1 | from .betheprofessional import BeTheProfessionalCog 2 | from .custom_commands import CustomCommandsCog 3 | from .discord_bot_token_deleter import DiscordBotTokenDeleterCog 4 | from .news import NewsCog 5 | from .polls import PollsCog 6 | from .reactionpin import ReactionPinCog 7 | from .reactionrole import ReactionRoleCog 8 | from .remind_me import RemindMeCog 9 | from .utils import UtilsCog 10 | from .voice_channel import VoiceChannelCog 11 | 12 | 13 | __all__ = [ 14 | "BeTheProfessionalCog", 15 | "CustomCommandsCog", 16 | "DiscordBotTokenDeleterCog", 17 | "NewsCog", 18 | "PollsCog", 19 | "ReactionPinCog", 20 | "ReactionRoleCog", 21 | "RemindMeCog", 22 | "UtilsCog", 23 | "VoiceChannelCog", 24 | ] 25 | -------------------------------------------------------------------------------- /general/betheprofessional/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import BeTheProfessionalCog 2 | 3 | 4 | __all__ = ["BeTheProfessionalCog"] 5 | -------------------------------------------------------------------------------- /general/betheprofessional/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | BeTheProfessional = MaterialColors.yellow["a200"] 6 | -------------------------------------------------------------------------------- /general/betheprofessional/documentation.md: -------------------------------------------------------------------------------- 1 | # BeTheProfessional 2 | 3 | Contains a system for self-assignable roles (further referred to as `topics`). 4 | 5 | 6 | ## `?` (list topics) 7 | 8 | Lists all available topics. 9 | 10 | ```css 11 | .? 12 | ``` 13 | 14 | 15 | ## `+` (assign topics) 16 | 17 | Assigns the user the specified topics. 18 | 19 | ```css 20 | .+ 21 | ``` 22 | 23 | Arguments: 24 | 25 | | Argument | Required | Description | 26 | |:--------:|:-------------------------:|:---------------------------------------------| 27 | | `topics` | :fontawesome-solid-check: | One or more topics (separated by `,` or `;`) | 28 | 29 | 30 | ## `-` (unassign topics) 31 | 32 | Unassigns the user the specified topics. 33 | 34 | ```css 35 | .- 36 | ``` 37 | 38 | Arguments: 39 | 40 | | Argument | Required | Description | 41 | |:--------:|:-------------------------:|:---------------------------------------------| 42 | | `topics` | :fontawesome-solid-check: | One or more topics (separated by `,` or `;`) | 43 | 44 | !!! hint 45 | You can use `.- *` to remove all topics at once. 46 | 47 | 48 | ## `*` (register topics) 49 | 50 | Adds new topics to the list of available topics. For each topic a new role will be created if there is no role with the same name yet (case insensitive). 51 | 52 | ```css 53 | .* 54 | ``` 55 | 56 | Arguments: 57 | 58 | | Argument | Required | Description | 59 | |:--------:|:-------------------------:|:---------------------------------------------| 60 | | `topics` | :fontawesome-solid-check: | One or more topics (separated by `,` or `;`) | 61 | 62 | Required Permissions: 63 | 64 | - `betheprofessional.manage` 65 | 66 | 67 | ## `/` (delete topics) 68 | 69 | Removes topics from the list of available topics and deletes the associated roles. 70 | 71 | ```css 72 | ./ 73 | ``` 74 | 75 | Arguments: 76 | 77 | | Argument | Required | Description | 78 | |:--------:|:-------------------------:|:---------------------------------------------| 79 | | `topics` | :fontawesome-solid-check: | One or more topics (separated by `,` or `;`) | 80 | 81 | Required Permissions: 82 | 83 | - `betheprofessional.manage` 84 | 85 | 86 | ## `%` (unregister topics) 87 | 88 | Unregisters topics without deleting the associated roles. 89 | 90 | ```css 91 | .% 92 | ``` 93 | 94 | Arguments: 95 | 96 | | Argument | Required | Description | 97 | |:--------:|:-------------------------:|:---------------------------------------------| 98 | | `topics` | :fontawesome-solid-check: | One or more topics (separated by `,` or `;`) | 99 | 100 | Required Permissions: 101 | 102 | - `betheprofessional.manage` 103 | -------------------------------------------------------------------------------- /general/betheprofessional/models.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from sqlalchemy import BigInteger, Column 4 | 5 | from PyDrocsid.database import Base, db 6 | 7 | 8 | class BTPRole(Base): 9 | __tablename__ = "btp_role" 10 | 11 | role_id: Union[Column, int] = Column(BigInteger, primary_key=True, unique=True) 12 | 13 | @staticmethod 14 | async def create(role_id: int) -> "BTPRole": 15 | row = BTPRole(role_id=role_id) 16 | await db.add(row) 17 | return row 18 | -------------------------------------------------------------------------------- /general/betheprofessional/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class BeTheProfessionalPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.betheprofessional.permissions[self.name] 11 | 12 | manage = auto() 13 | -------------------------------------------------------------------------------- /general/betheprofessional/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | manage: manage betheprofessional roles 3 | 4 | # betheprofessional 5 | betheprofessional: BeTheProfessional 6 | youre_not_the_first_one: "Topic `{}` not found.\nYou're not the first one to try this, {}" 7 | topic_not_found: Topic `{}` not found. 8 | topic_not_found_did_you_mean: Topic `{}` not found. Did you mean `{}`? 9 | available_topics_header: "Available Topics" 10 | no_topics_registered: No topics have been registered yet. 11 | 12 | topics_added: 13 | zero: No topic has been added. 14 | one: "Topic has been added successfully. :white_check_mark:" 15 | many: "{cnt} topics have been added successfully. :white_check_mark:" 16 | 17 | topics_removed: 18 | zero: No topic has been removed. 19 | one: "Topic has been removed successfully. :white_check_mark:" 20 | many: "{cnt} topics have been removed successfully. :white_check_mark:" 21 | 22 | topic_invalid_chars: Topic name `{}` contains invalid characters. 23 | topic_too_long: Topic name `{}` is too long. 24 | topic_already_registered: Topic `{}` has already been registered. 25 | topic_not_registered: Topic `{}` has not been registered. 26 | topic_not_registered_too_high: Topic could not be registered because `@{}` is higher than `@{}`. 27 | topic_not_registered_managed_role: Topic could not be registered because `@{}` cannot be assigned manually. 28 | 29 | topics_registered: 30 | one: "Topic has been registered successfully. :white_check_mark:" 31 | many: "{cnt} topics have been registered successfully. :white_check_mark:" 32 | log_topics_registered: 33 | one: "The new **topic** {topics} has been **registered**." 34 | many: "{cnt} **topics** have been **registered**: {topics}" 35 | 36 | topics_unregistered: 37 | one: "Topic has been deleted successfully. :white_check_mark:" 38 | many: "{cnt} topics have been deleted successfully. :white_check_mark:" 39 | log_topics_unregistered: 40 | one: "The **topic** {topics} has been **removed**." 41 | many: "{cnt} **topics** have been **removed**: {topics}" 42 | -------------------------------------------------------------------------------- /general/custom_commands/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import CustomCommandsCog 2 | 3 | 4 | __all__ = ["CustomCommandsCog"] 5 | -------------------------------------------------------------------------------- /general/custom_commands/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | CustomCommands = MaterialColors.teal[400] 6 | -------------------------------------------------------------------------------- /general/custom_commands/documentation.md: -------------------------------------------------------------------------------- 1 | # Custom Commands 2 | 3 | 4 | *Work in Progress* 5 | -------------------------------------------------------------------------------- /general/custom_commands/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Optional, Union 4 | from uuid import uuid4 5 | 6 | from sqlalchemy import BigInteger, Boolean, Column, ForeignKey, Integer, String, Text 7 | from sqlalchemy.orm import relationship 8 | 9 | from PyDrocsid.database import Base, db 10 | from PyDrocsid.permission import BasePermissionLevel 11 | 12 | 13 | class CustomCommand(Base): 14 | __tablename__ = "custom_command" 15 | 16 | id: Union[Column, str] = Column(String(36), primary_key=True, unique=True) 17 | name: Union[Column, str] = Column(Text, unique=True) 18 | description: Union[Column, str] = Column(Text) 19 | disabled: Union[Column, bool] = Column(Boolean) 20 | channel_parameter: Union[Column, bool] = Column(Boolean) 21 | channel_id: Union[Column, Optional[int]] = Column(BigInteger, nullable=True) 22 | delete_command: Union[Column, bool] = Column(Boolean) 23 | permission_level: Union[Column, bool] = Column(Integer) 24 | requires_confirmation: Union[Column, bool] = Column(Boolean) 25 | user_parameter: Union[Column, bool] = Column(Boolean) 26 | data: Union[Column, str] = Column(Text) 27 | aliases: list[Alias] = relationship("Alias", back_populates="command", cascade="all, delete", order_by="Alias.name") 28 | 29 | @staticmethod 30 | async def create(name: str, data: str, disabled: bool, permission_level: BasePermissionLevel) -> CustomCommand: 31 | row = CustomCommand( 32 | id=str(uuid4()), 33 | name=name, 34 | description=None, 35 | disabled=disabled, 36 | channel_parameter=False, 37 | channel_id=None, 38 | delete_command=False, 39 | permission_level=permission_level.level, 40 | requires_confirmation=False, 41 | user_parameter=True, 42 | data=data, 43 | ) 44 | await db.add(row) 45 | return row 46 | 47 | @property 48 | def alias_names(self) -> list[str]: 49 | return [alias.name for alias in self.aliases] 50 | 51 | async def add_alias(self, name: str) -> Alias: 52 | alias = Alias(id=str(uuid4()), name=name, command_id=self.id) 53 | self.aliases.append(alias) 54 | await db.add(alias) 55 | return alias 56 | 57 | 58 | class Alias(Base): 59 | __tablename__ = "custom_command_alias" 60 | 61 | id: Union[Column, str] = Column(String(36), primary_key=True, unique=True) 62 | name: Union[Column, str] = Column(Text, unique=True) 63 | command_id: Union[Column, str] = Column(String(36), ForeignKey("custom_command.id")) 64 | command: CustomCommand = relationship("CustomCommand", back_populates="aliases") 65 | -------------------------------------------------------------------------------- /general/custom_commands/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class CustomCommandsPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.custom_commands.permissions[self.name] 11 | 12 | read = auto() 13 | write = auto() 14 | -------------------------------------------------------------------------------- /general/discord_bot_token_deleter/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import DiscordBotTokenDeleterCog 2 | 3 | 4 | __all__ = ["DiscordBotTokenDeleterCog"] 5 | -------------------------------------------------------------------------------- /general/discord_bot_token_deleter/cog.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import binascii 3 | import re 4 | 5 | from aiohttp import ClientSession 6 | from discord import Embed, Forbidden, Message 7 | 8 | from PyDrocsid.cog import Cog 9 | from PyDrocsid.material_colors import MaterialColors 10 | from PyDrocsid.translations import t 11 | 12 | from ...contributor import Contributor 13 | from ...pubsub import send_alert 14 | 15 | 16 | tg = t.g 17 | t = t.discord_bot_token_deleter 18 | 19 | 20 | class DiscordBotTokenDeleterCog(Cog, name="Discord Bot Token Deleter"): 21 | CONTRIBUTORS = [Contributor.Tert0, Contributor.Defelo] 22 | RE_DC_TOKEN = re.compile(r"([A-Za-z\d\-_]+)\.[A-Za-z\d\-_]+\.[A-Za-z\d\-_]+") 23 | 24 | async def on_message(self, message: Message): 25 | """Delete a message if it contains a Discord bot token""" 26 | 27 | if message.author.id == self.bot.user.id or not message.guild: 28 | return 29 | 30 | for match in self.RE_DC_TOKEN.finditer(message.content): 31 | try: 32 | if not base64.urlsafe_b64decode(match.group(1)).isdigit(): 33 | continue 34 | except binascii.Error: 35 | continue 36 | 37 | async with ClientSession() as session, session.get( 38 | "https://discord.com/api/users/@me", headers={"Authorization": f"Bot {match.group(0)}"} 39 | ) as response: 40 | if response.ok: 41 | break 42 | else: 43 | return 44 | 45 | embed = Embed(title=t.title, colour=MaterialColors.bluegrey, description=t.description) 46 | await message.channel.send(message.author.mention, embed=embed) 47 | try: 48 | await message.delete() 49 | except Forbidden: 50 | await send_alert(message.guild, t.not_deleted(message.jump_url, message.channel.mention)) 51 | -------------------------------------------------------------------------------- /general/discord_bot_token_deleter/documentation.md: -------------------------------------------------------------------------------- 1 | # Discord Bot Token Deleter 2 | 3 | 4 | *Work in Progress* 5 | -------------------------------------------------------------------------------- /general/discord_bot_token_deleter/translations/en.yml: -------------------------------------------------------------------------------- 1 | title: Discord Bot Token Deleter 2 | description: > 3 | You have sent a message with a Discord bot token into this channel. 4 | I deleted this message because other people can use this token to control your bot! 5 | You can regenerate the token in the [Discord Developer Portal](https://discord.com/developers/applications): 6 | - Select the application 7 | - Click on "Bot" in the side menu 8 | - Click on "Regenerate" to regenerate the token 9 | not_deleted: > 10 | [This message]({}) contains a Discord bot token but I could not delete it because I don't have `manage_messages` permission in {}. 11 | -------------------------------------------------------------------------------- /general/news/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import NewsCog 2 | 3 | 4 | __all__ = ["NewsCog"] 5 | -------------------------------------------------------------------------------- /general/news/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | News = MaterialColors.orange 6 | -------------------------------------------------------------------------------- /general/news/documentation.md: -------------------------------------------------------------------------------- 1 | # News 2 | 3 | 4 | *Work in Progress* 5 | -------------------------------------------------------------------------------- /general/news/models.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union 2 | 3 | from sqlalchemy import BigInteger, Column 4 | 5 | from PyDrocsid.database import Base, db 6 | 7 | 8 | class NewsAuthorization(Base): 9 | __tablename__ = "news_authorization" 10 | 11 | user_id: Union[Column, int] = Column(BigInteger, primary_key=True) 12 | channel_id: Union[Column, int] = Column(BigInteger, primary_key=True) 13 | notification_role_id: Union[Column, int] = Column(BigInteger) 14 | 15 | @staticmethod 16 | async def create(user_id: int, channel_id: int, notification_role_id: Optional[int]) -> "NewsAuthorization": 17 | row = NewsAuthorization(user_id=user_id, channel_id=channel_id, notification_role_id=notification_role_id) 18 | await db.add(row) 19 | return row 20 | -------------------------------------------------------------------------------- /general/news/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class NewsPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.news.permissions[self.name] 11 | 12 | read = auto() 13 | write = auto() 14 | -------------------------------------------------------------------------------- /general/news/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | read: read news authorizations 3 | write: write news authorizations 4 | 5 | news: News 6 | news_already_authorized: User is already authorized to send news in this channel. 7 | news_authorized: "User has been authorized to send news in this channel. :white_check_mark:" 8 | log_news_authorized: "User {} has been **authorized** to **send news** in {}." 9 | news_not_authorized: User is not authorized to send news in this channel. 10 | news_unauthorized: "User has been unauthorized to send news in this channel. :white_check_mark:" 11 | log_news_unauthorized: "User {} has been **unauthorized** to **send news** in {}." 12 | no_news_authorizations: No user has been authorized to send news yet. 13 | news_not_added_no_permissions: > 14 | User could not be authorized because I don't have `send_messages` permission in this channel. 15 | news_you_are_not_authorized: You are not authorized to send news in this channel. 16 | sent_by: Sent by @{} ({}) 17 | send_message: Now send me the message! 18 | msg_could_not_be_sent: Message could not be sent. 19 | msg_sent: "Message has been sent successfully. :white_check_mark:" 20 | -------------------------------------------------------------------------------- /general/polls/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import PollsCog 2 | 3 | 4 | __all__ = ["PollsCog"] 5 | -------------------------------------------------------------------------------- /general/polls/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | Polls = MaterialColors.orange[800] 6 | -------------------------------------------------------------------------------- /general/polls/documentation.md: -------------------------------------------------------------------------------- 1 | # Polls 2 | 3 | Contains commands for simple "yes/no" polls, multiple choice polls and team polls. 4 | 5 | 6 | ## `yesno` 7 | 8 | Creates a "yes/no" poll by adding :thumbsup: and :thumbsdown: reactions to the message (pictures and other files work, too). You can also specify a different message to which the reactions should be added. 9 | 10 | ```css 11 | .[yesno|yn] [content|message] 12 | ``` 13 | 14 | Arguments: 15 | 16 | | Argument | Required | Description | 17 | |:---------:|:--------:|:--------------------------------| 18 | | `content` | | The message content | 19 | | `message` | | The link to a different message | 20 | 21 | 22 | ## `poll` 23 | 24 | Creates a poll with 1 to a maximum of 19 options. 25 | 26 | ```css 27 | .[poll|vote] 28 | [emoji1] 29 | [emojiX] [optionX] 30 | ``` 31 | 32 | Arguments: 33 | 34 | | Argument | Required | Description | 35 | |:----------:|:-------------------------:|:--------------------------------| 36 | | `question` | :fontawesome-solid-check: | The poll topic/question | 37 | | `emojiX` | | The reaction emote for option X | 38 | | `option1` | :fontawesome-solid-check: | The first poll option | 39 | | `optionX` | | The Xth poll option | 40 | 41 | !!! info 42 | Multiline titles and options can be specified using a \ at the end of a line 43 | 44 | 45 | ## `team_yesno` 46 | 47 | Creates a "yes/no" poll and shows which team members have not voted yet. 48 | 49 | ```css 50 | .[team_yesno|tyn] 51 | ``` 52 | 53 | Arguments: 54 | 55 | | Argument | Required | Description | 56 | |:--------:|:-------------------------:|:------------------------| 57 | | `text` | :fontawesome-solid-check: | The poll topic/question | 58 | 59 | Required Permissions: 60 | 61 | - `polls.team_poll` 62 | 63 | 64 | ## `teampoll` 65 | 66 | Creates a poll with 1 to a maximum of 20 options and shows which team members have not voted yet. 67 | 68 | ```css 69 | .[teampoll|teamvote|tp] 70 | [emoji1] 71 | [emojiX] [optionX] 72 | ``` 73 | 74 | Arguments: 75 | 76 | | Argument | Required | Description | 77 | |:----------:|:-------------------------:|:--------------------------------| 78 | | `question` | :fontawesome-solid-check: | The poll topic/question | 79 | | `emojiX` | | The reaction emote for option X | 80 | | `option1` | :fontawesome-solid-check: | The first poll option | 81 | | `optionX` | | The Xth poll option | 82 | 83 | Required Permissions: 84 | 85 | - `polls.team_poll` 86 | 87 | !!! info 88 | Multiline titles and options can be specified using a \ at the end of a line 89 | -------------------------------------------------------------------------------- /general/polls/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class PollsPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.polls.permissions[self.name] 11 | 12 | team_poll = auto() 13 | delete = auto() 14 | -------------------------------------------------------------------------------- /general/polls/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | team_poll: start a team poll 3 | delete: delete polls 4 | 5 | poll: Poll 6 | team_poll: Team Poll 7 | vote_explanation: Vote using the reactions below! 8 | too_many_options: You specified too many options. The maximum amount is {}. 9 | option_too_long: Options are limited to {} characters. 10 | missing_options: Missing options 11 | option_duplicated: You may not use the same emoji twice! 12 | empty_option: Empty option 13 | poll_usage: | 14 | 15 | [emoji1] 16 | [emojiX] [optionX] 17 | team_role_not_set: Team role is not set. 18 | team_role_no_members: The team role has no members. 19 | teampoll_all_voted: "All teamlers voted :white_check_mark:" 20 | teamlers_missing: 21 | one: "{last} hasn't voted yet." 22 | many: "{teamlers} and {last} haven't voted yet." 23 | created_by: Created by @{} ({}) 24 | can_not_use_wastebucket_as_option: "You can not use :wastebasket: as option" 25 | foreign_message: "You are not allowed to add yes/no reactions to foreign messages!" 26 | could_not_add_reactions: Could not add reactions because I don't have `add_reactions` permission in {}. 27 | -------------------------------------------------------------------------------- /general/reactionpin/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import ReactionPinCog 2 | 3 | 4 | __all__ = ["ReactionPinCog"] 5 | -------------------------------------------------------------------------------- /general/reactionpin/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | ReactionPin = MaterialColors.blue["a700"] 6 | -------------------------------------------------------------------------------- /general/reactionpin/documentation.md: -------------------------------------------------------------------------------- 1 | # ReactionPin 2 | 3 | 4 | *Work in Progress* 5 | -------------------------------------------------------------------------------- /general/reactionpin/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Union 4 | 5 | from sqlalchemy import BigInteger, Column 6 | 7 | from PyDrocsid.database import Base, db 8 | 9 | 10 | class ReactionPinChannel(Base): 11 | __tablename__ = "reactionpin_channel" 12 | 13 | channel: Union[Column, int] = Column(BigInteger, primary_key=True, unique=True) 14 | 15 | @staticmethod 16 | async def create(channel: int) -> ReactionPinChannel: 17 | row = ReactionPinChannel(channel=channel) 18 | await db.add(row) 19 | return row 20 | -------------------------------------------------------------------------------- /general/reactionpin/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class ReactionPinPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.reactionpin.permissions[self.name] 11 | 12 | pin = auto() 13 | read = auto() 14 | write = auto() 15 | -------------------------------------------------------------------------------- /general/reactionpin/settings.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.settings import Settings 2 | 3 | 4 | class ReactionPinSettings(Settings): 5 | keep_pin_message = True 6 | -------------------------------------------------------------------------------- /general/reactionpin/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | pin: pin other user's messages 3 | read: read reactionpin configuration 4 | write: write reactionpin configuration 5 | 6 | reactionpin: ReactionPin 7 | no_permission: I don't have `manage_messages` permission in this channel. 8 | no_permission_alert: ReactionPin does not work because I don't have `manage_messages` permission in {}. 9 | msg_not_pinned_system: Message could not be pinned, because it is a system message. 10 | msg_not_pinned_limit: Message could not be pinned, because 50 messages are already pinned in this channel. 11 | whitelisted_channels: Whitelisted Channels 12 | no_whitelisted_channels: No whitelisted channels. 13 | channel_already_whitelisted: Channel is already whitelisted. 14 | channel_whitelisted: "Channel has been whitelisted. :white_check_mark:" 15 | log_channel_whitelisted_rp: "**Channel** {} has been **added** to the **ReactionPin whitelist**." 16 | channel_not_whitelisted: ":x: Channel is not whitelisted." 17 | channel_removed: "Channel has been removed from the whitelist. :white_check_mark:" 18 | log_channel_removed_rp: "**Channel** {} has been **removed** from the **ReactionPin whitelist**." 19 | pin_messages: "Pin Messages" 20 | pin_messages_now_enabled: "Pin Messages have been enabled. :white_check_mark:" 21 | log_pin_messages_now_enabled: "Pin Messages have been **enabled**." 22 | pin_messages_now_disabled: "Pin Messages have been disabled. :white_check_mark:" 23 | log_pin_messages_now_disabled: "**Pin Messages** have been **disabled**." 24 | no_blocked_role: No blocked role configured. 25 | blocked_role: "Blocked role: `@{}`" 26 | blocked_role_updated: Blocked role has been updated. 27 | -------------------------------------------------------------------------------- /general/reactionrole/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import ReactionRoleCog 2 | 3 | 4 | __all__ = ["ReactionRoleCog"] 5 | -------------------------------------------------------------------------------- /general/reactionrole/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | ReactionRole = MaterialColors.blue["a700"] 6 | -------------------------------------------------------------------------------- /general/reactionrole/documentation.md: -------------------------------------------------------------------------------- 1 | # ReactionRole 2 | 3 | 4 | *Work in Progress* 5 | -------------------------------------------------------------------------------- /general/reactionrole/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Optional, Union 4 | 5 | from sqlalchemy import BigInteger, Boolean, Column, String 6 | 7 | from PyDrocsid.database import Base, db, select 8 | 9 | 10 | class ReactionRole(Base): 11 | __tablename__ = "reactionrole" 12 | 13 | channel_id: Union[Column, int] = Column(BigInteger, primary_key=True) 14 | message_id: Union[Column, int] = Column(BigInteger, primary_key=True) 15 | emoji: Union[Column, str] = Column(String(64), primary_key=True) 16 | role_id: Union[Column, int] = Column(BigInteger) 17 | reverse: Union[Column, bool] = Column(Boolean) 18 | auto_remove: Union[Column, bool] = Column(Boolean) 19 | 20 | @staticmethod 21 | async def create( 22 | channel_id: int, message_id: int, emoji: str, role_id: int, reverse: bool, auto_remove: bool 23 | ) -> ReactionRole: 24 | row = ReactionRole( 25 | channel_id=channel_id, 26 | message_id=message_id, 27 | emoji=emoji, 28 | role_id=role_id, 29 | reverse=reverse, 30 | auto_remove=auto_remove, 31 | ) 32 | await db.add(row) 33 | return row 34 | 35 | @staticmethod 36 | async def get(channel_id: int, message_id: int, emoji: str) -> Optional[ReactionRole]: 37 | return await db.first(select(ReactionRole).filter_by(channel_id=channel_id, message_id=message_id, emoji=emoji)) 38 | -------------------------------------------------------------------------------- /general/reactionrole/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class ReactionRolePermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.reactionrole.permissions[self.name] 11 | 12 | read = auto() 13 | write = auto() 14 | -------------------------------------------------------------------------------- /general/reactionrole/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | read: read reactionrole configuration 3 | write: write reactionrole configuration 4 | 5 | commands: 6 | reactionrole: manage reactionrole 7 | reactionrole_list: list configured reactionrole links for a specific message 8 | reactionrole_add: add a new reactionrole link 9 | reactionrole_remove: remove a reactionrole link 10 | reactionrole_reinialize: reinialize reactions on a reactionrole message 11 | 12 | reactionrole: ReactionRole 13 | no_reactionrole_links: No ReactionRole links have been created yet. 14 | no_reactionrole_links_for_msg: No ReactionRole links have been created yet for this message. 15 | rr_link_already_exists: A link already exists for this reaction on this message. 16 | rr_link_created: "Link has been created successfully. :white_check_mark:" 17 | rr_link_not_created_no_permissions: > 18 | Link could not be created because I don't have `add_reactions` permission in this channel. 19 | log_rr_link_created: "**ReactionRole link** for {} -> <@&{}> has been **created** on [this message]({}) in {}" 20 | rr_link_not_found: Such a link does not exist. 21 | rr_link_removed: "Link has been removed successfully. :white_check_mark:" 22 | could_not_add_reactions: "Could not add reaction to message!" 23 | could_not_remove_reactions: "Could not remove reaction from message!" 24 | log_rr_link_removed: "**ReactionRole link** for {} -> <@&{}> has been **deleted** on [this message]({}) in {}" 25 | rr_link: "{} -> {}" 26 | auto_remove: auto remove 27 | reverse: reverse 28 | manage_role_error: "ReactionRole could not manage role {role.mention} on {member.mention} ({member.id}) on [this message]({message.jump_url})." 29 | remove_emoji_error: "ReactionRole could not remove {emoji} reaction from [this message]({message.jump_url})." 30 | -------------------------------------------------------------------------------- /general/remind_me/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import RemindMeCog 2 | 3 | 4 | __all__ = ["RemindMeCog"] 5 | -------------------------------------------------------------------------------- /general/remind_me/cog.py: -------------------------------------------------------------------------------- 1 | from discord import Forbidden, Member, Message, PartialEmoji 2 | 3 | from PyDrocsid.cog import Cog 4 | from PyDrocsid.emojis import name_to_emoji 5 | from PyDrocsid.events import StopEventHandling 6 | from PyDrocsid.translations import t 7 | 8 | from ...contributor import Contributor 9 | from ...pubsub import send_alert 10 | 11 | 12 | tg = t.g 13 | t = t.remind_me 14 | 15 | EMOJIS = { 16 | name_to_emoji["star"], 17 | name_to_emoji["mailbox"], 18 | name_to_emoji["e_mail"], 19 | name_to_emoji["envelope"], 20 | name_to_emoji["incoming_envelope"], 21 | name_to_emoji["envelope_with_arrow"], 22 | name_to_emoji["floppy_disk"], 23 | name_to_emoji["bookmark"], 24 | } 25 | 26 | WASTEBASKET = name_to_emoji["wastebasket"] 27 | 28 | 29 | async def remove_member_reaction(emoji, member, message): 30 | """Remove the RemindMe reaction from a member, if they do not allow private messages.""" 31 | try: 32 | await message.remove_reaction(emoji, member) 33 | except Forbidden: 34 | await send_alert(message.guild, t.cannot_send(message.jump_url, message.channel.mention)) 35 | 36 | 37 | class RemindMeCog(Cog, name="RemindMe"): 38 | """Add a "Remind Me"-functionality by sending the user a copy of the message they reacted on.""" 39 | 40 | CONTRIBUTORS = [Contributor.Tristan] 41 | 42 | async def on_raw_reaction_add(self, message: Message, emoji: PartialEmoji, member: Member): 43 | """ 44 | Check, if the reaction is in a list of set emojis. If so, the message is sent as a direct message to the user. 45 | Remove the user reaction, if the user does not accept direct messages. 46 | Send an alert in case of missing permissions. 47 | """ 48 | 49 | if not message.guild and str(emoji) == WASTEBASKET and message.author == self.bot.user != member: 50 | await message.delete() 51 | raise StopEventHandling 52 | 53 | if str(emoji) not in EMOJIS or member.bot or message.guild is None: 54 | return 55 | 56 | if message.attachments: 57 | try: 58 | message_to_user = await member.send("\n".join(attachment.url for attachment in message.attachments)) 59 | await message_to_user.add_reaction(WASTEBASKET) 60 | except Forbidden: 61 | await remove_member_reaction(emoji, member, message) 62 | return 63 | 64 | embed = message.embeds[0] if message.embeds else None 65 | 66 | if message.content or embed: 67 | try: 68 | message_to_user = await member.send(message.content, embed=embed) 69 | await message_to_user.add_reaction(WASTEBASKET) 70 | except Forbidden: 71 | await remove_member_reaction(emoji, member, message) 72 | -------------------------------------------------------------------------------- /general/remind_me/documentation.md: -------------------------------------------------------------------------------- 1 | # RemindMe 2 | 3 | Contains a simple remind feature. When a user reacts on a message with one of the following emojis, the bot sends a copy of that message to the user as a direct message: 4 | 5 | | Emoji | Name | 6 | |:---------------------:|:------------------------| 7 | | :star: | `:star:` | 8 | | :mailbox: | `:mailbox:` | 9 | | :email: | `:email:` | 10 | | :envelope: | `:envelope:` | 11 | | :incoming_envelope: | `:incoming_envelope:` | 12 | | :envelope_with_arrow: | `:envelope_with_arrow:` | 13 | | :floppy_disk: | `:floppy_disk:` | 14 | | :bookmark: | `:bookmark:` | 15 | -------------------------------------------------------------------------------- /general/remind_me/translations/en.yml: -------------------------------------------------------------------------------- 1 | cannot_send: Could not remove RemindMe reaction from [this message]({}) because I don't have `manage_messages` permission in {}. 2 | -------------------------------------------------------------------------------- /general/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import UtilsCog 2 | 3 | 4 | __all__ = ["UtilsCog"] 5 | -------------------------------------------------------------------------------- /general/utils/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | Utils = MaterialColors.green["a700"] 6 | -------------------------------------------------------------------------------- /general/utils/documentation.md: -------------------------------------------------------------------------------- 1 | # Utils 2 | 3 | Contains some utility commands for general use as well as for testing and development. 4 | 5 | 6 | ## `ping` 7 | 8 | Determines the latency of the bot to Discord in milliseconds. 9 | 10 | ```css 11 | .ping 12 | ``` 13 | 14 | 15 | ## `snowflake` 16 | 17 | Extracts and displays the timestamp from any [Discord snowflake ID](https://discord.com/developers/docs/reference#snowflakes){target=_blank}. It can be used to find out the date and time of creation of any Discord user, guild, channel, message, role, custom emoji or anything else that has an ID. 18 | 19 | ```css 20 | .[snowflake|sf|time] 21 | ``` 22 | 23 | Arguments: 24 | 25 | | Argument | Required | Description | 26 | |:--------:|:-------------------------:|:-----------------| 27 | | `ID` | :fontawesome-solid-check: | The snowflake ID | 28 | 29 | 30 | ## `encode` 31 | 32 | Applies Python's [`str.encode` function](https://docs.python.org/3/library/stdtypes.html#str.encode){target=_blank} to the username and nickname of a given user. 33 | 34 | ```css 35 | .[encode|enc] 36 | ``` 37 | 38 | Argument: 39 | 40 | | Argument | Required | Description | 41 | |:--------:|:-------------------------:|:-------------------| 42 | | `user` | :fontawesome-solid-check: | The user or member | 43 | 44 | 45 | ## `suggest_role_color` 46 | 47 | Suggests the color for a new role, trying to avoid colors already in use. Optionally you can specify a list of colors to also avoid. 48 | 49 | ```css 50 | .[suggest_role_color|rc] [avoid...] 51 | ``` 52 | 53 | Arguments: 54 | 55 | | Argument | Required | Description | 56 | |:--------:|:--------:|:-----------------------------------| 57 | | `avoid` | | A list of color hex codes to avoid | 58 | 59 | Required Permissions: 60 | 61 | - `utils.suggest_role_color` 62 | -------------------------------------------------------------------------------- /general/utils/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class UtilsPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.utils.permissions[self.name] 11 | 12 | suggest_role_color = auto() 13 | -------------------------------------------------------------------------------- /general/utils/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | suggest_role_color: use the `suggest_role_color` command 3 | 4 | commands: 5 | ping: Display bot latency 6 | snowflake: Display snowflake timestamp 7 | encode: Apply Python's `str.encode()` function to a user's name 8 | suggest_role_color: Suggest a new role color based on the colors of all existing roles 9 | 10 | pong: Pong! 11 | pong_latency: ":hourglass: {:.0f} ms" 12 | invalid_snowflake: Invalid snowflake! 13 | could_not_generate_color: Could not generate a new role color. 14 | username: Username 15 | nickname: Nickname 16 | -------------------------------------------------------------------------------- /general/voice_channel/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import VoiceChannelCog 2 | 3 | 4 | __all__ = ["VoiceChannelCog"] 5 | -------------------------------------------------------------------------------- /general/voice_channel/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | Voice = MaterialColors.blue["a700"] 6 | unlocked = MaterialColors.lightgreen["a700"] 7 | locked = MaterialColors.blue["a700"] 8 | -------------------------------------------------------------------------------- /general/voice_channel/documentation.md: -------------------------------------------------------------------------------- 1 | # Voice Channels 2 | 3 | 4 | *Work in Progress* 5 | -------------------------------------------------------------------------------- /general/voice_channel/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from datetime import datetime 4 | from typing import Optional, Union 5 | from uuid import uuid4 6 | 7 | from discord.utils import utcnow 8 | from sqlalchemy import BigInteger, Boolean, Column, ForeignKey, String 9 | from sqlalchemy.orm import relationship 10 | 11 | from PyDrocsid.database import Base, UTCDateTime, db 12 | 13 | 14 | class DynGroup(Base): 15 | __tablename__ = "dynvoice_group" 16 | 17 | id: Union[Column, str] = Column(String(36), primary_key=True, unique=True) 18 | user_role: Union[Column, int] = Column(BigInteger) 19 | channels: list[DynChannel] = relationship("DynChannel", back_populates="group", cascade="all, delete") 20 | 21 | @staticmethod 22 | async def create(channel_id: int, user_role: int) -> DynGroup: 23 | group = DynGroup(id=str(uuid4()), user_role=user_role) 24 | group.channels.append(await DynChannel.create(channel_id, group.id)) 25 | await db.add(group) 26 | return group 27 | 28 | 29 | class DynChannel(Base): 30 | __tablename__ = "dynvoice_channel" 31 | 32 | channel_id: Union[Column, int] = Column(BigInteger, primary_key=True, unique=True) 33 | text_id: Union[Column, int] = Column(BigInteger) 34 | locked: Union[Column, bool] = Column(Boolean) 35 | group_id: Union[Column, str] = Column(String(36), ForeignKey("dynvoice_group.id")) 36 | group: DynGroup = relationship("DynGroup", back_populates="channels") 37 | owner_id: Union[Column, str] = Column(String(36)) 38 | owner_override: Union[Column, int] = Column(BigInteger) 39 | members: list[DynChannelMember] = relationship( 40 | "DynChannelMember", back_populates="channel", cascade="all, delete", order_by="DynChannelMember.timestamp" 41 | ) 42 | 43 | @staticmethod 44 | async def create(channel_id: int, group_id: int) -> DynChannel: 45 | channel = DynChannel(channel_id=channel_id, text_id=None, locked=False, group_id=group_id) 46 | await db.add(channel) 47 | return channel 48 | 49 | @staticmethod 50 | async def get(**kwargs) -> Optional[DynChannel]: 51 | return await db.get(DynChannel, [DynChannel.group, DynGroup.channels], DynChannel.members, **kwargs) 52 | 53 | 54 | class DynChannelMember(Base): 55 | __tablename__ = "dynvoice_channel_member" 56 | 57 | id: Union[Column, str] = Column(String(36), primary_key=True, unique=True) 58 | member_id: Union[Column, int] = Column(BigInteger) 59 | channel_id: Union[Column, int] = Column(BigInteger, ForeignKey("dynvoice_channel.channel_id")) 60 | channel: DynChannel = relationship("DynChannel", back_populates="members") 61 | timestamp: Union[Column, datetime] = Column(UTCDateTime) 62 | 63 | @staticmethod 64 | async def create(member_id: int, channel_id: int) -> DynChannelMember: 65 | member = DynChannelMember(id=str(uuid4()), member_id=member_id, channel_id=channel_id, timestamp=utcnow()) 66 | await db.add(member) 67 | return member 68 | 69 | 70 | class RoleVoiceLink(Base): 71 | __tablename__ = "role_voice_link" 72 | 73 | role: Union[Column, int] = Column(BigInteger, primary_key=True) 74 | voice_channel: Union[Column, str] = Column(String(36), primary_key=True) 75 | 76 | @staticmethod 77 | async def create(role: int, voice_channel: str) -> RoleVoiceLink: 78 | link = RoleVoiceLink(role=role, voice_channel=voice_channel) 79 | await db.add(link) 80 | return link 81 | -------------------------------------------------------------------------------- /general/voice_channel/names/computer_science_pioneers.txt: -------------------------------------------------------------------------------- 1 | Franz Alt 2 | Marc Andreessen 3 | John Atanasoff 4 | Charles Babbage 5 | John W. Backus 6 | Rudolf Bayer 7 | Andreas von Bechtolsheim 8 | Alexander Graham Bell 9 | Tim Berners-Lee 10 | Clifford Berry 11 | Barry W. Boehm 12 | George Boole 13 | Karlheinz Brandenburg 14 | Vannevar Bush 15 | Peter Chen 16 | Noam Chomsky 17 | Edgar F. Codd 18 | Stephen A. Cook 19 | Fernando José Corbató 20 | Seymour Cray 21 | Ole-Johan Dahl 22 | Edsger W. Dijkstra 23 | John Presper Eckert 24 | Douglas C. Engelbart 25 | Michael Fagan 26 | Christiane Floyd 27 | Jay Wright Forrester 28 | Erich Gamma 29 | Wolfgang Giloi 30 | Kurt Gödel 31 | Marcel J. E. Golay 32 | Emanuel Goldberg 33 | Charles F. Goldfarb 34 | James Gosling 35 | John Guttag 36 | Richard Hamming 37 | Ralph Hartley 38 | Tony Hoare 39 | Herman Hollerith 40 | Grace Hopper 41 | David A. Huffman 42 | Bill Joy 43 | Alan Kay 44 | Brian W. Kernighan 45 | Alonzo Church 46 | Donald E. Knuth 47 | Lawrence Landweber 48 | Nikolaus Joachim Lehmann 49 | Gottfried Wilhelm Leibniz 50 | J. C. R. Licklider 51 | Marvin Lee Minsky 52 | John William Mauchly 53 | Frieder Nake 54 | John von Neumann 55 | Heinz Nixdorf 56 | Harry Nyquist 57 | Ada Lovelace 58 | Ken Olsen 59 | Pier Giorgio Perotto 60 | Carl Adam Petri 61 | Dennis Ritchie 62 | Claude Shannon 63 | Richard Stallman 64 | Karl Steinbuch 65 | Ivan Sutherland 66 | Bjarne Stroustrup 67 | Andrew S. Tanenbaum 68 | Ken Thompson 69 | Ray Tomlinson 70 | Linus Torvalds 71 | Alan Turing 72 | An Wang 73 | Mark Weiser 74 | Joseph Weizenbaum 75 | Adriaan van Wijngaarden 76 | Niklaus Wirth 77 | Heinz Zemanek 78 | Konrad Zuse 79 | Corrado Böhm 80 | Guido van Rossum 81 | Joan Daemen 82 | Vincent Rijmen 83 | -------------------------------------------------------------------------------- /general/voice_channel/names/data_structures.txt: -------------------------------------------------------------------------------- 1 | Boolean 2 | Float 3 | Integer 4 | Reference 5 | Enum 6 | Array 7 | List 8 | Tuple 9 | Map 10 | Multimap 11 | Set 12 | Multiset 13 | Stack 14 | Queue 15 | Deque 16 | Graph 17 | Tree 18 | Heap 19 | Bit array 20 | Bit field 21 | Bitboard 22 | Bitmap 23 | Circular buffer 24 | Control table 25 | Image 26 | Dope vector 27 | Dynamic array 28 | Gap buffer 29 | Hashed array tree 30 | Lookup table 31 | Matrix 32 | Parallel array 33 | Sorted array 34 | Sparse matrix 35 | Iliffe vector 36 | Variable-length array 37 | Doubly linked list 38 | Array list 39 | Linked list 40 | Association list 41 | Self-organizing list 42 | Skip list 43 | Unrolled linked list 44 | VList 45 | Conc-tree list 46 | Xor linked list 47 | Zipper 48 | Difference list 49 | Free list 50 | AA tree 51 | AVL tree 52 | Binary search tree 53 | Binary tree 54 | Cartesian tree 55 | Conc-tree list 56 | Order statistic tree 57 | Pagoda 58 | Red–black tree 59 | Rope 60 | Scapegoat tree 61 | Splay tree 62 | T-tree 63 | Tango tree 64 | Threaded binary tree 65 | Top tree 66 | Treap 67 | WAVL tree 68 | Weight-balanced tree 69 | B-tree 70 | B+ tree 71 | B*-tree 72 | B sharp tree 73 | Dancing tree 74 | 2–3 tree 75 | 2–3–4 tree 76 | Queap 77 | Fusion tree 78 | Bx-tree 79 | AList 80 | Heap 81 | Binary heap 82 | B-heap 83 | Weak heap 84 | Binomial heap 85 | Fibonacci heap 86 | AF-heap 87 | Leonardo heap 88 | 2–3 heap 89 | Soft heap 90 | Pairing heap 91 | Leftist heap 92 | Treap 93 | Beap 94 | Skew heap 95 | Ternary heap 96 | D-ary heap 97 | Brodal queue 98 | Radix tree 99 | Suffix tree 100 | Suffix array 101 | Compressed suffix array 102 | FM-index 103 | Generalised suffix tree 104 | B-tree 105 | Judy array 106 | X-fast trie 107 | Y-fast trie 108 | Merkle tree 109 | C tree 110 | Ternary tree 111 | K-ary tree 112 | And–or tree 113 | (a,b)-tree 114 | Link/cut tree 115 | SPQR-tree 116 | Spaghetti stack 117 | Union-find 118 | Fusion tree 119 | Enfilade 120 | Exponential tree 121 | Fenwick tree 122 | Van Emde Boas tree 123 | Rose tree 124 | Segment tree 125 | Interval tree 126 | Range tree 127 | Bin 128 | K-d tree 129 | Implicit k-d tree 130 | Min/max k-d tree 131 | Relaxed k-d tree 132 | Adaptive k-d tree 133 | Quadtree 134 | Octree 135 | Linear octree 136 | Z-order 137 | UB-tree 138 | R-tree 139 | R+ tree 140 | R* tree 141 | Hilbert R-tree 142 | X-tree 143 | Metric tree 144 | Cover tree 145 | M-tree 146 | VP-tree 147 | BK-tree 148 | Bounding volume hierarchy 149 | BSP tree 150 | Abstract syntax tree 151 | Parse tree 152 | Decision tree 153 | Alternating decision tree 154 | Minimax tree 155 | Expectiminimax tree 156 | Finger tree 157 | Expression tree 158 | Log-structured merge-tree 159 | Lexicographic Search Tree 160 | Bloom filter 161 | Count–min sketch 162 | Distributed hash table 163 | Double hashing 164 | Hash array mapped trie 165 | Hash list 166 | Hash table 167 | Hash tree 168 | Hash trie 169 | Koorde 170 | Prefix hash tree 171 | Rolling hash 172 | MinHash 173 | Quotient filter 174 | Ctrie 175 | Graph 176 | Adjacency list 177 | Adjacency matrix 178 | Graph-structured stack 179 | Scene graph 180 | Decision tree 181 | Binary decision diagram 182 | And-inverter graph 183 | Directed graph 184 | Directed acyclic graph 185 | Multigraph 186 | Hypergraph 187 | Lightmap 188 | Winged edge 189 | Quad-edge 190 | Routing table 191 | Symbol table 192 | -------------------------------------------------------------------------------- /general/voice_channel/names/elements.txt: -------------------------------------------------------------------------------- 1 | hydrogen 2 | helium 3 | lithium 4 | beryllium 5 | boron 6 | carbon 7 | nitrogen 8 | oxygen 9 | fluorine 10 | neon 11 | sodium 12 | magnesium 13 | aluminium 14 | silicon 15 | phosphorus 16 | sulfur 17 | chlorine 18 | argon 19 | potassium 20 | calcium 21 | scandium 22 | titanium 23 | vanadium 24 | chromium 25 | manganese 26 | iron 27 | cobalt 28 | nickel 29 | copper 30 | zinc 31 | gallium 32 | germanium 33 | arsenic 34 | selenium 35 | bromine 36 | krypton 37 | rubidium 38 | strontium 39 | yttrium 40 | zirconium 41 | niobium 42 | molybdenum 43 | technetium 44 | ruthenium 45 | rhodium 46 | palladium 47 | silver 48 | cadmium 49 | indium 50 | tin 51 | antimony 52 | tellurium 53 | iodine 54 | xenon 55 | caesium 56 | barium 57 | lanthanum 58 | cerium 59 | praseodymium 60 | neodymium 61 | promethium 62 | samarium 63 | europium 64 | gadolinium 65 | terbium 66 | dysprosium 67 | holmium 68 | erbium 69 | thulium 70 | ytterbium 71 | lutetium 72 | hafnium 73 | tantalum 74 | tungsten 75 | rhenium 76 | osmium 77 | iridium 78 | platinum 79 | gold 80 | mercury 81 | thallium 82 | lead 83 | bismuth 84 | polonium 85 | astatine 86 | radon 87 | francium 88 | radium 89 | actinium 90 | thorium 91 | protactinium 92 | uranium 93 | neptunium 94 | plutonium 95 | americium 96 | curium 97 | berkelium 98 | californium 99 | einsteinium 100 | fermium 101 | mendelevium 102 | nobelium 103 | lawrencium 104 | rutherfordium 105 | dubnium 106 | seaborgium 107 | bohrium 108 | hassium 109 | meitnerium 110 | darmstadtium 111 | roentgenium 112 | copernicium 113 | nihonium 114 | flerovium 115 | moscovium 116 | livermorium 117 | tennessine 118 | oganesson 119 | -------------------------------------------------------------------------------- /general/voice_channel/names/famous_mathematicians.txt: -------------------------------------------------------------------------------- 1 | Abraham De Moivre 2 | Ada Lovelace 3 | Al Khwarizmi 4 | Alan Turing 5 | Albert Einstein 6 | Albertus Magnus 7 | Alexandre Grothendieck 8 | Alfred North Whitehead 9 | Andrew Wiles 10 | Archimedes 11 | Aristotle 12 | Arthur Cayley 13 | Aryabhata 14 | Augustin-Louis Cauchy 15 | Augustus De Morgan 16 | Benjamin Banneker 17 | Bernhard Riemann 18 | Bertrand Russell 19 | Blaise Pascal 20 | Brahmagupta 21 | Brook Taylor 22 | Carl Friedrich Gauss 23 | Charles Babbage 24 | Charles Babbage 25 | Christiaan Huygens 26 | Daniel Bernoulli 27 | David Hilbert 28 | Democritus 29 | Diophantus 30 | Edmund Halley 31 | Edward Lorenz 32 | Edward Witten 33 | Emanuel Lasker 34 | Emmy Noether 35 | Eratosthenes 36 | Euclid 37 | Évariste Galois 38 | Felix Klein 39 | Fibonacci 40 | Filippo Brunelleschi 41 | G.H. Hardy 42 | Galileo Galilei 43 | Gaspard Monge 44 | Georg Cantor 45 | George Boole 46 | Giuseppe Peano 47 | Gottfried Leibniz 48 | Gottlob Frege 49 | Grace Murray Hopper 50 | Grigori Perelman 51 | Henri Poincaré 52 | Heron Of Alexandria 53 | Hipparchus 54 | Isaac Newton 55 | Jacob Bernoulli 56 | Johann Bernoulli 57 | Johannes Kepler 58 | John Napier 59 | John Nash 60 | John Venn 61 | John von Neumann 62 | John Wallis 63 | Joseph Fourier 64 | Joseph Louis Lagrange 65 | Julia Robinson 66 | Kurt Gödel 67 | Leonhard Euler 68 | Liu Hui 69 | Luca Pacioli 70 | Madhava 71 | Maurits Escher 72 | Niels Henrik Abel 73 | Omar Khayyam 74 | Paul Cohen 75 | Pierre de Fermat 76 | Pierre-Simon Laplace 77 | Plato 78 | Ptolemy 79 | Pythagoras 80 | René Descartes 81 | Sophie Germain 82 | Srinivasa Ramanujan 83 | Thales 84 | William Rowan Hamilton 85 | -------------------------------------------------------------------------------- /general/voice_channel/names/fields_medalists.txt: -------------------------------------------------------------------------------- 1 | Akshay Venkatesh 2 | Alain Connes 3 | Alan Baker 4 | Alessio Figalli 5 | Alexander Grothendieck 6 | Andrei Okounkov 7 | Artur Avila 8 | Atle Selberg 9 | Caucher Birkar 10 | Cédric Villani 11 | Charles Louis Fefferman 12 | Curtis T. Mcmullen 13 | Daniel G. Quillen 14 | David Bryant Mumford 15 | Edward Witten 16 | Efim Zelmanov 17 | Elon Lindenstrauss 18 | Enrico Bombieri 19 | Gerd Faltings 20 | Gregori Alexandrovitch Margulis 21 | Heisuke Hironaka 22 | Jean Bourgain 23 | Jean-Christophe Yoccoz 24 | Jean-Pierre Serre 25 | Jesse Douglas 26 | John Griggs Thompson 27 | John Willard Milnor 28 | Klaus Friedrich Roth 29 | Kunihiko Kodaira 30 | Lars Hörmander 31 | Lars Valerian Ahlfors 32 | Laurent Lafforgue 33 | Laurent Schwartz 34 | Manjul Bhargava 35 | Martin Hairer 36 | Maryam Mirzakhani 37 | Maxim Kontsevich 38 | Michael Francis Atiyah 39 | Michael H. Freedman 40 | Ngô Bào Châu 41 | Paul Joseph Cohen 42 | Peter Scholze 43 | Pierre René Deligne 44 | Pierre-Louis Lions 45 | René Thom 46 | Richard E. Borcherds 47 | Serge Novikov 48 | Shigefumi Mori 49 | Shing-Tung Yau 50 | Simon K. Donaldson 51 | Stanislav Smirnov 52 | Stephen Smale 53 | Terence Tao 54 | Vaughan F.R. Jones 55 | Vladimir Drinfeld 56 | Vladimir Voevodsky 57 | W. Timothy Gowers 58 | Wendelin Werner 59 | William P. Thurston 60 | -------------------------------------------------------------------------------- /general/voice_channel/names/greek_alphabet.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 | Ξ 28 | ξ 29 | Ο 30 | ο 31 | Π 32 | π 33 | Ρ 34 | ρ 35 | Σ 36 | σ 37 | Τ 38 | τ 39 | Υ 40 | υ 41 | Φ 42 | φ 43 | Χ 44 | χ 45 | Ψ 46 | ψ 47 | Ω 48 | ω 49 | -------------------------------------------------------------------------------- /general/voice_channel/names/greek_alphabet_names.txt: -------------------------------------------------------------------------------- 1 | alpha 2 | beta 3 | gamma 4 | delta 5 | epsilon 6 | zeta 7 | eta 8 | theta 9 | iota 10 | kappa 11 | lambda 12 | mu 13 | nu 14 | xi 15 | omicron 16 | pi 17 | rho 18 | sigma 19 | tau 20 | upsilon 21 | phi 22 | chi 23 | psi 24 | omega 25 | -------------------------------------------------------------------------------- /general/voice_channel/names/greek_gods_olymp.txt: -------------------------------------------------------------------------------- 1 | Zeus 2 | Poseidon 3 | Hestia 4 | Hades 5 | Demeter 6 | Hera 7 | Athene 8 | Apollon 9 | Artemis 10 | Hermes 11 | Dionysos 12 | Hephaistos 13 | Ares 14 | Aphrodite 15 | Hebe 16 | Persephone 17 | Herakles 18 | -------------------------------------------------------------------------------- /general/voice_channel/names/intel_codenames.txt: -------------------------------------------------------------------------------- 1 | Apollo Lake 2 | Bakerville 3 | Basin Falls 4 | Bay Trail 5 | Braswell 6 | Briarwood 7 | Brickland 8 | Broadwell DE 9 | Broadwell H 10 | Broadwell U/Y 11 | Cascade Lake 12 | Cedar Trail 13 | Cherry Trail 14 | Cherry Trail CR 15 | Chief River 16 | Coffee Lake H 17 | Coffee Lake Refresh 18 | Coffee Lake S 19 | Coffee Lake U 20 | Comet Lake S 21 | Crystal Forest 22 | Denlow 23 | Denverton NS 24 | East Beach 25 | Edisonville 26 | Elkhart Lake 27 | Foxhollow 28 | Gemini Lake 29 | Glen Forest 30 | Gordon Peak 31 | Grangeville 32 | Grantley 33 | Greenlow 34 | Groveport 35 | Haswell DT R 36 | Ice Lake SP 37 | Kaby Lake H 38 | Kaby Lake S 39 | Kaby Lake U/Y 40 | Moorefield 41 | Oak Trail 42 | Purley 43 | Rangeley 44 | River Forest 45 | Romley 46 | Shark Bay 47 | Skylake H 48 | Skylake S 49 | Skylake U/Y 50 | SoFIA 3G 51 | SoFIA 3G-R 52 | Tiger Lake UP3 53 | Tylersburg/Westmere 54 | Whiskey Lake 55 | -------------------------------------------------------------------------------- /general/voice_channel/names/np_complete_problems.txt: -------------------------------------------------------------------------------- 1 | 1-planarity 2 | 3-dim matching 3 | 3-partition 4 | 3SAT 5 | achromatic number 6 | bandwidth problem 7 | berth allocation problem 8 | betweenness 9 | bin packing 10 | bipartite dim 11 | block sorting 12 | capacitated MST 13 | circuit SAT 14 | clique 15 | clique cover 16 | closest string 17 | conjunctive Boolean query 18 | cyclic ordering 19 | degree-constrained ST 20 | domatic number 21 | dominating set 22 | exact cover 23 | feedback arc set 24 | feedback vertex set 25 | flow shop scheduling 26 | graph coloring 27 | graph homomorphism 28 | graph intersection number 29 | graph partition 30 | Hamiltonian completion 31 | Hamiltonian path 32 | integer programming 33 | k-Chinese postman 34 | knapsack problem 35 | Latin squares 36 | longest common subsequence 37 | longest path 38 | maximum 2-SAT 39 | maximum cut 40 | maximum independent set 41 | maximum induced path 42 | maximum volume submatrix 43 | metric graph dimension 44 | metric k-center 45 | min max independent set 46 | minimum degree ST 47 | minimum k-cut 48 | minimum k-ST 49 | modal logic S5-SAT 50 | modularity maximization 51 | monochromatic triangle 52 | open-shop scheduling 53 | pancake sorting distance 54 | partition problem 55 | pathwidth 56 | post correspondence 57 | quadratic assignment 58 | quadratic programming 59 | rank coloring 60 | route inspection 61 | SAT 62 | second order instantiation 63 | set cover 64 | set packing 65 | set splitting 66 | shortest common supersequence 67 | slope number two testing 68 | sparse approximation 69 | Steiner tree 70 | subgraph isomorphism problem 71 | subset sum 72 | traveling salesman 73 | treewidth 74 | treewidth 75 | upward planarity testing 76 | vehicle routing 77 | vertex cover 78 | -------------------------------------------------------------------------------- /general/voice_channel/names/programming_languages.txt: -------------------------------------------------------------------------------- 1 | Java 2 | C 3 | C++ 4 | C# 5 | F# 6 | Go 7 | Python 8 | Visual Basic 9 | PHP 10 | Delphi 11 | Pascal 12 | Swift 13 | Ruby 14 | Fortran 15 | Rust 16 | Prolog 17 | Matlab 18 | Objective-C 19 | Perl 20 | Haskell 21 | Lua 22 | Scala 23 | ActionScript 24 | JavaScript 25 | TypeScript 26 | CoffeeScript 27 | AppleScript 28 | Julia 29 | Kotlin 30 | Dart 31 | Erlang 32 | Lisp 33 | WhiteSpace 34 | ArnoldC 35 | Brainfuck 36 | Groovy 37 | -------------------------------------------------------------------------------- /general/voice_channel/names/roman_gods_olymp.txt: -------------------------------------------------------------------------------- 1 | Jupiter 2 | Neptun 3 | Vesta 4 | Pluto 5 | Ceres 6 | Juno 7 | Minerva 8 | Apollo 9 | Diana 10 | Mercurius 11 | Bacchus 12 | Vulcanus 13 | Mars 14 | Venus 15 | Iuventas 16 | Proserpina 17 | Herkules 18 | -------------------------------------------------------------------------------- /general/voice_channel/names/spaces.txt: -------------------------------------------------------------------------------- 1 | Affine space 2 | Algebraic space 3 | Baire space 4 | Banach space 5 | Base space 6 | Bergman space 7 | Berkovich space 8 | Besov space 9 | Borel space 10 | Calabi-Yau space 11 | Cantor space 12 | Cauchy space 13 | Cellular space 14 | Chu space 15 | Closure space 16 | Complex analytic space 17 | Conformal space 18 | Drinfeld's symmetric space 19 | Eilenberg–Mac Lane space 20 | Euclidean space 21 | Fiber space 22 | Finsler space 23 | First-countable space 24 | Fréchet space 25 | Function space 26 | G-space 27 | Green space 28 | Hardy space 29 | Hausdorff space 30 | Heisenberg space 31 | Hilbert space 32 | Homogeneous space 33 | Inner product space 34 | Kolmogorov space 35 | Lens space 36 | Liouville space 37 | Locally finite space 38 | Loop space 39 | Lorentz space 40 | Lp-space 41 | Mapping space 42 | Measure space 43 | Metric space 44 | Minkowski space 45 | Müntz space 46 | Normed space 47 | Paracompact space 48 | Perfectoid space 49 | Planar space 50 | Polish space 51 | Probability space 52 | Projective space 53 | Proximity space 54 | Quadratic space 55 | Quotient space 56 | Riemann's Moduli space 57 | Sample space 58 | Sequence space 59 | Sierpiński space 60 | Sobolev space 61 | Standard space 62 | State space 63 | Stone space 64 | Symplectic space 65 | T2-space 66 | Teichmüller space 67 | Tensor space 68 | Topological space 69 | Topological vector space 70 | Total space 71 | Uniform space 72 | Vector space 73 | -------------------------------------------------------------------------------- /general/voice_channel/names/turing_award.txt: -------------------------------------------------------------------------------- 1 | Alan J. Perlis 2 | Maurice V. Wilkes 3 | Richard Hamming 4 | Marvin Minsky 5 | James H. Wilkinson 6 | John McCarthy 7 | Edsger W. Dijkstra 8 | Charles Bachman 9 | Donald E. Knuth 10 | Allen Newell 11 | Herbert A. Simon 12 | Michael O. Rabin 13 | Dana Scott 14 | John W. Backus 15 | Robert Floyd 16 | Kenneth E. Iverson 17 | Tony Hoare 18 | Edgar F. Codd 19 | Stephen A. Cook 20 | Ken Thompson 21 | Dennis Ritchie 22 | Niklaus Wirth 23 | Richard M. Karp 24 | John E. Hopcroft 25 | Robert Tarjan 26 | John Cocke 27 | Ivan Sutherland 28 | William Kahan 29 | Fernando José Corbató 30 | Robin Milner 31 | Butler Lampson 32 | Juris Hartmanis 33 | Richard E. Stearns 34 | Edward Feigenbaum 35 | Raj Reddy 36 | Manuel Blum 37 | Amir Pnueli 38 | Douglas C. Engelbart 39 | Jim Gray 40 | Frederick P. Brooks 41 | Andrew Yao 42 | Ole-Johan Dahl 43 | Kristen Nygaard 44 | Ronald L. Rivest 45 | Adi Shamir 46 | Leonard Adleman 47 | Alan Kay 48 | Vinton G. Cerf 49 | Robert E. Kahn 50 | Peter Naur 51 | Frances E. Allen 52 | Edmund M. Clarke 53 | E. Allen Emerson 54 | Joseph Sifakis 55 | Barbara Liskov 56 | Charles P. Thacker 57 | Leslie Valiant 58 | Judea Pearl 59 | Silvio Micali 60 | Shafi Goldwasser 61 | Leslie Lamport 62 | Michael Stonebraker 63 | Whitfield Diffie 64 | Martin E. Hellman 65 | Tim Berners-Lee 66 | John L. Hennessy 67 | David A. Patterson 68 | Yoshua Bengio 69 | Yann LeCun 70 | Geoffrey Hinton 71 | Edwin Catmull 72 | Pat Hanrahan 73 | Alfred Vaino Aho 74 | Jeffrey David Ullman 75 | -------------------------------------------------------------------------------- /general/voice_channel/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class VoiceChannelPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.voice_channel.permissions[self.name] 11 | 12 | override_owner = auto() 13 | dyn_read = auto() 14 | dyn_write = auto() 15 | dyn_rename = auto() 16 | link_read = auto() 17 | link_write = auto() 18 | -------------------------------------------------------------------------------- /information/__init__.py: -------------------------------------------------------------------------------- 1 | from .bot_info import BotInfoCog 2 | from .heartbeat import HeartbeatCog 3 | from .help import HelpCog 4 | from .inactivity import InactivityCog 5 | from .server_info import ServerInfoCog 6 | from .user_info import UserInfoCog 7 | 8 | 9 | __all__ = ["BotInfoCog", "HeartbeatCog", "HelpCog", "InactivityCog", "ServerInfoCog", "UserInfoCog"] 10 | -------------------------------------------------------------------------------- /information/bot_info/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import BotInfoCog 2 | 3 | 4 | __all__ = ["BotInfoCog"] 5 | -------------------------------------------------------------------------------- /information/bot_info/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | github = MaterialColors.teal[800] 6 | info = MaterialColors.indigo 7 | version = MaterialColors.indigo 8 | -------------------------------------------------------------------------------- /information/bot_info/documentation.md: -------------------------------------------------------------------------------- 1 | # Bot Info 2 | 3 | Contains information about the bot and its tasks. 4 | 5 | 6 | ## `info` 7 | 8 | Shows information about the bot. 9 | 10 | ```css 11 | .[info|infos|about] 12 | ``` 13 | 14 | The information given by this command includes: 15 | 16 | - Bot name and description 17 | - Author 18 | - Contributors 19 | - Version 20 | - Number of enabled cogs 21 | - Github repository 22 | - PyDrocsid [Discord](../../../discord){target=_blank} and [GitHub](https://github.com/PyDrocsid){target=_blank} links 23 | - Prefix 24 | - Help command 25 | - Where to submit bug reports and feature requests 26 | 27 | 28 | ## `version` 29 | 30 | Returns the bot's current version. 31 | 32 | ```css 33 | .[version|v] 34 | ``` 35 | 36 | 37 | ## `github` 38 | 39 | Returns information about the bot's GitHub repository. 40 | 41 | ```css 42 | .[github|gh] 43 | ``` 44 | 45 | 46 | ## `contributors` 47 | 48 | Returns a list of all people that contributed to the bot. 49 | 50 | ```css 51 | .[contributors|contri|con] 52 | ``` 53 | 54 | 55 | ## `cogs` 56 | 57 | Returns a list of all cogs currently in use. 58 | 59 | ```css 60 | .cogs 61 | ``` 62 | 63 | 64 | ## Status Message 65 | 66 | The bot displays a status message that is updated every 20 seconds. The list of status strings is defined under the `profile_status` [translation key](../../../library/translations/){target=_blank}. 67 | -------------------------------------------------------------------------------- /information/bot_info/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | admininfo: view admininfo 3 | 4 | commands: 5 | github: return the github link 6 | version: show version 7 | info: show information about the bot 8 | admininfo: show information about the bot (admin view) 9 | contributors: show list of contributors 10 | cogs: show list of enabled cogs 11 | 12 | author_title: Author 13 | cnt_contributors: 14 | one: "{cnt} Contributor" 15 | many: "{cnt} Contributors" 16 | github_title: GitHub Repository 17 | version_title: Version 18 | prefix_title: Prefix 19 | help_command_title: Help Command 20 | bugs_features_title: Bug Reports / Feature Requests 21 | 22 | profile_status: 23 | - Developed by Defelo#2022 24 | - github.com/PyDrocsid 25 | 26 | bot_description: Default Bot Description 27 | bugs_features: Please [create an issue]({repo}/issues/new) in the GitHub repository. 28 | 29 | enabled_cogs: Enabled Cogs 30 | cnt_cogs_enabled: 31 | one: "{cnt} Cog" 32 | many: "{cnt} Cogs" 33 | 34 | pydrocsid: PyDrocsid 35 | pydrocsid_info: | 36 | **Discord Server:** https://discord.gg/{code} 37 | **GitHub Organization:** https://github.com/PyDrocsid 38 | -------------------------------------------------------------------------------- /information/heartbeat/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import HeartbeatCog 2 | 3 | 4 | __all__ = ["HeartbeatCog"] 5 | -------------------------------------------------------------------------------- /information/heartbeat/cog.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from pathlib import Path 3 | from typing import Optional 4 | 5 | from discord import Forbidden, User 6 | from discord.ext import tasks 7 | from discord.utils import format_dt, utcnow 8 | 9 | from PyDrocsid.cog import Cog 10 | from PyDrocsid.config import Config 11 | from PyDrocsid.environment import OWNER_ID 12 | from PyDrocsid.translations import t 13 | from PyDrocsid.util import send_editable_log 14 | 15 | from ...contributor import Contributor 16 | 17 | 18 | tg = t.g 19 | t = t.heartbeat 20 | 21 | 22 | class HeartbeatCog(Cog, name="Heartbeat"): 23 | CONTRIBUTORS = [Contributor.Defelo, Contributor.wolflu] 24 | 25 | def __init__(self): 26 | super().__init__() 27 | 28 | self.initialized = False 29 | 30 | def get_owner(self) -> Optional[User]: 31 | return self.bot.get_user(OWNER_ID) 32 | 33 | @tasks.loop(seconds=20) 34 | async def status_loop(self): 35 | if (owner := self.get_owner()) is None: 36 | return 37 | try: 38 | await send_editable_log( 39 | owner, 40 | t.online_status, 41 | t.status_description(Config.NAME, Config.VERSION), 42 | t.heartbeat, 43 | format_dt(now := utcnow(), style="D") + " " + format_dt(now, style="T"), 44 | ) 45 | Path("health").write_text(str(int(datetime.now().timestamp()))) 46 | except Forbidden: 47 | pass 48 | 49 | async def on_ready(self): 50 | if (owner := self.get_owner()) is not None: 51 | try: 52 | await send_editable_log( 53 | owner, 54 | t.online_status, 55 | t.status_description(Config.NAME, Config.VERSION), 56 | t.logged_in, 57 | format_dt(now := utcnow(), style="D") + " " + format_dt(now, style="T"), 58 | force_resend=True, 59 | force_new_embed=not self.initialized, 60 | ) 61 | except Forbidden: 62 | pass 63 | 64 | if owner is not None: 65 | try: 66 | self.status_loop.start() 67 | except RuntimeError: 68 | self.status_loop.restart() 69 | 70 | self.initialized = True 71 | -------------------------------------------------------------------------------- /information/heartbeat/documentation.md: -------------------------------------------------------------------------------- 1 | # Heartbeat 2 | 3 | Contains the heartbeat function, which sends a status embed to the bot owner every 20 seconds. 4 | 5 | The information given is when the bot was last started and when it last updated the embed. With each restart, a new embed is sent, which the bot edits every 20 seconds until it is switched off. This is helpful for troubleshooting, for example if the bot freezes. 6 | 7 | This cog is also responsible for updating the Docker healthcheck. If this cog is not loaded, the healthcheck will constantly fail (if not explicitly disabled). 8 | 9 | !!! Note 10 | To set the owner, you have to set `OWNER_ID` in the `.env` file. 11 | -------------------------------------------------------------------------------- /information/heartbeat/translations/en.yml: -------------------------------------------------------------------------------- 1 | online_status: ":vertical_traffic_light: Online Status" 2 | status_description: ":label: **{} v{}**" 3 | logged_in: ":robot: Logged in" 4 | heartbeat: ":heartbeat: Heartbeat" 5 | -------------------------------------------------------------------------------- /information/help/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import HelpCog 2 | 3 | 4 | __all__ = ["HelpCog"] 5 | -------------------------------------------------------------------------------- /information/help/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | help = 0x008080 6 | -------------------------------------------------------------------------------- /information/help/documentation.md: -------------------------------------------------------------------------------- 1 | # Help 2 | 3 | Contains the `.help` command, which can be used to get either a list of all available commands or detailed information about a specific command. 4 | 5 | 6 | ## `.help` 7 | 8 | ```css 9 | .help [cog|command] 10 | ``` 11 | 12 | Arguments: 13 | 14 | | Argument | Required | Description | 15 | |:---------:|:--------:|:------------------------------------------------------------| 16 | | `cog` | | The cog whose command list is requested | 17 | | `command` | | The bot command for which detailed information is requested | 18 | 19 | 20 | ### Command List 21 | 22 | You can either use `.help` to get a list of all commands grouped by cogs or `.help ` to list all commands of a given cog. 23 | 24 | !!! note 25 | The command list does not include commands that cannot be executed by the requesting user (e.g. due to missing permissions). 26 | 27 | 28 | ### Detailed Information 29 | 30 | Detailed information about a given command is provided by executing `.help `. 31 | 32 | The information given by this command includes: 33 | 34 | - Aliases 35 | - Parameters 36 | - Description 37 | - Subcommands 38 | - Required/Optional permissions 39 | - Link to the PyDrocsid documentation 40 | -------------------------------------------------------------------------------- /information/help/translations/en.yml: -------------------------------------------------------------------------------- 1 | commands: 2 | help: shows this message 3 | 4 | cog_or_command_not_found: This cog or command does not exist. 5 | description: Description 6 | subcommands: Subcommands 7 | no_category: No category 8 | help: Help 9 | help_usage: | 10 | Type `{0}help ` for more info on a command. 11 | Type `{0}help ` for more info on a cog. 12 | required_permissions: Required Permissions 13 | required_permission_level: Required Permission Level 14 | optional_permissions: Optional Permissions 15 | documentation: Documentation 16 | -------------------------------------------------------------------------------- /information/inactivity/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import InactivityCog 2 | 3 | 4 | __all__ = ["InactivityCog"] 5 | -------------------------------------------------------------------------------- /information/inactivity/documentation.md: -------------------------------------------------------------------------------- 1 | # Inactivity 2 | 3 | 4 | *Work in Progress* 5 | -------------------------------------------------------------------------------- /information/inactivity/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from datetime import datetime 4 | from typing import Union 5 | 6 | from sqlalchemy import BigInteger, Column 7 | 8 | from PyDrocsid.database import Base, UTCDateTime, db 9 | 10 | 11 | class Activity(Base): 12 | __tablename__ = "activity" 13 | 14 | id: Union[Column, int] = Column(BigInteger, primary_key=True, unique=True) 15 | timestamp: Union[Column, datetime] = Column(UTCDateTime) 16 | 17 | @staticmethod 18 | async def create(object_id: int, timestamp: datetime) -> Activity: 19 | row = Activity(id=object_id, timestamp=timestamp) 20 | await db.add(row) 21 | return row 22 | 23 | @staticmethod 24 | async def update(object_id: int, timestamp: datetime) -> Activity: 25 | if not (row := await db.get(Activity, id=object_id)): 26 | row = await Activity.create(object_id, timestamp) 27 | elif timestamp > row.timestamp: 28 | row.timestamp = timestamp 29 | return row 30 | -------------------------------------------------------------------------------- /information/inactivity/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class InactivityPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.inactivity.permissions[self.name] 11 | 12 | read = auto() 13 | write = auto() 14 | scan = auto() 15 | -------------------------------------------------------------------------------- /information/inactivity/settings.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.settings import Settings 2 | 3 | 4 | class InactivitySettings(Settings): 5 | inactive_days = 14 6 | -------------------------------------------------------------------------------- /information/inactivity/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | read: list inactive users and read inactivity configuration 3 | write: write inactivity configuration 4 | scan: scan all channels for latest message of each user 5 | 6 | scanning: "Scanning..." 7 | scanning_channel: 8 | one: "Scanning {cnt} channel ({} / {} done):" 9 | many: "Scanning {cnt} channels ({} / {} done):" 10 | scan_complete: 11 | one: "Scanned {cnt} channel." 12 | many: "Scanned {cnt} channels." 13 | updating_members: "Updating members..." 14 | updated_members: 15 | one: "Updated {cnt} member." 16 | many: "Updated {cnt} members." 17 | activity: Activity 18 | status: 19 | active: ":white_check_mark: Active ({})" 20 | inactive: ":warning: Inactive" 21 | inactive_since: ":warning: Inactive ({})" 22 | inactive_users: Inactive Users 23 | inactive_users_cnt: Inactive Users ({}) 24 | no_inactive_users: "No inactive users :white_check_mark:" 25 | user_inactive: "{} {} ({})" 26 | user_inactive_since: "{} {} ({}, {})" 27 | inactive_duration: 28 | one: Users that have not sent a message for {cnt} day are marked as inactive. 29 | many: Users that have not sent a message for {cnt} days are marked as inactive. 30 | inactive_duration_set: 31 | one: Inactivity duration has been set to {cnt} day. 32 | many: Inactivity duration has been set to {cnt} days. 33 | -------------------------------------------------------------------------------- /information/server_info/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import ServerInfoCog 2 | 3 | 4 | __all__ = ["ServerInfoCog"] 5 | -------------------------------------------------------------------------------- /information/server_info/cog.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from discord import Embed, Guild, Member, Status 4 | from discord.ext import commands 5 | from discord.ext.commands import Context, UserInputError, guild_only 6 | 7 | from PyDrocsid.cog import Cog 8 | from PyDrocsid.command import docs, reply 9 | from PyDrocsid.embeds import send_long_embed 10 | from PyDrocsid.translations import t 11 | 12 | from .colors import Colors 13 | from ...contributor import Contributor 14 | 15 | 16 | tg = t.g 17 | t = t.server_info 18 | 19 | 20 | class ServerInfoCog(Cog, name="Server Information"): 21 | CONTRIBUTORS = [Contributor.Defelo] 22 | 23 | async def get_users(self, guild: Guild) -> list[tuple[str, list[Member]]]: 24 | return [] 25 | 26 | async def get_additional_fields(self, guild: Guild) -> list[tuple[str, str]]: 27 | return [] 28 | 29 | @commands.group() 30 | @guild_only() 31 | @docs(t.commands.server) 32 | async def server(self, ctx: Context): 33 | if ctx.subcommand_passed is not None: 34 | if ctx.invoked_subcommand is None: 35 | raise UserInputError 36 | return 37 | 38 | guild: Guild = ctx.guild 39 | embed = Embed(title=guild.name, description=t.info_description, color=Colors.ServerInformation) 40 | if guild.icon: 41 | embed.set_thumbnail(url=guild.icon.url) 42 | created = guild.created_at.date() 43 | embed.add_field(name=t.creation_date, value=f"{created.day}.{created.month}.{created.year}") 44 | online_count = sum([m.status != Status.offline for m in guild.members]) 45 | embed.add_field(name=t.cnt_members(cnt=guild.member_count), value=t.cnt_online(online_count)) 46 | embed.add_field(name=t.owner, value=guild.owner.mention) 47 | 48 | for title, users in await self.get_users(guild): 49 | embed.add_field(name=title, value="\n".join(":small_orange_diamond: " + m.mention for m in users)) 50 | 51 | bots = [m for m in guild.members if m.bot] 52 | bots_online = sum([m.status != Status.offline for m in bots]) 53 | embed.add_field(name=t.cnt_bots(cnt=len(bots)), value=t.cnt_online(bots_online)) 54 | 55 | for name, value in await self.get_additional_fields(guild): 56 | embed.add_field(name=name, value=value) 57 | 58 | await send_long_embed(ctx, embed) 59 | 60 | @server.command(name="bots") 61 | @docs(t.commands.bots) 62 | async def server_bots(self, ctx: Context): 63 | guild: Guild = ctx.guild 64 | online: List[Member] = [] 65 | offline: List[Member] = [] 66 | for member in guild.members: # type: Member 67 | if member.bot: 68 | [offline, online][member.status != Status.offline].append(member) 69 | 70 | cnt = len(online) + len(offline) 71 | embed = Embed(title=t.cnt_bots(cnt=cnt), color=Colors.ServerInformation) 72 | 73 | if not cnt: 74 | embed.colour = Colors.error 75 | embed.description = t.no_bots 76 | await reply(ctx, embed=embed) 77 | return 78 | 79 | if cnt := len(online): 80 | embed.add_field( 81 | name=t.online(cnt=cnt), value="\n".join(":small_orange_diamond: " + m.mention for m in online) 82 | ) 83 | if cnt := len(offline): 84 | embed.add_field( 85 | name=t.offline(cnt=cnt), value="\n".join(":small_blue_diamond: " + m.mention for m in offline) 86 | ) 87 | 88 | await send_long_embed(ctx, embed=embed) 89 | -------------------------------------------------------------------------------- /information/server_info/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | ServerInformation = MaterialColors.indigo 6 | -------------------------------------------------------------------------------- /information/server_info/documentation.md: -------------------------------------------------------------------------------- 1 | # Server Info 2 | 3 | Contains commands to show information about the server and its bots. 4 | 5 | 6 | ## `server` 7 | 8 | Shows information about the server the command is executed in. 9 | 10 | ```css 11 | .server 12 | ``` 13 | 14 | The server information given by this command include: 15 | 16 | - Server name and icon 17 | - Creation date 18 | - Member count (total and online counts) 19 | - Owner 20 | - Team roles and their members 21 | - Bot count (total and online counts) 22 | - Custom information about the server 23 | 24 | 25 | ### `bots` 26 | 27 | Shows a list of all bots in this server and their online statuses. 28 | 29 | ```css 30 | .server bots 31 | ``` 32 | -------------------------------------------------------------------------------- /information/server_info/translations/en.yml: -------------------------------------------------------------------------------- 1 | commands: 2 | server: displays information about this discord server 3 | bots: list all bots on the server 4 | 5 | info_description: Information about this Discord Server 6 | creation_date: Creation Date 7 | cnt_members: 8 | one: "{cnt} Member" 9 | many: "{cnt} Members" 10 | cnt_online: "{} online" 11 | owner: Owner 12 | cnt_bots: 13 | one: "{cnt} Bot" 14 | many: "{cnt} Bots" 15 | no_bots: No bots found. 16 | online: Online ({cnt}) 17 | offline: Offline ({cnt}) 18 | -------------------------------------------------------------------------------- /information/user_info/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import UserInfoCog 2 | 3 | 4 | __all__ = ["UserInfoCog"] 5 | -------------------------------------------------------------------------------- /information/user_info/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | UserInfo = MaterialColors.green[800] 6 | stats = MaterialColors.green 7 | userlog = MaterialColors.green["a400"] 8 | joined = 0x339204 9 | -------------------------------------------------------------------------------- /information/user_info/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from datetime import datetime, timedelta 4 | from typing import Optional, Union 5 | 6 | from discord.utils import utcnow 7 | from sqlalchemy import BigInteger, Boolean, Column, Integer, Text 8 | 9 | from PyDrocsid.database import Base, UTCDateTime, db, filter_by 10 | 11 | 12 | class Join(Base): 13 | __tablename__ = "join" 14 | id: Union[Column, int] = Column(Integer, primary_key=True, unique=True, autoincrement=True) 15 | member: Union[Column, int] = Column(BigInteger) 16 | member_name: Union[Column, str] = Column(Text) 17 | timestamp: Union[Column, datetime] = Column(UTCDateTime) 18 | join_msg_channel_id: Union[Column, int] = Column(BigInteger, nullable=True) 19 | join_msg_id: Union[Column, int] = Column(BigInteger, nullable=True) 20 | 21 | @staticmethod 22 | async def create(member: int, member_name: str, timestamp: Optional[datetime] = None) -> Join: 23 | row = Join(member=member, member_name=member_name, timestamp=timestamp or utcnow()) 24 | await db.add(row) 25 | await db.session.flush() 26 | return row 27 | 28 | @staticmethod 29 | async def update(member: int, member_name: str, joined_at: datetime): 30 | if await db.exists(filter_by(Join, member=member).filter(Join.timestamp >= joined_at - timedelta(minutes=1))): 31 | return 32 | 33 | await Join.create(member, member_name, joined_at) 34 | 35 | 36 | class Leave(Base): 37 | __tablename__ = "leave" 38 | id: Union[Column, int] = Column(Integer, primary_key=True, unique=True, autoincrement=True) 39 | member: Union[Column, int] = Column(BigInteger) 40 | member_name: Union[Column, str] = Column(Text) 41 | timestamp: Union[Column, datetime] = Column(UTCDateTime) 42 | 43 | @staticmethod 44 | async def create(member: int, member_name: str) -> Leave: 45 | row = Leave(member=member, member_name=member_name, timestamp=utcnow()) 46 | await db.add(row) 47 | return row 48 | 49 | 50 | class UsernameUpdate(Base): 51 | __tablename__ = "username_update" 52 | 53 | id: Union[Column, int] = Column(Integer, primary_key=True, unique=True, autoincrement=True) 54 | member: Union[Column, int] = Column(BigInteger) 55 | member_name: Union[Column, str] = Column(Text) 56 | new_name: Union[Column, str] = Column(Text) 57 | nick: Union[Column, bool] = Column(Boolean) 58 | timestamp: Union[Column, datetime] = Column(UTCDateTime) 59 | 60 | @staticmethod 61 | async def create(member: int, member_name: str, new_name: str, nick: bool) -> UsernameUpdate: 62 | row = UsernameUpdate(member=member, member_name=member_name, new_name=new_name, nick=nick, timestamp=utcnow()) 63 | await db.add(row) 64 | return row 65 | 66 | 67 | class Verification(Base): 68 | __tablename__ = "verification" 69 | 70 | id: Union[Column, int] = Column(Integer, primary_key=True, unique=True, autoincrement=True) 71 | member: Union[Column, int] = Column(BigInteger) 72 | member_name: Union[Column, str] = Column(Text) 73 | accepted: Union[Column, bool] = Column(Boolean) 74 | timestamp: Union[Column, datetime] = Column(UTCDateTime) 75 | 76 | @staticmethod 77 | async def create(member: int, member_name: str, accepted: bool) -> Verification: 78 | row = Verification(member=member, member_name=member_name, accepted=accepted, timestamp=utcnow()) 79 | await db.add(row) 80 | return row 81 | -------------------------------------------------------------------------------- /information/user_info/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class UserInfoPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.user_info.permissions[self.name] 11 | 12 | view_userinfo = auto() 13 | view_userlog = auto() 14 | init_join_log = auto() 15 | -------------------------------------------------------------------------------- /information/user_info/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | view_userinfo: view other user's information 3 | view_userlog: view other user's log 4 | init_join_log: initialize join log 5 | 6 | not_allowed: You are not allowed to access this information. 7 | 8 | could_not_send_dm: Message could not be sent. Please enable direct messages from this server! 9 | userinfo: User Information 10 | userlogs: User Logs 11 | membership: Membership 12 | member_since: ":clock2: Member since {}" 13 | not_a_member: ":x: User is not a member of this server." 14 | 15 | ulog: 16 | created: ":sparkles: **Created** this account." 17 | joined: ":tada: **Joined** this server as `{}`." 18 | left: ":door: **Left** this server." 19 | nick: 20 | set: ":label: **Nickname set** to `{}`" 21 | updated: ":label: **Nickname changed** from `{}` to `{}`" 22 | cleared: ":label: **Nickname** `{}` **cleared**" 23 | username_updated: ":label: **Username changed** from `{}` to `{}`" 24 | verification: 25 | accepted: ":green_circle: **Completed verification**" 26 | revoked: ":red_circle: **Revoked verification**" 27 | 28 | init_join_log: Join Log Initialization 29 | filling_join_log: 30 | one: ":hourglass_flowing_sand: Creating join log entries for {cnt} member. This may take a while." 31 | many: ":hourglass_flowing_sand: Creating join log entries for {cnt} members. This may take a while." 32 | join_log_filled: "Join log has been initialized successfully. :white_check_mark:" 33 | 34 | joined_days: "joined less than a week ago" 35 | joined_weeks: 36 | one: "joined {cnt} week ago" 37 | many: "joined {cnt} weeks ago" 38 | joined_months: 39 | one: "joined {cnt} month ago" 40 | many: "joined {cnt} months ago" 41 | joined_years: 42 | one: "joined {cnt} year ago" 43 | many: "joined {cnt} years ago" 44 | -------------------------------------------------------------------------------- /integrations/__init__.py: -------------------------------------------------------------------------------- 1 | from .adventofcode import AdventOfCodeCog 2 | from .cleverbot import CleverBotCog 3 | from .python_docs import PythonDocsCog 4 | from .reddit import RedditCog 5 | from .run_code import RunCodeCog 6 | 7 | 8 | __all__ = ["AdventOfCodeCog", "CleverBotCog", "PythonDocsCog", "RedditCog", "RunCodeCog"] 9 | -------------------------------------------------------------------------------- /integrations/adventofcode/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import AdventOfCodeCog 2 | 3 | 4 | __all__ = ["AdventOfCodeCog"] 5 | -------------------------------------------------------------------------------- /integrations/adventofcode/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | AdventOfCode = 0x009900 # advent of code color 6 | -------------------------------------------------------------------------------- /integrations/adventofcode/documentation.md: -------------------------------------------------------------------------------- 1 | # Advent of Code Integration 2 | 3 | 4 | *Work in Progress* 5 | -------------------------------------------------------------------------------- /integrations/adventofcode/models.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from sqlalchemy import BigInteger, Column, Text 4 | 5 | from PyDrocsid.database import Base, db 6 | 7 | 8 | class AOCLink(Base): 9 | __tablename__ = "aoc_link" 10 | 11 | discord_id: Union[Column, int] = Column(BigInteger, primary_key=True, unique=True) 12 | aoc_id: Union[Column, str] = Column(Text, unique=True) 13 | solutions: Union[Column, str] = Column(Text, nullable=True) 14 | 15 | @staticmethod 16 | async def create(discord_id: int, aoc_id: str) -> "AOCLink": 17 | link = AOCLink(discord_id=discord_id, aoc_id=aoc_id) 18 | await db.add(link) 19 | return link 20 | 21 | @staticmethod 22 | async def publish(discord_id: int, url: str): 23 | row = await db.get(AOCLink, discord_id=discord_id) 24 | row.solutions = url 25 | 26 | @staticmethod 27 | async def unpublish(discord_id: int): 28 | row = await db.get(AOCLink, discord_id=discord_id) 29 | row.solutions = None 30 | -------------------------------------------------------------------------------- /integrations/adventofcode/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class AdventOfCodePermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.adventofcode.permissions[self.name] 11 | 12 | clear = auto() 13 | link_read = auto() 14 | link_write = auto() 15 | role_read = auto() 16 | role_write = auto() 17 | -------------------------------------------------------------------------------- /integrations/adventofcode/settings.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.settings import Settings 2 | 3 | 4 | class AdventOfCodeSettings(Settings): 5 | role = -1 6 | rank = 10 7 | -------------------------------------------------------------------------------- /integrations/adventofcode/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | clear: clear aoc leaderboard cache 3 | link_read: read aoc links 4 | link_write: write aoc links 5 | role_read: read aoc role configuration 6 | role_write: write aoc role configuration 7 | 8 | join_title: How to Join the Private Leaderboard 9 | join_instructions: | 10 | 1. Log in at https://adventofcode.com/ 11 | 2. Go to https://adventofcode.com/leaderboard/private 12 | 3. Enter the code `{}` 13 | Good Luck, Have Fun! :christmas_tree: :gift: 14 | 15 | leaderboard_header: "**Private Leaderboard (Advent of Code {})**" 16 | last_update: "Last Update:" 17 | 18 | links: Advent of Code - User Links 19 | no_links: No member has been linked yet. 20 | link_already_exists: A link already exists for this user. 21 | link_created: Link has been created. 22 | link_not_found: Link not found. 23 | link_removed: Link has been removed. 24 | 25 | aoc_role: Advent of Code - Role 26 | min_rank: Minimum Rank 27 | role_set: Advent of Code role has been configured. 28 | log_role_set: "Advent of Code role has been set to `@{}` ({})." 29 | role_disabled: Advent of Code role has been disabled. 30 | rank_set: Minimum rank for Advent of Code role has been configured. 31 | log_rank_set: Minimum rank for Advent of Code role has been set to {}. 32 | invalid_rank: Invalid rank. 33 | 34 | not_verified: You have to be verified to use this command. 35 | invalid_url: Invalid URL. Only GitHub repositories are allowed. 36 | published: Repository has been published successfully. 37 | not_published: No repository has been published yet. 38 | unpublished: Repository has been removed from your profile. 39 | 40 | solutions: Advent of Code - Solution Repositories 41 | no_solutions: No solutions have been published yet. 42 | -------------------------------------------------------------------------------- /integrations/cleverbot/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import CleverBotCog 2 | 3 | 4 | __all__ = ["CleverBotCog"] 5 | -------------------------------------------------------------------------------- /integrations/cleverbot/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | CleverBot = 0x8EBBF6 # clever bot color 6 | -------------------------------------------------------------------------------- /integrations/cleverbot/documentation.md: -------------------------------------------------------------------------------- 1 | # CleverBot 2 | 3 | 4 | *Work in Progress* 5 | -------------------------------------------------------------------------------- /integrations/cleverbot/models.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from sqlalchemy import BigInteger, Column 4 | 5 | from PyDrocsid.database import Base, db 6 | 7 | 8 | class CleverBotChannel(Base): 9 | __tablename__ = "cleverbot_channel" 10 | 11 | channel: Union[Column, int] = Column(BigInteger, primary_key=True, unique=True) 12 | 13 | @staticmethod 14 | async def create(channel: int) -> "CleverBotChannel": 15 | row = CleverBotChannel(channel=channel) 16 | await db.add(row) 17 | return row 18 | -------------------------------------------------------------------------------- /integrations/cleverbot/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class CleverBotPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.cleverbot.permissions[self.name] 11 | 12 | list = auto() 13 | manage = auto() 14 | reset = auto() 15 | -------------------------------------------------------------------------------- /integrations/cleverbot/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | list: list cleverbot channels 3 | manage: manage cleverbot 4 | reset: reset cleverbot sessions 5 | 6 | cleverbot: Cleverbot 7 | whitelisted_channels: Whitelisted Channels 8 | no_whitelisted_channels: No whitelisted channels. 9 | channel_already_whitelisted: Channel is already whitelisted. 10 | channel_whitelisted: "Channel has been whitelisted. :white_check_mark:" 11 | log_channel_whitelisted: "**Channel** {} has been **whitelisted** for CleverBot." 12 | channel_not_whitelisted: ":x: Channel is not whitelisted." 13 | channel_removed: "Channel has been removed from the whitelist. :white_check_mark:" 14 | log_channel_removed: "**Channel** {} has been **removed** from the CleverBot whitelist." 15 | session_reset: Session for {} was successfully reset. 16 | -------------------------------------------------------------------------------- /integrations/python_docs/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import PythonDocsCog 2 | 3 | 4 | __all__ = ["PythonDocsCog"] 5 | -------------------------------------------------------------------------------- /integrations/python_docs/colors.py: -------------------------------------------------------------------------------- 1 | from discord import Color 2 | 3 | from PyDrocsid.material_colors import MaterialColors 4 | 5 | 6 | class Colors(MaterialColors): 7 | PythonDocs = Color.blurple() 8 | -------------------------------------------------------------------------------- /integrations/python_docs/documentation.md: -------------------------------------------------------------------------------- 1 | # Python Documentation 2 | 3 | Contains commands to access and search the documentation of [Python](https://docs.python.org/3/){target=_blank} and [pycord](https://docs.pycord.dev/en/master/){target=_blank}. When using these commands to search for Python entities (e.g. functions, classes, objects, modules), the documentation is downloaded into the Redis cache to avoid repetitive queries. 4 | 5 | 6 | ## `python_docs` 7 | 8 | Searches the [Python documentation](https://docs.python.org/3/){target=_blank} for any Python entity. 9 | 10 | ```css 11 | .[python_docs|py] [entity] 12 | ``` 13 | 14 | Arguments: 15 | 16 | | Argument | Required |Description | 17 | |:--------:|:--------:|:----------------------------------------------------------------------------------------------------------------------| 18 | | `entity` | | The name of the Python entity to search for. If omitted, you get the direct link to the Python documentation instead. | 19 | 20 | 21 | ## `pycord_docs` 22 | 23 | Searches the [pycord documentation](https://docs.pycord.dev/en/master/){target=_blank} for any pycord entity. 24 | 25 | ```css 26 | .[pycord_docs|pycord|pyc|dpy] [entity] 27 | ``` 28 | 29 | Arguments: 30 | 31 | | Argument | Required | Description | 32 | |:--------:|:--------:|:----------------------------------------------------------------------------------------------------------------------| 33 | | `entity` | | The name of the pycord entity to search for. If omitted, you get the direct link to the pycord documentation instead. | 34 | -------------------------------------------------------------------------------- /integrations/python_docs/translations/en.yml: -------------------------------------------------------------------------------- 1 | no_results: Could not find anything. Sorry. 2 | documentation: "{} Documentation" 3 | -------------------------------------------------------------------------------- /integrations/reddit/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import RedditCog 2 | 3 | 4 | __all__ = ["RedditCog"] 5 | -------------------------------------------------------------------------------- /integrations/reddit/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | Reddit = 0xFF4500 # Reddit color 6 | -------------------------------------------------------------------------------- /integrations/reddit/documentation.md: -------------------------------------------------------------------------------- 1 | # Reddit 2 | 3 | 4 | *Work in Progress* 5 | -------------------------------------------------------------------------------- /integrations/reddit/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from datetime import datetime, timedelta 4 | from typing import Union 5 | 6 | from discord.utils import utcnow 7 | from sqlalchemy import BigInteger, Column, String 8 | 9 | from PyDrocsid.database import Base, UTCDateTime, db, delete, filter_by 10 | 11 | 12 | class RedditChannel(Base): 13 | __tablename__ = "reddit_channel" 14 | 15 | subreddit: Union[Column, str] = Column(String(32), primary_key=True) 16 | channel: Union[Column, int] = Column(BigInteger, primary_key=True) 17 | 18 | @staticmethod 19 | async def create(subreddit: str, channel: int) -> RedditChannel: 20 | row = RedditChannel(subreddit=subreddit, channel=channel) 21 | await db.add(row) 22 | return row 23 | 24 | 25 | class RedditPost(Base): 26 | __tablename__ = "reddit_post" 27 | 28 | post_id: Union[Column, str] = Column(String(16), primary_key=True, unique=True) 29 | timestamp: Union[Column, datetime] = Column(UTCDateTime) 30 | 31 | @staticmethod 32 | async def create(post_id: str) -> RedditPost: 33 | row = RedditPost(post_id=post_id, timestamp=utcnow()) 34 | await db.add(row) 35 | return row 36 | 37 | @staticmethod 38 | async def clean(): 39 | drop_before_timestamp = utcnow() - timedelta(weeks=1) 40 | await db.exec(delete(RedditPost).filter(RedditPost.timestamp < drop_before_timestamp)) 41 | 42 | @staticmethod 43 | async def post(post_id: str) -> bool: 44 | if await db.exists(filter_by(RedditPost, post_id=post_id)): 45 | return False 46 | 47 | await RedditPost.create(post_id) 48 | return True 49 | -------------------------------------------------------------------------------- /integrations/reddit/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class RedditPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.reddit.permissions[self.name] 11 | 12 | read = auto() 13 | write = auto() 14 | trigger = auto() 15 | -------------------------------------------------------------------------------- /integrations/reddit/settings.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.settings import Settings 2 | 3 | 4 | class RedditSettings(Settings): 5 | interval = 4 6 | limit = 4 7 | filter_nsfw = True 8 | -------------------------------------------------------------------------------- /integrations/reddit/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | read: read reddit integration configuration 3 | write: write reddit integration configuration 4 | trigger: manually pull hot posts and reset timer 5 | 6 | reddit: Reddit 7 | interval: Interval 8 | x_hours: 9 | one: "{cnt} hour" 10 | many: "{cnt} hours" 11 | limit: Limit 12 | reddit_links: Reddit links 13 | no_reddit_links: No reddit links have been created yet. 14 | subreddit_not_found: Subreddit does not exist. 15 | reddit_link_not_created_permission: > 16 | Reddit link could not be created because I don't have `send_messages` permission in this channel. 17 | reddit_link_already_exists: Reddit link already exists. 18 | reddit_link_created: Reddit link has been created. 19 | log_reddit_link_created: "**Reddit link** `r/{}` -> {} has been **created**." 20 | reddit_link_not_found: Reddit link does not exist. 21 | reddit_link_removed: "Reddit link has been removed. :white_check_mark:" 22 | log_reddit_link_removed: "**Reddit link** `r/{}` -> {} has been **removed**." 23 | done: "Done :white_check_mark:" 24 | reddit_interval: 25 | one: "Reddit lookup interval: {cnt} hour" 26 | many: "Reddit lookup interval: {cnt} hours" 27 | invalid_interval: Invalid interval. 28 | reddit_interval_set: "Reddit lookup interval has been updated. :white_check_mark:" 29 | log_reddit_interval_set: 30 | one: "**Reddit lookup interval** has been **set** to {cnt} hour." 31 | many: "**Reddit lookup interval** has been **set** to {cnt} hours." 32 | invalid_limit: Invalid limit. 33 | reddit_limit: "Reddit limit: {}" 34 | reddit_limit_set: "Reddit limit has been updated. :white_check_mark:" 35 | log_reddit_limit_set: "**Reddit limit** has been **set** to {}." 36 | could_not_fetch: Could not fetch reddit posts of r/{}. 37 | cannot_send: Reddit integration cannot send messages to {}. 38 | nsfw_filter: "NSFW filter" 39 | nsfw_filter_now_enabled: "Reddit NSFW filter has been enabled." 40 | log_nsfw_filter_now_enabled: "**Reddit NSFW filter** has been **enabled**." 41 | nsfw_filter_now_disabled: "Reddit NSFW filter has been disabled." 42 | log_nsfw_filter_now_disabled: "**Reddit NSFW filter** has been **disabled**." 43 | -------------------------------------------------------------------------------- /integrations/run_code/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import RunCodeCog 2 | 3 | 4 | __all__ = ["RunCodeCog"] 5 | -------------------------------------------------------------------------------- /integrations/run_code/api.py: -------------------------------------------------------------------------------- 1 | from aiohttp import ClientSession 2 | 3 | 4 | class PistonException(BaseException): 5 | @property 6 | def error(self) -> str: 7 | return self.args[0]["message"] 8 | 9 | 10 | class PistonAPI: 11 | ENVIRONMENTS_URL = "https://emkc.org/api/v2/piston/runtimes" 12 | EXECUTE_URL = "https://emkc.org/api/v2/piston/execute" 13 | 14 | def __init__(self): 15 | self.environments: dict[str, str] = {} 16 | self.aliases: dict[str, str] = {} 17 | 18 | def get_language(self, language: str) -> str | None: 19 | if language in self.environments: 20 | return language 21 | 22 | return self.aliases.get(language) 23 | 24 | async def load_environments(self): 25 | async with ClientSession() as session, session.get(self.ENVIRONMENTS_URL) as response: 26 | if response.status != 200: 27 | raise PistonException 28 | 29 | environments = await response.json() 30 | 31 | self.environments = {env["language"]: env["version"] for env in environments} 32 | self.aliases = {alias: env["language"] for env in environments for alias in env["aliases"]} 33 | 34 | async def run_code(self, language: str, source: str, stdin: str = "") -> dict: 35 | async with ClientSession() as session, session.post( 36 | PistonAPI.EXECUTE_URL, 37 | json={ 38 | "language": language, 39 | "version": self.environments[language], 40 | "stdin": stdin, 41 | "files": [{"content": source}], 42 | }, 43 | ) as response: 44 | if response.status != 200: 45 | raise PistonException(await response.json()) 46 | 47 | return await response.json() 48 | -------------------------------------------------------------------------------- /integrations/run_code/cog.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from aiohttp import ClientError 4 | from discord import Embed 5 | from discord.ext import commands 6 | from discord.ext.commands import CommandError, Context, UserInputError 7 | from sentry_sdk import capture_exception 8 | 9 | from PyDrocsid.cog import Cog 10 | from PyDrocsid.command import docs 11 | from PyDrocsid.embeds import send_long_embed 12 | from PyDrocsid.logger import get_logger 13 | from PyDrocsid.material_colors import MaterialColors 14 | from PyDrocsid.translations import t 15 | 16 | from .api import PistonAPI, PistonException 17 | from ...contributor import Contributor 18 | 19 | 20 | logger = get_logger(__name__) 21 | 22 | tg = t.g 23 | t = t.run_code 24 | 25 | N_CHARS = 1000 26 | 27 | 28 | class RunCodeCog(Cog, name="Run Code"): 29 | CONTRIBUTORS = [Contributor.Florian, Contributor.Defelo] 30 | 31 | def __init__(self): 32 | self.api = PistonAPI() 33 | 34 | async def on_ready(self): 35 | logger.info("Loading piston environments") 36 | try: 37 | await self.api.load_environments() 38 | except PistonException: 39 | logger.error("Could not load piston environments!") 40 | return 41 | 42 | self.run.help = ( 43 | t.commands.run + "\n\n" + t.supported_languages(", ".join(f"`{lang}`" for lang in self.api.environments)) 44 | ) 45 | 46 | async def execute(self, ctx: Context, language: str, source: str, stdin: str = ""): 47 | if not (language := self.api.get_language(language)): 48 | raise CommandError(t.error_unsupported_language(language)) 49 | 50 | await ctx.trigger_typing() 51 | 52 | try: 53 | result: dict = await self.api.run_code(language, source, stdin) 54 | except PistonException as e: 55 | capture_exception() 56 | raise CommandError(f"{t.error_run_code}: {e.error}") 57 | except ClientError: 58 | capture_exception() 59 | raise CommandError(t.error_run_code) 60 | 61 | output: str = result["run"]["output"] 62 | if len(output) > N_CHARS: 63 | newline = output.find("\n", N_CHARS, N_CHARS + 20) 64 | if newline == -1: 65 | newline = N_CHARS 66 | output = output[:newline] + "\n..." 67 | 68 | description = "```\n" + output.replace("`", "`\u200b") + "\n```" 69 | 70 | lang = result["language"] 71 | version = result["version"] 72 | embed = Embed(title=t.run_output(lang, version), color=MaterialColors.green, description=description) 73 | if result["run"]["code"] != 0: 74 | embed.colour = MaterialColors.error 75 | 76 | embed.set_footer(text=tg.requested_by(ctx.author, ctx.author.id), icon_url=ctx.author.display_avatar.url) 77 | 78 | await send_long_embed(ctx, embed) 79 | 80 | @commands.command(usage=t.run_usage) 81 | @docs(t.commands.run) 82 | async def run(self, ctx: Context, *, code: str): 83 | if not (match := re.fullmatch(r"(```)([a-zA-Z\d]+)\n(.+?)\1(\n(.+?))?", code, re.DOTALL)): 84 | raise UserInputError 85 | 86 | _, lang, source, _, stdin = match.groups() 87 | await self.execute(ctx, lang, source, stdin) 88 | 89 | @commands.command(aliases=["="]) 90 | @docs(t.commands.eval) 91 | async def eval(self, ctx: Context, *, expr: str): 92 | if not (match := re.fullmatch(r"(`*)([a-zA-Z\d]*\n)?(.+?)\1", expr, re.DOTALL)): 93 | raise UserInputError 94 | 95 | code = ( 96 | "from functools import reduce\n" 97 | "from itertools import *\n" 98 | "from operator import *\n" 99 | "from random import *\n" 100 | "from string import *\n" 101 | "from math import *\n" 102 | "print(eval(open(0).read()))" 103 | ) 104 | 105 | *_, expr = match.groups() 106 | await self.execute(ctx, "python", code, expr.strip()) 107 | -------------------------------------------------------------------------------- /integrations/run_code/documentation.md: -------------------------------------------------------------------------------- 1 | # Run Code 2 | 3 | Contains commands to run code using the [Piston API](https://github.com/engineer-man/piston){target=_blank}. 4 | 5 | 6 | ## `run` 7 | 8 | Executes code using the [Piston API](https://github.com/engineer-man/piston){target=_blank}. 9 | 10 | ````css 11 | .run ``` 12 | 13 | ``` 14 | [stdin] 15 | ```` 16 | 17 | Arguments: 18 | 19 | | Argument | Required | Description | 20 | |:-----------:|:-------------------------:|:--------------------------------------------------| 21 | | `language` | :fontawesome-solid-check: | The language to use for execution | 22 | | `your code` | :fontawesome-solid-check: | The code you want to execute | 23 | | `stdin` | | The text to pass as standard input to the program | 24 | 25 | !!! info 26 | You can use the [`/runtimes`](https://emkc.org/api/v2/piston/runtimes){target=_blank} endpoint to get a list of all supported programming languages. This list is also shown in the help embed of the `run` command (`.help run`). 27 | 28 | 29 | ## `eval` 30 | 31 | Evaluates a Python expression using the [Piston API](https://github.com/engineer-man/piston){target=_blank}. 32 | 33 | ```css 34 | .[eval|=] 35 | ``` 36 | 37 | Arguments: 38 | 39 | | Argument | Required | Description | 40 | |:--------:|:-------------------------:|:----------------------------------| 41 | | `expr` | :fontawesome-solid-check: | The Python expression to evaluate | 42 | -------------------------------------------------------------------------------- /integrations/run_code/translations/en.yml: -------------------------------------------------------------------------------- 1 | commands: 2 | run: Run some code using the [Piston API](https://github.com/engineer-man/piston) 3 | eval: Evaluate a Python expression using the [Piston API](https://github.com/engineer-man/piston) 4 | 5 | error_run_code: An error occurred while executing the code 6 | error_unsupported_language: "Unsupported language: {}" 7 | run_usage: | 8 | `​`​` 9 | 10 | `​`​` 11 | [stdin] 12 | run_output: Run Output ({} {}) 13 | supported_languages: "Supported languages: {}" 14 | -------------------------------------------------------------------------------- /moderation/__init__.py: -------------------------------------------------------------------------------- 1 | from .autoclear import AutoClearCog 2 | from .automod import AutoModCog 3 | from .autorole import AutoRoleCog 4 | from .content_filter import ContentFilterCog 5 | from .invites import InvitesCog 6 | from .logging import LoggingCog 7 | from .mediaonly import MediaOnlyCog 8 | from .message import MessageCog 9 | from .mod import ModCog 10 | from .role_notifications import RoleNotificationsCog 11 | from .spam_detection import SpamDetectionCog 12 | from .threads import ThreadsCog 13 | from .user_notes import UserNoteCog 14 | from .verification import VerificationCog 15 | 16 | 17 | __all__ = [ 18 | "AutoClearCog", 19 | "AutoModCog", 20 | "AutoRoleCog", 21 | "ContentFilterCog", 22 | "InvitesCog", 23 | "LoggingCog", 24 | "MediaOnlyCog", 25 | "MessageCog", 26 | "ModCog", 27 | "RoleNotificationsCog", 28 | "SpamDetectionCog", 29 | "ThreadsCog", 30 | "UserNoteCog", 31 | "VerificationCog", 32 | ] 33 | -------------------------------------------------------------------------------- /moderation/autoclear/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import AutoClearCog 2 | 3 | 4 | __all__ = ["AutoClearCog"] 5 | -------------------------------------------------------------------------------- /moderation/autoclear/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | AutoClear = MaterialColors.pink[900] 6 | -------------------------------------------------------------------------------- /moderation/autoclear/documentation.md: -------------------------------------------------------------------------------- 1 | # AutoClear 2 | 3 | Contains functionality to delete messages that have exceeded a certain age from configured channels. Pinned messages are excluded from this and are not deleted. 4 | 5 | 6 | ## `autoclear` 7 | 8 | Contains subcommands to manage the automatic message deletion.
9 | If not subcommand is given, this command shows a list of all configured channels with their respective TTLs (time to live) in minutes. Unpinned messages in one of those channels will be deleted when their age exceeds this TTL. 10 | 11 | Usage: 12 | 13 | ```css 14 | .[autoclear|ac] 15 | ``` 16 | 17 | Required Permissions: 18 | 19 | - `autoclear.read` 20 | 21 | 22 | ### `set` 23 | 24 | Configures the TTL for a given channel. 25 | 26 | ```css 27 | .autoclear [set|s|add|a|+|=] 28 | ``` 29 | 30 | Arguments: 31 | 32 | | Argument | Required | Description | 33 | |:---------:|:-------------------------:|:----------------------------------------------------| 34 | | `channel` | :fontawesome-solid-check: | The channel for which AutoClear is to be configured | 35 | | `minutes` | :fontawesome-solid-check: | The TTL in minutes | 36 | 37 | Required Permissions: 38 | 39 | - `autoclear.read` 40 | - `autoclear.write` 41 | 42 | !!! note 43 | Messages cannot be deleted exactly after the configured time has elapsed, since the task for deleting old messages only runs every 5 minutes. In the worst case, a message will therefore be deleted 5 minutes after the configured TTL has been exceeded. 44 | 45 | 46 | ### `disable` 47 | 48 | Disables AutoClear in a given channel. 49 | 50 | ```css 51 | .autoclear [disable|d|delete|del|remove|r|-] 52 | ``` 53 | 54 | Arguments: 55 | 56 | | Argument | Required | Description | 57 | |:---------:|:-------------------------:|:-------------------------------------------------| 58 | | `channel` | :fontawesome-solid-check: | The channel in which AutoClear is to be disabled | 59 | 60 | Required Permissions: 61 | 62 | - `autoclear.read` 63 | - `autoclear.write` 64 | -------------------------------------------------------------------------------- /moderation/autoclear/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Union 4 | 5 | from sqlalchemy import BigInteger, Column, Integer 6 | 7 | from PyDrocsid.database import Base, db 8 | 9 | 10 | class AutoClearChannel(Base): 11 | __tablename__ = "autoclear_channel" 12 | 13 | channel: Union[Column, int] = Column(BigInteger, primary_key=True, unique=True) 14 | minutes: Union[Column, int] = Column(Integer) 15 | 16 | @staticmethod 17 | async def create(channel: int, minutes: int) -> AutoClearChannel: 18 | row = AutoClearChannel(channel=channel, minutes=minutes) 19 | await db.add(row) 20 | return row 21 | -------------------------------------------------------------------------------- /moderation/autoclear/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class AutoClearPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.autoclear.permissions[self.name] 11 | 12 | read = auto() 13 | write = auto() 14 | -------------------------------------------------------------------------------- /moderation/autoclear/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | read: Read AutoClear configuration 3 | write: Write AutoClear configuration 4 | 5 | commands: 6 | autoclear: Manage AutoClear channels 7 | set: Configure message ttl of a given channel 8 | disable: Disable AutoClear in a given channel 9 | 10 | autoclear: AutoClear 11 | no_autoclear_channels: ":x: No AutoClear channels." 12 | cannot_read: > 13 | AutoClear could not delete some messages in {} because I don't have `read_message_history` permission there. 14 | not_deleted: > 15 | AutoClear could not delete some messages in {} because I don't have `manage_messages` permission there. 16 | not_configured: AutoClear has not been configured for this channel. 17 | cannot_add: Channel could not be configured because I don't have `manage_messages` permission there. 18 | set: 19 | one: "**Message TTL** for {} has been **set** to {cnt} minute." 20 | many: "**Message TTL** for {} has been **set** to {cnt} minutes." 21 | disabled: "**AutoClear** has been **disabled** for {}." 22 | 23 | minutes: 24 | one: "{cnt} minute" 25 | many: "{cnt} minutes" 26 | -------------------------------------------------------------------------------- /moderation/automod/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import AutoModCog 2 | 3 | 4 | __all__ = ["AutoModCog"] 5 | -------------------------------------------------------------------------------- /moderation/automod/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | AutoMod = MaterialColors.blue["a700"] 6 | -------------------------------------------------------------------------------- /moderation/automod/documentation.md: -------------------------------------------------------------------------------- 1 | # AutoMod 2 | 3 | 4 | *Work in Progress* 5 | -------------------------------------------------------------------------------- /moderation/automod/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class AutoModPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.automod.permissions[self.name] 11 | 12 | autokick_read = auto() 13 | autokick_write = auto() 14 | instantkick_read = auto() 15 | instantkick_write = auto() 16 | -------------------------------------------------------------------------------- /moderation/automod/settings.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.settings import Settings 2 | 3 | 4 | class AutoKickMode: 5 | off = 0 6 | normal = 1 7 | reverse = 2 8 | 9 | 10 | class AutoModSettings(Settings): 11 | autokick_mode = AutoKickMode.off 12 | autokick_delay = 30 13 | autokick_role = -1 14 | instantkick_role = -1 15 | -------------------------------------------------------------------------------- /moderation/automod/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | autokick_read: read autokick configuration 3 | autokick_write: write autokick configuration 4 | instantkick_read: read instantkick configuration 5 | instantkick_write: write instantkick configuration 6 | 7 | cannot_kick: AutoMod could not kick {} ({}) because I don't have `kick_members` permission. 8 | 9 | # autokick 10 | autokick: AutoKick 11 | autokick_mode_configured: 12 | - "**AutoKick** has been **disabled**. :white_check_mark:" 13 | - "**AutoKick mode** has been **set to normal**. :white_check_mark:" 14 | - "**AutoKick mode** has been **set to reverse**. :white_check_mark:" 15 | autokick_delay_configured: "AutoKick delay has been configured. :white_check_mark:" 16 | log_autokick_delay_configured: 17 | one: "**AutoKick delay** has been **set to {cnt} second**." 18 | many: "**AutoKick delay** has been **set to {cnt} seconds**." 19 | autokick_role_configured: "AutoKick role has been configured. :white_check_mark:" 20 | log_autokick_role_configured: "**AutoKick role** has been **set to** {} ({})" 21 | autokick_disabled: AutoKick is disabled. 22 | autokick_mode: 23 | - Mode is set to **normal**. 24 | - Mode is set to **reverse**. 25 | x_seconds: 26 | one: "{cnt} second" 27 | many: "{cnt} seconds" 28 | autokicked: "You have been automatically kicked from {}." 29 | log_autokicked: "Kicked automatically." 30 | 31 | # instantkick 32 | instantkick: InstantKick 33 | instantkick_disabled: ":x: InstantKick is disabled." 34 | instantkick_enabled: "InstantKick is enabled. :white_check_mark:" 35 | instantkick_set_disabled: "**InstantKick** has been **disabled**. :white_check_mark:" 36 | instantkick_role_configured: "InstantKick role has been configured. :white_check_mark:" 37 | log_instantkick_role_configured: "**InstantKick role** has been **set to** {} ({})" 38 | instantkick_cannot_kick: Members with this role cannot be kicked. 39 | -------------------------------------------------------------------------------- /moderation/autorole/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import AutoRoleCog 2 | 3 | 4 | __all__ = ["AutoRoleCog"] 5 | -------------------------------------------------------------------------------- /moderation/autorole/cog.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from discord import Embed, Member, Role 4 | from discord.ext import commands 5 | from discord.ext.commands import CommandError, Context, UserInputError, guild_only 6 | 7 | from PyDrocsid.cog import Cog 8 | from PyDrocsid.config import Contributor 9 | from PyDrocsid.embeds import send_long_embed 10 | from PyDrocsid.emojis import name_to_emoji 11 | from PyDrocsid.translations import t 12 | from PyDrocsid.util import check_role_assignable 13 | 14 | from .colors import Colors 15 | from .models import AutoRole 16 | from .permissions import AutoRolePermission 17 | from ...pubsub import send_to_changelog 18 | 19 | 20 | tg = t.g 21 | t = t.autorole 22 | 23 | 24 | class AutoRoleCog(Cog, name="AutoRole"): 25 | CONTRIBUTORS = [Contributor.Defelo] 26 | 27 | async def on_member_join(self, member: Member): 28 | roles: list[Role] = [] 29 | invalid: list[Role] = [] 30 | 31 | role: Role 32 | for role in map(member.guild.get_role, await AutoRole.all()): 33 | if not role: 34 | continue 35 | 36 | try: 37 | check_role_assignable(role) 38 | except CommandError: 39 | invalid.append(role) 40 | else: 41 | roles.append(role) 42 | 43 | await member.add_roles(*roles) 44 | 45 | if invalid: 46 | raise PermissionError( 47 | member.guild, 48 | t.cannot_assign(cnt=len(invalid), member=member, roles=", ".join(role.mention for role in invalid)), 49 | ) 50 | 51 | @commands.group(aliases=["ar"]) 52 | @AutoRolePermission.read.check 53 | @guild_only() 54 | async def autorole(self, ctx: Context): 55 | """ 56 | configure autorole 57 | """ 58 | 59 | if ctx.subcommand_passed is not None: 60 | if ctx.invoked_subcommand is None: 61 | raise UserInputError 62 | return 63 | 64 | embed = Embed(title=t.autorole, colour=Colors.AutoRole) 65 | out = [] 66 | for role_id in await AutoRole.all(): 67 | role: Optional[Role] = ctx.guild.get_role(role_id) 68 | if role is None: 69 | await AutoRole.remove(role_id) 70 | else: 71 | out.append(f":small_orange_diamond: {role.mention}") 72 | if not out: 73 | embed.description = t.no_autorole 74 | embed.colour = Colors.error 75 | else: 76 | embed.description = "\n".join(out) 77 | await send_long_embed(ctx, embed) 78 | 79 | @autorole.command(name="add", aliases=["a", "+"]) 80 | @AutoRolePermission.write.check 81 | async def autorole_add(self, ctx: Context, *, role: Role): 82 | """ 83 | add a role 84 | """ 85 | 86 | if await AutoRole.exists(role.id): 87 | raise CommandError(t.ar_already_set) 88 | 89 | check_role_assignable(role) 90 | 91 | await AutoRole.add(role.id) 92 | await ctx.message.add_reaction(name_to_emoji["white_check_mark"]) 93 | await send_to_changelog(ctx.guild, t.log_ar_added(role)) 94 | 95 | @autorole.command(name="remove", aliases=["r", "del", "d", "-"]) 96 | @AutoRolePermission.write.check 97 | async def autorole_remove(self, ctx: Context, *, role: Role): 98 | """ 99 | remove a role 100 | """ 101 | 102 | if not await AutoRole.exists(role.id): 103 | raise CommandError(t.ar_not_set) 104 | 105 | await AutoRole.remove(role.id) 106 | await ctx.message.add_reaction(name_to_emoji["white_check_mark"]) 107 | await send_to_changelog(ctx.guild, t.log_ar_removed(role)) 108 | -------------------------------------------------------------------------------- /moderation/autorole/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | AutoRole = MaterialColors.grey[900] 6 | -------------------------------------------------------------------------------- /moderation/autorole/documentation.md: -------------------------------------------------------------------------------- 1 | # AutoRole 2 | 3 | 4 | *Work in Progress* 5 | -------------------------------------------------------------------------------- /moderation/autorole/models.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from sqlalchemy import BigInteger, Column 4 | 5 | from PyDrocsid.async_thread import lock_deco 6 | from PyDrocsid.database import Base, db, delete, select 7 | from PyDrocsid.environment import CACHE_TTL 8 | from PyDrocsid.redis import redis 9 | 10 | 11 | @lock_deco 12 | async def load_cache(): 13 | if await redis.exists(load_key := "autorole_loaded"): 14 | return 15 | 16 | roles = [row.role_id async for row in await db.stream(select(AutoRole))] 17 | 18 | async with redis.pipeline() as pipe: 19 | await pipe.delete(role_key := "autorole_roles") 20 | if roles: 21 | await pipe.sadd(role_key, *roles) 22 | await pipe.expire(role_key, CACHE_TTL) 23 | await pipe.setex(load_key, CACHE_TTL, 1) 24 | await pipe.execute() 25 | 26 | 27 | class AutoRole(Base): 28 | __tablename__ = "autorole" 29 | 30 | role_id: Union[Column, int] = Column(BigInteger, primary_key=True, unique=True) 31 | 32 | @staticmethod 33 | async def add(role: int): 34 | await load_cache() 35 | await redis.sadd(key := "autorole_roles", role) 36 | await redis.expire(key, CACHE_TTL) 37 | await db.add(AutoRole(role_id=role)) 38 | 39 | @staticmethod 40 | async def exists(role: int) -> bool: 41 | await load_cache() 42 | return await redis.sismember("autorole_roles", role) 43 | 44 | @staticmethod 45 | async def all() -> list[int]: 46 | await load_cache() 47 | return list(map(int, await redis.smembers("autorole_roles"))) 48 | 49 | @staticmethod 50 | async def remove(role: int): 51 | await load_cache() 52 | await redis.srem(key := "autorole_roles", role) 53 | await redis.expire(key, CACHE_TTL) 54 | await db.exec(delete(AutoRole).filter_by(role_id=role)) 55 | -------------------------------------------------------------------------------- /moderation/autorole/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class AutoRolePermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.autorole.permissions[self.name] 11 | 12 | read = auto() 13 | write = auto() 14 | -------------------------------------------------------------------------------- /moderation/autorole/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | read: read autorole configuration 3 | write: write autorole configuration 4 | 5 | autorole: AutoRole 6 | no_autorole: No roles have been configured yet. 7 | ar_already_set: Role already set. 8 | ar_added: AutoRole has been added successfully. 9 | log_ar_added: AutoRole `@{}` has been added successfully. 10 | ar_not_set: Role not set. 11 | ar_removed: AutoRole has been removed successfully. 12 | log_ar_removed: AutoRole `@{}` has been removed successfully. 13 | role_not_added_too_high: Role could not be added because `@{}` is higher than `@{}`. 14 | role_not_added_managed_role: Role could not be added because `@{}` cannot be assigned manually. 15 | cannot_assign: 16 | one: AutoRole could not assign {roles} to {member.mention} ({member.id}). 17 | many: "AutoRole could not assign the following roles to {member.mention} ({member.id}): {roles}" 18 | -------------------------------------------------------------------------------- /moderation/content_filter/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import ContentFilterCog 2 | 3 | 4 | __all__ = ["ContentFilterCog"] 5 | -------------------------------------------------------------------------------- /moderation/content_filter/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | ContentFilter = MaterialColors.bluegrey[400] 6 | -------------------------------------------------------------------------------- /moderation/content_filter/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from datetime import datetime 4 | from typing import Union 5 | 6 | from discord.utils import utcnow 7 | from sqlalchemy import BigInteger, Boolean, Column, Integer, Text 8 | 9 | from PyDrocsid.database import Base, UTCDateTime, db, select 10 | from PyDrocsid.environment import CACHE_TTL 11 | from PyDrocsid.redis import redis 12 | 13 | 14 | async def sync_redis() -> list[str]: 15 | out = [] 16 | 17 | async with redis.pipeline() as pipe: 18 | await pipe.delete(key := "content_filter") 19 | 20 | regex: BadWord 21 | async for regex in await db.stream(select(BadWord)): 22 | out.append(regex.regex) 23 | await pipe.lpush(key, regex.regex) 24 | 25 | await pipe.lpush(key, "") 26 | await pipe.expire(key, CACHE_TTL) 27 | 28 | await pipe.execute() 29 | 30 | return out 31 | 32 | 33 | class BadWord(Base): 34 | __tablename__ = "bad_word_list" 35 | 36 | id: Union[Column, int] = Column(Integer, primary_key=True, unique=True, autoincrement=True) 37 | regex: Union[Column, str] = Column(Text, unique=True) 38 | description: Union[Column, str] = Column(Text) 39 | delete: Union[Column, bool] = Column(Boolean) 40 | timestamp: Union[Column, datetime] = Column(UTCDateTime) 41 | 42 | @staticmethod 43 | async def create(regex: str, description: str, delete: bool) -> BadWord: 44 | row = BadWord(regex=regex, description=description, delete=delete, timestamp=utcnow()) 45 | await db.add(row) 46 | await sync_redis() 47 | return row 48 | 49 | async def remove(self) -> None: 50 | await db.delete(self) 51 | await sync_redis() 52 | 53 | @staticmethod 54 | async def get_all_redis() -> list[str]: 55 | if out := await redis.lrange("content_filter", 0, -1): 56 | return [x for x in out if x] 57 | 58 | return await sync_redis() 59 | 60 | @staticmethod 61 | async def get_all_db() -> list[BadWord]: 62 | return await db.all(select(BadWord)) 63 | 64 | 65 | class BadWordPost(Base): 66 | __tablename__ = "bad_word_post" 67 | 68 | id: Union[Column, int] = Column(Integer, primary_key=True, unique=True, autoincrement=True) 69 | member: Union[Column, int] = Column(BigInteger) 70 | member_name: Union[Column, str] = Column(Text) 71 | channel: Union[Column, int] = Column(BigInteger) 72 | content: Union[Column, str] = Column(Text) 73 | deleted_message: Union[Column, bool] = Column(Boolean) 74 | timestamp: Union[Column, datetime] = Column(UTCDateTime) 75 | 76 | @staticmethod 77 | async def create(member: int, member_name: str, channel: int, content: str, deleted: bool) -> BadWordPost: 78 | row = BadWordPost( 79 | member=member, 80 | member_name=member_name, 81 | channel=channel, 82 | content=content, 83 | deleted_message=deleted, 84 | timestamp=utcnow(), 85 | ) 86 | await db.add(row) 87 | return row 88 | -------------------------------------------------------------------------------- /moderation/content_filter/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class ContentFilterPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.content_filter.permissions[self.name] 11 | 12 | bypass = auto() 13 | read = auto() 14 | write = auto() 15 | -------------------------------------------------------------------------------- /moderation/content_filter/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | bypass: bypass content filter checks 3 | read: read content filter configuration 4 | write: write content filter configuration 5 | 6 | commands: 7 | content_filter: "manage content filter settings" 8 | add: "add a regex to the blacklist" 9 | remove: "remove a regex from the blacklist" 10 | check: "check if a string matches a regex \n(set `regex` to `-1` to check against all existing patterns)" 11 | update: "update a content filter pattern" 12 | update_description: "update the description of a pattern" 13 | update_regex: "update regex of a pattern" 14 | delete_message: "change whether to delete messages that match a pattern" 15 | 16 | ulog_message: ":stop_sign: **Sent** a message with the forbidden string `{}` in <#{}> (not deleted)." 17 | ulog_message_deleted: ":stop_sign: **Sent** a message with the forbidden string `{}` in <#{}> (deleted)." 18 | 19 | already_blacklisted: "This regex is already in the list!" 20 | not_blacklisted: "This regex is not in the blacklist!" 21 | description_length: "The description has to be 500 or less characters long!" 22 | invalid_regex: "Not a valid regular expression!" 23 | 24 | log_content_filter_added: "**Regex** `{}` was **added** to **Blacklist** by {}" 25 | confirm_text: "Are you sure that you want to remove the filter `{}` ({})?" 26 | log_content_filter_removed: "**Regex** `{}` was **removed** from **Blacklist** by {}" 27 | log_description_updated: "**Description** was **updated** for regex *{}*\nfrom: `{}`\nto: `{}`" 28 | log_regex_updated: "**Regex** was **updated**\nfrom: `{}`\nto: `{}`" 29 | log_delete_updated: "**Delete** was set to **{}** for `{}`" 30 | log_forbidden_posted: | 31 | {} sent a **[message]({})** in {}, which contained one or more new **forbidden expressions**: `{}` (no delete required) 32 | All matched ID's: `{}` 33 | log_forbidden_posted_deleted: | 34 | {}'s **[message]({})** in {} was **deleted** because it contained one or more new **forbidden expressions**: `{}` 35 | All matched ID's: `{}` 36 | log_forbidden_posted_not_deleted: | 37 | {} sent a **[message]({})** in {}, which contained one or more new **forbidden expressions**: `{}` 38 | All matched ID's: `{}` 39 | **The message could not be deleted** because I do not have `manage_messages` permission in this channel. 40 | 41 | bad_word_list_header: "Blacklisted Expressions" 42 | no_pattern_listed: "No blacklisted patterns yet!" 43 | 44 | embed_field_name: ":dna: ID `{}` - {}" 45 | embed_field_value: "Regex: `{}`\nDelete: *{}*" 46 | delete: "True" 47 | not_delete: "False" 48 | 49 | checked_expressions: "Checked Expressions" 50 | matches: "Matches:" 51 | no_matches: "No matches found!" 52 | invalid_pattern: "Invalid pattern!" 53 | -------------------------------------------------------------------------------- /moderation/invites/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import InvitesCog 2 | 3 | 4 | __all__ = ["InvitesCog"] 5 | -------------------------------------------------------------------------------- /moderation/invites/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | Invites = MaterialColors.lightgreen["a700"] 6 | -------------------------------------------------------------------------------- /moderation/invites/documentation.md: -------------------------------------------------------------------------------- 1 | # Invite Whitelist 2 | 3 | Contains commands to manage a whitelist for discord invites that may be sent in the chat. Invites not on the list will be deleted automatically. 4 | 5 | 6 | ## `invites` 7 | 8 | Contains subcommands to manage the whitelist of allowed discord servers. 9 | 10 | ```css 11 | .[invites|i] 12 | ``` 13 | 14 | 15 | ### `list` 16 | 17 | Returns a list of all Discord servers on the whitelist. 18 | 19 | ```css 20 | .invites [list|l|?] 21 | ``` 22 | 23 | 24 | ### `show` 25 | 26 | Shows detailed information about a given server on the whitelist. 27 | 28 | ```css 29 | .invites [show|info|s|i] 30 | ``` 31 | 32 | Arguments: 33 | 34 | | Argument | Required | Description | 35 | |:--------:|:-------------------------:|:--------------------------------| 36 | | `server` | :fontawesome-solid-check: | The server's name, id or invite | 37 | 38 | 39 | ### `add` 40 | 41 | Adds a Discord server to the whitelist. 42 | 43 | ```css 44 | .invites [add|+|a] 45 | ``` 46 | 47 | Arguments: 48 | 49 | | Argument | Required | Description | 50 | |:-----------:|:-------------------------:|:------------------------------------------------------------| 51 | | `invite` | :fontawesome-solid-check: | The invite link (should be permanent with unlimited usages) | 52 | | `applicant` | :fontawesome-solid-check: | The user who wants to add the server to the list | 53 | 54 | Required Permissions: 55 | 56 | - `invites.manage` 57 | 58 | 59 | ### `remove` 60 | 61 | Removes a server from the whitelist. 62 | 63 | ```css 64 | .invites [remove|r|del|d|-] 65 | ``` 66 | 67 | Arguments: 68 | 69 | | Argument | Required | Description | 70 | |:--------:|:-------------------------:|:--------------------------------| 71 | | `server` | :fontawesome-solid-check: | The server's name, id or invite | 72 | 73 | Required Permissions: 74 | 75 | - `invites.manage` 76 | 77 | 78 | ### `update` 79 | 80 | Contains subcommands to update the invite link and description of a server on the whitelist. 81 | 82 | ```css 83 | .invites [update|u] 84 | ``` 85 | 86 | !!! note 87 | These commands can be used by the applicant of the respective server as well as by anyone who has the `invites.manage` permission. 88 | 89 | 90 | #### `description` 91 | 92 | Changes the description of a server on the whitelist. 93 | 94 | ```css 95 | .invites update [description|d] [description] 96 | ``` 97 | 98 | Arguments: 99 | 100 | | Argument | Required | Description | 101 | |:-------------:|:--------:|:---------------------------------------------------------------------| 102 | | `description` | | The new description. If omitted, the current description is cleared. | 103 | 104 | 105 | #### `invite` 106 | 107 | Updates the invite link to a server on the whitelist. 108 | 109 | ```css 110 | .invites update [invite|i] 111 | ``` 112 | 113 | Arguments: 114 | 115 | | Argument | Required | Description | 116 | |:--------:|:-------------------------:|:----------------------------------------------------------------| 117 | | `invite` | :fontawesome-solid-check: | The new invite link (should be permanent with unlimited usages) | 118 | -------------------------------------------------------------------------------- /moderation/invites/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from datetime import datetime 4 | from typing import Optional, Union 5 | 6 | from discord.utils import utcnow 7 | from sqlalchemy import BigInteger, Boolean, Column, Integer, String, Text 8 | 9 | from PyDrocsid.database import Base, UTCDateTime, db 10 | 11 | 12 | class AllowedInvite(Base): 13 | __tablename__ = "allowed_invite" 14 | 15 | guild_id: Union[Column, int] = Column(BigInteger, primary_key=True, unique=True) 16 | code: Union[Column, str] = Column(String(16)) 17 | guild_name: Union[Column, str] = Column(String(128)) 18 | applicant: Union[Column, int] = Column(BigInteger) 19 | approver: Union[Column, int] = Column(BigInteger) 20 | description: Union[Column, Optional[str]] = Column(Text, nullable=True) 21 | created_at: Union[Column, datetime] = Column(UTCDateTime) 22 | 23 | @staticmethod 24 | async def create(guild_id: int, code: str, guild_name: str, applicant: int, approver: int) -> AllowedInvite: 25 | row = AllowedInvite( 26 | guild_id=guild_id, 27 | code=code, 28 | guild_name=guild_name, 29 | applicant=applicant, 30 | approver=approver, 31 | description=None, 32 | created_at=utcnow(), 33 | ) 34 | await db.add(row) 35 | return row 36 | 37 | @staticmethod 38 | async def update(guild_id: int, code: str, guild_name: str): 39 | row: AllowedInvite = await db.get(AllowedInvite, guild_id=guild_id) 40 | row.code = code 41 | row.guild_name = guild_name 42 | 43 | 44 | class InviteLog(Base): 45 | __tablename__ = "invite_log" 46 | 47 | id: Union[Column, int] = Column(Integer, primary_key=True, unique=True, autoincrement=True) 48 | guild_id: Union[Column, int] = Column(BigInteger) 49 | guild_name: Union[Column, str] = Column(String(128)) 50 | applicant: Union[Column, int] = Column(BigInteger) 51 | mod: Union[Column, int] = Column(BigInteger) 52 | timestamp: Union[Column, datetime] = Column(UTCDateTime) 53 | approved: Union[Column, bool] = Column(Boolean) 54 | 55 | @staticmethod 56 | async def create(guild_id: int, guild_name: str, applicant: int, mod: int, approved: bool) -> InviteLog: 57 | row = InviteLog( 58 | guild_id=guild_id, 59 | guild_name=guild_name, 60 | applicant=applicant, 61 | mod=mod, 62 | timestamp=utcnow(), 63 | approved=approved, 64 | ) 65 | await db.add(row) 66 | return row 67 | 68 | 69 | class IllegalInvitePost(Base): 70 | __tablename__ = "illegal_invite_post" 71 | 72 | id: Union[Column, int] = Column(Integer, primary_key=True, unique=True, autoincrement=True) 73 | member: Union[Column, int] = Column(BigInteger) 74 | member_name: Union[Column, str] = Column(Text) 75 | channel: Union[Column, int] = Column(BigInteger) 76 | name: Union[Column, str] = Column(Text) 77 | timestamp: Union[Column, datetime] = Column(UTCDateTime) 78 | 79 | @staticmethod 80 | async def create(member: int, member_name: str, channel: int, name: str) -> IllegalInvitePost: 81 | row = IllegalInvitePost(member=member, member_name=member_name, timestamp=utcnow(), channel=channel, name=name) 82 | await db.add(row) 83 | return row 84 | -------------------------------------------------------------------------------- /moderation/invites/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class InvitesPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.invites.permissions[self.name] 11 | 12 | bypass = auto() 13 | manage = auto() 14 | -------------------------------------------------------------------------------- /moderation/invites/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | bypass: bypass invite checks 3 | manage: manage allowed invites 4 | 5 | illegal_invite_link: > 6 | :x: Illegal discord invite link! 7 | Please contact a team member to submit a request for whitelisting the invitation. 8 | Use the command `{}` to get a list of all allowed discord servers. 9 | log_illegal_invite: | 10 | **Deleted** a message of {} in {} because it contained one or more **illegal discord invite** links: {} 11 | log_illegal_invite_not_deleted: | 12 | {} sent a **message** in {} which contained one or more **illegal discord invite** links: {} 13 | **The message could not be deleted** because I don't have `manage_messages` permission in this channel. 14 | allowed_servers_title: Allowed Discord Servers 15 | allowed_servers_description: "Any link to these servers is allowed to post:\n\n" 16 | no_server_allowed: ":x: No discord servers allowed." 17 | invalid_invite: Invalid invite. 18 | allowed_server_not_found: Allowed discord server not found. 19 | allowed_server: Allowed Discord Server 20 | server_name: Server Name 21 | server_id: Server ID 22 | invite_link: Invite Link 23 | invite_link_expired: Invite Link (expired) 24 | applicant: Applicant 25 | approver: Approver 26 | date: Date 27 | server_already_whitelisted: This server has already been whitelisted. 28 | server_whitelisted: "Server has been whitelisted successfully. :white_check_mark:" 29 | log_server_whitelisted: "**Discord Server** `{}` has been **added** to the whitelist. :white_check_mark:" 30 | server_not_whitelisted: Server is not whitelisted. 31 | server_removed: "Server has been removed from the whitelist successfully. :white_check_mark:" 32 | log_server_removed: "**Discord Server** `{}` has been **removed** from the whitelist." 33 | invite_updated: "Invite link for `{}` has been updated. :white_check_mark:" 34 | log_invite_updated: "{} just **updated** the **invite link** for `{}`." 35 | invites: Invites 36 | ulog_invite_approved: ":+1: **Invite approved** by {}: {}" 37 | ulog_invite_removed: ":-1: **Invite removed** by {}: {}" 38 | ulog_illegal_post: ":no_entry_sign: **Posted an illegal invite** in {}: {}" 39 | description: Description 40 | confirm: "Confirm" 41 | clear_description: "Are you sure that you want to delete the description?" 42 | description_cleared: "Description for `{}` was cleared!" 43 | log_description_cleared: "{} just **cleared** the **description** for `{}`" 44 | description_updated: "Updated description from `{}` to `{}` for `{}`!" 45 | log_description_updated: "{} just **updated** the **description** for `{}` from `{}` to `{}`" 46 | description_too_long: "The length of the description has to be smaller than 500 characters!" 47 | -------------------------------------------------------------------------------- /moderation/logging/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import LoggingCog 2 | 3 | 4 | __all__ = ["LoggingCog"] 5 | -------------------------------------------------------------------------------- /moderation/logging/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | Logging = MaterialColors.blue["a700"] 6 | edit = MaterialColors.yellow[800] 7 | delete = MaterialColors.red[900] 8 | changelog = MaterialColors.teal 9 | -------------------------------------------------------------------------------- /moderation/logging/documentation.md: -------------------------------------------------------------------------------- 1 | # Logging 2 | 3 | 4 | *Work in Progress* 5 | -------------------------------------------------------------------------------- /moderation/logging/models.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from sqlalchemy import BigInteger, Column 4 | 5 | from PyDrocsid.database import Base, db, delete, filter_by, select 6 | 7 | 8 | class LogExclude(Base): 9 | __tablename__ = "log_exclude" 10 | 11 | channel_id: Union[Column, int] = Column(BigInteger, primary_key=True, unique=True) 12 | 13 | @staticmethod 14 | async def add(channel_id: int): 15 | await db.add(LogExclude(channel_id=channel_id)) 16 | 17 | @staticmethod 18 | async def exists(channel_id: int) -> bool: 19 | return await db.exists(filter_by(LogExclude, channel_id=channel_id)) 20 | 21 | @staticmethod 22 | async def all() -> list[int]: 23 | return [le.channel_id async for le in await db.stream(select(LogExclude))] 24 | 25 | @staticmethod 26 | async def remove(channel_id: int): 27 | await db.exec(delete(LogExclude).filter_by(channel_id=channel_id)) 28 | -------------------------------------------------------------------------------- /moderation/logging/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class LoggingPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.logging.permissions[self.name] 11 | 12 | read = auto() 13 | write = auto() 14 | -------------------------------------------------------------------------------- /moderation/logging/settings.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.settings import Settings 2 | 3 | 4 | class LoggingSettings(Settings): 5 | maxage = -1 6 | edit_mindiff = 1 7 | 8 | edit_channel = -1 9 | delete_channel = -1 10 | alert_channel = -1 11 | changelog_channel = -1 12 | member_join_channel = -1 13 | member_leave_channel = -1 14 | -------------------------------------------------------------------------------- /moderation/mediaonly/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import MediaOnlyCog 2 | 3 | 4 | __all__ = ["MediaOnlyCog"] 5 | -------------------------------------------------------------------------------- /moderation/mediaonly/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | MediaOnly = MaterialColors.yellow["a200"] 6 | -------------------------------------------------------------------------------- /moderation/mediaonly/documentation.md: -------------------------------------------------------------------------------- 1 | # MediaOnly 2 | 3 | Contains the `mediaonly` command to set up and manage channels only pictures can be sent to. 4 | 5 | 6 | ## `mediaonly` 7 | 8 | Contains subcommands to manage media-only channels.
9 | If no subcommand is given, this command shows a list of all media-only channels. 10 | 11 | ```css 12 | .[mediaonly|mo] 13 | ``` 14 | 15 | Required Permissions: 16 | 17 | - `mediaonly.read` 18 | 19 | 20 | ### `add` 21 | 22 | Sets the media-only flag for a given text channel. 23 | 24 | ```css 25 | .mediaonly [add|a|+] 26 | ``` 27 | 28 | Arguments: 29 | 30 | | Argument | Required | Description | 31 | |:---------:|:-------------------------:|:------------| 32 | | `channel` | :fontawesome-solid-check: | The channel | 33 | 34 | Required Permissions: 35 | 36 | - `mediaonly.read` 37 | - `mediaonly.write` 38 | 39 | !!! note 40 | Responses to [slash commands](https://blog.discord.com/slash-commands-are-here-8db0a385d9e6){target=_blank} are ignored by the bot, so you should disable slash commands manually in media-only channels. 41 | 42 | 43 | ### `remove` 44 | 45 | Removes the media-only flag from a given text channel. 46 | 47 | ```css 48 | .mediaonly [remove|del|r|d|-] 49 | ``` 50 | 51 | Arguments: 52 | 53 | | Argument | Required | Description | 54 | |:---------:|:-------------------------:|:------------| 55 | | `channel` | :fontawesome-solid-check: | The channel | 56 | 57 | Required Permissions: 58 | 59 | - `mediaonly.read` 60 | - `mediaonly.write` 61 | -------------------------------------------------------------------------------- /moderation/mediaonly/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from datetime import datetime 4 | from typing import AsyncIterable, Union 5 | 6 | from discord.utils import utcnow 7 | from sqlalchemy import BigInteger, Column, Integer, Text 8 | 9 | from PyDrocsid.database import Base, UTCDateTime, db, delete, filter_by, select 10 | from PyDrocsid.environment import CACHE_TTL 11 | from PyDrocsid.redis import redis 12 | 13 | 14 | class MediaOnlyChannel(Base): 15 | __tablename__ = "mediaonly_channel" 16 | 17 | channel: Union[Column, int] = Column(BigInteger, primary_key=True, unique=True) 18 | 19 | @staticmethod 20 | async def add(channel: int): 21 | await redis.setex(f"mediaonly:channel={channel}", CACHE_TTL, 1) 22 | await db.add(MediaOnlyChannel(channel=channel)) 23 | 24 | @staticmethod 25 | async def exists(channel: int) -> bool: 26 | if result := await redis.get(key := f"mediaonly:channel={channel}"): 27 | return result == "1" 28 | 29 | result = await db.exists(filter_by(MediaOnlyChannel, channel=channel)) 30 | await redis.setex(key, CACHE_TTL, int(result)) 31 | return result 32 | 33 | @staticmethod 34 | async def stream() -> AsyncIterable[int]: 35 | row: MediaOnlyChannel 36 | async with redis.pipeline() as pipe: 37 | async for row in await db.stream(select(MediaOnlyChannel)): 38 | channel = row.channel 39 | await pipe.setex(f"mediaonly:channel={channel}", CACHE_TTL, 1) 40 | yield channel 41 | await pipe.execute() 42 | 43 | @staticmethod 44 | async def remove(channel: int): 45 | await redis.setex(f"mediaonly:channel={channel}", CACHE_TTL, 0) 46 | await db.exec(delete(MediaOnlyChannel).filter_by(channel=channel)) 47 | 48 | 49 | class MediaOnlyDeletion(Base): 50 | __tablename__ = "mediaonly_deletion" 51 | 52 | id: Union[Column, int] = Column(Integer, primary_key=True, unique=True, autoincrement=True) 53 | member: Union[Column, int] = Column(BigInteger) 54 | member_name: Union[Column, str] = Column(Text) 55 | channel: Union[Column, int] = Column(BigInteger) 56 | timestamp: Union[Column, datetime] = Column(UTCDateTime) 57 | 58 | @staticmethod 59 | async def create(member: int, member_name: str, channel: int) -> MediaOnlyDeletion: 60 | row = MediaOnlyDeletion(member=member, member_name=member_name, timestamp=utcnow(), channel=channel) 61 | await db.add(row) 62 | return row 63 | -------------------------------------------------------------------------------- /moderation/mediaonly/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class MediaOnlyPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.mediaonly.permissions[self.name] 11 | 12 | bypass = auto() 13 | read = auto() 14 | write = auto() 15 | -------------------------------------------------------------------------------- /moderation/mediaonly/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | bypass: bypass mediaonly checks 3 | read: read mediaonly configuration 4 | write: write mediaonly configuration 5 | 6 | commands: 7 | mediaonly: manage media only channels 8 | list: list media only channels 9 | add: add a media only channel 10 | remove: remove a media only channel 11 | 12 | mediaonly: MediaOnly 13 | deleted_nomedia: > 14 | Only pictures are allowed in this channel. 15 | For conversations please use the channels designated for this purpose. 16 | log_deleted_nomedia: "**Deleted a message** of {} in **media only channel** {} because it did not contain an image." 17 | log_nomedia_not_deleted: > 18 | MediaOnly could not delete a message of {} in {} because I don't have `manage_messages` permission there. 19 | media_only_channels_header: "Media only channels" 20 | no_media_only_channels: ":x: No media only channels." 21 | channel_already_media_only: Channel is already a media only channel. 22 | channel_now_media_only: "Channel is now a media only channel. :white_check_mark:" 23 | log_channel_now_media_only: "**Channel** {} has been **added** to the list of **media only channels**." 24 | channel_not_media_only: Channel is not a media only channel. 25 | channel_not_media_only_anymore: "Channel is not a media only channel anymore. :white_check_mark:" 26 | log_channel_not_media_only_anymore: "**Channel** {} has been **removed** from the list of **media only channels**." 27 | media_only_not_changed_no_permissions: > 28 | Media only channel could not be added because I don't have `manage_messages` permission there. 29 | ulog_deletion: ":frame_photo: **Sent** a message without a picture to the mediaonly channel {}." 30 | -------------------------------------------------------------------------------- /moderation/message/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import MessageCog 2 | 3 | 4 | __all__ = ["MessageCog"] 5 | -------------------------------------------------------------------------------- /moderation/message/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | MessageCommands = MaterialColors.blue["a700"] 6 | -------------------------------------------------------------------------------- /moderation/message/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class MessagePermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.message.permissions[self.name] 11 | 12 | send = auto() 13 | edit = auto() 14 | delete = auto() 15 | clear = auto() 16 | -------------------------------------------------------------------------------- /moderation/message/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | send: send a message as the bot 3 | edit: edit messages sent by the bot 4 | delete: delete a message 5 | clear: delete lots of messages at once 6 | 7 | commands: 8 | send: send messages as the bot 9 | send_text: send a normal message 10 | send_embed: send an embed 11 | send_copy: copy a message (specify message link) 12 | send_discohook: | 13 | send a discohook message 14 | Go to {}, compose your message, click on `Share Message` and copy the link. 15 | edit: edit messages sent by the bot 16 | edit_text: edit a normal message (specify message link) 17 | edit_embed: edit an embed (specify message link) 18 | edit_copy: copy a message into another message (specify message links) 19 | edit_discohook: | 20 | edit a discohook message 21 | Go to {}, compose your message, click on `Share Message` and copy the link. 22 | delete: delete a message (specify message link) 23 | clear: "delete the last messages in this channel" 24 | discohook: create a discohook link for an existing message 25 | 26 | messages: Messages 27 | send_embed_title: "Send me the title of the embed! Send {} to cancel." 28 | send_embed_content: "Ok, now send me the content of the embed! Send {} to cancel." 29 | could_not_send_message: Message could not be sent because I don't have `send_messages` permission in this channel. 30 | send_message: "Now send me the message! Send {} to cancel." 31 | msg_could_not_be_sent: Message could not be sent. 32 | msg_sent: "Message has been sent successfully. :white_check_mark:" 33 | msg_send_cancel: "Sending a message has been cancelled :x:" 34 | could_not_send_embed: Message could not be sent because I don't have `embed_links` permission in this channel. 35 | could_not_edit: This message cannot be edited because the bot is not the author of the message. 36 | send_new_message: "Now send me the new message! Send {} to cancel!" 37 | msg_edited: "Message has been edited successfully. :white_check_mark:" 38 | cannot_delete_dm: Private messages cannot be deleted. 39 | could_not_delete: Message could not be deleted because I don't have `manage_messages` permission in this channel. 40 | msg_deleted: "Message has been deleted successfully. :white_check_mark:" 41 | title_too_long: Title is too long. 42 | cannot_edit_files: Files cannot be edited. 43 | cancel: CANCEL 44 | 45 | confirmation: Confirmation 46 | clear_channel: Clean Channel 47 | count_between: You cannot delete less than 1 or more than 100 messages. 48 | confirm: 49 | one: Are you sure you want to delete the last message in {}? 50 | many: Are you sure you want to delete the last {cnt} messages in {}? 51 | canceled: ":x: Canceled" 52 | confirmed: "Confirmed :white_check_mark:" 53 | clearing: Deleting messages in {} ({}/{}) 54 | msg_not_deleted: Some messages could not be deleted! 55 | deleted_messages: 56 | one: "{cnt} message has been deleted in {}!" 57 | many: "{cnt} messages have been deleted in {}!" 58 | log_cleared: 59 | one: "{} deleted {cnt} message in {}." 60 | many: "{} deleted {cnt} messages in {}." 61 | 62 | cannot_read_messages: You cannot read messages in {}. 63 | discohook_empty: This discohook link does not contain any messages. 64 | discohook_multiple_messages: This discohook link contains more than one message. 65 | discohook_multiple_embeds: This discohook link contains more than one embed. 66 | discohook_create_failed: Discohook link could not be created. 67 | discohook_invalid: Invalid discohook link 68 | -------------------------------------------------------------------------------- /moderation/mod/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import ModCog 2 | 3 | 4 | __all__ = ["ModCog"] 5 | -------------------------------------------------------------------------------- /moderation/mod/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | ModTools = MaterialColors.blue["a700"] 6 | 7 | report = MaterialColors.yellow[800] 8 | warn = MaterialColors.yellow["a200"] 9 | mute = MaterialColors.yellow[600] 10 | unmute = MaterialColors.green["a700"] 11 | kick = MaterialColors.orange[700] 12 | ban = MaterialColors.red[500] 13 | unban = MaterialColors.green["a700"] 14 | -------------------------------------------------------------------------------- /moderation/mod/documentation.md: -------------------------------------------------------------------------------- 1 | # Mod Tools 2 | 3 | 4 | *Work in Progress* 5 | -------------------------------------------------------------------------------- /moderation/mod/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class ModPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.mod.permissions[self.name] 11 | 12 | warn = auto() 13 | mute = auto() 14 | kick = auto() 15 | ban = auto() 16 | -------------------------------------------------------------------------------- /moderation/role_notifications/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import RoleNotificationsCog 2 | 3 | 4 | __all__ = ["RoleNotificationsCog"] 5 | -------------------------------------------------------------------------------- /moderation/role_notifications/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | RoleNotifications = MaterialColors.indigo[400] 6 | -------------------------------------------------------------------------------- /moderation/role_notifications/documentation.md: -------------------------------------------------------------------------------- 1 | # Role Notifications 2 | 3 | 4 | *Work in Progress* 5 | -------------------------------------------------------------------------------- /moderation/role_notifications/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Union 4 | 5 | from sqlalchemy import BigInteger, Boolean, Column 6 | 7 | from PyDrocsid.database import Base, db 8 | 9 | 10 | class RoleNotification(Base): 11 | __tablename__ = "role_notification" 12 | 13 | role_id: Union[Column, int] = Column(BigInteger, primary_key=True) 14 | channel_id: Union[Column, int] = Column(BigInteger, primary_key=True) 15 | ping_role: Union[Column, bool] = Column(Boolean) 16 | ping_user: Union[Column, bool] = Column(Boolean) 17 | 18 | @staticmethod 19 | async def create(role_id: int, channel_id: int, ping_role: bool, ping_user: bool) -> RoleNotification: 20 | row = RoleNotification(role_id=role_id, channel_id=channel_id, ping_role=ping_role, ping_user=ping_user) 21 | await db.add(row) 22 | return row 23 | -------------------------------------------------------------------------------- /moderation/role_notifications/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class RoleNotificationsPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.role_notifications.permissions[self.name] 11 | 12 | read = auto() 13 | write = auto() 14 | -------------------------------------------------------------------------------- /moderation/role_notifications/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | read: read role notification links 3 | write: write role notification links 4 | 5 | rn_created: Role notification link has been created. 6 | log_rn_created: Role notification link `@{}` -> {} has been created. 7 | rn_removed: Role notification link has been removed. 8 | log_rn_removed: Role notification link `@{}` -> {} has been removed. 9 | rn_links: Role Notification Links 10 | rn_no_links: No role notification links have been created yet. 11 | rn_role_added: Role {} has been added to {}. 12 | rn_role_removed: Role {} has been removed from {}. 13 | rn_ping_role: ping role 14 | rn_ping_user: ping user 15 | cannot_send: RoleNotifications cannot send messages to {}. 16 | -------------------------------------------------------------------------------- /moderation/spam_detection/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import SpamDetectionCog 2 | 3 | 4 | __all__ = ["SpamDetectionCog"] 5 | -------------------------------------------------------------------------------- /moderation/spam_detection/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | SpamDetection = MaterialColors.deeporange[700] 6 | -------------------------------------------------------------------------------- /moderation/spam_detection/documentation.md: -------------------------------------------------------------------------------- 1 | # Spam Detection 2 | 3 | 4 | *Work in Progress* 5 | -------------------------------------------------------------------------------- /moderation/spam_detection/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class SpamDetectionPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.spam_detection.permissions[self.name] 11 | 12 | read = auto() 13 | write = auto() 14 | bypass = auto() 15 | -------------------------------------------------------------------------------- /moderation/spam_detection/settings.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.settings import Settings 2 | 3 | 4 | class SpamDetectionSettings(Settings): 5 | max_hops_alert = 0 6 | max_hops_warning = 0 7 | max_hops_temp_mute = 0 8 | temp_mute_duration = 10 9 | -------------------------------------------------------------------------------- /moderation/spam_detection/translations/en.yml: -------------------------------------------------------------------------------- 1 | commands: 2 | spam_detection: view and change spam detection settings 3 | channel_hopping: edit spam detection settings 4 | alert: set the number of allowed hops before a alert-channel notification is used (0 for disabling) 5 | warning: set the number of allowed hops before a dm notification is used (0 for disabling) 6 | temp_mute: edit the channel hopping mute settings 7 | temp_mute_hops: set the number of allowed hops before a alert-channel notification is used (0 for disabling) 8 | temp_mute_duration: set the number of seconds a user is muted (0 for disabling) 9 | 10 | permissions: 11 | read: read spam detection configuration 12 | write: write spam detection configuration 13 | bypass: bypass the channel hopping check 14 | 15 | spam_detection: Spam Detection 16 | 17 | max_x_hops: 18 | one: "Max.: `{cnt}` channel hop per minute" 19 | many: "Max.: `{cnt}` channel hops per minute" 20 | seconds_muted: 21 | one: "{cnt} second" 22 | many: "{cnt} seconds" 23 | 24 | member_id: Member ID 25 | 26 | channel_hopping: Channel hopping 27 | channel_hopping_warning: Warning via DM 28 | channel_hopping_alert: Alert message 29 | channel_hopping_mute: Temporare mute 30 | mute_duration: Mute duration 31 | reason: was timouted because of channelhopping 32 | 33 | cant_mute: "Cannot mute {} ({}) for channel hopping because of missing permissions" 34 | 35 | current_channel: Current Channel 36 | 37 | new_amount: New amount 38 | hop_amount_set: "The **maximum amount** of **channel hops per minute** has been **set to {}** for **{}**." 39 | mute_time_set: "The **muted time** for channel hooping has been **set to {} seconds**." 40 | hop_detection_disabled: "**Channel Hopping Detection** has been **disabled** for **{}**." 41 | change_types: 42 | warnings: warnings 43 | alerts: alerts 44 | mutes: mutes 45 | 46 | hops_in_last_minute: 47 | one: "{cnt} hop in the last minute" 48 | many: "{cnt} hops in the last minute" 49 | 50 | channel_hopping_warning_sent: "Please stop channel hopping!" 51 | -------------------------------------------------------------------------------- /moderation/threads/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import ThreadsCog 2 | 3 | 4 | __all__ = ["ThreadsCog"] 5 | -------------------------------------------------------------------------------- /moderation/threads/cog.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from datetime import datetime 3 | from typing import Optional 4 | 5 | from discord import Embed, Forbidden, Role, TextChannel, Thread 6 | from discord.ext import commands 7 | from discord.ext.commands import Context, guild_only 8 | from discord.utils import format_dt, snowflake_time 9 | 10 | from PyDrocsid.cog import Cog 11 | from PyDrocsid.command import docs 12 | from PyDrocsid.embeds import send_long_embed 13 | from PyDrocsid.redis import redis 14 | from PyDrocsid.settings import RoleSettings 15 | from PyDrocsid.translations import t 16 | 17 | from .colors import Colors 18 | from .permissions import ThreadsPermission 19 | from ...contributor import Contributor 20 | from ...pubsub import send_alert 21 | 22 | 23 | tg = t.g 24 | t = t.threads 25 | 26 | 27 | class ThreadsCog(Cog, name="Thread Utils"): 28 | CONTRIBUTORS = [Contributor.Defelo] 29 | 30 | async def on_thread_create(self, thread: Thread): 31 | await self.on_thread_join(thread) 32 | 33 | async def on_thread_join(self, thread: Thread): 34 | if await redis.exists(key := f"thread_created:{thread.id}"): 35 | return 36 | await redis.setex(key, 24 * 3600, "1") 37 | 38 | role: Optional[Role] = thread.guild.get_role(await RoleSettings.get("thread_auto_join")) 39 | if not role: 40 | return 41 | 42 | try: 43 | await asyncio.sleep(1) 44 | msg = await thread.send(role.mention) 45 | await msg.delete() 46 | except Forbidden: 47 | await send_alert(thread.guild, t.new_thread_alert(thread.mention, thread.parent.mention)) 48 | 49 | @commands.command(aliases=["t"]) 50 | @ThreadsPermission.list.check 51 | @guild_only() 52 | @docs(t.commands.threads) 53 | async def threads(self, ctx: Context): 54 | 55 | await ctx.trigger_typing() 56 | 57 | async def get_threads(channel: TextChannel) -> list[tuple[Thread, bool]]: 58 | out_ = channel.threads.copy() 59 | out_ += await channel.archived_threads(limit=None).flatten() 60 | if channel.permissions_for(ctx.guild.me).manage_threads and not channel.is_news(): 61 | out_ += await channel.archived_threads(limit=None, private=True).flatten() 62 | return [ 63 | (thread_, any(member.id == ctx.author.id for member in await thread_.fetch_members())) 64 | for thread_ in out_ 65 | ] 66 | 67 | def last_timestamp(thread: Thread) -> datetime: 68 | return snowflake_time(thread.last_message_id) 69 | 70 | threads = [ 71 | *{ 72 | thread 73 | for threads in await asyncio.gather( 74 | *[ 75 | get_threads(channel) 76 | for channel in ctx.guild.text_channels 77 | if channel.permissions_for(ctx.guild.me).read_message_history 78 | and channel.permissions_for(ctx.author).view_channel 79 | ] 80 | ) 81 | for thread in threads 82 | } 83 | ] 84 | threads.sort(key=lambda x: last_timestamp(x[0]), reverse=True) 85 | 86 | out = [] 87 | thread: Thread 88 | for thread, joined in threads: 89 | if not thread.permissions_for(ctx.author).view_channel: 90 | continue 91 | 92 | line = f":small_{'blue' if thread.archived else 'orange'}_diamond: " 93 | line += ":white_check_mark: " if joined else ":x: " 94 | if thread.archived: 95 | line += f"[#{thread.name}](https://discord.com/channels/{thread.guild.id}/{thread.id})" 96 | else: 97 | line += f"{thread.mention}" 98 | line += f" ({thread.parent.mention}, {format_dt(last_timestamp(thread), style='R')})" 99 | out.append(line) 100 | 101 | embed = Embed(title=t.threads, description="\n".join(out), color=Colors.Threads) 102 | if not out: 103 | embed.description = t.no_threads 104 | embed.colour = Colors.error 105 | 106 | await send_long_embed(ctx, embed, paginate=True) 107 | -------------------------------------------------------------------------------- /moderation/threads/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | Threads = MaterialColors.indigo[900] 6 | -------------------------------------------------------------------------------- /moderation/threads/documentation.md: -------------------------------------------------------------------------------- 1 | # Thread Utils 2 | 3 | 4 | *Work in Progress* 5 | -------------------------------------------------------------------------------- /moderation/threads/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class ThreadsPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.threads.permissions[self.name] 11 | 12 | list = auto() 13 | -------------------------------------------------------------------------------- /moderation/threads/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | list: list all existing threads 3 | 4 | commands: 5 | threads: list all existing threads 6 | 7 | threads: Threads 8 | no_threads: No threads have been created yet! 9 | new_thread_alert: Thread {} has been created in {} but auto join members could not be added, because I don't have permission to send messages there! -------------------------------------------------------------------------------- /moderation/user_notes/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import UserNoteCog 2 | 3 | 4 | __all__ = ["UserNoteCog"] 5 | -------------------------------------------------------------------------------- /moderation/user_notes/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | user_notes = MaterialColors.lime["a700"] 6 | -------------------------------------------------------------------------------- /moderation/user_notes/documentation.md: -------------------------------------------------------------------------------- 1 | # User Notes 2 | 3 | Contains the `.user_notes` command to manage user notes. 4 | 5 | 6 | ## `user_notes` 7 | 8 | Contains subcommands to manage user notes. 9 | 10 | ```css 11 | .[user_notes|un] 12 | ``` 13 | 14 | Required Permissions: 15 | 16 | - `user_notes.read` 17 | 18 | 19 | ### `show` 20 | 21 | Shows all notes for a specific user. 22 | 23 | ```css 24 | .user_notes [show|s|list|l] 25 | ``` 26 | 27 | Arguments: 28 | 29 | | Argument | Required | Description | 30 | |:--------:|:-------------------------:|:------------| 31 | | `user` | :fontawesome-solid-check: | A user | 32 | 33 | Required Permissions: 34 | 35 | - `user_notes.read` 36 | 37 | 38 | ### `add` 39 | 40 | Adds a note to a specific user. 41 | 42 | ```css 43 | .user_notes [add|a|+] 44 | ``` 45 | 46 | Arguments: 47 | 48 | | Argument | Required | Description | 49 | |:---------:|:-------------------------:|:----------------| 50 | | `user` | :fontawesome-solid-check: | A user | 51 | | `content` | :fontawesome-solid-check: | The note to add | 52 | 53 | Required Permissions: 54 | 55 | - `user_notes.read` 56 | - `user_notes.write` 57 | 58 | 59 | ### `remove` 60 | 61 | Removes a user note by note id. 62 | 63 | ```css 64 | .user_notes [remove|r|delete|d|-] 65 | ``` 66 | 67 | Arguments: 68 | 69 | | Argument | Required | Description | 70 | |:---------:|:-------------------------:|:------------| 71 | | `note_id` | :fontawesome-solid-check: | A note id | 72 | 73 | Required Permissions: 74 | 75 | - `user_notes.read` 76 | - `user_notes.write` 77 | -------------------------------------------------------------------------------- /moderation/user_notes/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from datetime import datetime 4 | from typing import Union 5 | 6 | from discord.utils import utcnow 7 | from sqlalchemy import BigInteger, Column, Text 8 | 9 | from PyDrocsid.database import Base, UTCDateTime, db 10 | 11 | 12 | class UserNote(Base): 13 | __tablename__ = "user_notes" 14 | 15 | id: Union[Column, int] = Column(BigInteger, primary_key=True, unique=True, autoincrement=True) 16 | member_id: Union[Column, int] = Column(BigInteger) 17 | author_id: Union[Column, int] = Column(BigInteger) 18 | content: Union[Column, str] = Column(Text) 19 | timestamp: Union[Column, datetime] = Column(UTCDateTime) 20 | 21 | @staticmethod 22 | async def create(member_id: int, author_id: int, content: str) -> UserNote: 23 | row = UserNote(member_id=member_id, author_id=author_id, content=content, timestamp=utcnow()) 24 | await db.add(row) 25 | return row 26 | -------------------------------------------------------------------------------- /moderation/user_notes/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class UserNotePermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.user_notes.permissions[self.name] 11 | 12 | read = auto() 13 | write = auto() 14 | -------------------------------------------------------------------------------- /moderation/user_notes/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | read: read user notes 3 | write: create and delete user notes 4 | 5 | commands: 6 | user_notes: mange notes for users 7 | user_notes_show: display notes for a specific user 8 | user_notes_add: add a note for a specific user 9 | user_notes_remove: delete a user note by specifying its id 10 | 11 | user_notes: "User Notes" 12 | too_long: Note content is too long. 13 | no_notes: No notes have been created yet for this user. 14 | user_note_entry: | 15 | ID: `{id}`, Author: {author} 16 | {content} 17 | note_not_found: "The note was not found." 18 | new_note: | 19 | {} added a new note for {}: 20 | {} 21 | removed_note: | 22 | {} removed a note from {}: 23 | {} 24 | content: Content 25 | ulog_entry: ":notepad_spiral: **User note created** (by {}): {}" 26 | 27 | confirmation: "Confirmation" 28 | confirm: | 29 | Are you sure you want to delete this note from {}? 30 | {} 31 | canceled: ":x: Canceled" 32 | confirmed: "Confirmed :white_check_mark:" 33 | -------------------------------------------------------------------------------- /moderation/verification/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import VerificationCog 2 | 3 | 4 | __all__ = ["VerificationCog"] 5 | -------------------------------------------------------------------------------- /moderation/verification/colors.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.material_colors import MaterialColors 2 | 3 | 4 | class Colors(MaterialColors): 5 | Verification = MaterialColors.green["a100"] 6 | -------------------------------------------------------------------------------- /moderation/verification/documentation.md: -------------------------------------------------------------------------------- 1 | # Verification 2 | 3 | Contains a verification system that adds or removes certain roles from a member when they send the bot a command with a pre-set password via direct messages. 4 | 5 | 6 | ## `verification` 7 | 8 | Shows the current verification configuration. This includes the roles that will be added to or removed from the member (verification roles), the configured password and the amount of time a member has to be in the server before they can verify. 9 | 10 | ```css 11 | .[verification|vf] 12 | ``` 13 | 14 | Required Permissions: 15 | 16 | - `verification.read` 17 | 18 | 19 | ### `add` 20 | 21 | Adds a verification role. If you set `reverse` to `true`, the role will be removed from the member instead of being added. Note that verification will fail if the member does not have all reverse verification roles! 22 | 23 | ```css 24 | .verification [add|a|+] [reverse=False] 25 | ``` 26 | 27 | Arguments: 28 | 29 | | Argument | Required | Description | 30 | |:---------:|:-------------------------:|:-----------------------------------------------------| 31 | | `role` | :fontawesome-solid-check: | The verification role | 32 | | `reverse` | | Remove this role instead of adding it to the member. | 33 | 34 | Required Permissions: 35 | 36 | - `verification.read` 37 | - `verification.write` 38 | 39 | 40 | ### `remove` 41 | 42 | Removes an existing verification role. 43 | 44 | ```css 45 | .verification [remove|r|-] 46 | ``` 47 | 48 | Arguments: 49 | 50 | | Argument | Required | Description | 51 | |:--------:|:-------------------------:|:----------------------| 52 | | `role` | :fontawesome-solid-check: | The verification role | 53 | 54 | Required Permissions: 55 | 56 | - `verification.read` 57 | - `verification.write` 58 | 59 | 60 | ### `password` 61 | 62 | Sets the *secret* password the member will need to verify with. 63 | 64 | ```css 65 | .verification [password|p] 66 | ``` 67 | 68 | Arguments: 69 | 70 | | Argument | Required | Description | 71 | |:----------:|:-------------------------:|:-------------| 72 | | `password` | :fontawesome-solid-check: | The password | 73 | 74 | Required Permissions: 75 | 76 | - `verification.read` 77 | - `verification.write` 78 | 79 | !!! note 80 | The password has a maximum length of 256 characters. 81 | 82 | 83 | ### `delay` 84 | 85 | Sets the amount of time a member has to be in the server before they can verify. 86 | 87 | ```css 88 | .verification [delay|d] 89 | ``` 90 | 91 | Arguments: 92 | 93 | | Argument | Required | Description | 94 | |:--------:|:-------------------------:|:------------------------------| 95 | | `delay` | :fontawesome-solid-check: | The amount of time in seconds | 96 | 97 | Required Permissions: 98 | 99 | - `verification.read` 100 | - `verification.write` 101 | 102 | 103 | ## `verify` 104 | 105 | Allows server members to verify themselves. If the specified password is correct and the configured delay has elapsed, the configured verification roles will be added to or removed from the member. 106 | 107 | ```css 108 | verify 109 | ``` 110 | 111 | Arguments: 112 | 113 | | Argument | Required | Description | 114 | |:----------:|:-------------------------:|:--------------------------| 115 | | `password` | :fontawesome-solid-check: | The verification password | 116 | 117 | !!! note 118 | As this command can only be used in direct messages, it does not start with the configured bot prefix! So, for example, if the configured password is `Tr0ub4dor&3`, a member would have to send this exact message to the bot to complete verification: 119 | 120 | ``` 121 | verify Tr0ub4dor&3 122 | ``` 123 | -------------------------------------------------------------------------------- /moderation/verification/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Union 4 | 5 | from sqlalchemy import BigInteger, Boolean, Column 6 | 7 | from PyDrocsid.database import Base, db 8 | 9 | 10 | class VerificationRole(Base): 11 | __tablename__ = "verification_role" 12 | 13 | role_id: Union[Column, int] = Column(BigInteger, primary_key=True, unique=True) 14 | reverse: Union[Column, bool] = Column(Boolean) 15 | 16 | @staticmethod 17 | async def create(role_id: int, reverse: bool) -> VerificationRole: 18 | row = VerificationRole(role_id=role_id, reverse=reverse) 19 | await db.add(row) 20 | return row 21 | -------------------------------------------------------------------------------- /moderation/verification/permissions.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from PyDrocsid.permission import BasePermission 4 | from PyDrocsid.translations import t 5 | 6 | 7 | class VerificationPermission(BasePermission): 8 | @property 9 | def description(self) -> str: 10 | return t.verification.permissions[self.name] 11 | 12 | read = auto() 13 | write = auto() 14 | -------------------------------------------------------------------------------- /moderation/verification/settings.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.settings import Settings 2 | 3 | 4 | class VerificationSettings(Settings): 5 | password = "" # noqa: S105 6 | delay = -1 7 | -------------------------------------------------------------------------------- /moderation/verification/translations/en.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | read: read verification command configuration 3 | write: write verification command configuration 4 | 5 | verification: Verification 6 | verification_role_added: "Verification role has been added. :white_check_mark:" 7 | log_verification_role_added: "**Verification role** has been **added**: `@{}` ({}) (normal)" 8 | log_verification_role_added_reverse: "**Verification role** has been **added**: `@{}` ({}) (reverse)" 9 | verification_role_removed: " Verification role has been removed. :white_check_mark:" 10 | log_verification_role_removed: "**Verification role** has been **removed**: `@{}` ({})" 11 | password_too_long: Password is too long, sorry. 12 | verification_password_configured: "Verification password has been configured. :white_check_mark:" 13 | log_verification_password_configured: "**Verification password** has been **set** to `{}`" 14 | verification_delay_configured: "Verification delay has been configured. :white_check_mark:" 15 | verification_delay_disabled: "**Verification delay** has been **disabled**. :white_check_mark:" 16 | log_verification_delay_configured: 17 | one: "**Verification delay** has been **set** to {cnt} second." 18 | many: "**Verification delay** has been **set** to {cnt} seconds." 19 | verification_disabled: Verification command is disabled. 20 | verification_enabled: Verification command is enabled. 21 | password: Password 22 | roles_normal: Roles (normal) 23 | roles_reverse: Roles (reverse) 24 | verification_role: " - `@{}` ({}) (normal)" 25 | verification_role_reverse: " - `@{}` ({}) (reverse)" 26 | private_only: This command can only be used in direct messages. 27 | already_verified: You are already verified. 28 | password_incorrect: Password is incorrect! 29 | too_soon: Wow, you must have read the rules pretty quickly! Maybe just read the rules again more thoroughly and try again later... 30 | verified: "Verified successfully :white_check_mark:" 31 | verification_role_already_set: Role already set. 32 | verification_role_not_set: Role not set. 33 | verification_failed: Verification failed! 34 | x_seconds: 35 | one: "{cnt} second" 36 | many: "{cnt} seconds" 37 | role_not_set_too_high: Role could not be set because `@{}` is higher than `@{}`. 38 | role_not_set_managed_role: Role could not be set because `@{}` cannot be assigned manually. 39 | cannot_assign: 40 | one: Verification could not assign {roles} to {member.mention} ({member.id}). 41 | many: "Verification could not assign the following roles to {member.mention} ({member.id}): {roles}" 42 | -------------------------------------------------------------------------------- /pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # install via ./pre-commit.sh install 4 | 5 | SELF=$(realpath $0) 6 | SRC=$(realpath pre-commit.sh) 7 | _HOOK=$(git rev-parse --git-path hooks)/pre-commit 8 | HOOK=$(realpath $_HOOK) 9 | 10 | install() { 11 | echo Installing pre-commit hook... 12 | rm -f $_HOOK 13 | cp $SRC $HOOK && chmod +x $HOOK && echo Pre-Commit hook has been installed successfully! 14 | } 15 | 16 | uninstall() { 17 | echo Uninstalling pre-commit hook... 18 | rm -f $_HOOK && echo Pre-Commit hook has been uninstalled successfully! 19 | } 20 | 21 | save() { 22 | git submodule foreach "git name-rev --name-only --always --exclude='tags/*' HEAD > .head.ref && /bin/bash $SELF save" && git submodule update 23 | 24 | git diff > .unstaged.patch 25 | git diff --staged > .staged.patch 26 | git restore -WS . 27 | } 28 | 29 | restore() { 30 | git submodule foreach "git restore . && git checkout \$(cat .head.ref) && rm .head.ref && /bin/bash $SELF restore" 31 | 32 | git apply -3 --allow-empty .staged.patch && rm .staged.patch || echo "Warning: Could not restore staged changes in $(pwd)" 33 | git apply --allow-empty .unstaged.patch && rm .unstaged.patch || echo "Warning: Could not restore unstaged changes in $(pwd)" 34 | } 35 | 36 | if [[ "$1" =~ ^(install|uninstall|save|restore)$ ]]; then 37 | $1 38 | exit $? 39 | fi 40 | 41 | if ! cmp -s $SRC $HOOK; then 42 | set -e 43 | echo Updating pre-commit hook... 44 | rm -f $_HOOK 45 | cp $SRC $HOOK && chmod +x $HOOK && echo Pre-Commit hook has been updated successfully! 46 | /bin/bash $HOOK "$@" 47 | exit $? 48 | elif [[ "$SELF" != "$HOOK" ]]; then 49 | /bin/bash $HOOK "$@" 50 | exit $? 51 | fi 52 | 53 | save 54 | git apply --allow-empty --index .staged.patch && rm .staged.patch && touch .staged.patch 55 | git add -u 56 | 57 | $HOME/.local/bin/poe pre-commit 58 | code=$? 59 | 60 | git add -u 61 | 62 | restore 63 | 64 | exit $code 65 | -------------------------------------------------------------------------------- /pubsub.py: -------------------------------------------------------------------------------- 1 | from PyDrocsid.pubsub import PubSubChannel 2 | 3 | 4 | send_to_changelog = PubSubChannel() 5 | send_alert = PubSubChannel() 6 | log_auto_kick = PubSubChannel() 7 | get_user_info_entries = PubSubChannel() 8 | get_user_status_entries = PubSubChannel() 9 | get_userlog_entries = PubSubChannel() 10 | revoke_verification = PubSubChannel() 11 | can_respond_on_reaction = PubSubChannel() 12 | ignore_message_edit = PubSubChannel() 13 | ignore_message_delete = PubSubChannel() 14 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "cogs" 3 | version = "0" 4 | description = "" 5 | authors = ["Defelo "] 6 | readme = "README.md" 7 | license = "GPL-3.0-only" 8 | homepage = "https://github.com/PyDrocsid/cogs" 9 | repository = "https://github.com/PyDrocsid/cogs" 10 | 11 | [tool.poetry.dependencies] 12 | python = "^3.10" 13 | PyDrocsid = "^2.3.4" 14 | SQLAlchemy = "^1.4.32" 15 | aiohttp = "^3.8.1" 16 | 17 | # general: custom_commands 18 | # integrations: adventofcode, cleverbot 19 | # moderation: invites 20 | requests = "^2.27.1" 21 | 22 | # information: user_info 23 | python-dateutil = "^2.8.2" 24 | 25 | 26 | [tool.poetry.dev-dependencies] 27 | flake8 = "^4.0.1" 28 | isort = "^5.10.1" 29 | black = "^22.3.0" 30 | wemake-python-styleguide = "^0.16.1" 31 | mypy = "^0.942" 32 | SQLAlchemy = { extras = ["mypy"], version = "^1.4.32" } 33 | types-python-dateutil = "^2.8.10" 34 | types-requests = "^2.27.15" 35 | types-PyYAML = "^6.0.5" 36 | 37 | [tool.poe.tasks] 38 | flake8 = "flake8 . --count --statistics --show-source" 39 | isort = "isort ." 40 | black = "black ." 41 | format = ["isort", "black"] 42 | mypy = "mypy ." 43 | lint = ["format", "flake8"] 44 | pre-commit = ["lint"] 45 | 46 | [tool.poe.tasks.setup] 47 | shell = """ 48 | set -ex 49 | poetry install --no-root 50 | ./pre-commit.sh install 51 | """ 52 | interpreter = "bash" 53 | 54 | [tool.black] 55 | target-version = ["py310"] 56 | line-length = 120 57 | skip-magic-trailing-comma = true 58 | 59 | [tool.isort] 60 | profile = "black" 61 | py_version = 310 62 | line_length = 120 63 | lines_after_imports = 2 64 | reverse_relative = true 65 | known_pydrocsid = ["PyDrocsid"] 66 | sections = ["FUTURE", "STDLIB", "FIRSTPARTY", "THIRDPARTY", "PYDROCSID", "LOCALFOLDER"] 67 | 68 | [tool.mypy] 69 | strict = true 70 | ignore_missing_imports = true 71 | no_incremental = true 72 | namespace_packages = true 73 | plugins = ["sqlalchemy.ext.mypy.plugin"] 74 | 75 | [build-system] 76 | requires = ["setuptools", "poetry-core>=1.0.0"] 77 | build-backend = "poetry.core.masonry.api" 78 | -------------------------------------------------------------------------------- /translations.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from PyDrocsid.translations import load_translations 4 | 5 | 6 | load_translations(Path(__file__).parent) 7 | --------------------------------------------------------------------------------