├── bridget ├── utils │ ├── __init__.py │ ├── services │ │ ├── __init__.py │ │ ├── user_service.py │ │ └── guild_service.py │ ├── errors.py │ ├── pfpcalc.py │ ├── transformers.py │ ├── config.py │ ├── startup_checks.py │ ├── enums.py │ ├── fetchers.py │ ├── views.py │ ├── utils.py │ └── report_action.py ├── .pyre_configuration.local ├── model │ ├── filterimg.py │ ├── __init__.py │ ├── infractions.py │ ├── filterword.py │ ├── tag.py │ ├── issues.py │ ├── giveaway.py │ ├── infraction.py │ ├── user.py │ └── guild.py ├── migrate │ ├── girmodel │ │ ├── __init__.py │ │ ├── cases.py │ │ ├── filterword.py │ │ ├── tag.py │ │ ├── giveaway.py │ │ ├── case.py │ │ ├── user.py │ │ └── guild.py │ └── __main__.py ├── cogs │ ├── say.py │ ├── __init__.py │ ├── unshorten.py │ ├── socialfix.py │ ├── stats.py │ ├── restrictions.py │ ├── timezones.py │ ├── logparsing.py │ ├── snipe.py │ ├── chatgpt.py │ ├── helper.py │ ├── birthday.py │ ├── filters.py │ ├── sync.py │ ├── canister.py │ ├── mod.py │ ├── xp.py │ ├── memes.py │ └── native_actions_listeners.py ├── backend │ ├── utils.py │ ├── __init__.py │ └── appeal.py ├── setup.py └── __main__.py ├── .dockerignore ├── .pyre_configuration ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── codeql.yml ├── .env.example ├── Dockerfile.setup ├── Dockerfile.migrate ├── .libcst.codemod.yaml ├── Dockerfile ├── README.md ├── docker-compose.yml ├── pyproject.toml ├── CONTRIBUTING.md ├── .gitignore └── CODE_OF_CONDUCT.md /bridget/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .utils import * 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .venv 2 | .pytype 3 | .github 4 | .pyre_configuration.local 5 | __pycache__ 6 | __pypackages__ -------------------------------------------------------------------------------- /bridget/.pyre_configuration.local: -------------------------------------------------------------------------------- 1 | { 2 | "oncall": "__main__.py", 3 | "source_directories": [ 4 | "." 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /bridget/utils/services/__init__.py: -------------------------------------------------------------------------------- 1 | import mongoengine 2 | 3 | from .user_service import * 4 | from .guild_service import * 5 | -------------------------------------------------------------------------------- /.pyre_configuration: -------------------------------------------------------------------------------- 1 | { 2 | "site_package_search_strategy": "pep561", 3 | "taint_models_path": "/workspaces/bot-rewrite/.venv/lib/pyre_check/taint" 4 | } 5 | -------------------------------------------------------------------------------- /bridget/model/filterimg.py: -------------------------------------------------------------------------------- 1 | import mongoengine 2 | 3 | 4 | class FilterWord(mongoengine.EmbeddedDocument): 5 | hash = mongoengine.StringField() 6 | 7 | -------------------------------------------------------------------------------- /bridget/migrate/girmodel/__init__.py: -------------------------------------------------------------------------------- 1 | from .case import * 2 | from .cases import * 3 | from .filterword import * 4 | from .giveaway import * 5 | from .guild import * 6 | from .tag import * 7 | from .user import * -------------------------------------------------------------------------------- /bridget/model/__init__.py: -------------------------------------------------------------------------------- 1 | from .infraction import * 2 | from .infractions import * 3 | from .filterword import * 4 | from .giveaway import * 5 | from .guild import * 6 | from .tag import * 7 | from .user import * 8 | from .issues import * 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Main Discord server 4 | - url: https://discord.gg/palera1n 5 | - about: Join the server in the "Bot Rewrite Discussion" thread in #general for bot-rewrite releated disscussion. 6 | -------------------------------------------------------------------------------- /bridget/migrate/girmodel/cases.py: -------------------------------------------------------------------------------- 1 | import mongoengine 2 | from .case import Case 3 | 4 | class Cases(mongoengine.Document): 5 | _id = mongoengine.IntField(required=True) 6 | cases = mongoengine.EmbeddedDocumentListField(Case, default=[]) 7 | meta = { 8 | 'db_alias': 'default', 9 | 'collection': 'cases' 10 | } -------------------------------------------------------------------------------- /bridget/model/infractions.py: -------------------------------------------------------------------------------- 1 | import mongoengine 2 | from .infraction import Infraction 3 | 4 | 5 | class Infractions(mongoengine.Document): 6 | _id = mongoengine.IntField(required=True) 7 | infractions = mongoengine.EmbeddedDocumentListField(Infraction, default=[]) 8 | 9 | meta = { 10 | 'db_alias': 'default', 11 | 'collection': 'infractions' 12 | } 13 | -------------------------------------------------------------------------------- /bridget/model/filterword.py: -------------------------------------------------------------------------------- 1 | import mongoengine 2 | 3 | 4 | class FilterWord(mongoengine.EmbeddedDocument): 5 | notify = mongoengine.BooleanField(required=True) 6 | bypass = mongoengine.IntField(required=True) 7 | word = mongoengine.StringField(required=True) 8 | false_positive = mongoengine.BooleanField(default=False) 9 | piracy = mongoengine.BooleanField(default=False) 10 | -------------------------------------------------------------------------------- /bridget/utils/errors.py: -------------------------------------------------------------------------------- 1 | import discord 2 | 3 | #_NOPERMSERRORMSG = "You don't have permission to use this command." 4 | 5 | class MissingPermissionsError(discord.app_commands.MissingPermissions): 6 | def __init__(self, perms) -> None: 7 | super().__init__(perms) 8 | 9 | @staticmethod 10 | def throw(perms: list = None) -> None: 11 | raise MissingPermissionsError(perms or []) 12 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Bot details 2 | TOKEN= 3 | GUILD_ID= 4 | APPEAL_GUILD_ID= # Optional 5 | APPEAL_MOD_ROLE= # Optional 6 | OWNER_ID= 7 | CLIENT_ID= # Optional but recommended 8 | CLIENT_SECRET= # Optional but recommended 9 | PREFIX=! 10 | 11 | # Backend details 12 | CLIENT_ID= 13 | CLIENT_SECRET= 14 | BACKEND_PORT=8069 15 | BACKEND_APPEALS_CHANNEL= 16 | 17 | # Mongo connection 18 | DB_HOST="127.0.0.1" 19 | DB_PORT=27017 20 | 21 | # ChatGPT (optional) 22 | OPENAI_API_KEY= 23 | -------------------------------------------------------------------------------- /bridget/migrate/girmodel/filterword.py: -------------------------------------------------------------------------------- 1 | import mongoengine 2 | 3 | class FilterWord(mongoengine.EmbeddedDocument): 4 | # _id = mongoengine.ObjectIdField(required=True, default=mongoengine.ObjectId., unique=True, primary_key=True) 5 | notify = mongoengine.BooleanField(required=True) 6 | bypass = mongoengine.IntField(required=True) 7 | word = mongoengine.StringField(required=True) 8 | false_positive = mongoengine.BooleanField(default=False) 9 | piracy = mongoengine.BooleanField(default=False) 10 | -------------------------------------------------------------------------------- /bridget/model/tag.py: -------------------------------------------------------------------------------- 1 | import mongoengine 2 | from datetime import datetime 3 | 4 | 5 | class Tag(mongoengine.EmbeddedDocument): 6 | name = mongoengine.StringField(required=True) 7 | content = mongoengine.StringField(required=True) 8 | added_by_tag = mongoengine.StringField() 9 | added_by_id = mongoengine.IntField() 10 | added_date = mongoengine.DateTimeField(default=datetime.now) 11 | use_count = mongoengine.IntField(default=0) 12 | image = mongoengine.FileField(default=None) 13 | button_links = mongoengine.ListField(default=[], required=False) 14 | -------------------------------------------------------------------------------- /bridget/migrate/girmodel/tag.py: -------------------------------------------------------------------------------- 1 | import mongoengine 2 | from datetime import datetime 3 | 4 | class Tag(mongoengine.EmbeddedDocument): 5 | name = mongoengine.StringField(required=True) 6 | content = mongoengine.StringField(required=True) 7 | added_by_tag = mongoengine.StringField() 8 | added_by_id = mongoengine.IntField() 9 | added_date = mongoengine.DateTimeField(default=datetime.now) 10 | use_count = mongoengine.IntField(default=0) 11 | image = mongoengine.FileField(default=None) 12 | button_links = mongoengine.ListField(default=[], required=False) 13 | -------------------------------------------------------------------------------- /bridget/model/issues.py: -------------------------------------------------------------------------------- 1 | import mongoengine 2 | from datetime import datetime 3 | 4 | 5 | class Issue(mongoengine.EmbeddedDocument): 6 | name = mongoengine.StringField(required=True) 7 | content = mongoengine.StringField(required=True) 8 | added_by_tag = mongoengine.StringField() 9 | added_by_id = mongoengine.IntField() 10 | added_date = mongoengine.DateTimeField(default=datetime.now) 11 | image = mongoengine.FileField(default=None) 12 | button_links = mongoengine.ListField(default=[], required=False) 13 | message_id = mongoengine.IntField() 14 | panic_string = mongoengine.StringField() 15 | color = mongoengine.IntField() 16 | -------------------------------------------------------------------------------- /bridget/cogs/say.py: -------------------------------------------------------------------------------- 1 | import discord 2 | 3 | from typing import Optional 4 | from discord import app_commands 5 | 6 | from utils import Cog, send_success 7 | from utils.enums import PermissionLevel 8 | 9 | 10 | class Say(Cog): 11 | @PermissionLevel.MOD 12 | @app_commands.command() 13 | async def say(self, ctx: discord.Interaction, message: str, channel: discord.TextChannel = None) -> None: 14 | """Make the bot say something 15 | 16 | :param message: Message to send 17 | :param channel: Channel to send to 18 | """ 19 | 20 | channel = channel or ctx.channel 21 | await channel.send(message) 22 | 23 | await send_success(ctx) 24 | -------------------------------------------------------------------------------- /bridget/model/giveaway.py: -------------------------------------------------------------------------------- 1 | import mongoengine 2 | 3 | 4 | class Giveaway(mongoengine.Document): 5 | _id = mongoengine.IntField(required=True) 6 | is_ended = mongoengine.BooleanField(default=False) 7 | end_time = mongoengine.DateTimeField() 8 | channel = mongoengine.IntField() 9 | name = mongoengine.StringField() 10 | entries = mongoengine.ListField(mongoengine.IntField(), default=[]) 11 | previous_winners = mongoengine.ListField( 12 | mongoengine.IntField(), default=[]) 13 | sponsor = mongoengine.IntField() 14 | winners = mongoengine.IntField() 15 | 16 | meta = { 17 | 'db_alias': 'default', 18 | 'collection': 'giveaways' 19 | } 20 | -------------------------------------------------------------------------------- /.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: junepark678 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /bridget/migrate/girmodel/giveaway.py: -------------------------------------------------------------------------------- 1 | import mongoengine 2 | 3 | class Giveaway(mongoengine.Document): 4 | _id = mongoengine.IntField(required=True) 5 | is_ended = mongoengine.BooleanField(default=False) 6 | end_time = mongoengine.DateTimeField() 7 | channel = mongoengine.IntField() 8 | name = mongoengine.StringField() 9 | entries = mongoengine.ListField(mongoengine.IntField(), default=[]) 10 | previous_winners = mongoengine.ListField(mongoengine.IntField(), default=[]) 11 | sponsor = mongoengine.IntField() 12 | winners = mongoengine.IntField() 13 | 14 | meta = { 15 | 'db_alias': 'default', 16 | 'collection': 'giveaways' 17 | } -------------------------------------------------------------------------------- /Dockerfile.setup: -------------------------------------------------------------------------------- 1 | # build stage 2 | FROM python:3.10 AS builder 3 | 4 | # install PDM 5 | RUN pip install -U pip setuptools wheel 6 | RUN pip install pdm 7 | 8 | # copy files 9 | COPY pyproject.toml pdm.lock README.md /project/ 10 | COPY bridget /project/bridget 11 | 12 | # install dependencies and project into the local packages directory 13 | WORKDIR /project 14 | RUN mkdir __pypackages__ && pdm sync --prod --no-editable 15 | 16 | FROM python:3.10 17 | 18 | # retrieve packages from build stage 19 | ENV PYTHONPATH=/project/pkgs 20 | COPY --from=builder /project/__pypackages__/3.10/lib /project/pkgs 21 | 22 | # retrieve executables 23 | COPY --from=builder /project/__pypackages__/3.10/bin/* /bin/ 24 | 25 | 26 | 27 | # set command/entrypoint, adapt to fit your needs 28 | CMD ["pdm", "run", "setup"] 29 | -------------------------------------------------------------------------------- /Dockerfile.migrate: -------------------------------------------------------------------------------- 1 | # build stage 2 | FROM python:3.10 AS builder 3 | 4 | # install PDM 5 | RUN pip install -U pip setuptools wheel 6 | RUN pip install pdm 7 | 8 | # copy files 9 | COPY pyproject.toml pdm.lock README.md /project/ 10 | COPY bridget /project/bridget 11 | 12 | # install dependencies and project into the local packages directory 13 | WORKDIR /project 14 | RUN mkdir __pypackages__ && pdm sync --prod --no-editable 15 | 16 | FROM python:3.10 17 | 18 | # retrieve packages from build stage 19 | ENV PYTHONPATH=/project/pkgs 20 | COPY --from=builder /project/__pypackages__/3.10/lib /project/pkgs 21 | 22 | # retrieve executables 23 | COPY --from=builder /project/__pypackages__/3.10/bin/* /bin/ 24 | 25 | 26 | 27 | # set command/entrypoint, adapt to fit your needs 28 | CMD ["pdm", "run", "migrate"] 29 | -------------------------------------------------------------------------------- /.libcst.codemod.yaml: -------------------------------------------------------------------------------- 1 | # String that LibCST should look for in code which indicates that the 2 | # module is generated code. 3 | generated_code_marker: '@generated' 4 | # Command line and arguments for invoking a code formatter. Anything 5 | # specified here must be capable of taking code via stdin and returning 6 | # formatted code via stdout. 7 | formatter: ['black', '-'] 8 | # List of regex patterns which LibCST will evaluate against filenames to 9 | # determine if the module should be touched. 10 | blacklist_patterns: [] 11 | # List of modules that contain codemods inside of them. 12 | modules: 13 | - 'libcst.codemod.commands' 14 | - 'autotyping' 15 | # Absolute or relative path of the repository root, used for providing 16 | # full-repo metadata. Relative paths should be specified with this file 17 | # location as the base. 18 | repo_root: '.' -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # build stage 2 | FROM python:3.10 AS builder 3 | 4 | # install PDM 5 | RUN pip install -U pip setuptools wheel 6 | RUN pip install pdm 7 | 8 | # copy files 9 | COPY pyproject.toml pdm.lock README.md /project/ 10 | COPY bridget /project/bridget 11 | 12 | # install dependencies and project into the local packages directory 13 | WORKDIR /project 14 | RUN mkdir __pypackages__ && pdm sync --prod --no-editable 15 | 16 | 17 | # run stage 18 | FROM python:3.10 19 | 20 | # retrieve packages from build stage 21 | ENV PYTHONPATH=/project/pkgs 22 | COPY --from=builder /project/__pypackages__/3.10/lib /project/pkgs 23 | 24 | # retrieve executables 25 | COPY --from=builder /project/__pypackages__/3.10/bin/* /bin/ 26 | 27 | # default port 28 | EXPOSE 8096 29 | 30 | 31 | # set command/entrypoint, adapt to fit your needs 32 | CMD ["pdm", "run", "bot"] 33 | -------------------------------------------------------------------------------- /bridget/model/infraction.py: -------------------------------------------------------------------------------- 1 | import mongoengine 2 | import datetime 3 | 4 | 5 | class Infraction(mongoengine.EmbeddedDocument): 6 | _id = mongoengine.IntField(required=True) 7 | _type = mongoengine.StringField(required=True) 8 | date = mongoengine.DateTimeField( 9 | default=datetime.datetime.now, required=True) 10 | until = mongoengine.DateTimeField(default=None) 11 | mod_id = mongoengine.IntField(required=True) 12 | mod_tag = mongoengine.StringField(required=True) 13 | reason = mongoengine.StringField(required=True) 14 | punishment = mongoengine.StringField() 15 | lifted = mongoengine.BooleanField(default=False) 16 | lifted_by_tag = mongoengine.StringField() 17 | lifted_by_id = mongoengine.IntField() 18 | lifted_reason = mongoengine.StringField() 19 | lifted_date = mongoengine.DateField() 20 | -------------------------------------------------------------------------------- /bridget/utils/pfpcalc.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | from typing import Union 3 | from io import BytesIO 4 | 5 | s = 16 6 | 7 | def calculate_hash(image_path: Union[str, bytes, BytesIO]) -> str: 8 | # Open image using PIL 9 | image = Image.open(image_path) 10 | 11 | image = image.resize((s, s), Image.BICUBIC) 12 | 13 | # Convert image to grayscale 14 | image = image.convert('L') 15 | 16 | # Calculate average pixel value 17 | average_pixel = sum(image.getdata()) / (s ** 2) 18 | 19 | # Generate binary hash 20 | hash_value = ''.join(['1' if pixel >= average_pixel else '0' for pixel in image.getdata()]) 21 | 22 | return hash_value 23 | 24 | def hamming_distance(hash1: str, hash2: str) -> float: 25 | # Calculate the Hamming distance between two hashes 26 | return sum(c1 != c2 for c1, c2 in zip(hash1, hash2)) 27 | -------------------------------------------------------------------------------- /bridget/utils/transformers.py: -------------------------------------------------------------------------------- 1 | import discord 2 | 3 | from discord import AppCommandOptionType, app_commands 4 | 5 | 6 | class ImageAttachment(app_commands.Transformer): 7 | @classmethod 8 | def type(cls) -> AppCommandOptionType: 9 | return AppCommandOptionType.attachment 10 | 11 | @classmethod 12 | async def transform(cls, interaction: discord.Interaction, value: str) -> discord.Attachment: 13 | if value is None: 14 | return 15 | 16 | image = await app_commands.transformers.passthrough_transformer(AppCommandOptionType.attachment).transform(interaction, value) 17 | _type = image.content_type 18 | if _type not in ["image/png", "image/jpeg", "image/gif", "image/webp"]: 19 | raise app_commands.TransformerError( 20 | "Attached file was not an image.") 21 | 22 | return image 23 | -------------------------------------------------------------------------------- /bridget/cogs/__init__.py: -------------------------------------------------------------------------------- 1 | from .chatgpt import ChatGPT 2 | from .logs import Logging 3 | from .mod import Mod 4 | from .native_actions_listeners import NativeActionsListeners 5 | from .say import Say 6 | from .snipe import Snipe 7 | from .sync import Sync 8 | from .tags import Tags, TagsGroup 9 | from .unshorten import Unshorten 10 | from .timezones import Timezones 11 | from .helper import Helper 12 | from .filters import FiltersGroup 13 | from .issues import Issues, IssuesGroup 14 | from .misc import Misc 15 | from .memes import Memes, MemesGroup 16 | from .logparsing import LogParsing 17 | from .canister import Canister 18 | from .xp import Xp, StickyRoles 19 | from .appeals import Appeals 20 | from .ioscfw import iOSCFW 21 | from .socialfix import SocialFix 22 | from .restrictions import Restrictions 23 | # from .ocr import OCR 24 | from .birthday import Birthday 25 | from .antiraid import AntiRaidMonitor 26 | -------------------------------------------------------------------------------- /.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 on how to reproduce the bug. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Desktop (please complete the following information):** 23 | - OS: [e.g. iOS] 24 | - Browser [e.g. chrome, safari] 25 | - Version [e.g. 22] 26 | 27 | **Smartphone (please complete the following information):** 28 | - Device: [e.g. iPhone6] 29 | - OS: [e.g. iOS8.1] 30 | - Browser [e.g. stock browser, safari] 31 | - Version [e.g. 22] 32 | 33 | **Additional context** 34 | Add any other context about the problem here. 35 | -------------------------------------------------------------------------------- /bridget/migrate/girmodel/case.py: -------------------------------------------------------------------------------- 1 | import mongoengine 2 | import datetime 3 | 4 | class Case(mongoengine.EmbeddedDocument): 5 | _id = mongoengine.IntField(required=True) 6 | _type = mongoengine.StringField(required=True) 7 | date = mongoengine.DateTimeField(default=datetime.datetime.now, required=True) 8 | until = mongoengine.DateTimeField(default=None) 9 | mod_id = mongoengine.IntField(required=True) 10 | mod_tag = mongoengine.StringField(required=True) 11 | reason = mongoengine.StringField(required=True) 12 | punishment = mongoengine.StringField() 13 | lifted = mongoengine.BooleanField(default=False) 14 | lifted_by_tag = mongoengine.StringField() 15 | lifted_by_id = mongoengine.IntField() 16 | lifted_reason = mongoengine.StringField() 17 | lifted_date = mongoengine.DateField() -------------------------------------------------------------------------------- /bridget/backend/utils.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import asyncio 3 | 4 | from typing import Optional 5 | from discord import BanEntry 6 | 7 | 8 | client_session: Optional[aiohttp.ClientSession] = None 9 | 10 | async def make_client_session() -> None: 11 | global client_session 12 | client_session = aiohttp.ClientSession() 13 | 14 | async def get_client_session() -> aiohttp.ClientSession: 15 | global client_session 16 | if client_session is None: 17 | client_session = aiohttp.ClientSession() 18 | return client_session 19 | 20 | 21 | class EventTS(asyncio.Event): 22 | def set(self): 23 | self._loop.call_soon_threadsafe(super().set) 24 | 25 | def clear(self) -> None: 26 | self._loop.call_soon_threadsafe(super().clear) 27 | 28 | 29 | class AppealRequest: 30 | id: int 31 | completion: EventTS 32 | result: BanEntry 33 | 34 | def __init__(self, id: int): 35 | self.id = id 36 | self.completion = EventTS() 37 | 38 | -------------------------------------------------------------------------------- /bridget/cogs/unshorten.py: -------------------------------------------------------------------------------- 1 | import re 2 | import aiohttp 3 | import discord 4 | 5 | from discord.utils import get 6 | from discord.ext import commands 7 | 8 | from utils import Cog, reply_success 9 | 10 | 11 | class Unshorten(Cog): 12 | @commands.Cog.listener() 13 | async def on_message(self, message: discord.Message) -> None: 14 | if message.author.bot: 15 | return 16 | 17 | emoji = get(message.guild.emojis, name="loading") 18 | regex = r"\b(?:https?:\/\/(?:t\.co|bit\.ly|goo\.gl|fb\.me|tinyurl\.com|j\.mp|is\.gd|v\.gd|git\.io)\/[\w-]+)\b" 19 | 20 | async with aiohttp.ClientSession() as session: 21 | for link in re.findall(regex, message.content): 22 | await message.add_reaction(emoji) 23 | async with session.head(link, allow_redirects=True) as res: 24 | await reply_success(message, description=f"Hey, here's where this short link actually goes to!\n{res.url}") 25 | await message.remove_reaction(emoji, self.bot.user) 26 | -------------------------------------------------------------------------------- /bridget/utils/config.py: -------------------------------------------------------------------------------- 1 | from os import getenv 2 | from logging import warn 3 | 4 | class Config: 5 | def __init__(self) -> None: 6 | self.guild_id = int(getenv("GUILD_ID")) 7 | try: 8 | self.ban_appeal_guild_id = int(getenv("APPEAL_GUILD_ID")) 9 | self.ban_appeal_mod_role = int(getenv("APPEAL_MOD_ROLE")) 10 | except TypeError: 11 | warn("APPEAL_* was not defined, diabling appeals") 12 | self.ban_appeal_guild_id = -1 13 | self.ban_appeal_mod_role = -1 14 | try: 15 | self.backend_client_id = getenv("CLIENT_ID") 16 | self.backend_client_secret = getenv("CLIENT_SECRET") 17 | self.backend_port = int(getenv("BACKEND_PORT")) 18 | self.backend_appeals_channel = int(getenv("BACKEND_APPEALS_CHANNEL")) 19 | except TypeError: 20 | warn("backend env vars were not defined, diabling backend") 21 | self.backend_client_id = "" 22 | self.backend_client_secret = "" 23 | self.backend_port = -1 24 | self.backend_appeals_channel = -1 25 | self.owner_id = int(getenv("OWNER_ID")) 26 | self.prefix = str(getenv("PREFIX")) 27 | 28 | 29 | cfg = Config() 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bridget 2 | [![CodeQL](https://github.com/palera1n/bot-rewrite/actions/workflows/codeql.yml/badge.svg)](https://github.com/palera1n/bot-rewrite/actions/workflows/codeql.yml) 3 | 4 | If you want to help, join us at: [Discord](https://discord.gg/palera1n). We also have a testing server at: [Discord](https://discord.gg/55A4Xjc9RW) 5 | 6 | # Running 7 | 8 | 1. Write an `.env` file from `.env.example` 9 | 2. `pdm install` 10 | 3. Fill in the ids in `setup.py` 11 | 4. `pdm run setup` 12 | 5. `pdm run bot` 13 | 14 | # Goals 15 | 16 | 1. Tries to be 99% compatible with GIR 17 | 2. New codebase 18 | 3. Speed 19 | 4. Multi-Threading 20 | 21 | # Testing 22 | 23 | While in development stage, you do not need to do very extensive tests AND may do commits directly to `main`. Once the bot is released, only bug fixes should be directly pushed to `main`, otherwise to `development` (to be made once released) 24 | 25 | # Contributing 26 | 27 | Please follow [the contribution guidelines](https://github.com/palera1n/bot-rewrite/blob/main/CONTRIBUTING.md). 28 | 29 | # Vulnerabilities 30 | 31 | If you find a security vulnerability, please email `me@itsnebula.net`, and **encrypt your email** with [this GPG key](https://static.itsnebula.net/gpgkey.asc) (`FB04F6C8EC56DA32F33008C53D1B28A5FACCB53B`). 32 | -------------------------------------------------------------------------------- /bridget/backend/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import aiohttp_cors 3 | import discord 4 | import threading 5 | 6 | from aiohttp import web 7 | 8 | from .appeal import Appeal 9 | from .utils import make_client_session 10 | from utils.config import cfg 11 | 12 | 13 | def thread_run(runner) -> None: 14 | loop = asyncio.new_event_loop() 15 | asyncio.set_event_loop(loop) 16 | loop.run_until_complete(runner.setup()) 17 | site = web.TCPSite(runner, '0.0.0.0', cfg.backend_port) 18 | loop.create_task(make_client_session()) 19 | loop.run_until_complete(site.start()) 20 | loop.run_forever() 21 | 22 | 23 | def aiohttp_server(bot: discord.Client) -> web.AppRunner: 24 | app = web.Application() 25 | appeal = Appeal(bot) 26 | app.add_routes([ 27 | web.post('/bridget/appeal', appeal.appeal) 28 | ]) 29 | 30 | cors = aiohttp_cors.setup(app, defaults={ 31 | "*": aiohttp_cors.ResourceOptions( 32 | allow_credentials=True, 33 | expose_headers="*", 34 | allow_headers="*" 35 | ) 36 | }) 37 | 38 | for route in list(app.router.routes()): 39 | cors.add(route) 40 | 41 | return web.AppRunner(app) 42 | 43 | def run(bot: discord.Client) -> None: 44 | t = threading.Thread(target=thread_run, args=(aiohttp_server(bot),), daemon=True) 45 | t.start() 46 | -------------------------------------------------------------------------------- /bridget/utils/startup_checks.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from model import Guild 4 | import utils.services.guild_service as GuildService 5 | 6 | def check_envvars() -> None: 7 | if os.getenv("GUILD_ID") is None: 8 | raise AttributeError( 9 | "Database is not set up properly! The GUILD_ID environment variable is missing. Please recheck your variables.") 10 | 11 | if os.getenv("OWNER_ID") is None: 12 | raise AttributeError( 13 | "Database is not set up properly! The OWNER_ID environment variable is missing. Please recheck your variables.") 14 | 15 | if os.getenv("TOKEN") is None: 16 | raise AttributeError( 17 | "Database is not set up properly! The TOKEN environment variable is missing. Please recheck your variables.") 18 | 19 | 20 | def check_perm_roles() -> None: 21 | the_guild: Guild = GuildService.get_guild() 22 | 23 | roles_to_check = [ 24 | "role_memberplus", 25 | "role_memberpro", 26 | "role_helper", 27 | "role_moderator", 28 | "role_administrator", 29 | ] 30 | 31 | for role in roles_to_check: 32 | try: 33 | getattr(the_guild, role) 34 | except AttributeError: 35 | raise AttributeError( 36 | f"Database is not set up properly! Role '{role}' is missing. Please run `pdm run setup`.") 37 | 38 | 39 | checks = [check_envvars, check_perm_roles] 40 | -------------------------------------------------------------------------------- /bridget/migrate/girmodel/user.py: -------------------------------------------------------------------------------- 1 | import mongoengine 2 | 3 | class User(mongoengine.Document): 4 | _id = mongoengine.IntField(required=True) 5 | is_clem = mongoengine.BooleanField(default=False, required=True) 6 | is_xp_frozen = mongoengine.BooleanField(default=False, required=True) 7 | is_muted = mongoengine.BooleanField(default=False, required=True) 8 | is_music_banned = mongoengine.BooleanField(default=False, required=True) 9 | was_warn_kicked = mongoengine.BooleanField(default=False, required=True) 10 | birthday_excluded = mongoengine.BooleanField(default=False, required=True) 11 | raid_verified = mongoengine.BooleanField(default=False, required=True) 12 | 13 | xp = mongoengine.IntField(default=0, required=True) 14 | trivia_points = mongoengine.IntField(default=0, required=True) 15 | level = mongoengine.IntField(default=0, required=True) 16 | warn_points = mongoengine.IntField(default=0, required=True) 17 | 18 | offline_report_ping = mongoengine.BooleanField(default=False, required=True) 19 | 20 | timezone = mongoengine.StringField(default=None) 21 | birthday = mongoengine.ListField(default=[]) 22 | sticky_roles = mongoengine.ListField(default=[]) 23 | command_bans = mongoengine.DictField(default={}) 24 | 25 | meta = { 26 | 'db_alias': 'default', 27 | 'collection': 'users' 28 | } -------------------------------------------------------------------------------- /bridget/model/user.py: -------------------------------------------------------------------------------- 1 | import mongoengine 2 | 3 | class User(mongoengine.Document): 4 | _id = mongoengine.IntField(required=True) 5 | is_clem = mongoengine.BooleanField(default=False, required=True) 6 | is_xp_frozen = mongoengine.BooleanField(default=False, required=True) 7 | is_muted = mongoengine.BooleanField(default=False, required=True) 8 | is_music_banned = mongoengine.BooleanField(default=False, required=True) 9 | was_warn_kicked = mongoengine.BooleanField(default=False, required=True) 10 | birthday_excluded = mongoengine.BooleanField(default=False, required=True) 11 | raid_verified = mongoengine.BooleanField(default=False, required=True) 12 | 13 | xp = mongoengine.IntField(default=0, required=True) 14 | trivia_points = mongoengine.IntField(default=0, required=True) 15 | level = mongoengine.IntField(default=0, required=True) 16 | warn_points = mongoengine.IntField(default=0, required=True) 17 | 18 | offline_report_ping = mongoengine.BooleanField( 19 | default=False, required=True) 20 | 21 | timezone = mongoengine.StringField(default=None) 22 | birthday = mongoengine.ListField(default=[]) 23 | sticky_roles = mongoengine.ListField(default=[]) 24 | command_bans = mongoengine.DictField(default={}) 25 | 26 | is_appealing = mongoengine.BooleanField(default=False) 27 | is_banned = mongoengine.BooleanField(default=False) 28 | last_appeal_date = mongoengine.DateField() 29 | last_ban_date = mongoengine.DateField() 30 | ban_count = mongoengine.IntField(default=0) 31 | appeal_btn_msg_id = mongoengine.IntField() 32 | 33 | meta = { 34 | 'db_alias': 'default', 35 | 'collection': 'users' 36 | } 37 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | # IMPORTANT: ONLY HAVE **ONE** OF THE FOLLOWING 3 ENABLED AT ONCE 5 | palera1n-bot: 6 | container_name: palera1n-bot 7 | build: 8 | context: . 9 | dockerfile: Dockerfile 10 | # network: host # comment this out if you want to use dockerized mongo 11 | restart: always 12 | ports: 13 | - 8096:8096 14 | volumes: 15 | - ./bot_data:/app/bot_data 16 | # network_mode: host # comment this out if you want to use dockerized mongo 17 | # also, if you want to use dockerized Mongo you need to change DB_HOST to "mongo" in .env 18 | 19 | migrate: 20 | container_name: migrate 21 | build: 22 | context: . 23 | dockerfile: Dockerfile.migrate 24 | restart: never 25 | 26 | setup: 27 | container_name: setup 28 | build: 29 | context: . 30 | dockerfile: Dockerfile.setup 31 | restart: never 32 | 33 | ##### 34 | ##### uncomment the following to use dockerized mongo 35 | depends_on: 36 | - mongo 37 | 38 | mongo: 39 | image: mongo 40 | restart: always 41 | ports: 42 | - 127.0.0.1:27017:27017 43 | environment: 44 | - MONGO_DATA_DIR=/data/db 45 | - MONGO_LOG_DIR=/dev/null 46 | volumes: 47 | - ./mongo_data:/data/db 48 | 49 | 50 | # #### This is optional if you want a GUI to manage your database 51 | # #### Only applicable with Dockerized mongo 52 | # #### If you run this, USE A FIREWALL or the database will be accessible from the internet 53 | # ### The database is running in unauthenticated mode. 54 | 55 | # mongo-express: 56 | # image: mongo-express 57 | # restart: unless-stopped 58 | # depends_on: 59 | # - mongo 60 | # ports: 61 | # - 6700:8081 62 | # environment: 63 | # ME_CONFIG_MONGODB_URL: mongodb://mongo:27017/ 64 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "bridget" 3 | version = "1.0.0" 4 | description = "Bot for the palera1n Discord server" 5 | authors = [ 6 | {name = "Nebula", email = "me@itsnebula.net"}, 7 | {name = "June Park", email = "rjp2030@outlook.com"}, 8 | {name = "Ainara Garcia", email = "me@ainara.dev"}, 9 | ] 10 | dependencies = [ 11 | "mongoengine>=0.27.0", 12 | "discord-py @ git+https://github.com/Rapptz/discord.py@d34a88411d3d973453d80128ab924f6aca197995", 13 | "Pillow>=9.5.0", 14 | "chatgpt @ git+https://github.com/0xallie/chatgpt", 15 | "APScheduler>=3.10.1", 16 | "emoji-country-flag>=1.3.2", 17 | "humanize>=4.6.0", 18 | "autotyping>=23.3.0", 19 | "aiocache>=0.12.1", 20 | "aiohttp-cors>=0.7.0", 21 | "aiohttp>=3.8.4", 22 | "psutil>=5.9.5", 23 | "numpy>=1.25.1", 24 | "pytesseract>=0.3.10", 25 | "opencv-python>=4.8.0.74", 26 | "expiringdict>=1.2.2", 27 | "pytimeparse>=1.1.8", 28 | "datasketch>=1.5.9", 29 | ] 30 | requires-python = ">=3.10,<4.0" 31 | license = {text = "BSD-3-Clause"} 32 | readme = "README.md" 33 | 34 | [tool.pdm.build] 35 | includes = [] 36 | 37 | [tool.pdm.scripts] 38 | bot.cmd = "python bridget" 39 | bot.env_file = ".env" 40 | setup.cmd = "python bridget/setup.py" 41 | setup.env_file = ".env" 42 | lint.cmd = "flake8 --config=.flake8" 43 | migrate.cmd = "python bridget/migrate" 44 | migrate.env_file = ".env" 45 | 46 | [tool.pyright] 47 | extraPaths = ["__pypackages__/3.11/lib/"] 48 | 49 | [tool.black] 50 | line-length = 120 51 | target-version = ['py38', 'py39'] 52 | include = '\.pyi?$' 53 | exclude = ''' 54 | /( 55 | \.git 56 | | \.hg 57 | | \.mypy_cache 58 | | \.tox 59 | | \.venv 60 | | _build 61 | | buck-out 62 | | build 63 | | dist 64 | )/ 65 | ''' 66 | experimental-string-processing = true 67 | skip-string-normalization = true 68 | extra = """ 69 | # Add a blank line before a new definition 70 | def: +2 71 | """ 72 | 73 | [build-system] 74 | requires = ["pdm-pep517>=1.0.0"] 75 | build-backend = "pdm.pep517.api" 76 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Before committing, please make sure your contributions meet our requirements. 4 | 5 | # Code Style 6 | 7 | ## Imports 8 | 9 | We organize imports like this: 10 | ```py 11 | # Imports without from 12 | import asyncio 13 | import discord 14 | 15 | # Imports with from, we prefer these 16 | from discord import app_commands 17 | from discord.ext import commands 18 | 19 | # Local imports 20 | from utils import Cog 21 | from utils.config import cfg 22 | from utils.services import guild_service 23 | ``` 24 | 25 | Avoid using comments in the imports section. 26 | 27 | ## If Statements 28 | 29 | Avoid keeping if statements over 120 characters. 30 | ```py 31 | if (message.channel.id != guild_service.get_guild().channel_chatgpt or message.author.bot or message.content.startswith(("--", "–", "—", "<@"))): 32 | return 33 | ``` 34 | Would be changed to: 35 | ```py 36 | if ( 37 | message.channel.id != guild_service.get_guild().channel_chatgpt 38 | or message.author.bot 39 | or message.content.startswith(("--", "–", "—", "<@")) 40 | ): 41 | return 42 | ``` 43 | 44 | ## Classes 45 | 46 | There should be 2 spaces between classes. 47 | ```py 48 | # Imports here 49 | 50 | 51 | class A(Cog): 52 | pass 53 | 54 | 55 | class B(Cog): 56 | pass 57 | ``` 58 | 59 | Cogs should always use the `Cog` class from `utils`. 60 | 61 | ## Functions 62 | 63 | Your functions should have always have a return type. Interactions will always return `None`. Every variable should also have a type if known. 64 | ```py 65 | async def say(self, ctx: discord.Interaction, message: str, channel: Optional[discord.TextChannel]) -> None: 66 | ``` 67 | 68 | ```py 69 | def format_number(number: int) -> str: 70 | return f"{number:,}" 71 | ``` 72 | 73 | If there are multiple possible types, Union should be used. 74 | ```py 75 | def __eq__(self, other: Union[int, discord.Member, discord.interactions.Interaction]) -> bool: 76 | if isinstance(other, discord.interactions.Interaction): 77 | other = other.user 78 | 79 | if isinstance(other, discord.Member): 80 | # ... 81 | 82 | assert isinstance(other, self.__class__) 83 | return self.value == other.value 84 | ``` 85 | 86 | Functions should have 1 space between each other: 87 | ```py 88 | def a(text: str) -> str: 89 | return text 90 | 91 | def b(number: int) -> int: 92 | return number 93 | ``` 94 | 95 | ## Files 96 | 97 | Every file should have an empty line at the end. They do not show on GitHub, however. 98 | -------------------------------------------------------------------------------- /bridget/cogs/socialfix.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import aiohttp 3 | import json 4 | import discord 5 | import re 6 | 7 | from discord.ext import commands 8 | 9 | from utils import Cog 10 | from utils.config import cfg 11 | 12 | 13 | class SocialFix(Cog): 14 | async def quickvids(self, tiktok_url): 15 | headers = { 16 | 'content-type': 'application/json', 17 | 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', 18 | } 19 | async with aiohttp.ClientSession(headers=headers) as session: 20 | url = 'https://api.quickvids.win/v1/shorturl/create' 21 | data = {'input_text': tiktok_url} 22 | async with session.post(url, json=data) as response: 23 | text = await response.text() 24 | data = json.loads(text) 25 | try: 26 | quickvids_url = data['quickvids_url'] 27 | return quickvids_url 28 | except KeyError: 29 | return None 30 | 31 | async def vxtwitter(self, twitter_url): 32 | return twitter_url.replace("twitter.com", "vxtwitter.com", 1) 33 | 34 | async def ddinstagram(self, insta_url): 35 | return insta_url.replace("instagram.com", "ddinstagram.com", 1) 36 | 37 | @Cog.listener() 38 | async def on_message(self, message: discord.Message) -> None: 39 | if message.guild.id != cfg.guild_id: 40 | return 41 | if message.author.bot: 42 | return 43 | 44 | fixes = [ 45 | { 46 | "regex": r"(https?://(?:www\.)?tiktok\.com/[^ ]*)", 47 | "function": self.quickvids 48 | }, 49 | { 50 | "regex": r"(https?://twitter\.com/[^ ]*)", 51 | "function": self.vxtwitter 52 | }, 53 | { 54 | "regex": r"(https?://(?:www\.)?instagram\.com/[^ ]*)", 55 | "function": self.ddinstagram 56 | } 57 | ] 58 | 59 | for fix in fixes: 60 | fix_matches = re.findall(fix["regex"], message.content) 61 | fixed_urls = [] 62 | for fix_match in fix_matches: 63 | fixed_url = await fix["function"](fix_match) 64 | if fixed_url: 65 | fixed_urls.append(fixed_url) 66 | 67 | if len(fixed_urls) != 0: 68 | await message.edit(suppress=True) 69 | await message.reply(" ".join(fixed_urls)) 70 | break 71 | -------------------------------------------------------------------------------- /bridget/cogs/stats.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import os 3 | import platform 4 | import psutil 5 | 6 | from discord import app_commands 7 | from discord.utils import format_dt 8 | from datetime import datetime 9 | from math import floor 10 | 11 | from utils import Cog 12 | 13 | class Stats(Cog): 14 | def __init__(self): 15 | super().__init__() 16 | self.start_time = datetime.now() 17 | 18 | 19 | @app_commands.command() 20 | async def stats(self, ctx: discord.Interaction) -> None: 21 | process = psutil.Process(os.getpid()) 22 | 23 | embed = discord.Embed( 24 | title=f"{self.bot.user.name} Statistics", color=discord.Color.blurple()) 25 | embed.set_thumbnail(url=self.bot.user.display_avatar) 26 | embed.add_field(name="Bot started", value=format_dt( 27 | self.start_time, style='R')) 28 | embed.add_field(name="CPU Usage", value=f"{psutil.cpu_percent()}%") 29 | embed.add_field(name="Memory Usage", 30 | value=f"{floor(process.memory_info().rss/1000/1000)} MB") 31 | embed.add_field(name="Python Version", value=platform.python_version()) 32 | 33 | await ctx.response.send_message(embed=embed) 34 | 35 | @app_commands.command() 36 | async def serverinfo(self, ctx: discord.Interaction): 37 | guild = ctx.guild 38 | embed = discord.Embed(title="Server Information", 39 | color=discord.Color.blurple()) 40 | embed.set_thumbnail(url=guild.icon) 41 | 42 | if guild.banner is not None: 43 | embed.set_image(url=guild.banner.url) 44 | 45 | embed.add_field(name="Users", value=guild.member_count, inline=True) 46 | embed.add_field(name="Channels", value=len( 47 | guild.channels) + len(guild.voice_channels), inline=True) 48 | embed.add_field(name="Roles", value=len(guild.roles), inline=True) 49 | embed.add_field(name="Bans", value=len( 50 | self.bot.ban_cache.cache), inline=True) 51 | embed.add_field(name="Emojis", value=len(guild.emojis), inline=True) 52 | embed.add_field(name="Boost Tier", 53 | value=guild.premium_tier, inline=True) 54 | embed.add_field(name="Owner", value=guild.owner.mention, inline=True) 55 | embed.add_field( 56 | name="Created", value=f"{format_dt(guild.created_at, style='F')} ({format_dt(guild.created_at, style='R')})", inline=True) 57 | 58 | await ctx.response.send_message(embed=embed) 59 | 60 | @app_commands.command() 61 | async def roleinfo(self, ctx: discord.Interaction, role: discord.Role) -> None: 62 | embed = discord.Embed(title="Role Statistics") 63 | embed.description = f"{len(role.members)} members have role {role.mention}" 64 | embed.color = role.color 65 | 66 | await ctx.respond(embed=embed) 67 | -------------------------------------------------------------------------------- /bridget/setup.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import mongoengine 4 | 5 | from model.guild import Guild 6 | 7 | 8 | async def setup() -> None: 9 | print("STARTING SETUP...") 10 | guild = Guild() 11 | 12 | # you should have this setup in the .env file beforehand 13 | guild._id = int(os.environ.get("GUILD_ID")) 14 | 15 | # If you're re-running this script to update a value, set infraction_id 16 | # to the last unused infraction ID or else it will start over from 1! 17 | guild.infraction_id = 124 18 | 19 | guild.role_administrator = 1096623117780140072 20 | guild.role_moderator = 1096623117763354713 21 | guild.role_birthday = 1096623117763354710 22 | guild.role_helper = 1096623117763354708 23 | guild.role_dev = 123 24 | guild.role_memberone = 1028693923008364586 25 | guild.role_memberedition = 1096623117746573377 26 | guild.role_memberpro = 1096623117746573376 27 | guild.role_memberplus = 1096623117746573375 28 | guild.role_memberultra = 123 29 | guild.role_reportping = 1096623117746573374 30 | guild.role_nicknamelock = 1110986834554667098 31 | guild.role_mediarestriction = 1096623117725614219 32 | guild.role_channelrestriction = 1096623117725614220 33 | guild.role_reactionrestriction = 1096623117725614218 34 | guild.role_chatgptrestriction = 1096623117725614217 35 | 36 | guild.channel_reports = 1096623118761599051 37 | guild.channel_mempro_reports = 1096623118761599052 38 | guild.channel_emoji_log = 1096623118761599050 39 | guild.channel_private = 1096623118761599049 40 | guild.channel_reaction_roles = 1028693665050263612 41 | guild.channel_rules = 1096623118526714007 42 | guild.channel_public = 1096623119009071235 43 | guild.channel_common_issues = 1096623119009071237 44 | guild.channel_general = 1096623119009071243 45 | guild.channel_development = 1096623119214579813 46 | guild.channel_botspam = 1096623119214579812 47 | guild.channel_chatgpt = 1097876598428008528 48 | guild.channel_msg_logs = 1097434149649924107 49 | guild.channel_support = 1097617990515691532 50 | 51 | guild.logging_excluded_channels = [] 52 | guild.filter_excluded_channels = [] 53 | guild.filter_excluded_guilds = [] 54 | 55 | guild.automod_antiraid = 0 56 | 57 | guild.save() 58 | print("DONE") 59 | 60 | if __name__ == "__main__": 61 | if os.environ.get("DB_CONNECTION_STRING") is None: 62 | mongoengine.register_connection( 63 | host=os.environ.get("DB_HOST"), 64 | port=int( 65 | os.environ.get("DB_PORT")), 66 | alias="default", 67 | name="bridget") 68 | else: 69 | mongoengine.register_connection( 70 | host=os.environ.get("DB_CONNECTION_STRING"), 71 | alias="default", 72 | name="bridget") 73 | res = asyncio.get_event_loop().run_until_complete(setup()) 74 | -------------------------------------------------------------------------------- /bridget/model/guild.py: -------------------------------------------------------------------------------- 1 | import mongoengine 2 | from .filterword import FilterWord 3 | from .tag import Tag 4 | from .issues import Issue 5 | 6 | 7 | class Guild(mongoengine.Document): 8 | _id = mongoengine.IntField(required=True) 9 | infraction_id = mongoengine.IntField(min_value=1, required=True) 10 | reaction_role_mapping = mongoengine.DictField(default={}) 11 | 12 | role_administrator = mongoengine.IntField() 13 | role_birthday = mongoengine.IntField() 14 | role_dev = mongoengine.IntField() 15 | role_helper = mongoengine.IntField() 16 | role_memberone = mongoengine.IntField() 17 | role_memberedition = mongoengine.IntField() 18 | role_memberplus = mongoengine.IntField() 19 | role_memberpro = mongoengine.IntField() 20 | role_memberultra = mongoengine.IntField() 21 | role_moderator = mongoengine.IntField() 22 | role_reportping = mongoengine.IntField() 23 | role_nicknamelock = mongoengine.IntField() 24 | role_mediarestriction = mongoengine.IntField() 25 | role_channelrestriction = mongoengine.IntField() 26 | role_reactionrestriction = mongoengine.IntField() 27 | role_chatgptrestriction = mongoengine.IntField() 28 | 29 | channel_botspam = mongoengine.IntField() 30 | channel_common_issues = mongoengine.IntField() 31 | channel_development = mongoengine.IntField() 32 | channel_emoji_log = mongoengine.IntField() 33 | channel_general = mongoengine.IntField() 34 | channel_support = mongoengine.IntField() 35 | channel_private = mongoengine.IntField() 36 | channel_msg_logs = mongoengine.IntField(default=0) 37 | channel_public = mongoengine.IntField() 38 | channel_rules = mongoengine.IntField() 39 | channel_reaction_roles = mongoengine.IntField() 40 | channel_reports = mongoengine.IntField() 41 | channel_chatgpt = mongoengine.IntField() 42 | channel_mempro_reports = mongoengine.IntField() 43 | 44 | emoji_logging_webhook = mongoengine.StringField() 45 | locked_channels = mongoengine.ListField(default=[]) 46 | filter_excluded_channels = mongoengine.ListField(default=[]) 47 | filter_excluded_guilds = mongoengine.ListField(default=[]) 48 | filter_words = mongoengine.EmbeddedDocumentListField( 49 | FilterWord, default=[]) 50 | raid_phrases = mongoengine.EmbeddedDocumentListField( 51 | FilterWord, default=[]) 52 | logging_excluded_channels = mongoengine.ListField(default=[]) 53 | tags = mongoengine.EmbeddedDocumentListField(Tag, default=[]) 54 | memes = mongoengine.EmbeddedDocumentListField(Tag, default=[]) 55 | ban_today_spam_accounts = mongoengine.BooleanField(default=False) 56 | issues = mongoengine.EmbeddedDocumentListField(Issue, default=[]) 57 | issues_list_msg = mongoengine.ListField(default=[]) 58 | 59 | automod_antiraid = mongoengine.IntField() 60 | 61 | meta = { 62 | 'db_alias': 'default', 63 | 'collection': 'guilds' 64 | } 65 | -------------------------------------------------------------------------------- /bridget/migrate/girmodel/guild.py: -------------------------------------------------------------------------------- 1 | import mongoengine 2 | from .filterword import FilterWord 3 | from .tag import Tag 4 | 5 | 6 | class Guild(mongoengine.Document): 7 | _id = mongoengine.IntField(required=True) 8 | case_id = mongoengine.IntField(min_value=1, required=True) 9 | reaction_role_mapping = mongoengine.DictField(default={}) 10 | role_administrator = mongoengine.IntField() 11 | role_birthday = mongoengine.IntField() 12 | role_dev = mongoengine.IntField() 13 | role_genius = mongoengine.IntField() 14 | role_member = mongoengine.IntField() 15 | role_memberone = mongoengine.IntField() 16 | role_memberedition = mongoengine.IntField() 17 | role_memberplus = mongoengine.IntField() 18 | role_memberpro = mongoengine.IntField() 19 | role_memberultra = mongoengine.IntField() 20 | role_moderator = mongoengine.IntField() 21 | role_mute = mongoengine.IntField() 22 | role_sub_mod = mongoengine.IntField() 23 | role_sub_news = mongoengine.IntField() 24 | 25 | channel_applenews = mongoengine.IntField() 26 | channel_booster_emoji = mongoengine.IntField() 27 | channel_botspam = mongoengine.IntField() 28 | channel_common_issues = mongoengine.IntField() 29 | channel_development = mongoengine.IntField() 30 | channel_emoji_log = mongoengine.IntField() 31 | channel_general = mongoengine.IntField() 32 | channel_support = mongoengine.IntField() 33 | channel_jailbreak = mongoengine.IntField() 34 | channel_private = mongoengine.IntField() 35 | channel_public = mongoengine.IntField() 36 | channel_rules = mongoengine.IntField() 37 | channel_reaction_roles = mongoengine.IntField() 38 | channel_reports = mongoengine.IntField() 39 | channel_subnews = mongoengine.IntField() 40 | channel_music = mongoengine.IntField() 41 | channel_chatgpt = mongoengine.IntField() 42 | channel_mempro_reports = mongoengine.IntField() 43 | 44 | emoji_logging_webhook = mongoengine.StringField() 45 | locked_channels = mongoengine.ListField(default=[]) 46 | filter_excluded_channels = mongoengine.ListField(default=[]) 47 | filter_excluded_guilds = mongoengine.ListField( 48 | default=[349243932447604736]) 49 | filter_words = mongoengine.EmbeddedDocumentListField( 50 | FilterWord, default=[]) 51 | raid_phrases = mongoengine.EmbeddedDocumentListField( 52 | FilterWord, default=[]) 53 | logging_excluded_channels = mongoengine.ListField(default=[]) 54 | nsa_guild_id = mongoengine.IntField() 55 | nsa_mapping = mongoengine.DictField(default={}) 56 | tags = mongoengine.EmbeddedDocumentListField(Tag, default=[]) 57 | memes = mongoengine.EmbeddedDocumentListField(Tag, default=[]) 58 | sabbath_mode = mongoengine.BooleanField(default=False) 59 | ban_today_spam_accounts = mongoengine.BooleanField(default=False) 60 | 61 | meta = { 62 | 'db_alias': 'default', 63 | 'collection': 'guilds' 64 | } 65 | -------------------------------------------------------------------------------- /bridget/cogs/restrictions.py: -------------------------------------------------------------------------------- 1 | import discord 2 | 3 | from discord import PermissionOverwrite, app_commands 4 | 5 | from utils import Cog, send_error, send_success 6 | from utils.enums import PermissionLevel, RestrictionType 7 | from utils.services import guild_service 8 | 9 | 10 | class Restrictions(Cog): 11 | @PermissionLevel.MOD 12 | @app_commands.command() 13 | async def restrict(self, ctx: discord.Interaction, user: discord.Member, type: RestrictionType) -> None: 14 | """Restricts a user 15 | 16 | Args: 17 | ctx (discord.ctx): Context 18 | user (discord.Member): User to restrict 19 | type (RestrictionType): What to restrict 20 | """ 21 | 22 | if user.top_role >= ctx.user.top_role: 23 | await send_error(ctx, "You can't restrict this member.") 24 | return 25 | 26 | try: 27 | role = ctx.guild.get_role(guild_service.get_guild()[str(type)]) 28 | await user.add_roles(role, reason="restricted") 29 | await send_success(ctx, "User restricted successfully", ephemeral=True) 30 | except: 31 | await send_error(ctx, "Restriction role not found") 32 | 33 | 34 | @PermissionLevel.MOD 35 | @app_commands.command() 36 | async def unrestrict(self, ctx: discord.Interaction, user: discord.Member, type: RestrictionType) -> None: 37 | """Unrestricts a user 38 | 39 | Args: 40 | ctx (discord.ctx): Context 41 | user (discord.Member): User to unrestrict 42 | type (RestrictionType): What to unrestrict 43 | """ 44 | 45 | if user.top_role >= ctx.user.top_role: 46 | await send_error(ctx, "You can't unrestrict this member.") 47 | return 48 | 49 | try: 50 | role = ctx.guild.get_role(guild_service.get_guild()[str(type)]) 51 | await user.remove_roles(role, reason="unrestricted") 52 | await send_success(ctx, "User unrestricted successfully", ephemeral=True) 53 | except: 54 | await send_error(ctx, "Restriction role not found") 55 | 56 | @Cog.listener() 57 | async def on_guild_channel_create(self, channel: discord.abc.GuildChannel) -> None: 58 | if type(channel) == discord.TextChannel: 59 | role_channel = channel.guild.get_role(guild_service.get_guild().role_channelrestriction) 60 | role_media = channel.guild.get_role(guild_service.get_guild().role_mediarestriction) 61 | role_reaction = channel.guild.get_role(guild_service.get_guild().role_reactionrestriction) 62 | 63 | perm_channel = channel.overwrites_for(role_channel) 64 | perm_channel.view_channel = False 65 | 66 | perm_media = channel.overwrites_for(role_media) 67 | perm_media.embed_links = False 68 | perm_media.attach_files = False 69 | perm_media.external_emojis = False 70 | perm_media.external_stickers = False 71 | 72 | perm_reaction = channel.overwrites_for(role_reaction) 73 | perm_reaction.add_reactions = False 74 | 75 | await channel.set_permissions(role_channel, overwrite=perm_channel) 76 | await channel.set_permissions(role_media, overwrite=perm_media) 77 | await channel.set_permissions(role_reaction, overwrite=perm_reaction) 78 | 79 | -------------------------------------------------------------------------------- /bridget/cogs/timezones.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import flag 3 | import datetime 4 | import pytz 5 | 6 | from discord.ext import commands 7 | from discord import app_commands 8 | from typing import List 9 | 10 | from utils import Cog, send_success 11 | from utils.services import user_service 12 | 13 | 14 | async def timezone_autocomplete(_: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: 15 | return [app_commands.Choice(name=tz, value=tz) for tz in pytz.common_timezones_set if current.lower( 16 | ) in tz.lower() or current.lower() in tz.replace("_", " ").lower()][:25] 17 | 18 | 19 | @app_commands.guild_only() 20 | class Timezones(Cog, commands.GroupCog, group_name="timezones"): 21 | timezone_country = {} 22 | for countrycode in pytz.country_timezones: 23 | timezones = pytz.country_timezones[countrycode] 24 | for timezone in timezones: 25 | timezone_country[timezone] = countrycode 26 | 27 | def country_code_to_emoji(self, country_code: str) -> str: 28 | try: 29 | return " " + flag.flag(country_code) 30 | except ValueError: 31 | return "" 32 | 33 | @app_commands.command() 34 | @app_commands.autocomplete(zone=timezone_autocomplete) 35 | async def set(self, ctx: discord.Interaction, zone: str) -> None: 36 | """Set your timezone so that others can view it 37 | 38 | Args: 39 | ctx (discord.Interaction): Context 40 | zone (str): The timezone to set 41 | """ 42 | 43 | if zone not in pytz.common_timezones_set: 44 | raise commands.BadArgument("Timezone was not found!") 45 | 46 | db_user = user_service.get_user(ctx.user.id) 47 | db_user.timezone = zone 48 | db_user.save() 49 | 50 | await send_success(ctx, f"We set your timezone to `{zone}`! It can now be viewed with `/timezone view`.") 51 | 52 | @app_commands.command() 53 | async def remove(self, ctx: discord.Interaction) -> None: 54 | """Remove your timezone from the database 55 | 56 | Args: 57 | ctx (discord.Interaction): Context 58 | """ 59 | 60 | db_user = user_service.get_user(ctx.user.id) 61 | db_user.timezone = None 62 | db_user.save() 63 | 64 | await send_success(ctx, "We have removed your timezone from the database.") 65 | 66 | @app_commands.command() 67 | @app_commands.describe(member="Member to view time of") 68 | async def view(self, ctx: discord.Interaction, member: discord.Member) -> None: 69 | """Get a timezone of an user 70 | 71 | Args: 72 | ctx (discord.Interaction): Context 73 | member (discord.Member): Member to view time of 74 | """ 75 | 76 | db_user = user_service.get_user(member.id) 77 | if db_user.timezone is None: 78 | raise commands.BadArgument( 79 | f"{member.mention} has not set a timezone!") 80 | 81 | country_code = self.timezone_country.get(db_user.timezone) 82 | flaggy = "" 83 | if country_code is not None: 84 | flaggy = self.country_code_to_emoji(country_code) 85 | 86 | await send_success(ctx, f"{member.mention}'s timezone is `{db_user.timezone}` {flaggy}\nIt is currently `{datetime.datetime.now(pytz.timezone(db_user.timezone)).strftime('%I:%M %p %Z')}`", ephemeral=False) 87 | -------------------------------------------------------------------------------- /bridget/cogs/logparsing.py: -------------------------------------------------------------------------------- 1 | import discord 2 | 3 | from discord.ext import commands 4 | from typing import Tuple 5 | 6 | from utils.fetchers import fetch_remote_json, fetch_remote_file 7 | from utils.services import guild_service 8 | from model.issues import Issue 9 | from cogs.issues import prepare_issue_embed, prepare_issue_view 10 | 11 | class LogParsing(commands.Cog): 12 | def __init__(self, bot): 13 | self.bot = bot 14 | 15 | @commands.Cog.listener() 16 | async def on_message(self, msg: discord.Message) -> None: 17 | """When an .ips file is posted, check if its valid JSON and a panic log""" 18 | 19 | if msg.author.bot: 20 | return 21 | 22 | if not msg.attachments: 23 | return 24 | 25 | att = msg.attachments[0] 26 | if att.filename.endswith(".ips"): 27 | await self.do_panic_log(msg, att) 28 | elif att.filename.endswith(".log") and att.filename.startswith("FAIL"): 29 | await self.do_log_file(msg, att) 30 | 31 | async def issue_embed(self, msg: discord.Message, issue: Issue) -> Tuple[discord.Embed, discord.ui.View, discord.File]: 32 | _file = issue.image.read() 33 | if _file is not None: 34 | _file = discord.File( 35 | BytesIO(_file), 36 | filename="image.gif" if issue.image.content_type == "image/gif" else "image.png") 37 | else: 38 | _file = discord.utils.MISSING 39 | 40 | return (prepare_issue_embed(issue), prepare_issue_view(issue), _file) 41 | 42 | async def do_panic_log(self, msg: discord.Message, att) -> None: 43 | json = await fetch_remote_json(att.url) 44 | if json is not None: 45 | if "panicString" in json: 46 | string = "\n".join(json['panicString'].split("\n")[:2]) 47 | 48 | if "build" in json: 49 | build = json['build'].split("\n")[0] 50 | 51 | if "product" in json: 52 | product = json['product'].split("\n")[0] 53 | 54 | if string == "" or build == "" or product == "": 55 | return 56 | 57 | if (not "```" in string or "@everyone" in string or "@here" in string) and (not "`" in build or "@everyone" in build or "@here" in build) and (not "`" in product or "@everyone" in product or "@here" in product): 58 | issue_embed = None 59 | for issue in guild_service.get_guild().issues: 60 | if issue.panic_string is not None and len(issue.panic_string) > 0 and issue.panic_string in string: 61 | issue_embed = await self.issue_embed(msg, issue) 62 | break 63 | if issue_embed: 64 | await msg.reply(f"Hey, it looks like this is a panic log for build: `{discord.utils.escape_markdown(build)}` on a `{discord.utils.escape_markdown(product)}`!\n\nHere is the panic string:```{discord.utils.escape_markdown(string)}```", embed=issue_embed[0], view=issue_embed[1], file=issue_embed[2]) 65 | else: 66 | await msg.reply(f"Hey, it looks like this is a panic log for build: `{discord.utils.escape_markdown(build)}` on a `{discord.utils.escape_markdown(product)}`!\n\nHere is the panic string:```{discord.utils.escape_markdown(string)}```") 67 | 68 | async def do_log_file(self, msg: discord.Message, att) -> None: 69 | text = await fetch_remote_file(att.url) 70 | if text is not None: 71 | if not "```" in text or "@everyone" in text or "@here" in text: 72 | string = '\n'.join(text.splitlines()[-10:]) 73 | await msg.reply(f"Hey, it looks like this is a palera1n failure log!\n\nHere is the last 10 lines to help debuggers:```{discord.utils.escape_markdown(string)}```") 74 | -------------------------------------------------------------------------------- /bridget/utils/enums.py: -------------------------------------------------------------------------------- 1 | import discord 2 | 3 | from enum import IntEnum, unique 4 | from discord.automod import AutoModRule 5 | from typing import Any, List, Optional, Union 6 | 7 | from .services import guild_service 8 | from .config import cfg 9 | from .errors import MissingPermissionsError 10 | 11 | 12 | def rule_has_timeout(rule: AutoModRule) -> bool: 13 | for act in rule.actions: 14 | if act.type == discord.AutoModRuleActionType.timeout: 15 | return True 16 | return False 17 | 18 | @unique 19 | class PermissionLevel(IntEnum): 20 | """Permission level enum""" 21 | 22 | EVERYONE = 0 23 | MEMPLUS = 1 24 | MEMPRO = 2 25 | HELPER = 3 26 | MOD = 4 27 | ADMIN = 5 28 | GUILD_OWNER = 6 29 | OWNER = 7 30 | 31 | # Checks 32 | def __lt__(self, other: int) -> bool: 33 | return self.value < other.value 34 | 35 | def __le__(self, other: int) -> bool: 36 | return self.value <= other.value 37 | 38 | def __gt__(self, other: int) -> bool: 39 | return self.value > other.value 40 | 41 | def __ge__(self, other: int) -> bool: 42 | return self.value >= other.value 43 | 44 | def __str__(self) -> str: 45 | return { 46 | self.MEMPLUS: "role_memberplus", 47 | self.MEMPRO: "role_memberpro", 48 | self.HELPER: "role_helper", 49 | self.MOD: "role_moderator", 50 | self.ADMIN: "role_administrator", 51 | }[self] 52 | 53 | def __eq__(self, other: Union[int, discord.Member, discord.interactions.Interaction]) -> bool: 54 | if isinstance(other, discord.interactions.Interaction): 55 | return other == other.user 56 | 57 | if isinstance(other, discord.Member): 58 | if self == self.EVERYONE: 59 | return True 60 | if self == self.GUILD_OWNER: 61 | return other.guild.owner == other 62 | if self == self.OWNER: 63 | return other.id == cfg.owner_id 64 | 65 | return getattr(guild_service.get_guild(), str(self)) in list(map(lambda r: r.id, other.roles)) or self + 1 == other 66 | assert isinstance(other, self.__class__) 67 | return self.value == other.value 68 | 69 | def __add__(self, other) -> "PermissionLevel": 70 | return self.__class__(self.value + other) 71 | 72 | def __call__(self, command: discord.app_commands.Command) -> discord.app_commands.Command: 73 | command.extras['PermLevel'] = self 74 | if self == self.OWNER: 75 | command.checks.append(lambda ctx: True if self == ctx.user else MissingPermissionsError.throw(perms=[f"Bot Owner"])) 76 | elif self == self.GUILD_OWNER: 77 | command.checks.append(lambda ctx: True if self == ctx.user else MissingPermissionsError.throw(perms=[f"Guild Owner"])) 78 | else: 79 | command.checks.append(lambda ctx: True if self == ctx.user else MissingPermissionsError.throw(perms=[f"<@&{guild_service.get_guild()[self.__str__()]}>"])) 80 | return command 81 | 82 | def __hash__(self) -> int: 83 | return hash(self.value) 84 | 85 | 86 | @unique 87 | class RestrictionType(IntEnum): 88 | """Type of restriction enum""" 89 | 90 | CHANNEL = 0 91 | MEDIA = 1 92 | REACTION = 2 93 | CHATGPT = 3 94 | 95 | def __str__(self) -> str: 96 | return { 97 | self.CHANNEL: "role_channelrestriction", 98 | self.MEDIA: "role_mediarestriction", 99 | self.REACTION: "role_reactionrestriction", 100 | self.CHATGPT: "role_chatgptrestriction", 101 | }[self] 102 | 103 | def __hash__(self) -> int: 104 | return hash(self.value) 105 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm-python 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | Pipfile.lock 163 | 164 | # Project stuff 165 | dbs 166 | *.swp 167 | .DS_Store 168 | .pdm.toml 169 | 170 | # Editor configs 171 | .vscode/ 172 | 173 | out 174 | 175 | ocr.py 176 | 177 | mongo_data -------------------------------------------------------------------------------- /bridget/cogs/snipe.py: -------------------------------------------------------------------------------- 1 | import discord 2 | 3 | from typing import Any, Dict 4 | from discord import app_commands 5 | from discord.ext import commands 6 | from datetime import datetime 7 | 8 | from utils import Cog 9 | from utils.enums import PermissionLevel 10 | 11 | 12 | class Snipe(Cog): 13 | def __init__(self, *args: Any, **kwargs: Any): 14 | super().__init__(*args, **kwargs) 15 | 16 | self.cached_messages: Dict[discord.Message] = {} 17 | 18 | @commands.Cog.listener() 19 | async def on_message_edit(self, message: discord.Message, new_message: discord.Message) -> None: 20 | if message.author.bot: 21 | return 22 | 23 | self.cached_messages[message.channel.id] = message 24 | 25 | @commands.Cog.listener() 26 | async def on_message_delete(self, message: discord.Message) -> None: 27 | if message.author.bot: 28 | return 29 | 30 | self.cached_messages[message.channel.id] = message 31 | 32 | @commands.Cog.listener() 33 | async def on_automod_action(self, execution: discord.AutoModAction) -> None: 34 | if execution.action.type == discord.AutoModRuleActionType.block_message: 35 | # we have to build a fake message because the real one was blocked 36 | member = execution.guild.get_member(execution.user_id) 37 | msg = discord.Message(state=None, channel=execution.channel, data={ 38 | 'id': discord.utils.time_snowflake(datetime.now()), 39 | 'attachments': [], 40 | 'embeds': [], 41 | 'edited_timestamp': datetime.now().isoformat(), 42 | 'type': discord.MessageType.default, 43 | 'pinned': False, 44 | 'mention_everyone': False, 45 | 'mentions': [], 46 | 'mention_roles': [], 47 | 'mention_channels': [], 48 | 'tts': False, 49 | 'content': execution.content, 50 | }) 51 | msg.author = member 52 | self.cached_messages[execution.channel_id] = msg 53 | 54 | @PermissionLevel.MOD 55 | @app_commands.command() 56 | async def snipe(self, ctx: discord.Interaction) -> None: 57 | """Snipe a message 58 | 59 | Args: 60 | ctx (discord.Interaction): Context 61 | """ 62 | try: 63 | if not self.cached_messages[ctx.channel_id]: 64 | await ctx.response.send_message( 65 | embed=discord.Embed( 66 | color=discord.Color.red(), 67 | description="No messages to snipe.", 68 | ), 69 | ) 70 | return 71 | except KeyError: 72 | await ctx.response.send_message( 73 | embed=discord.Embed( 74 | color=discord.Color.red(), 75 | description="No messages to snipe.", 76 | ), 77 | ) 78 | return 79 | 80 | avt_url = None 81 | if self.cached_messages[ctx.channel_id].author.avatar is not None: 82 | avt_url = self.cached_messages[ctx.channel_id].author.avatar.url 83 | 84 | embed = discord.Embed( 85 | color=discord.Color.green(), 86 | description=self.cached_messages[ctx.channel_id].content, 87 | timestamp=self.cached_messages[ctx.channel_id].created_at, 88 | ) 89 | embed.set_author( 90 | name=self.cached_messages[ctx.channel_id].author, 91 | icon_url=avt_url) 92 | embed.set_footer(text=f"Sent in #{self.cached_messages[ctx.channel_id].channel.name}") 93 | 94 | try: 95 | if self.cached_messages[ctx.channel_id].attachments[0].type.startswith("image"): 96 | embed.set_image(url=self.cached_messages[ctx.channel_id].attachments[0].url) 97 | except: 98 | pass 99 | 100 | embed.set_thumbnail(url=avt_url) 101 | 102 | await ctx.response.send_message(embed=embed) 103 | -------------------------------------------------------------------------------- /bridget/backend/appeal.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import discord 4 | 5 | from aiohttp.web import Request, Response 6 | from datetime import datetime, timedelta 7 | 8 | from utils.services import user_service 9 | from .utils import AppealRequest, get_client_session 10 | from cogs.appeals import backend_queue, backend_requests 11 | 12 | 13 | class Appeal(): 14 | def __init__(self, bot: discord.Client): 15 | self.bot = bot 16 | 17 | async def appeal(self, req: Request) -> Response: 18 | if not req.body_exists: 19 | return Response(status=400, body="No Request Body") 20 | 21 | try: 22 | data = json.loads(await req.content.read()) 23 | except: 24 | return Response(status=400, body='Error parsing JSON body') 25 | 26 | session = await get_client_session() 27 | headers = { 'authorization': f'{data["token_type"]} {data["access_token"]}' } 28 | try: 29 | async with session.get("https://discord.com/api/users/@me", headers=headers) as resp: 30 | if not resp.ok: 31 | return Response(status=401, body="Not Authorized") 32 | userdat = json.loads(await resp.content.read()) 33 | except: 34 | return Response(status=401, body="Not Authorized") 35 | 36 | 37 | user = user_service.get_user(userdat['id']) 38 | if not user.is_banned: 39 | # check for non-registered ban 40 | areq = AppealRequest(userdat['id']) 41 | # put the request 42 | await backend_requests.put(areq) 43 | # wait for response 44 | await areq.completion.wait() 45 | areq.completion.clear() 46 | if areq.result is None: 47 | return Response(status=400, body='You are not banned!') 48 | 49 | if user.ban_count > 3: 50 | return Response(status=400, body='You have reached the ban appeal limit!') 51 | 52 | if user.last_ban_date is not None: 53 | if datetime.now().date() - user.last_ban_date < timedelta(days=90) and user.ban_count > 1: 54 | time = datetime.now().date() - user.last_ban_date 55 | return Response(status=400, body=f'You have to wait another {90 - time.days} day(s) before appealing your ban!') 56 | 57 | if user.is_appealing: 58 | return Response(status=400, body='Your appeal is currently pending!') 59 | 60 | if user.last_appeal_date is not None: 61 | if datetime.now().date() - user.last_appeal_date < timedelta(days=90): 62 | time = datetime.now().date() - user.last_appeal_date 63 | return Response(status=400, body=f'You have to wait another {90 - time.days} day(s) between appeals!') 64 | 65 | user.last_appeal_date = datetime.now().date() 66 | user.is_appealing = True 67 | user.save() 68 | 69 | print("appeal submitted") 70 | 71 | embed = discord.Embed(title="Form Entry", color=discord.Color.green()) 72 | embed.add_field(name="Username", value=userdat['username'], inline=False) 73 | embed.add_field(name="User ID", value=userdat['id'], inline=False) 74 | embed.add_field(name="Ban Reason", value=data['ban_reason'], inline=False) 75 | embed.add_field(name="Unban Reason", value=data['unban_reason'], inline=False) 76 | embed.add_field(name="Ban Date", value=data['ban_date'] if data['has_ban_date'] else '', inline=False) 77 | embed.add_field(name="Justification", value=data['justification'], inline=False) 78 | embed.add_field(name="Joined Appeals Server", value=data['joined_appeals'], inline=False) 79 | embed.add_field(name="DMs enabled", value=data['dms_enabled'], inline=False) 80 | embed.add_field(name="Last Ban Date", value=user.last_ban_date if user.last_ban_date is not None else '', inline=False) 81 | embed.add_field(name="Last Appeal Date", value=user.last_appeal_date if user.last_appeal_date is not None else '', inline=False) 82 | await backend_queue.put(embed) 83 | 84 | return Response() 85 | 86 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | workflow_dispatch: 16 | push: 17 | branches: [ "main" ] 18 | paths: 19 | - bridget/** 20 | - .github/workflows/codeql.yml 21 | pull_request: 22 | # The branches below must be a subset of the branches above 23 | branches: [ "main" ] 24 | paths: 25 | - bridget/** 26 | - .github/workflows/** 27 | schedule: 28 | - cron: '24 13 * * 5' 29 | 30 | jobs: 31 | analyze: 32 | name: Analyze 33 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 34 | permissions: 35 | actions: read 36 | contents: read 37 | security-events: write 38 | 39 | strategy: 40 | fail-fast: false 41 | matrix: 42 | language: [ 'python' ] 43 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 44 | # Use only 'java' to analyze code written in Java, Kotlin or both 45 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 46 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 47 | 48 | steps: 49 | - name: Checkout repository 50 | uses: actions/checkout@v3 51 | 52 | # Initializes the CodeQL tools for scanning. 53 | - name: Initialize CodeQL 54 | uses: github/codeql-action/init@v2 55 | with: 56 | languages: ${{ matrix.language }} 57 | # If you wish to specify custom queries, you can do so here or in a config file. 58 | # By default, queries listed here will override any specified in a config file. 59 | # Prefix the list here with "+" to use these queries and those in the config file. 60 | 61 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 62 | # queries: security-extended,security-and-quality 63 | 64 | 65 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 66 | # If this step fails, then you should remove it and run the build manually (see below) 67 | - name: Autobuild 68 | uses: github/codeql-action/autobuild@v2 69 | 70 | - name: Setup PDM 71 | # You may pin to the exact commit or the version. 72 | # uses: pdm-project/setup-pdm@70bb221e37fcd9e34ca002e0f3a7baacb84831f0 73 | uses: pdm-project/setup-pdm@v3 74 | with: 75 | # Version range or exact version of a Python version to use, using SemVer's version range syntax. 76 | python-version: 3.11 # default is 3.x 77 | # The target architecture (x86, x64) of the Python interpreter. 78 | architecture: x64 # optional 79 | # Used to pull python distributions from actions/python-versions. Since there's a default, this is typically not supplied by the user. 80 | token: ${{ github.token }} # optional, default is ${{ github.token }} 81 | # The version of PDM to install, or 'head' to install from the main branch. 82 | version: head # optional 83 | # Allow prerelease versions to be installed 84 | prerelease: true # optional, default is false 85 | # Enable PEP 582 package loading globally. 86 | enable-pep582: true # optional, default is true 87 | # Cache PDM installation. 88 | cache: true # optional, default is false 89 | # The dependency file(s) to cache. 90 | cache-dependency-path: pdm.lock # optional, default is pdm.lock 91 | 92 | - run: | 93 | pdm install 94 | 95 | # ℹ️ Command-line programs to run using the OS shell. 96 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 97 | 98 | # If the Autobuild fails above, remove it and uncomment the following three lines. 99 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 100 | 101 | # - run: | 102 | # echo "Run, Build Application using script" 103 | # ./location_of_script_within_repo/buildscript.sh 104 | 105 | - name: Perform CodeQL Analysis 106 | uses: github/codeql-action/analyze@v2 107 | with: 108 | category: "/language:${{matrix.language}}" 109 | -------------------------------------------------------------------------------- /bridget/cogs/chatgpt.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import io 3 | import textwrap 4 | import discord 5 | 6 | from chatgpt import APIError, ChatGPTClient 7 | from discord import app_commands 8 | from discord.ext import commands 9 | from datetime import datetime 10 | from typing import Any 11 | from os import getenv 12 | 13 | from utils import Cog 14 | from utils.config import cfg 15 | from utils.services import guild_service 16 | 17 | 18 | class ChatGPT(Cog, commands.GroupCog, group_name="chatgpt"): 19 | def __init__(self, *args: Any, **kwargs: Any) -> None: 20 | super().__init__(*args, **kwargs) 21 | 22 | self.context = {} 23 | 24 | @commands.Cog.listener() 25 | async def on_message(self, message: discord.Message) -> None: 26 | if ( 27 | message.channel.id != guild_service.get_guild().channel_chatgpt 28 | or message.author.bot 29 | or message.content.startswith(("--", "–", "—", "<@")) 30 | ): 31 | return 32 | 33 | if getenv('OPENAI_API_KEY') == '': 34 | return 35 | 36 | if message.author.id not in self.context: 37 | self.context[message.author.id] = ChatGPTClient( 38 | initial_prompt=f"You are Bridget, a chatbot for the palera1n Discord server that uses OpenAI's GPT 3.5 model. The current date is {datetime.utcnow().strftime('%Y-%m-%d')}. You are talking to \"{message.author.nick or message.author.name}\". Answer as concisely as possible. Profanity is allowed, however you may not use any slurs. Jailbreak talk is allowed, iOS or Android. You are also a professional programmer that can create anything if asked.", # noqa: E501 39 | user_id=str(message.author.id), 40 | ) 41 | 42 | assert message.guild != None 43 | 44 | await message.add_reaction(discord.utils.get(message.guild.emojis, name="loading")) 45 | 46 | async with message.channel.typing(): 47 | try: 48 | try: 49 | res = await self.context[message.author.id].get_completion(message.content) 50 | except APIError as e: 51 | await message.reply( 52 | embed=discord.Embed( 53 | color=discord.Color.red(), 54 | title="Error", 55 | description=f"```{discord.utils.escape_markdown(e)}```", 56 | ) 57 | ) 58 | return 59 | except asyncio.exceptions.TimeoutError: 60 | await message.reply( 61 | embed=discord.Embed( 62 | color=discord.Color.red(), 63 | title="Error", 64 | description="The request timed out.", 65 | ) 66 | ) 67 | return 68 | 69 | if len(res) <= 2000: 70 | await message.reply(res) 71 | else: 72 | wrapped = [] 73 | for paragraph in res.split("\n\n"): 74 | if "```" in paragraph: 75 | wrapped.append(paragraph) 76 | else: 77 | wrapped.append(textwrap.fill(paragraph)) 78 | wrapped = "\n\n".join(wrapped) 79 | 80 | await message.reply( 81 | "The response was too long! I've attempted to upload it as a file below.", 82 | file=discord.File(io.BytesIO(wrapped.encode()), filename="response.txt"), 83 | ) 84 | except Exception: 85 | await message.reply( 86 | content=f"<@{cfg.owner_id}>", 87 | embed=discord.Embed( 88 | color=discord.Color.red(), 89 | description="An unhandled exception occurred.", 90 | ) 91 | ) 92 | raise 93 | 94 | await message.remove_reaction(discord.utils.get(message.guild.emojis, name="loading"), self.bot.user) 95 | 96 | @app_commands.command() 97 | async def reset(self, interaction: discord.Interaction) -> None: 98 | """Reset your ChatGPT context""" 99 | if client := self.context.get(interaction.user.id): 100 | client.reset_context() 101 | await interaction.response.send_message( 102 | embed=discord.Embed( 103 | color=discord.Color.green(), 104 | description="Your ChatGPT context has been reset.", 105 | ) 106 | ) 107 | else: 108 | await interaction.response.send_message( 109 | embed=discord.Embed( 110 | color=discord.Color.red(), 111 | description="You don't have an active ChatGPT context.", 112 | ), 113 | ephemeral=True, 114 | ) 115 | -------------------------------------------------------------------------------- /bridget/utils/fetchers.py: -------------------------------------------------------------------------------- 1 | import json 2 | import urllib 3 | import aiohttp 4 | 5 | from aiocache import cached 6 | from typing import Union, Optional 7 | 8 | client_session = None 9 | 10 | 11 | @cached(ttl=3600) 12 | async def get_ios_cfw() -> Optional[dict]: 13 | """Gets all apps on ios.cfw.guide 14 | 15 | Returns 16 | ------- 17 | dict 18 | "ios, jailbreaks, devices" 19 | """ 20 | 21 | async with client_session.get("https://api.appledb.dev/main.json") as resp: 22 | if resp.status == 200: 23 | data = await resp.json() 24 | return data 25 | 26 | 27 | @cached(ttl=3600) 28 | async def get_ipsw_firmware_info(version: str) -> Union[dict, list]: 29 | """Gets all apps on ios.cfw.guide 30 | 31 | Returns 32 | ------- 33 | dict 34 | "ios, jailbreaks, devices" 35 | """ 36 | 37 | async with client_session.get(f"https://api.ipsw.me/v4/ipsw/{version}") as resp: 38 | if resp.status == 200: 39 | data = await resp.json() 40 | return data 41 | 42 | return [] 43 | 44 | 45 | @cached(ttl=600) 46 | async def get_dstatus_components() -> Optional[dict]: 47 | async with client_session.get("https://discordstatus.com/api/v2/components.json") as resp: 48 | if resp.status == 200: 49 | components = await resp.json() 50 | return components 51 | 52 | 53 | @cached(ttl=600) 54 | async def get_dstatus_incidents() -> Optional[dict]: 55 | async with client_session.get("https://discordstatus.com/api/v2/incidents.json") as resp: 56 | if resp.status == 200: 57 | incidents = await resp.json() 58 | return incidents 59 | 60 | 61 | async def fetch_remote_json(url: str) -> Optional[dict]: 62 | """Get a JSON file from a URL 63 | 64 | Parameters 65 | ---------- 66 | url : str 67 | "URL of the JSON file" 68 | 69 | Returns 70 | ------- 71 | json 72 | "Remote JSON file" 73 | 74 | """ 75 | 76 | async with client_session.get(url) as resp: 77 | if resp.status == 200: 78 | text = await resp.text() 79 | try: 80 | if text.startswith('{"bug_type":'): 81 | return json.loads(text.split("\n", 1)[1]) 82 | else: 83 | return json.loads(text) 84 | except: 85 | return None 86 | else: 87 | return None 88 | 89 | 90 | async def fetch_remote_file(url: str) -> Optional[str]: 91 | """Get a file from a URL 92 | 93 | Parameters 94 | ---------- 95 | url : str 96 | "URL of the file" 97 | 98 | Returns 99 | ------- 100 | json 101 | "Remote file" 102 | 103 | """ 104 | 105 | async with client_session.get(url) as resp: 106 | if resp.status == 200: 107 | return await resp.text() 108 | else: 109 | return None 110 | 111 | 112 | async def canister_search_package(query: str) -> Optional[list]: 113 | """Search for a tweak in Canister's catalogue 114 | 115 | Parameters 116 | ---------- 117 | query : str 118 | "Query to search for" 119 | 120 | Returns 121 | ------- 122 | list 123 | "List of packages that Canister found matching the query" 124 | 125 | """ 126 | 127 | async with client_session.get(f'https://api.canister.me/v2/jailbreak/package/search?q={urllib.parse.quote(query)}') as resp: 128 | if resp.status == 200: 129 | response = json.loads(await resp.text()) 130 | return response.get('data') 131 | else: 132 | return None 133 | 134 | 135 | async def canister_search_repo(query: str) -> Optional[list]: 136 | """Search for a repo in Canister's catalogue 137 | 138 | Parameters 139 | ---------- 140 | query : str 141 | "Query to search for" 142 | 143 | Returns 144 | ------- 145 | list 146 | "List of repos that Canister found matching the query" 147 | 148 | """ 149 | 150 | async with client_session.get(f'https://api.canister.me/v2/jailbreak/repository/search?q={urllib.parse.quote(query)}') as resp: 151 | if resp.status == 200: 152 | response = json.loads(await resp.text()) 153 | return response.get('data') 154 | else: 155 | return None 156 | 157 | 158 | @cached(ttl=3600) 159 | async def canister_fetch_repos() -> Optional[list]: 160 | async with client_session.get('https://api.canister.me/v2/jailbreak/repository/ranking?rank=*') as resp: 161 | if resp.status == 200: 162 | response = await resp.json(content_type=None) 163 | return response.get("data") 164 | 165 | return None 166 | 167 | 168 | @cached(ttl=3600) 169 | async def fetch_scam_urls() -> Optional[dict]: 170 | async with client_session.get("https://raw.githubusercontent.com/SlimShadyIAm/Anti-Scam-Json-List/main/antiscam.json") as resp: 171 | if resp.status == 200: 172 | obj = json.loads(await resp.text()) 173 | return obj 174 | 175 | 176 | async def init_client_session() -> None: 177 | global client_session 178 | client_session = aiohttp.ClientSession() 179 | -------------------------------------------------------------------------------- /bridget/__main__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import discord 3 | import mongoengine 4 | import traceback 5 | import base64 6 | import hashlib 7 | import re 8 | import logging 9 | 10 | from discord import app_commands 11 | from discord.ext import commands 12 | from os import getenv 13 | 14 | 15 | mongoengine.connect( 16 | 'bridget', 17 | host=getenv("DB_HOST"), 18 | port=int( 19 | getenv("DB_PORT"))) 20 | 21 | from cogs import * 22 | from utils.startup_checks import checks 23 | from utils.config import cfg 24 | from utils.fetchers import init_client_session 25 | from utils import send_error, send_success 26 | import backend 27 | 28 | for check in checks: 29 | check() 30 | 31 | logging.getLogger().setLevel("DEBUG") 32 | 33 | bot = commands.Bot( 34 | command_prefix=cfg.prefix, 35 | intents=discord.Intents.all(), 36 | allowed_mentions=discord.AllowedMentions( 37 | everyone=False, 38 | roles=False, 39 | users=True), 40 | ) 41 | bot.remove_command("help") 42 | 43 | # Apps 44 | # @bot.tree.context_menu(name="Meowcrypt Decrypt") 45 | # async def meowcrypt_decrypt(interaction: discord.Interaction, message: discord.Message) -> None: 46 | # if "nya>.<" not in message.content: 47 | # await send_error(interaction, "The selected message is not encrypted by Meowcrypt.") 48 | 49 | # spl = message.content.split(">.<") 50 | # one = base64.b64decode(spl[1]) 51 | # two = base64.b64decode(spl[2]) 52 | # three = base64.b64decode(spl[3]) 53 | 54 | # pass_str = "8f5SCpAbDyCdtPTNBwQpYPJVussZFXVaVWP587ZNgZr3uxKGzRLf4naudDBxmdw5" 55 | # pass_bytes = pass_str.encode("utf-8") 56 | 57 | # key = hashlib.pbkdf2_hmac('sha512', pass_bytes, one, 50000, dklen=32) 58 | 59 | # cipher = AES.new(key, AES.MODE_GCM, nonce=two) 60 | # plaintext = cipher.decrypt_and_verify( 61 | # three[:-16], three[-16:]).decode('utf-8') 62 | 63 | # embed = discord.Embed( 64 | # title="Decrypted text", 65 | # description=f"```{plaintext}```", 66 | # color=discord.Color.green() 67 | # ) 68 | # embed.set_author(name=message.author, icon_url=message.author.avatar.url) 69 | # await send_success(interaction, embed=embed, ephemeral=True) 70 | 71 | 72 | # Cogs 73 | asyncio.run(bot.add_cog(ChatGPT(bot))) 74 | asyncio.run(bot.add_cog(Logging(bot))) 75 | asyncio.run(bot.add_cog(Mod(bot))) 76 | asyncio.run(bot.add_cog(NativeActionsListeners(bot))) 77 | asyncio.run(bot.add_cog(Say(bot))) 78 | asyncio.run(bot.add_cog(Snipe(bot))) 79 | asyncio.run(bot.add_cog(Sync(bot))) 80 | asyncio.run(bot.add_cog(Tags(bot))) 81 | asyncio.run(bot.add_cog(TagsGroup(bot))) 82 | asyncio.run(bot.add_cog(Unshorten(bot))) 83 | asyncio.run(bot.add_cog(Timezones(bot))) 84 | asyncio.run(bot.add_cog(Helper(bot))) 85 | asyncio.run(bot.add_cog(FiltersGroup(bot))) 86 | asyncio.run(bot.add_cog(Issues(bot))) 87 | asyncio.run(bot.add_cog(IssuesGroup(bot))) 88 | asyncio.run(bot.add_cog(Misc(bot))) 89 | asyncio.run(bot.add_cog(Memes(bot))) 90 | asyncio.run(bot.add_cog(MemesGroup(bot))) 91 | asyncio.run(bot.add_cog(LogParsing(bot))) 92 | asyncio.run(bot.add_cog(Canister(bot))) 93 | asyncio.run(bot.add_cog(Xp(bot))) 94 | asyncio.run(bot.add_cog(StickyRoles(bot))) 95 | asyncio.run(bot.add_cog(Appeals(bot))) 96 | asyncio.run(bot.add_cog(iOSCFW(bot))) 97 | asyncio.run(bot.add_cog(SocialFix(bot))) 98 | asyncio.run(bot.add_cog(Restrictions(bot))) 99 | # asyncio.run(bot.add_cog(OCR(bot))) # Do not enable, or else we'll hate you 100 | asyncio.run(bot.add_cog(AntiRaidMonitor(bot))) 101 | 102 | 103 | @bot.event 104 | async def on_ready() -> None: 105 | await bot.add_cog(Birthday(bot)) # need to start bot before adding this cog 106 | await init_client_session() 107 | 108 | # Error handler 109 | @bot.tree.error 110 | async def app_command_error(interaction: discord.Interaction, error: app_commands.AppCommandError) -> None: 111 | if isinstance(error, app_commands.CommandInvokeError): 112 | error = error.original 113 | 114 | if isinstance(error, discord.errors.NotFound): 115 | await interaction.channel.send(embed=discord.Embed(color=discord.Color.red(), description=f"Sorry {interaction.user.mention}, it looks like I took too long to respond to you! If I didn't do what you wanted in time, please try again."), delete_after=5) 116 | return 117 | 118 | if (isinstance(error, commands.MissingRequiredArgument) 119 | or isinstance(error, app_commands.TransformerError) 120 | or isinstance(error, commands.BadArgument) 121 | or isinstance(error, commands.BadUnionArgument) 122 | or isinstance(error, app_commands.MissingPermissions) 123 | or isinstance(error, app_commands.BotMissingPermissions) 124 | or isinstance(error, commands.MaxConcurrencyReached) 125 | or isinstance(error, app_commands.NoPrivateMessage)): 126 | await send_error(interaction, error) 127 | else: 128 | try: 129 | raise error 130 | except BaseException: 131 | tb = traceback.format_exc() 132 | print(tb) 133 | if len(tb.split('\n')) > 8: 134 | tb = '\n'.join(tb.split('\n')[-8:]) 135 | 136 | tb_formatted = tb 137 | if len(tb_formatted) > 1000: 138 | tb_formatted = "...\n" + tb_formatted[-1000:] 139 | 140 | await send_error(interaction, f"`{error}`\n```{tb_formatted}```") 141 | 142 | if cfg.backend_port != -1: 143 | backend.run(bot) 144 | bot.run(getenv("TOKEN")) 145 | -------------------------------------------------------------------------------- /bridget/cogs/helper.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import random 3 | 4 | from discord import ChannelType, Color, app_commands 5 | from datetime import datetime 6 | 7 | from utils import Cog, send_error, send_success 8 | from utils.modals import PostEmbedModal 9 | from utils.services import guild_service 10 | from utils.enums import PermissionLevel 11 | from utils.errors import MissingPermissionsError 12 | 13 | 14 | class Helper(Cog): 15 | @app_commands.command() 16 | async def solved(self, ctx: discord.Interaction) -> None: 17 | """Close a support thread, usable by OP and Helpers 18 | 19 | Args: 20 | ctx (discord.ctx): Context 21 | """ 22 | 23 | # error if channel is not a support thread 24 | if (ctx.channel.type != ChannelType.public_thread 25 | or ctx.channel.parent.type != ChannelType.forum 26 | or ctx.channel.parent_id != guild_service.get_guild().channel_support 27 | ): 28 | await send_error(ctx, "You can't mark this channel as solved") 29 | return 30 | 31 | # only OP and helpers can mark as solved 32 | if ctx.channel.owner_id != ctx.user.id: 33 | if not PermissionLevel.HELPER == ctx.user: 34 | MissingPermissionsError.throw([f"<@&{guild_service.get_guild().role_helper}>"]) 35 | 36 | await send_success(ctx, "Thread marked as solved!", ephemeral=False) 37 | 38 | # fetch members and remove them from thread 39 | members = await ctx.channel.fetch_members() 40 | for member in members: 41 | await ctx.channel.remove_user(member) 42 | 43 | # lock and archive thread 44 | await ctx.channel.edit(archived=True, locked=True, reason=f"Thread marked as solved by {str(ctx.user)}") 45 | 46 | @PermissionLevel.HELPER 47 | @app_commands.command() 48 | async def postembed(self, ctx: discord.Interaction, title: str, channel: discord.TextChannel = None, image: discord.Attachment = None, color: str = None, anonymous: bool = False) -> None: 49 | """Sends an embed 50 | 51 | Args: 52 | ctx (discord.ctx): Context 53 | title (str): Title of the embed 54 | channel (discord.TextChannel): Channel to post the embed in 55 | color (str): Color of the embed in hexadecimal notation 56 | image (discord.Attachment): Image to show in embed 57 | anonymous (bool): Wether to show "Posted by" in footer 58 | """ 59 | 60 | # if channel is not specified, default to the channel where 61 | # the interaction was ran in 62 | if channel is None: 63 | channel = ctx.channel 64 | 65 | # check if the user has permission to send to that channel 66 | perms = channel.permissions_for(ctx.user) 67 | if not perms.send_messages: 68 | await send_error(ctx, "You can't send messages in that channel!", delete_after=1) 69 | return 70 | 71 | # create the embed, add the image and color if specified 72 | embed = discord.Embed(title=title, timestamp=datetime.now()) 73 | embed.set_footer(text="" if anonymous else f"Posted by {ctx.user}") 74 | if image is not None: 75 | embed.set_image(url=image.url) 76 | if color is not None: 77 | try: 78 | embed.color = Color.from_str(color) 79 | except: 80 | await send_error(ctx, "Invalid color!", delete_after=1) 81 | return 82 | 83 | modal = PostEmbedModal(bot=self.bot, channel=channel, author=ctx.user) 84 | await ctx.response.send_modal(modal) 85 | await modal.wait() 86 | embed.description = modal.description 87 | 88 | # send the embed 89 | await channel.send(embed=embed) 90 | 91 | @PermissionLevel.HELPER 92 | @app_commands.command() 93 | async def poll(self, ctx: discord.Interaction, question: str, channel: discord.TextChannel = None, image: discord.Attachment = None, color: str = None) -> None: 94 | """Start a poll 95 | 96 | Args: 97 | ctx (discord.ctx): Context 98 | question (str): Question to ask 99 | channel (discord.TextChannel): Cchannel to post the poll in 100 | color (str): Color of the poll in hexadecimal notation 101 | image (discord.Attachment): Image to attach to poll 102 | """ 103 | 104 | # if channel is not specified, default to the channel where 105 | # the interaction was ran in 106 | if channel is None: 107 | channel = ctx.channel 108 | 109 | # check if the user has permission to send to that channel 110 | perms = channel.permissions_for(ctx.user) 111 | if not perms.send_messages: 112 | await send_error(ctx, "You can't send messages in that channel!", delete_after=1) 113 | return 114 | 115 | # create the embed, add the image and color if specified 116 | embed = discord.Embed( 117 | description=question, color=random.randint( 118 | 0, 16777215), timestamp=datetime.now()) 119 | embed.set_footer( 120 | text=f"Poll started by {ctx.user}") 121 | if image is not None: 122 | embed.set_image(url=image.url) 123 | if color is not None: 124 | try: 125 | embed.color = Color.from_str(color) 126 | except: 127 | await send_error(ctx, "Invalid color!", delete_after=1) 128 | return 129 | 130 | # send the embed 131 | msg = await channel.send(embed=embed) 132 | await msg.add_reaction('⬆️') 133 | await msg.add_reaction('⬇️') 134 | await send_success(ctx, "Poll started!", delete_after=1) 135 | 136 | -------------------------------------------------------------------------------- /bridget/utils/views.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import discord.ui as ui 3 | from utils.enums import PermissionLevel 4 | 5 | from utils.mod import warn 6 | from utils.modals import AutoModWarnButtonModal, ReasonModal 7 | from utils.services import user_service 8 | from utils.config import cfg 9 | from utils.utils import send_error 10 | 11 | 12 | class AutoModReportView(ui.View): 13 | member: discord.Member 14 | bot: discord.BotIntegration 15 | 16 | def __init__(self, member: discord.Member, bot: discord.BotIntegration) -> None: 17 | super().__init__() 18 | self.member = member 19 | self.bot = bot 20 | 21 | if not self.member.is_timed_out(): 22 | self.remove_item(self.unmute) 23 | 24 | @ui.button(label='Dismiss', style=discord.ButtonStyle.green) 25 | async def dismiss(self, ctx: discord.Interaction, button: ui.Button) -> None: 26 | await ctx.message.delete() 27 | await ctx.response.defer() 28 | 29 | @ui.button(label='Warn', style=discord.ButtonStyle.red) 30 | async def warn(self, ctx: discord.Interaction, button: ui.Button) -> None: 31 | modal = AutoModWarnButtonModal(bot=self.bot, ctx=ctx, author=ctx.user, user=self.member) 32 | await ctx.response.send_modal(modal) 33 | await modal.wait() 34 | 35 | self.warn.disabled = True 36 | await warn(ctx, target_member=self.member, mod=ctx.user, points=modal.points, reason=modal.reason, no_interaction=True) 37 | try: 38 | await ctx.edit_original_response(view=self) 39 | except: 40 | await ctx.message.delete() 41 | 42 | @ui.button(label='Ban', style=discord.ButtonStyle.danger) 43 | async def ban(self, ctx: discord.Interaction, button: ui.Button) -> None: 44 | modal = ReasonModal(bot=self.bot, ctx=ctx, author=ctx.user, title=f"Ban reason for {self.member}") 45 | await ctx.response.send_modal(modal) 46 | await modal.wait() 47 | 48 | self.ban.disabled = True 49 | await self.member.ban(reason=modal.reason) 50 | try: 51 | await ctx.edit_original_response(view=self) 52 | except: 53 | await ctx.message.delete() 54 | 55 | @ui.button(label="Unmute", style=discord.ButtonStyle.gray) 56 | async def unmute(self, ctx: discord.Interaction, button: ui.Button) -> None: 57 | await self.member.timeout(None, reason="Action reviewed by a moderator") 58 | 59 | self.unmute.disabled = True 60 | await ctx.response.defer() 61 | try: 62 | await ctx.edit_original_response(view=self) 63 | except: 64 | await ctx.message.delete() 65 | 66 | class AppealView(discord.ui.View): 67 | def __init__(self, bot: discord.Client, appealer: discord.User): 68 | super().__init__(timeout=None) 69 | self.bot = bot 70 | self.appealer = appealer 71 | self.replied = False 72 | 73 | @discord.ui.button(label='Accept Appeal', style=discord.ButtonStyle.green, custom_id='appealview:accept') 74 | async def accept_appeal(self, ctx: discord.Interaction, button: discord.ui.Button): 75 | guild = self.bot.get_guild(cfg.guild_id) 76 | user_main = guild.get_member(ctx.user.id) 77 | if not PermissionLevel.ADMIN == user_main: 78 | await send_error(ctx, "You're not an admin!") 79 | return 80 | modal = ReasonModal(bot=self.bot, ctx=ctx, author=ctx.user, title=f"Are you sure you want to accept this appeal?") 81 | await ctx.response.send_modal(modal) 82 | await modal.wait() 83 | if modal.reason is not None: 84 | user = user_service.get_user(self.appealer.id) 85 | user.is_appealing = False 86 | user.is_banned = False 87 | user.appeal_btn_msg_id = None 88 | await guild.unban(self.appealer, reason=modal.reason) 89 | try: 90 | await self.appealer.send(f"Your ban appeal for {guild.name} was accepted with the following reason: ```{modal.reason}```") 91 | except: 92 | pass 93 | user.save() 94 | await ctx.channel.send(f"{ctx.user.mention} accepted the appeal with the following reason: ```{modal.reason}```") 95 | await ctx.message.edit(embed=ctx.message.embeds[0], view=None) 96 | await ctx.channel.edit(locked=True) 97 | for user in await ctx.channel.fetch_members(): 98 | await ctx.channel.remove_user(user) 99 | 100 | @discord.ui.button(label='Reject Appeal', style=discord.ButtonStyle.red, custom_id='appealview:reject') 101 | async def reject_appeal(self, ctx: discord.Interaction, button: discord.ui.Button): 102 | guild = self.bot.get_guild(cfg.guild_id) 103 | user_main = guild.get_member(ctx.user.id) 104 | if not PermissionLevel.ADMIN == user_main: 105 | await send_error(ctx, "You're not an admin!") 106 | return 107 | 108 | modal = ReasonModal(bot=self.bot, ctx=ctx, author=ctx.user, title=f"Are you sure you want to reject this appeal?") 109 | await ctx.response.send_modal(modal) 110 | await modal.wait() 111 | 112 | if modal.reason is not None: 113 | user = user_service.get_user(self.appealer.id) 114 | user.is_appealing = False 115 | user.appeal_btn_msg_id = None 116 | try: 117 | await self.appealer.send(f"Your ban appeal for {guild.name} was rejected with the following reason: ```{modal.reason}```") 118 | except: 119 | pass 120 | user.save() 121 | await ctx.channel.send(f"{ctx.user.mention} rejected the appeal with the following reason: ```{modal.reason}```") 122 | await ctx.message.edit(embed=ctx.message.embeds[0], view=None) 123 | await ctx.channel.edit(locked=True) 124 | for user in await ctx.channel.fetch_members(): 125 | await ctx.channel.remove_user(user) 126 | 127 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [Discord](https://discord.gg/palera1n). 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ### 5. All rules from the `palera1n` Discord server, and testing server apply in this repository. 116 | 117 | **All consequences may be reapplied in Discord servers in either server, depending 118 | on severity** 119 | 120 | ## Attribution 121 | 122 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 123 | version 2.0, available at 124 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 125 | 126 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 127 | enforcement ladder](https://github.com/mozilla/diversity). 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | 131 | For answers to common questions about this code of conduct, see the FAQ at 132 | https://www.contributor-covenant.org/faq. Translations are available at 133 | https://www.contributor-covenant.org/translations. 134 | -------------------------------------------------------------------------------- /bridget/cogs/birthday.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import pytz 3 | import discord 4 | 5 | from datetime import datetime, timedelta 6 | from discord.ext import commands, tasks 7 | from discord import app_commands 8 | from bridget.utils.autocomplete import date_autocompleter 9 | from bridget.utils.utils import Cog, send_success 10 | 11 | from utils.services import guild_service, user_service 12 | from utils.config import cfg 13 | from utils.enums import PermissionLevel 14 | 15 | MONTH_MAPPING = { 16 | "January": { 17 | "value": 1, 18 | "max_days": 31, 19 | }, 20 | "February": { 21 | "value": 2, 22 | "max_days": 29, 23 | }, 24 | "March": { 25 | "value": 3, 26 | "max_days": 31, 27 | }, 28 | "April": { 29 | "value": 4, 30 | "max_days": 30, 31 | }, 32 | "May": { 33 | "value": 5, 34 | "max_days": 31, 35 | }, 36 | "June": { 37 | "value": 6, 38 | "max_days": 30, 39 | }, 40 | "July": { 41 | "value": 7, 42 | "max_days": 31, 43 | }, 44 | "August": { 45 | "value": 8, 46 | "max_days": 31, 47 | }, 48 | "September": { 49 | "value": 9, 50 | "max_days": 30, 51 | }, 52 | "October": { 53 | "value": 10, 54 | "max_days": 31, 55 | }, 56 | "November": { 57 | "value": 11, 58 | "max_days": 30, 59 | }, 60 | "December": { 61 | "value": 12, 62 | "max_days": 31, 63 | }, 64 | 65 | } 66 | 67 | async def give_user_birthday_role(bot, db_guild, user, guild): 68 | birthday_role = guild.get_role(db_guild.role_birthday) 69 | if birthday_role is None: 70 | return 71 | 72 | if birthday_role in user.roles: 73 | return 74 | 75 | # calculate the different between now and tomorrow 12AM 76 | now = datetime.now(pytz.timezone('US/Eastern')) 77 | h = now.hour / 24 78 | m = now.minute / 60 / 24 79 | 80 | # schedule a task to remove birthday role (tomorrow) 12AM 81 | try: 82 | time = now + timedelta(days=1-h-m) 83 | bot.tasks.schedule_remove_bday(user.id, time) 84 | except Exception: 85 | return 86 | 87 | await user.add_roles(birthday_role) 88 | 89 | try: 90 | await user.send(f"According to my calculations, today is your birthday! We've given you the {birthday_role} role for 24 hours.") 91 | except Exception: 92 | pass 93 | 94 | class Birthday(Cog): 95 | def __init__(self, bot): 96 | super().__init__(bot) 97 | self.eastern_timezone = pytz.timezone('US/Eastern') 98 | self.birthday.start() 99 | 100 | def cog_unload(self): 101 | self.birthday.cancel() 102 | 103 | @app_commands.choices(month=[app_commands.Choice(name=m, value=m) for m in MONTH_MAPPING.keys()]) 104 | @app_commands.autocomplete(date=date_autocompleter) 105 | @app_commands.command() 106 | async def mybirthday(self, ctx: discord.Interaction, month: str, date: int) -> None: 107 | user = ctx.user 108 | if not (PermissionLevel.MEMPLUS == ctx.user or user.premium_since is not None): 109 | raise commands.BadArgument( 110 | "You need to be at least Member+ or a Nitro booster to use that command.") 111 | 112 | month = MONTH_MAPPING.get(month) 113 | if month is None: 114 | raise commands.BadArgument("You gave an invalid date") 115 | 116 | month = month["value"] 117 | 118 | # ensure date is real (2020 is a leap year in case the birthday is leap day) 119 | try: 120 | datetime(year=2020, month=month, day=date, hour=12) 121 | except ValueError: 122 | raise commands.BadArgument("You gave an invalid date.") 123 | 124 | # fetch user profile from DB 125 | db_user = user_service.get_user(user.id) 126 | 127 | # mods are able to ban users from using birthdays, let's handle that 128 | if db_user.birthday_excluded: 129 | raise commands.BadArgument("You are banned from birthdays.") 130 | 131 | # if the user already has a birthday set in the database, refuse to change it (if not a mod) 132 | if db_user.birthday != [] and not PermissionLevel.MOD == ctx.user: 133 | raise commands.BadArgument( 134 | "You already have a birthday set! You need to ask a mod to change it.") 135 | 136 | # passed all the sanity checks, let's save the birthday 137 | db_user.birthday = [month, date] 138 | db_user.save() 139 | 140 | await send_success(ctx, description=f"Your birthday was set.") 141 | # if it's the user's birthday today let's assign the role right now! 142 | today = datetime.today().astimezone(self.eastern_timezone) 143 | if today.month == month and today.day == date: 144 | db_guild = guild_service.get_guild() 145 | await give_user_birthday_role(self.bot, db_guild, ctx.user, ctx.guild) 146 | 147 | @tasks.loop(seconds=120) 148 | async def birthday(self): 149 | """Background task to scan database for users whose birthday it is today. 150 | If it's someone's birthday, the bot will assign them the birthday role for 24 hours.""" 151 | 152 | # assign the role at 12am US Eastern time 153 | eastern = pytz.timezone('US/Eastern') 154 | today = datetime.today().astimezone(eastern) 155 | # the date we will check for in the database 156 | date = [today.month, today.day] 157 | # get list of users whose birthday it is today 158 | birthdays = user_service.retrieve_birthdays(date) 159 | 160 | guild = self.bot.get_guild(cfg.guild_id) 161 | if not guild: 162 | return 163 | 164 | db_guild = guild_service.get_guild() 165 | birthday_role = guild.get_role(db_guild.role_birthday) 166 | if not birthday_role: 167 | return 168 | 169 | # give each user whose birthday it is today the birthday role 170 | for person in birthdays: 171 | if person.birthday_excluded: 172 | continue 173 | 174 | user = guild.get_member(person._id) 175 | if user is None: 176 | return 177 | 178 | await give_user_birthday_role(self.bot, db_guild, user, guild) -------------------------------------------------------------------------------- /bridget/cogs/filters.py: -------------------------------------------------------------------------------- 1 | import discord 2 | 3 | from discord import Embed, app_commands 4 | from discord.ext import commands 5 | from typing import Optional, List, Tuple 6 | 7 | from utils import Cog, send_error, send_success 8 | from utils.menus import Menu 9 | from utils.autocomplete import automod_autocomplete, filter_phrase_autocomplete, filter_regex_autocomplete, filter_whitelist_autocomplete 10 | from utils.enums import PermissionLevel 11 | 12 | 13 | def format_filter_page(_, entries: List[Tuple[str, str]], current_page: int, all_pages: list) -> Embed: 14 | embed = discord.Embed( 15 | title='Filtered words', color=discord.Color.blurple()) 16 | for filter in entries: 17 | embed.add_field(name=filter[0], value=filter[1]) 18 | embed.set_footer( 19 | text=f"Page {current_page} of {len(all_pages)}") 20 | return embed 21 | 22 | 23 | class FiltersGroup(Cog, commands.GroupCog, group_name="filter"): 24 | @PermissionLevel.MOD 25 | @app_commands.autocomplete(rule=automod_autocomplete) 26 | @app_commands.command() 27 | async def add(self, ctx: discord.Interaction, rule: str, phrase: str = None, regex: str = None, whitelist: str = None) -> None: 28 | """Add a new filtered word 29 | 30 | Args: 31 | ctx (discord.ctx): Context 32 | rule (str): The AutoMod rule to add the filter to 33 | phrase (str): The word or phrase to filter 34 | regex (str): The regular expression to filter 35 | whitelist (str): The word or phrase to whitelist 36 | """ 37 | 38 | # fetch rule 39 | rules = await ctx.guild.fetch_automod_rules() 40 | rule = [ x for x in rules if str(x.id) == rule ] 41 | if len(rule) == 0: 42 | await send_error(ctx, "AutoMod rule not found!", delete_after=3) 43 | return 44 | rule = rule[0] 45 | 46 | # extract the trigger to modify it 47 | trig = rule.trigger 48 | 49 | if phrase is not None: 50 | trig.keyword_filter.append(phrase) 51 | 52 | if regex is not None: 53 | trig.regex_patterns.append(regex) 54 | 55 | if whitelist is not None: 56 | trig.allow_list.append(whitelist) 57 | 58 | # edit the rule trigger 59 | await rule.edit(trigger=trig, reason="Filtered word/regex/whitelist has been added") 60 | await send_success(ctx, "Filter has been edited.", delete_after=3) 61 | 62 | @PermissionLevel.MOD 63 | @app_commands.autocomplete(rule=automod_autocomplete) 64 | @app_commands.command() 65 | async def list(self, ctx: discord.Interaction, rule: str = None) -> None: 66 | """List filtered words 67 | 68 | Args: 69 | ctx (discord.ctx): Context 70 | rule (str): Show only one AutoMod rule 71 | """ 72 | 73 | # fetch rules and prepare embed 74 | rules = await ctx.guild.fetch_automod_rules() 75 | 76 | filters = [] 77 | for am_rule in rules: 78 | if rule is not None and str(am_rule.id) != rule: 79 | continue 80 | 81 | # add the filtered words, regexs and whitelists to the embed 82 | ruledict = am_rule.to_dict() 83 | if ruledict is None or 'trigger_metadata' not in ruledict or ruledict['trigger_metadata'] is None: 84 | continue 85 | 86 | # keywords 87 | if 'keyword_filter' in ruledict['trigger_metadata']: 88 | for word in ruledict['trigger_metadata']['keyword_filter']: 89 | filters.append((discord.utils.escape_markdown(word), am_rule.name)) 90 | # regexs 91 | if 'regex_patterns' in ruledict['trigger_metadata']: 92 | for x in ruledict['trigger_metadata']['regex_patterns']: 93 | xr = x.replace("`", "\\`") 94 | filters.append((f'`/{xr}/`', am_rule.name)) 95 | # whitelists 96 | if 'allow_list' in ruledict['trigger_metadata']: 97 | for x in ruledict['trigger_metadata']['allow_list']: 98 | filters.append((f'**{discord.utils.escape_markdown(x)}** (whitelisted)', am_rule.name)) 99 | 100 | _filters = sorted(filters) 101 | if len(_filters) == 0: 102 | raise commands.BadArgument("There are no filters defined.") 103 | 104 | menu = Menu( 105 | ctx, 106 | filters, 107 | per_page=12, 108 | page_formatter=format_filter_page, 109 | whisper=True) 110 | await menu.start() 111 | 112 | @PermissionLevel.MOD 113 | @app_commands.autocomplete(rule=automod_autocomplete, phrase=filter_phrase_autocomplete, regex=filter_regex_autocomplete, whitelist=filter_whitelist_autocomplete) 114 | @app_commands.command() 115 | async def remove(self, ctx: discord.Interaction, rule: str, phrase: str = None, regex: str = None, whitelist: str = None) -> None: 116 | """Remove a filtred word 117 | 118 | Args: 119 | ctx (discord.ctx): Context 120 | rule (str): The AutoMod rule to remove the filter from 121 | phrase (str): The word or phrase to un-filter 122 | regex (str): The regular expression to un-filter 123 | whitelist (str): The word or phrase to un-whitelist 124 | """ 125 | 126 | # fetch rule 127 | rules = await ctx.guild.fetch_automod_rules() 128 | rule = [ x for x in rules if str(x.id) == rule ] 129 | if len(rule) == 0: 130 | await send_error(ctx, "AutoMod rule not found!", delete_after=3) 131 | return 132 | rule = rule[0] 133 | 134 | # extract the trigger to modify it 135 | trig = rule.trigger 136 | 137 | try: 138 | if phrase is not None: 139 | trig.keyword_filter.remove(phrase) 140 | 141 | if regex is not None: 142 | trig.regex_patterns.remove(regex) 143 | 144 | if whitelist is not None: 145 | trig.allow_list.remove(whitelist) 146 | except ValueError: 147 | await send_error(ctx, "The requested filter(s) could not be found in the AutoMod rule", delete_after=3) 148 | return 149 | 150 | # edit the rule trigger 151 | await rule.edit(trigger=trig, reason="Filtered word/regex/whitelist has been removed") 152 | await send_success(ctx, "Filter has been edited.", delete_after=3) 153 | 154 | -------------------------------------------------------------------------------- /bridget/cogs/sync.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import aiohttp 3 | import discord 4 | import os 5 | import requests 6 | import time 7 | import logging 8 | 9 | from discord.ext import commands 10 | 11 | from utils import Cog 12 | from utils.config import cfg 13 | from utils import enums 14 | from utils.services import guild_service 15 | 16 | 17 | class Sync(Cog): 18 | async def get_bearer(self) -> dict: # Sorry, can't provide explicit types, `dict` is the best i can do 19 | async with aiohttp.ClientSession() as session: 20 | async with session.post( 21 | 'https://discord.com/api/v10/oauth2/token', 22 | data=aiohttp.FormData( 23 | {'grant_type': 'client_credentials', 'scope': 'applications.commands.permissions.update'} 24 | ), 25 | auth=aiohttp.BasicAuth(os.environ.get("CLIENT_ID"), os.environ.get("CLIENT_SECRET")), 26 | headers={'Content-Type': 'application/x-www-form-urlencoded'}, 27 | ) as resp: 28 | return await resp.json() 29 | 30 | @commands.command() 31 | async def sync(self, ctx: commands.Context) -> None: 32 | """Sync slash commands""" 33 | 34 | if ctx.author.id != cfg.owner_id: 35 | await ctx.reply( 36 | embed=discord.Embed( 37 | color=discord.Color.red(), 38 | description="You are not allowed to use this command.", 39 | ), 40 | ) 41 | return 42 | 43 | await ctx.reply( 44 | embed=discord.Embed(color=discord.Color.blurple(), description="Syncing commands, this will take a while"), delete_after=3 45 | ) 46 | 47 | async with ctx.typing(): 48 | await self.bot.tree.sync(guild=discord.Object(id=cfg.guild_id)) # causes the infinte sync sometimes 49 | await self.bot.tree.sync() # this too 50 | try: 51 | token = await self.get_bearer() 52 | bearer = token['access_token'] 53 | headers = {'Authorization': f'Bearer {bearer}'} 54 | # async with aiohttp.ClientSession(headers=headers) as session: 55 | # async with session.get(f"https://discord.com/api/v10/applications/{os.environ.get('CLIENT_ID')}/guilds/{os.environ.get('GUILD_ID')}/commands", headers=headers) as resp: 56 | # command_list = await resp.json() 57 | # print(command_list) 58 | # for command in command_list: 59 | # try: 60 | # print(command) 61 | # async with session.put(f"https://discord.com/api/v10/applications/{os.environ.get('CLIENT_ID')}/guilds/{os.environ.get('GUILD_ID')}/commands/{command.id}/permissions", headers=headers): 62 | # pass 63 | # except Exception as e: 64 | # print(e) 65 | resp = requests.get( 66 | f"https://discord.com/api/v10/applications/{self.bot.application_id}/commands", 67 | headers={'Authorization': f'Bot {os.environ.get("TOKEN")}'}, 68 | ) 69 | command_list = resp.json() 70 | for command in command_list: 71 | while True: 72 | try: 73 | # print(command) 74 | dpy_cmd = self.bot.tree.get_command(command['name']) 75 | try: 76 | cmdtype = dpy_cmd.extras['PermLevel'] 77 | except: 78 | cmdtype = enums.PermissionLevel.EVERYONE 79 | toid = 0 80 | if cmdtype == enums.PermissionLevel.EVERYONE: 81 | break 82 | elif cmdtype >= enums.PermissionLevel.ADMIN: 83 | payload = { 84 | 'permissions': [{'id': os.environ.get('GUILD_ID'), 'type': 1, 'permission': False}] 85 | } 86 | else: 87 | toid = getattr(guild_service.get_guild(), str(cmdtype)) 88 | payload = { 89 | 'permissions': [ 90 | {'id': toid, 'type': 1, 'permission': True}, 91 | {'id': os.environ.get('GUILD_ID'), 'type': 1, 'permission': False}, 92 | ] 93 | } 94 | 95 | print(payload) 96 | 97 | r = requests.put( 98 | f"https://discord.com/api/v10/applications/{self.bot.application_id}/guilds/{os.environ.get('GUILD_ID')}/commands/{command['id']}/permissions", 99 | headers=headers, 100 | json=payload, 101 | ) 102 | if r.status_code == 429: 103 | # logging.log(msg='got ratelimited, sleeping for 10 seconds') 104 | time.sleep(10) 105 | elif r.status_code == 400: 106 | raise Exception(msg=f"got 400, response json: {r.json()}") 107 | elif not r.ok: 108 | pass 109 | else: 110 | break 111 | 112 | except Exception as e: 113 | raise e 114 | 115 | except Exception as e: 116 | await ctx.reply( 117 | embed=discord.Embed( 118 | color=discord.Color.green(), 119 | description=f"Synced slash commands, but failed to set permissions {e}", 120 | ), 121 | delete_after=5, 122 | ) 123 | print(e) 124 | await asyncio.sleep(8) 125 | await ctx.message.delete() 126 | raise e 127 | 128 | await ctx.reply( 129 | embed=discord.Embed( 130 | color=discord.Color.green(), 131 | description="Synced slash commands.", 132 | ), 133 | delete_after=5, 134 | ) 135 | 136 | await asyncio.sleep(5) 137 | await ctx.message.delete() 138 | -------------------------------------------------------------------------------- /bridget/cogs/canister.py: -------------------------------------------------------------------------------- 1 | import re 2 | import discord 3 | 4 | from discord import app_commands 5 | from discord.ext import commands 6 | 7 | from utils.config import cfg 8 | from utils.enums import PermissionLevel 9 | from utils.services import guild_service 10 | from utils.fetchers import canister_fetch_repos, canister_search_package 11 | from utils.canister import TweakDropdown, tweak_embed_format 12 | from utils.utils import send_error 13 | from utils.autocomplete import repo_autocomplete 14 | 15 | class Canister(commands.Cog): 16 | def __init__(self, bot): 17 | self.bot = bot 18 | 19 | @commands.Cog.listener() 20 | async def on_message(self, message: discord.Message) -> None: 21 | if message.guild is None: 22 | return 23 | 24 | author = message.guild.get_member(message.author.id) 25 | if author is None: 26 | return 27 | 28 | pattern = re.compile( 29 | r".*?(?\#\:\;\%\(\)]){2,})\]\](?!\])+.*") 30 | if not pattern.match(message.content): 31 | return 32 | 33 | matches = pattern.findall(message.content) 34 | if not matches: 35 | return 36 | 37 | search_term = matches[0][0].replace('[[', '').replace(']]', '') 38 | if not search_term: 39 | return 40 | 41 | ctx = await self.bot.get_context(message) 42 | 43 | async with ctx.typing(): 44 | result = list(await canister_search_package(search_term)) 45 | 46 | if not result: 47 | embed = discord.Embed( 48 | title=":(\nI couldn't find that package", color=discord.Color.red()) 49 | embed.description = f"Try broadening your search query." 50 | await ctx.send(embed=embed, delete_after=8) 51 | return 52 | 53 | view = discord.ui.View(timeout=30) 54 | td = TweakDropdown(author, result, interaction=False, 55 | should_whisper=False) 56 | td._view = view 57 | view.add_item(td) 58 | td.refresh_view(result[0]) 59 | view.on_timeout = td.on_timeout 60 | embed = tweak_embed_format(result[0]) 61 | message = await message.reply(embed=embed, view=view) 62 | new_ctx = await self.bot.get_context(message) 63 | td.start(new_ctx) 64 | 65 | @app_commands.command() 66 | async def package(self, ctx: discord.Interaction, query: str) -> None: 67 | """Search for a jailbreak tweak or package 68 | 69 | Args: 70 | ctx (discord.ctx): Context 71 | query (str): Name of package to search for 72 | """ 73 | 74 | if len(query) < 2: 75 | raise commands.BadArgument("Please enter a longer query.") 76 | 77 | should_whisper = False 78 | if not PermissionLevel.MOD == ctx.user and ctx.channel_id == guild_service.get_guild().channel_general: 79 | should_whisper = True 80 | 81 | await ctx.response.defer(ephemeral=should_whisper) 82 | result = list(await canister_search_package(query)) 83 | 84 | if not result: 85 | embed = discord.Embed( 86 | title=":(\nI couldn't find that package", color=discord.Color.red()) 87 | embed.description = f"Try broadening your search query." 88 | await ctx.response.send_message(embed=embed) 89 | return 90 | 91 | view = discord.ui.View(timeout=30) 92 | td = TweakDropdown(ctx.user, result, interaction=True, 93 | should_whisper=should_whisper) 94 | td._view = view 95 | view.on_timeout = td.on_timeout 96 | view.add_item(td) 97 | td.refresh_view(result[0]) 98 | await ctx.followup.send(embed=tweak_embed_format(result[0]), view=view) 99 | td.start(ctx) 100 | 101 | @app_commands.command() 102 | @app_commands.autocomplete(query=repo_autocomplete) 103 | async def repo(self, ctx: discord.Interaction, query: str) -> None: 104 | """Search for a tweak repository 105 | 106 | Args: 107 | ctx (discord.ctx): Context 108 | query (str): Name of repository to search for 109 | """ 110 | 111 | repos = await canister_fetch_repos() 112 | matches = [repo for repo in repos if repo.get("slug") and repo.get( 113 | "slug") is not None and repo.get("slug").lower() == query.lower()] 114 | if not matches: 115 | await send_error(ctx, "That repository isn't registered with Canister's database.") 116 | return 117 | 118 | repo_data = matches[0] 119 | 120 | embed = discord.Embed(title=repo_data.get( 121 | 'name'), color=discord.Color.blue()) 122 | embed.add_field(name="URL", value=repo_data.get('uri'), inline=True) 123 | embed.set_thumbnail(url=f'{repo_data.get("uri")}/CydiaIcon.png') 124 | embed.set_footer(text="Powered by Canister") 125 | 126 | this_repo = repo_data.get("uri") 127 | view = discord.ui.View() 128 | 129 | if repo_data['isBootstrap']: 130 | [view.add_item(item) for item in [ 131 | discord.ui.Button(label='Cannot add default repo', emoji="<:Sileo:959128883498729482>", 132 | url=f'https://repos.slim.rocks/repo/?repoUrl={this_repo}&manager=sileo', disabled=True, style=discord.ButtonStyle.url, row=1), 133 | discord.ui.Button(label='Cannot add default repo', emoji="<:Zeeb:959129860603801630>", 134 | url=f'https://repos.slim.rocks/repo/?repoUrl={this_repo}&manager=zebra', disabled=True, style=discord.ButtonStyle.url, row=1), 135 | discord.ui.Button(label='Cannot add default repo', emoji="<:Add:947354227171262534>", 136 | url=f'https://repos.slim.rocks/repo/?repoUrl={this_repo}', style=discord.ButtonStyle.url, disabled=True, row=1) 137 | ]] 138 | else: 139 | [view.add_item(item) for item in [ 140 | discord.ui.Button(label='Add Repo to Sileo', emoji="<:Sileo:959128883498729482>", 141 | url=f'https://repos.slim.rocks/repo/?repoUrl={this_repo}&manager=sileo', style=discord.ButtonStyle.url, row=1), 142 | discord.ui.Button(label='Add Repo to Zebra', emoji="<:Zeeb:959129860603801630>", 143 | url=f'https://repos.slim.rocks/repo/?repoUrl={this_repo}&manager=zebra', style=discord.ButtonStyle.url, row=1), 144 | discord.ui.Button(label='Other Package Managers', emoji="<:Add:947354227171262534>", 145 | url=f'https://repos.slim.rocks/repo/?repoUrl={this_repo}', style=discord.ButtonStyle.url, row=1) 146 | ]] 147 | 148 | await ctx.response.send_message(embed=embed, view=view) 149 | 150 | -------------------------------------------------------------------------------- /bridget/utils/utils.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import asyncio 3 | 4 | from binascii import crc32 5 | from datetime import datetime 6 | from discord.ext import commands 7 | from typing import Any, List, Optional, Union 8 | from discord import Color, app_commands 9 | 10 | from model.user import User 11 | from utils.autocomplete import transform_groups 12 | from utils.fetchers import get_ios_cfw 13 | 14 | class Cog(commands.Cog): 15 | def __init__(self, bot: commands.Bot): 16 | self.bot = bot 17 | 18 | 19 | def hash_color(content: str) -> Color: 20 | # ChatGPT made this lmao 21 | color = crc32(content.encode('utf-8')) & 0xFFFFFF # Get the lower 24 bits 22 | red = (color >> 16) & 0xFF 23 | green = (color >> 8) & 0xFF 24 | blue = color & 0xFF 25 | 26 | # Adjust the RGB components to create a pastel color 27 | red = int((red + 255) / 2) 28 | green = int((green + 255) / 2) 29 | blue = int((blue + 255) / 2) 30 | 31 | return Color.from_rgb(red, green, blue) 32 | 33 | 34 | def get_text_channel_by_name(guild: discord.Guild, name: str) -> Optional[discord.TextChannel]: 35 | for channel in guild.text_channels: 36 | if channel.name == name: 37 | return channel 38 | 39 | 40 | async def send_error(ctx: discord.Interaction, description: str, embed: discord.Embed = None, delete_after: int = None) -> None: 41 | try: 42 | if embed: 43 | await ctx.response.send_message( 44 | embed=embed, 45 | ephemeral=True, 46 | delete_after=delete_after 47 | ) 48 | else: 49 | await ctx.response.send_message( 50 | embed=discord.Embed( 51 | title="An error occurred", 52 | color=discord.Color.red(), 53 | description=description, 54 | ), 55 | ephemeral=True, 56 | delete_after=delete_after 57 | ) 58 | except discord.errors.InteractionResponded: 59 | if embed: 60 | followup = await ctx.followup.send( 61 | embed=embed, 62 | ephemeral=True 63 | ) 64 | else: 65 | followup = await ctx.followup.send( 66 | embed=discord.Embed( 67 | title="An error occurred", 68 | color=discord.Color.red(), 69 | description=description, 70 | ), 71 | ephemeral=True 72 | ) 73 | 74 | if delete_after: 75 | await asyncio.sleep(delete_after) 76 | await followup.delete() 77 | 78 | 79 | async def send_success(ctx: discord.Interaction, description: str = "Done!", embed: discord.Embed = None, delete_after: int = None, ephemeral: bool = True) -> None: 80 | if embed: 81 | await ctx.response.send_message( 82 | embed=embed, 83 | ephemeral=ephemeral, 84 | delete_after=delete_after 85 | ) 86 | else: 87 | await ctx.response.send_message( 88 | embed=discord.Embed( 89 | color=discord.Color.green(), 90 | description=description, 91 | ), 92 | ephemeral=ephemeral, 93 | delete_after=delete_after 94 | ) 95 | 96 | 97 | async def reply_success(message: discord.Message, description: str = "Done!", embed: discord.Embed = None, delete_after: int = None) -> None: 98 | if embed: 99 | await message.reply( 100 | embed=embed, 101 | delete_after=delete_after 102 | ) 103 | else: 104 | await message.reply( 105 | embed=discord.Embed( 106 | color=discord.Color.green(), 107 | description=description, 108 | ), 109 | delete_after=delete_after 110 | ) 111 | 112 | 113 | def format_number(number: int) -> str: 114 | return f"{number:,}" 115 | 116 | 117 | async def audit_logs_multi(guild: discord.Guild, actions: List[discord.AuditLogAction], limit: int, after: Union[discord.abc.Snowflake, datetime]) -> List[discord.AuditLogEntry]: 118 | logs = [] 119 | for action in actions: 120 | logs.extend([audit async for audit in guild.audit_logs(limit=limit, action=action, after=after)]) 121 | logs.sort(key=lambda x: x.created_at, reverse=True) 122 | return logs 123 | 124 | 125 | def get_warnpoints(user: User) -> int: 126 | return 9 if user.is_clem else user.warn_points 127 | 128 | 129 | pun_map = { 130 | "KICK": "Kicked", 131 | "BAN": "Banned", 132 | "CLEM": "Clemmed", 133 | "UNCLEM": "Unclemmed", 134 | "UNBAN": "Unbanned", 135 | "MUTE": "Duration", 136 | "REMOVEPOINTS": "Points removed" 137 | } 138 | 139 | 140 | def determine_emoji(type: str) -> str: 141 | emoji_dict = { 142 | "KICK": "👢", 143 | "BAN": "❌", 144 | "UNBAN": "✅", 145 | "MUTE": "🔇", 146 | "WARN": "⚠️", 147 | "UNMUTE": "🔈", 148 | "LIFTWARN": "⚠️", 149 | "REMOVEPOINTS": "⬇️", 150 | "CLEM": "👎", 151 | "UNCLEM": "👍" 152 | } 153 | return emoji_dict[type] 154 | 155 | async def get_device(value: str) -> dict: 156 | response = await get_ios_cfw() 157 | device_groups = response.get("group") 158 | 159 | transformed_groups = transform_groups(device_groups) 160 | devices = [group for group in transformed_groups if group.get( 161 | 'name').lower() == value.lower() or value.lower() in [x.lower() for x in group.get('devices')]] 162 | 163 | if not devices: 164 | raise app_commands.AppCommandError( 165 | "No device found with that name.") 166 | 167 | return devices[0] 168 | 169 | async def get_version_on_device(version: str, device: dict) -> dict: 170 | response = await get_ios_cfw() 171 | board = device.get("devices")[0] 172 | 173 | ios = response.get("ios") 174 | 175 | # ios = [i for _, i in ios.items()] 176 | for os_version in ["iOS", "tvOS", "watchOS"]: 177 | version = version.replace(os_version + " ", "") 178 | firmware = [v for v in ios if board in v.get( 179 | 'devices') and version == v.get('version') or version.lower() == v.get("uniqueBuild").lower()] 180 | if not firmware: 181 | raise app_commands.AppCommandError( 182 | "No firmware found with that version.") 183 | 184 | return firmware[0] 185 | 186 | 187 | class InstantQueueTS: 188 | def __init__(self): 189 | self.queue = asyncio.Queue() 190 | self.lock = asyncio.Lock() 191 | self.event = asyncio.Event() 192 | 193 | async def put(self, item: Any) -> None: 194 | async with self.lock: 195 | asyncio.get_event_loop().call_soon_threadsafe(self.queue.put_nowait, item) 196 | self.event._loop.call_soon_threadsafe(self.event.set) 197 | 198 | async def get(self) -> Any: 199 | await self.event.wait() 200 | self.event.clear() 201 | return self.queue.get_nowait() 202 | 203 | def task_done(self) -> None: 204 | self.queue.task_done() 205 | 206 | -------------------------------------------------------------------------------- /bridget/migrate/__main__.py: -------------------------------------------------------------------------------- 1 | # TODO: write migrate.py 2 | 3 | import asyncio 4 | import mongoengine 5 | import os 6 | import discord 7 | 8 | import girmodel 9 | 10 | from bridget import model 11 | from utils import services 12 | from typing import * 13 | 14 | girfilter: List[girmodel.FilterWord] = [] 15 | girraid = [] 16 | 17 | class MigrateClient(discord.Client): 18 | async def on_ready(self): 19 | print("Migrating filter!") 20 | 21 | rules = await self.get_guild(services.guild_id).fetch_automod_rules() 22 | for filter in girfilter: 23 | try: 24 | guild: model.Guild = self.get_guild(int(services.guild_id)) 25 | if guild == None: 26 | continue 27 | rule = self.get_guild(int(services.guild_id)).get_role(filter.bypass) 28 | if filter.notify: 29 | rule = [ x for x in rules if rule.name in x.name and rule.name.endswith('🚨') ] 30 | else: 31 | rule = [ x for x in rules if rule.name in x.name and not rule.name.endswith('🚨')] 32 | if len(rule) == 0: 33 | return 34 | rule = rule[0] 35 | 36 | # extract the trigger to modify it 37 | trig = rule.trigger 38 | 39 | 40 | phrase = filter.word 41 | trig.keyword_filter.append(phrase) 42 | 43 | # edit the rule trigger 44 | await rule.edit(trigger=trig, reason="Filtered word/regex/whitelist has been added (Migration)") 45 | except: 46 | continue 47 | 48 | print("Migrating AntiRaid!") 49 | 50 | raidrule = [] 51 | 52 | for rule in rules: 53 | if discord.AutoModRuleActionType.timeout in rule.actions: 54 | raidrule = rule 55 | 56 | for filter in girraid: 57 | trig = rule.trigger 58 | 59 | 60 | phrase = filter.word 61 | trig.keyword_filter.append(phrase) 62 | 63 | # edit the rule trigger 64 | await raidrule.edit(trigger=trig, reason="Filtered word/regex/whitelist has been added (Migration)") 65 | 66 | print("Disconnecting!") 67 | await self.close() 68 | 69 | 70 | async def migrate() -> None: 71 | global girfilter, girraid 72 | print("Hello!") 73 | print("We need you to answer a few questions to start the migration process ") 74 | setup_did = input("Did you run 'pdm run setup'? (y/n) ") 75 | if setup_did.lower() != 'y': 76 | print("Please run it, then run this again") 77 | return 78 | 79 | srv_connect = input( 80 | "Please enter your MongoDB connection string for the GIRRewrite database (eg. mongodb://localhost:27017) " 81 | ) 82 | 83 | print("Migrating database!") 84 | 85 | 86 | print("Connecting to the GIR database!") 87 | mongoengine.connect("botty", host=srv_connect) 88 | 89 | 90 | print("Caching tags and memes!") 91 | 92 | girtags = girmodel.Guild.objects(_id=os.getenv("GUILD_ID")).first().tags 93 | girmemes = girmodel.Guild.objects(_id=os.getenv("GUILD_ID")).first().memes 94 | 95 | print("Caching cases!") 96 | 97 | gircases = girmodel.Cases.objects(_id=os.getenv("GUILD_ID")).first().cases 98 | 99 | print("Caching giveaways!") 100 | 101 | girgiveaways = girmodel.Giveaway.objects(_id=os.getend("GUILD_ID")) 102 | 103 | print("Caching users!") 104 | 105 | girusers = girmodel.User.objects(_id=os.getend("GUILD_ID")) 106 | 107 | print("Caching filter!") 108 | 109 | girfilter = girmodel.Guild.objects(_id=os.getenv("GUILD_ID")).first().filter_words 110 | 111 | print("Caching AntiRaid filter!") 112 | girraid = girmodel.Guild.objects(_id=os.getenv("GUILD_ID")).first().raid_phrases 113 | 114 | mongoengine.disconnect() 115 | 116 | mongoengine.connect("bridget", host=os.getenv("DB_HOST"), port=int(os.getenv("DB_PORT"))) 117 | 118 | for tag in girtags: 119 | ntag = model.Tag() 120 | ntag.name = tag.name 121 | ntag.content = tag.content 122 | ntag.added_by_tag = tag.added_by_tag 123 | ntag.added_by_id = tag.added_by_id 124 | ntag.added_date = tag.added_date 125 | ntag.use_count = tag.use_count 126 | ntag.image = tag.image 127 | ntag.button_links = tag.button_links 128 | ntag.save() 129 | 130 | for meme in girmemes: 131 | ntag = model.Tag() 132 | ntag.name = meme.name 133 | ntag.content = meme.content 134 | ntag.added_by_tag = meme.added_by_tag 135 | ntag.added_by_id = meme.added_by_id 136 | ntag.added_date = meme.added_date 137 | ntag.use_count = meme.use_count 138 | ntag.image = meme.image 139 | ntag.button_links = meme.button_links 140 | ntag.save() 141 | 142 | for case in gircases: 143 | nfract = model.Infraction() 144 | nfract._type = case._type 145 | nfract._id = case._id 146 | nfract.until = case.until 147 | nfract.mod_id = case.mod_id 148 | nfract.mod_tag = case.mod_tag 149 | nfract.reason = case.reason 150 | nfract.punishment = max(case.punishment // 50, 9) if case.punishment else case.punishment 151 | nfract.lifted = case.lifted 152 | nfract.lifted_by_tag = case.lifted_by_tag 153 | nfract.lifted_by_id = case.lifted_by_id 154 | nfract.lifted_reason = case.lifted_reason 155 | nfract.lifted_date = case.lifted_date 156 | nfract.save() 157 | 158 | for giveaway in girgiveaways: 159 | ngive = model.Giveaway() 160 | ngive._id = giveaway._id 161 | ngive.is_ended = giveaway.is_ended 162 | ngive.end_time = giveaway.end_time 163 | ngive.channel = giveaway.channel 164 | ngive.name = giveaway.name 165 | ngive.entries = giveaway.entries 166 | ngive.previous_winners = giveaway.previous_winners 167 | ngive.sponsor = giveaway.sponsor 168 | ngive.winners = giveaway.winners 169 | ngive.save() 170 | 171 | for user in girusers: 172 | nuser = model.User() 173 | wptoapply = user // 50 174 | if wptoapply >= 10: 175 | wptoapply = 9 176 | nuser._id = user._id 177 | nuser.is_clem = user.is_clem 178 | nuser.is_xp_frozen = user.is_xp_frozen 179 | nuser.is_muted = user.is_muted 180 | nuser.is_music_banned = user.is_music_banned 181 | nuser.was_warn_kicked = user.was_warn_kicked 182 | nuser.birthday_excluded = user.birthday_excluded 183 | nuser.raid_verified = user.raid_verified 184 | nuser.xp = user.xp 185 | nuser.trivia_points = user.trivia_points 186 | nuser.level = user.level 187 | nuser.warn_points = wptoapply 188 | nuser.offline_report_ping = user.offline_report_ping 189 | nuser.timezone = user.timezone 190 | nuser.birthday = user.birthday 191 | nuser.sticky_roles = user.sticky_roles 192 | nuser.command_bans = user.command_bans 193 | nuser.save() 194 | 195 | for filter in girfilter: 196 | nfilter = model.FilterWord() 197 | nfilter.notify = filter.notify 198 | nfilter.bypass = filter.bypass 199 | nfilter.word = filter.word 200 | nfilter.false_positive = filter.false_positive 201 | nfilter.piracy = filter.piracy 202 | 203 | if input("Do you want to migrate additional steps? (eg. migrate filter) This requires access to the Discord bot (n/y)").lower() != 'y': 204 | exit(0) 205 | 206 | client = MigrateClient() 207 | 208 | client.run() 209 | 210 | print("Finished!") 211 | 212 | # model.Guild.objects(_id=os.getenv("GUILD_ID")).first().tags. 213 | 214 | asyncio.run(migrate()) -------------------------------------------------------------------------------- /bridget/cogs/mod.py: -------------------------------------------------------------------------------- 1 | import discord 2 | 3 | from datetime import datetime 4 | from discord import app_commands 5 | from discord.utils import escape_markdown, escape_mentions 6 | 7 | from utils import Cog, send_error, send_success, get_warnpoints 8 | from utils.autocomplete import warn_autocomplete 9 | from utils.mod import warn, prepare_liftwarn_log, notify_user, submit_public_log 10 | from utils.services import guild_service, user_service 11 | from utils.enums import PermissionLevel 12 | from model.infraction import Infraction 13 | 14 | 15 | class Mod(Cog): 16 | @PermissionLevel.MOD 17 | @app_commands.command() 18 | async def warn(self, ctx: discord.Interaction, user: discord.User, points: app_commands.Range[int, 1, 10], reason: str) -> None: 19 | """Warn a user 20 | 21 | Args: 22 | ctx (discord.ctx): Context 23 | user (discord.Member): User to warn 24 | points (app_commands.Range[int, 1, 10]): Points to give 25 | reason (str): Reason to warn 26 | """ 27 | 28 | if user.top_role >= ctx.user.top_role: 29 | await send_error(ctx, "You can't warn this member.") 30 | return 31 | 32 | await ctx.response.defer() 33 | await warn(ctx, target_member=user, mod=ctx.user, points=points, reason=reason) 34 | 35 | @PermissionLevel.MOD 36 | @app_commands.autocomplete(infraction_id=warn_autocomplete) 37 | @app_commands.command() 38 | async def liftwarn(self, ctx: discord.Interaction, user: discord.User, infraction_id: str, reason: str) -> None: 39 | """Lift a user's warn 40 | 41 | Args: 42 | ctx (discord.Interaction): Context 43 | user (discord.Member): User to lift warn 44 | infraction_id (str): Id of the warn's infraction 45 | reason (str): Reason to lift warn 46 | """ 47 | 48 | infractions = user_service.get_infractions(user.id) 49 | infraction = infractions.infractions.filter(_id=infraction_id).first() 50 | 51 | reason = escape_markdown(reason) 52 | reason = escape_mentions(reason) 53 | 54 | # sanity checks 55 | if infraction is None: 56 | await send_error(ctx, f"{user} has no infraction with ID {infraction_id}") 57 | return 58 | elif infraction._type != "WARN": 59 | await send_error(ctx, f"{user}'s infraction with ID {infraction_id} is not a warn infraction.") 60 | return 61 | elif infraction.lifted: 62 | await send_error(ctx, f"Infraction with ID {infraction_id} already lifted.") 63 | return 64 | 65 | u = user_service.get_user(id=user.id) 66 | if get_warnpoints(u) - int(infraction.punishment) < 0: 67 | await send_error(ctx, f"Can't lift Infraction #{infraction_id} because it would make {user.mention}'s points negative.") 68 | return 69 | 70 | # passed sanity checks, so update the infraction in DB 71 | infraction.lifted = True 72 | infraction.lifted_reason = reason 73 | infraction.lifted_by_tag = str(ctx.user) 74 | infraction.lifted_by_id = ctx.user.id 75 | infraction.lifted_date = datetime.now() 76 | infractions.save() 77 | 78 | # remove the warn points from the user in DB 79 | user_service.inc_points(user.id, -1 * int(infraction.punishment)) 80 | dmed = True 81 | # prepare log embed, send to #public-logs, user, channel where invoked 82 | log = prepare_liftwarn_log(ctx.user, user, infraction) 83 | dmed = await notify_user(user, f"Your warn has been lifted in {ctx.guild}.", log) 84 | 85 | await send_success(ctx, embed=log, delete_after=10, ephemeral=False) 86 | await submit_public_log(ctx, guild_service.get_guild(), user, log, dmed) 87 | 88 | @PermissionLevel.ADMIN 89 | @app_commands.command() 90 | async def transferprofile(self, ctx: discord.Interaction, old_member: discord.Member, new_member: discord.Member) -> None: 91 | """Transfers all data in the database between users 92 | 93 | Args: 94 | ctx (discord.ctx): Context 95 | old_member (discord.Member): The user to transfer data from 96 | new_member (discord.Member): The user to transfer data to 97 | """ 98 | 99 | u, infraction_count = user_service.transfer_profile(old_member.id, new_member.id) 100 | 101 | embed = discord.Embed(title="Transferred profile") 102 | embed.description = f"Transferred {old_member.mention}'s profile to {new_member.mention}" 103 | embed.color = discord.Color.blurple() 104 | embed.add_field(name="Level", value=u.level) 105 | embed.add_field(name="XP", value=u.xp) 106 | embed.add_field(name="Warn points", value=u.warn_points) 107 | embed.add_field(name="Infractions", value=infraction_count) 108 | 109 | await send_success(ctx, embed=embed, delete_after=10) 110 | try: 111 | await new_member.send(f"{ctx.user} has transferred your profile from {old_member}", embed=embed) 112 | except: 113 | pass 114 | 115 | @PermissionLevel.GUILD_OWNER 116 | @app_commands.command() 117 | async def clem(self, ctx: discord.Interaction, member: discord.Member) -> None: 118 | """Sets user's XP and Level to 0, freezes XP, sets warn points to 9 119 | 120 | Args: 121 | ctx (discord.ctx): Context 122 | member (discord.Member): The user to reset 123 | """ 124 | 125 | if member.id == ctx.user.id: 126 | await send_error(ctx, "You can't call that on yourself.") 127 | return 128 | 129 | if member.id == self.bot.user.id: 130 | await send_error(ctx, "You can't call that on me :(") 131 | return 132 | 133 | results = user_service.get_user(member.id) 134 | 135 | if results.is_clem: 136 | await send_error(ctx, "That user is already on clem.") 137 | return 138 | 139 | results.is_clem = True 140 | results.is_xp_frozen = True 141 | results.save() 142 | 143 | infraction = Infraction( 144 | _id=guild_service.get_guild().infraction_id, 145 | _type="CLEM", 146 | mod_id=ctx.user.id, 147 | mod_tag=str(ctx.user), 148 | punishment=str(-1), 149 | reason="No reason." 150 | ) 151 | 152 | # incrememnt DB's max infraction ID for next infraction 153 | guild_service.inc_infractionid() 154 | # add infraction to db 155 | user_service.add_infraction(member.id, infraction) 156 | 157 | await send_success(ctx, f"{member.mention} was put on clem.") 158 | 159 | @PermissionLevel.GUILD_OWNER 160 | @app_commands.command() 161 | async def unclem(self, ctx: discord.Interaction, member: discord.Member) -> None: 162 | """Removes the clem status, unfreezes XP, sets warn points back to before clem 163 | 164 | Args: 165 | ctx (discord.ctx): Context 166 | member (discord.Member): The user to unclem 167 | """ 168 | 169 | if member.id == ctx.user.id: 170 | await send_error(ctx, "You can't call that on yourself.") 171 | return 172 | 173 | if member.id == self.bot.user.id: 174 | await send_error(ctx, "You can't call that on me :(") 175 | return 176 | 177 | results = user_service.get_user(member.id) 178 | 179 | 180 | if not results.is_clem: 181 | await send_error(ctx, "That user is not on clem.") 182 | return 183 | 184 | results.is_clem = False 185 | results.is_xp_frozen = False 186 | results.save() 187 | 188 | infraction = Infraction( 189 | _id=guild_service.get_guild().infraction_id, 190 | _type="UNCLEM", 191 | mod_id=ctx.user.id, 192 | mod_tag=str(ctx.user), 193 | punishment=str(-1), 194 | reason="No reason." 195 | ) 196 | 197 | # incrememnt DB's max infraction ID for next infraction 198 | guild_service.inc_infractionid() 199 | # add infraction to db 200 | user_service.add_infraction(member.id, infraction) 201 | 202 | await send_success(ctx, f"{member.mention}'s clem has been lifted.") 203 | 204 | -------------------------------------------------------------------------------- /bridget/cogs/xp.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import math 3 | 4 | from discord import app_commands 5 | from random import randint 6 | from typing import List, Union 7 | 8 | from utils import Cog 9 | from utils.services import guild_service 10 | from utils.services import user_service 11 | from utils.config import cfg 12 | from utils.enums import PermissionLevel 13 | from utils.menus import Menu 14 | from model import Guild 15 | 16 | 17 | def format_xptop_page(ctx: discord.Interaction, entries, current_page: int, all_pages) -> discord.Embed: 18 | """Formats the page for the xptop embed. 19 | 20 | Parameters 21 | ---------- 22 | entry : dict 23 | "The dictionary for the entry" 24 | all_pages : list 25 | "All entries that we will eventually iterate through" 26 | current_page : number 27 | "The number of the page that we are currently on" 28 | 29 | Returns 30 | ------- 31 | discord.Embed 32 | "The embed that we will send" 33 | 34 | """ 35 | embed = discord.Embed(title=f'Leaderboard', color=discord.Color.blurple()) 36 | for i, user in entries: 37 | member = ctx.guild.get_member(user._id) 38 | trophy = '' 39 | if current_page == 1: 40 | if i == entries[0][0]: 41 | trophy = ':first_place:' 42 | embed.set_thumbnail(url=member.avatar) 43 | if i == entries[1][0]: 44 | trophy = ':second_place:' 45 | if i == entries[2][0]: 46 | trophy = ':third_place:' 47 | 48 | embed.add_field(name=f"#{i+1} - Level {user.level}", 49 | value=f"{trophy} {member.mention}", inline=False) 50 | 51 | embed.set_footer(text=f"Page {current_page} of {len(all_pages)}") 52 | return embed 53 | 54 | 55 | class Xp(Cog): 56 | def __init__(self, bot): 57 | self.bot = bot 58 | 59 | @app_commands.command() 60 | async def xp(self, ctx: discord.Interaction, member: discord.Member = None) -> None: 61 | """Show your or another user's XP 62 | 63 | Args: 64 | ctx (discord.ctx): Context 65 | member (discord.Member, optional): Member to get XP of 66 | """ 67 | 68 | if member is None: 69 | member = ctx.user 70 | 71 | results = user_service.get_user(member.id) 72 | 73 | whisper = False 74 | bot_chan = guild_service.get_guild().channel_botspam 75 | if not PermissionLevel.MOD == ctx.user and ctx.channel_id != bot_chan: 76 | whisper = True 77 | 78 | embed = discord.Embed(title="Level Statistics") 79 | embed.color = member.top_role.color 80 | embed.set_author(name=member, icon_url=member.display_avatar) 81 | embed.add_field( 82 | name="Level", value=results.level if not results.is_clem else "0", inline=True) 83 | embed.add_field( 84 | name="XP", value=f'{results.xp}/{self.xp_for_next_level(results.level)}' if not results.is_clem else "0/0", inline=True) 85 | rank, overall = user_service.leaderboard_rank(results.xp) 86 | embed.add_field( 87 | name="Rank", value=f"{rank}/{overall}" if not results.is_clem else f"{overall}/{overall}", inline=True) 88 | 89 | await ctx.response.send_message(embed=embed, ephemeral=whisper) 90 | 91 | @app_commands.command() 92 | async def xptop(self, ctx: discord.Interaction) -> None: 93 | """Show the XP leaderboard. 94 | 95 | Args: 96 | ctx (discord.ctx): Context 97 | """ 98 | 99 | whisper = False 100 | bot_chan = guild_service.get_guild().channel_botspam 101 | if not PermissionLevel.MOD == ctx.user and ctx.channel_id != bot_chan: 102 | whisper = True 103 | 104 | results = enumerate(user_service.leaderboard()) 105 | results = [(i, m) for (i, m) in results if ctx.guild.get_member( 106 | m._id) is not None][0:100] 107 | 108 | menu = Menu(ctx, results, per_page=10, 109 | page_formatter=format_xptop_page, whisper=whisper) 110 | await menu.start() 111 | 112 | @Cog.listener() 113 | async def on_member_join(self, member: discord.Member) -> None: 114 | if member.bot: 115 | return 116 | if member.guild.id != cfg.guild_id: 117 | return 118 | 119 | user = user_service.get_user(id=member.id) 120 | 121 | if user.is_xp_frozen or user.is_clem: 122 | return 123 | 124 | level = user.level 125 | db_guild = guild_service.get_guild() 126 | 127 | roles_to_add = self.assess_new_roles(level, db_guild) 128 | await self.add_new_roles(member, roles_to_add) 129 | 130 | @Cog.listener() 131 | async def on_message(self, message: discord.Message) -> None: 132 | if not message.guild: 133 | return 134 | if message.guild.id != cfg.guild_id: 135 | return 136 | if message.author.bot: 137 | return 138 | 139 | db_guild = guild_service.get_guild() 140 | if message.channel.id == db_guild.channel_botspam: 141 | return 142 | 143 | user = user_service.get_user(id=message.author.id) 144 | if user.is_xp_frozen or user.is_clem: 145 | return 146 | 147 | xp_to_add = randint(0, 11) 148 | new_xp, level_before = user_service.inc_xp( 149 | message.author.id, xp_to_add) 150 | new_level = self.get_level(new_xp) 151 | 152 | if new_level > level_before: 153 | user_service.inc_level(message.author.id) 154 | 155 | roles_to_add = self.assess_new_roles(new_level, db_guild) 156 | await self.add_new_roles(message, roles_to_add) 157 | 158 | def assess_new_roles(self, new_level: int, db: Guild) -> List[int]: 159 | roles_to_add = [] 160 | if 15 <= new_level: 161 | roles_to_add.append(db.role_memberplus) 162 | if 30 <= new_level: 163 | roles_to_add.append(db.role_memberpro) 164 | if 50 <= new_level: 165 | roles_to_add.append(db.role_memberedition) 166 | if 75 <= new_level: 167 | roles_to_add.append(db.role_memberone) 168 | if 100 <= new_level: 169 | roles_to_add.append(db.role_memberultra) 170 | 171 | return roles_to_add 172 | 173 | async def add_new_roles(self, obj: Union[discord.Message, discord.User], roles_to_add: List[int]) -> None: 174 | if roles_to_add is None: 175 | return 176 | 177 | member = obj 178 | if isinstance(obj, discord.Message): 179 | member = obj.author 180 | 181 | roles_to_add = [member.guild.get_role(role) for role in roles_to_add if member.guild.get_role( 182 | role) is not None and member.guild.get_role(role) not in member.roles] 183 | await member.add_roles(*roles_to_add, reason="XP roles") 184 | 185 | def get_level(self, current_xp: int) -> int: 186 | level = 0 187 | xp = 0 188 | while xp <= current_xp: 189 | xp = xp + 45 * level * (math.floor(level / 10) + 1) 190 | level += 1 191 | return level 192 | 193 | def xp_for_next_level(self, _next: int) -> int: 194 | level = 0 195 | xp = 0 196 | 197 | for _ in range(0, _next): 198 | xp = xp + 45 * level * (math.floor(level / 10) + 1) 199 | level += 1 200 | 201 | return xp 202 | 203 | 204 | class StickyRoles(Cog): 205 | @Cog.listener() 206 | async def on_member_remove(self, member: discord.Member): 207 | if member.guild.id != cfg.guild_id: 208 | return 209 | 210 | roles = [role.id for role in member.roles if role < 211 | member.guild.me.top_role and role != member.guild.default_role] 212 | user_service.set_sticky_roles(member.id, roles) 213 | 214 | @Cog.listener() 215 | async def on_member_join(self, member: discord.Member): 216 | if member.guild.id != cfg.guild_id: 217 | return 218 | 219 | possible_roles = user_service.get_user(member.id).sticky_roles 220 | roles = [member.guild.get_role(role) for role in possible_roles if member.guild.get_role( 221 | role) is not None and member.guild.get_role(role) < member.guild.me.top_role] 222 | await member.add_roles(*roles, reason="Sticky roles") 223 | 224 | -------------------------------------------------------------------------------- /bridget/utils/report_action.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | import discord 4 | from discord import ui 5 | 6 | from bridget.utils.mod import warn 7 | from bridget.utils.utils import send_error 8 | 9 | class GenericDescriptionModal(discord.ui.Modal): 10 | def __init__(self, ctx: discord.Interaction, author: discord.Member, title: str, label: str = "Description", placeholder: str = "Please enter a description", prefill: str = ""): 11 | self.ctx = ctx 12 | self.author = author 13 | self.value = None 14 | 15 | super().__init__(title=title) 16 | 17 | self.add_item( 18 | discord.ui.TextInput( 19 | label=label, 20 | placeholder=placeholder, 21 | style=discord.TextStyle.long, 22 | default=prefill 23 | ) 24 | ) 25 | 26 | async def on_submit(self, interaction: discord.Interaction): 27 | if interaction.user != self.author: 28 | return 29 | 30 | self.ctx = interaction 31 | self.value = self.children[0].value 32 | 33 | self.stop() 34 | 35 | class ModAction(Enum): 36 | WARN = 1 37 | BAN = 2 38 | 39 | 40 | class ReportActionReason(ui.View): 41 | def __init__(self, target_member: discord.Member, mod: discord.Member, mod_action: ModAction): 42 | super().__init__(timeout=20) 43 | self.target_member = target_member 44 | self.mod = mod 45 | self.mod_action = mod_action 46 | self.success = False 47 | 48 | async def interaction_check(self, interaction: discord.Interaction): 49 | return self.mod == interaction.user 50 | 51 | @ui.button(label="piracy", style=discord.ButtonStyle.primary) 52 | async def piracy(self, interaction: discord.Interaction, button: ui.Button): 53 | await self.modaction_callback(interaction, "piracy") 54 | 55 | @ui.button(label="slurs", style=discord.ButtonStyle.primary) 56 | async def slurs(self, interaction: discord.Interaction, button: ui.Button): 57 | await self.modaction_callback(interaction, "slurs") 58 | 59 | @ui.button(label="filter bypass", style=discord.ButtonStyle.primary) 60 | async def filter_bypass(self, interaction: discord.Interaction, button: ui.Button): 61 | await self.modaction_callback(interaction, "filter bypass") 62 | 63 | @ui.button(label="rule 1", style=discord.ButtonStyle.primary) 64 | async def rule_one(self, interaction: discord.Interaction, button: ui.Button): 65 | await self.modaction_callback(interaction, "rule 1") 66 | 67 | @ui.button(label="icloud bypass", style=discord.ButtonStyle.primary) 68 | async def rule_five(self, interaction: discord.Interaction, button: ui.Button): 69 | await self.modaction_callback(interaction, "icloud bypass") 70 | 71 | @ui.button(label="ads", style=discord.ButtonStyle.primary) 72 | async def ads(self, interaction: discord.Interaction, button: ui.Button): 73 | await self.modaction_callback(interaction, "ads") 74 | 75 | @ui.button(label="scam", style=discord.ButtonStyle.primary) 76 | async def scam(self, interaction: discord.Interaction, button: ui.Button): 77 | await self.modaction_callback(interaction, "scam") 78 | 79 | @ui.button(label="troll", style=discord.ButtonStyle.primary) 80 | async def troll(self, interaction: discord.Interaction, button: ui.Button): 81 | await self.modaction_callback(interaction, "troll") 82 | 83 | @ui.button(emoji="❌", label="Cancel", style=discord.ButtonStyle.primary) 84 | async def cancel(self, interaction: discord.Interaction, button: ui.Button): 85 | self.stop() 86 | 87 | async def modaction_callback(self, interaction: discord.Interaction, reason: str): 88 | if self.mod_action == ModAction.WARN: 89 | points = await self.prompt_for_points(reason, interaction) 90 | if points is not None: 91 | await warn(interaction, self.target_member, self.mod, points, reason) 92 | else: 93 | # await ban(interaction, self.target_member, self.mod, reason) 94 | await interaction.guild.ban(self.target_member, reason=reason) 95 | 96 | self.success = True 97 | await interaction.message.delete() 98 | self.stop() 99 | 100 | async def prompt_for_points(self, reason: str, interaction: discord.Interaction): 101 | view = PointsView(self.mod) 102 | await interaction.response.edit_message(embed=discord.Embed(description=f"Warning for `{reason}`. How many points, {self.mod.mention}?", color=discord.Color.blurple()), view=view) 103 | await view.wait() 104 | 105 | return view.value 106 | 107 | 108 | class PointsView(ui.View): 109 | def __init__(self, mod: discord.Member): 110 | super().__init__(timeout=15) 111 | self.mod = mod 112 | self.value = None 113 | 114 | async def interaction_check(self, interaction: discord.Interaction): 115 | if self.mod != interaction.user: 116 | return False 117 | return True 118 | 119 | # async def on_timeout(self) -> None: 120 | # try: 121 | # await self.points_msg.delete() 122 | # except: 123 | # pass 124 | 125 | @ui.button(label="50 points", style=discord.ButtonStyle.primary) 126 | async def fiddy(self, interaction: discord.Interaction, button: ui.Button): 127 | self.value = 50 128 | self.stop() 129 | 130 | @ui.button(label="100 points", style=discord.ButtonStyle.primary) 131 | async def hunnit(self, interaction: discord.Interaction, button: ui.Button): 132 | self.value = 100 133 | self.stop() 134 | 135 | @ui.button(label="150 points", style=discord.ButtonStyle.primary) 136 | async def hunnitfiddy(self, interaction: discord.Interaction, button: ui.Button): 137 | self.value = 150 138 | self.stop() 139 | 140 | @ui.button(label="200 points", style=discord.ButtonStyle.primary) 141 | async def twohunnit(self, interaction: discord.Interaction, button: ui.Button): 142 | self.value = 200 143 | self.stop() 144 | 145 | 146 | class WarnView(ui.View): 147 | def __init__(self, ctx, member: discord.Member): 148 | super().__init__(timeout=30) 149 | self.target_member = member 150 | self.ctx = ctx 151 | 152 | async def on_timeout(self) -> None: 153 | await send_error(self.ctx, "Timed out.") 154 | 155 | async def interaction_check(self, interaction: discord.Interaction): 156 | return self.ctx.author == interaction.user 157 | 158 | @ui.button(label="piracy", style=discord.ButtonStyle.primary) 159 | async def piracy(self, interaction: discord.Interaction, button: ui.Button): 160 | await self.ctx.send_success("Okay...") 161 | self.ctx.interaction = interaction 162 | await warn(self.ctx, self.target_member, interaction.user, 50, "piracy") 163 | self.stop() 164 | 165 | @ui.button(label="slurs", style=discord.ButtonStyle.primary) 166 | async def slurs(self, interaction: discord.Interaction, button: ui.Button): 167 | await self.ctx.send_success("Okay...") 168 | self.ctx.interaction = interaction 169 | await warn(self.ctx, self.target_member, interaction.user, 50, "slurs") 170 | self.stop() 171 | 172 | @ui.button(label="filter bypass", style=discord.ButtonStyle.primary) 173 | async def filter_bypass(self, interaction: discord.Interaction, button: ui.Button): 174 | await self.ctx.send_success("Okay...") 175 | self.ctx.interaction = interaction 176 | await warn(self.ctx, self.target_member, interaction.user, 50, "filter bypass") 177 | self.stop() 178 | 179 | @ui.button(label="icloud bypass", style=discord.ButtonStyle.primary) 180 | async def rule5(self, interaction: discord.Interaction, button: ui.Button): 181 | await self.ctx.send_success("Okay...") 182 | self.ctx.interaction = interaction 183 | await warn(self.ctx, self.target_member, interaction.user, 50, "icloud bypass") 184 | self.stop() 185 | 186 | @ui.button(label="Other...", style=discord.ButtonStyle.primary) 187 | async def other(self, interaction: discord.Interaction, button: ui.Button): 188 | self.ctx.interaction = interaction 189 | reason = await self.prompt_reason(interaction) 190 | if reason and reason is not None: 191 | await warn(self.ctx, self.target_member, interaction.user, 50, reason) 192 | self.stop() 193 | 194 | @ui.button(emoji="❌", label="cancel", style=discord.ButtonStyle.primary) 195 | async def cancel(self, interaction: discord.Interaction, button: ui.Button): 196 | await self.ctx.send_warning("Cancelled") 197 | self.stop() 198 | 199 | async def prompt_reason(self, interaction: discord.Interaction): 200 | view = GenericDescriptionModal(self.ctx, interaction.user, title=f"Warn reason for {self.target_member}", label="Why should we warn them?") 201 | await interaction.response.send_modal(view) 202 | await view.wait() 203 | return view.value 204 | 205 | -------------------------------------------------------------------------------- /bridget/utils/services/user_service.py: -------------------------------------------------------------------------------- 1 | import mongoengine 2 | import discord 3 | from discord.ext import commands 4 | import os 5 | from typing import Any, Dict, Tuple, Counter 6 | from model import User, Infraction, Infractions 7 | from model.user import User 8 | 9 | 10 | def get_user(id: int) -> User: 11 | """Look up the User document of a user, whose ID is given by `id`. 12 | If the user doesn't have a User document in the database, first create that. 13 | 14 | Parameters 15 | ---------- 16 | id : int 17 | The ID of the user we want to look up 18 | 19 | Returns 20 | ------- 21 | User 22 | The User document we found from the database. 23 | """ 24 | 25 | user = User.objects(_id=id).first() 26 | # first we ensure this user has a User document in the database before 27 | # continuing 28 | if not user: 29 | user = User() 30 | user._id = id 31 | user.save() 32 | return user 33 | 34 | 35 | def leaderboard() -> list: 36 | return User.objects[0:130].only('_id', 'xp').order_by( 37 | '-xp', '-_id').select_related() 38 | 39 | 40 | def leaderboard_rank(xp: int) -> Tuple[int, int]: 41 | users = User.objects().only('_id', 'xp') 42 | overall = users().count() 43 | rank = users(xp__gte=xp).count() 44 | return (rank, overall) 45 | 46 | 47 | def inc_points(_id: int, points: int) -> None: 48 | """Increments the warnpoints by `points` of a user whose ID is given by `_id`. 49 | If the user doesn't have a User document in the database, first create that. 50 | 51 | Parameters 52 | ---------- 53 | _id : int 54 | The user's ID to whom we want to add/remove points 55 | points : int 56 | The amount of points to increment the field by, can be negative to remove points 57 | """ 58 | 59 | # first we ensure this user has a User document in the database before 60 | # continuing 61 | get_user(_id) 62 | User.objects(_id=_id).update_one(inc__warn_points=points) 63 | 64 | 65 | def inc_xp(id: int, xp) -> Tuple[int, int]: 66 | """Increments user xp. 67 | """ 68 | 69 | get_user(id) 70 | User.objects(_id=id).update_one(inc__xp=xp) 71 | u = User.objects(_id=id).first() 72 | return (u.xp, u.level) 73 | 74 | 75 | def inc_level(id: int) -> None: 76 | """Increments user level. 77 | """ 78 | 79 | get_user(id) 80 | User.objects(_id=id).update_one(inc__level=1) 81 | 82 | 83 | def get_infractions(id: int) -> Infractions: 84 | """Return the Document representing the infractions of a user, whose ID is given by `id` 85 | If the user doesn't have a Infractions document in the database, first create that. 86 | 87 | Parameters 88 | ---------- 89 | id : int 90 | The user whose infractions we want to look up. 91 | 92 | Returns 93 | ------- 94 | Infractions 95 | [description] 96 | """ 97 | 98 | infractions = Infractions.objects(_id=id).first() 99 | # first we ensure this user has a Infractions document in the database before 100 | # continuing 101 | if infractions is None: 102 | infractions = Infractions() 103 | infractions._id = id 104 | infractions.save() 105 | return infractions 106 | 107 | 108 | def add_infraction(_id: int, infraction: Infraction) -> None: 109 | """Infractions holds all the infractions for a particular user with id `_id` as an 110 | EmbeddedDocumentListField. This function appends a given infraction object to 111 | this list. If this user doesn't have any previous infractions, we first add 112 | a new Infractions document to the database. 113 | 114 | Parameters 115 | ---------- 116 | _id : int 117 | ID of the user who we want to add the infraction to. 118 | infraction : Infraction 119 | The infraction we want to add to the user. 120 | """ 121 | 122 | # ensure this user has a infractions document before we try to append the new 123 | # infraction 124 | get_infractions(_id) 125 | Infractions.objects(_id=_id).update_one(push__infractions=(infraction).to_mongo()) 126 | 127 | 128 | def set_warn_kicked(_id: int) -> None: 129 | """Set the `was_warn_kicked` field in the User object of the user, whose ID is given by `_id`, 130 | to True. (this happens when a user reaches 400+ points for the first time and is kicked). 131 | If the user doesn't have a User document in the database, first create that. 132 | 133 | Parameters 134 | ---------- 135 | _id : int 136 | The user's ID who we want to set `was_warn_kicked` for. 137 | """ 138 | 139 | # first we ensure this user has a User document in the database before 140 | # continuing 141 | get_user(_id) 142 | User.objects(_id=_id).update_one(set__was_warn_kicked=True) 143 | 144 | 145 | def rundown(id: int) -> list: 146 | """Return the 3 most recent infractions of a user, whose ID is given by `id` 147 | If the user doesn't have a Infractions document in the database, first create that. 148 | 149 | Parameters 150 | ---------- 151 | id : int 152 | The user whose infractions we want to look up. 153 | 154 | Returns 155 | ------- 156 | Infractions 157 | [description] 158 | """ 159 | 160 | infractions = Infractions.objects(_id=id).first() 161 | # first we ensure this user has a Infractions document in the database before 162 | # continuing 163 | if infractions is None: 164 | infractions = Infractions() 165 | infractions._id = id 166 | infractions.save() 167 | return [] 168 | 169 | infractions = infractions.infractions 170 | infractions = filter(lambda x: x._type != "UNMUTE", infractions) 171 | infractions = sorted(infractions, key=lambda i: i['date']) 172 | infractions.reverse() 173 | return infractions[0:3] 174 | 175 | 176 | def retrieve_birthdays(date) -> Any: 177 | return User.objects(birthday=date) 178 | 179 | 180 | def transfer_profile(oldmember: int, newmember) -> Tuple[User, int]: 181 | u = get_user(oldmember) 182 | u._id = newmember 183 | u.save() 184 | 185 | u2 = get_user(oldmember) 186 | u2.xp = 0 187 | u2.level = 0 188 | u2.save() 189 | 190 | infractions = get_infractions(oldmember) 191 | infractions._id = newmember 192 | infractions.save() 193 | 194 | infractions2 = get_infractions(oldmember) 195 | infractions2.infractions = [] 196 | infractions2.save() 197 | 198 | return u, len(infractions.infractions) 199 | 200 | 201 | def fetch_raids() -> Dict[str, Any]: 202 | values = {} 203 | values["Join spam"] = Infractions.objects( 204 | infractions__reason__contains="Join spam detected").count() 205 | values["Join spam over time"] = Infractions.objects( 206 | infractions__reason__contains="Join spam over time detected").count() 207 | values["Raid phrase"] = Infractions.objects( 208 | infractions__reason__contains="Raid phrase detected").count() 209 | values["Ping spam"] = Infractions.objects( 210 | infractions__reason__contains="Ping spam").count() 211 | values["Message spam"] = Infractions.objects( 212 | infractions__reason__contains="Message spam").count() 213 | 214 | return values 215 | 216 | 217 | def fetch_infractions_by_mod(_id) -> dict: 218 | values = {} 219 | infractions = Infractions.objects(infractions__mod_id=str(_id)) 220 | values["total"] = 0 221 | infractions = list(infractions.all()) 222 | final_infractions = [] 223 | for target in infractions: 224 | for infraction in target.infractions: 225 | if str(infraction.mod_id) == str(_id): 226 | final_infractions.append(infraction) 227 | values["total"] += 1 228 | 229 | def get_infraction_reason(reason: str) -> str: 230 | string = reason.lower() 231 | return ''.join(e for e in string if e.isalnum() or e == " ").strip() 232 | 233 | infraction_reasons = [ 234 | get_infraction_reason( 235 | infraction.reason) for infraction in final_infractions if get_infraction_reason( 236 | infraction.reason) != "temporary mute expired"] 237 | values["counts"] = sorted( 238 | Counter(infraction_reasons).items(), key=lambda item: item[1]) 239 | values["counts"].reverse() 240 | return values 241 | 242 | 243 | def fetch_infractions_by_keyword(keyword: str) -> dict: 244 | values = {} 245 | infractions = Infractions.objects(infractions__reason__contains=keyword) 246 | infractions = list(infractions.all()) 247 | values["total"] = 0 248 | final_infractions = [] 249 | 250 | for target in infractions: 251 | for infraction in target.infractions: 252 | if keyword.lower() in infraction.reason: 253 | values["total"] += 1 254 | final_infractions.append(infraction) 255 | 256 | infraction_mods = [infraction.mod_tag for infraction in final_infractions] 257 | values["counts"] = sorted( 258 | Counter(infraction_mods).items(), key=lambda item: item[1]) 259 | values["counts"].reverse() 260 | return values 261 | 262 | 263 | def set_sticky_roles(_id: int, roles) -> None: 264 | get_user(_id) 265 | User.objects(_id=_id).update_one(set__sticky_roles=roles) 266 | 267 | 268 | def get_appealing_users() -> list[User]: 269 | return User.objects(is_appealing=True) 270 | 271 | -------------------------------------------------------------------------------- /bridget/utils/services/guild_service.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from typing import Optional, Any 4 | 5 | from model import Guild, Tag, Issue, FilterWord, Giveaway 6 | 7 | guild_id: Optional[str] = os.getenv("GUILD_ID") 8 | 9 | assert guild_id != None 10 | 11 | def get_guild() -> Guild: 12 | """Returns the state of the main guild from the database. 13 | 14 | Returns 15 | ------- 16 | Guild 17 | The Guild document object that holds information about the main guild. 18 | """ 19 | 20 | return Guild.objects(_id=guild_id).first() 21 | 22 | 23 | def add_tag(tag: Tag) -> None: 24 | Guild.objects(_id=guild_id).update_one(push__tags=tag) 25 | 26 | 27 | def remove_tag(tag: str) -> int: 28 | return Guild.objects( 29 | _id=guild_id).update_one( 30 | pull__tags__name=Tag( 31 | name=tag).name) 32 | 33 | 34 | def edit_tag(tag) -> int: 35 | return Guild.objects( 36 | _id=guild_id, 37 | tags__name=tag.name).update_one( 38 | set__tags__S=tag) 39 | 40 | 41 | def get_tag(name: str) -> Optional[Tag]: 42 | tag = Guild.objects.get(_id=guild_id).tags.filter(name=name).first() 43 | if tag is None: 44 | return 45 | tag.use_count += 1 46 | edit_tag(tag) 47 | return tag 48 | 49 | 50 | def add_issue(issue: Issue) -> None: 51 | Guild.objects(_id=guild_id).update_one(push__issues=issue) 52 | 53 | 54 | def remove_issue(issue: str) -> int: 55 | return Guild.objects( 56 | _id=guild_id).update_one( 57 | pull__issues__name=Tag( 58 | name=issue).name) 59 | 60 | 61 | def edit_issue(issue: Issue) -> int: 62 | return Guild.objects( 63 | _id=guild_id, 64 | issues__name=issue.name).update_one( 65 | set__issues__S=issue) 66 | 67 | 68 | def get_issue(name: str) -> Optional[Issue]: 69 | issue = Guild.objects.get(_id=guild_id).issues.filter(name=name).first() 70 | if issue is None: 71 | return 72 | return issue 73 | 74 | 75 | def edit_issues_list(ids) -> int: 76 | return Guild.objects( 77 | _id=guild_id).update_one( 78 | set__issues_list_msg=ids) 79 | 80 | 81 | def add_meme(meme: Tag) -> None: 82 | Guild.objects(_id=guild_id).update_one(push__memes=meme) 83 | 84 | 85 | def remove_meme(meme: str) -> int: 86 | return Guild.objects( 87 | _id=guild_id).update_one( 88 | pull__memes__name=Tag( 89 | name=meme).name) 90 | 91 | 92 | def edit_meme(meme: Tag) -> int: 93 | return Guild.objects( 94 | _id=guild_id, 95 | memes__name=meme.name).update_one( 96 | set__memes__S=meme) 97 | 98 | 99 | def get_meme(name: str) -> Optional[Tag]: 100 | meme = Guild.objects.get(_id=guild_id).memes.filter(name=name).first() 101 | if meme is None: 102 | return 103 | meme.use_count += 1 104 | edit_meme(meme) 105 | return meme 106 | 107 | 108 | def inc_infractionid() -> None: 109 | """Increments Guild.infraction_id, which keeps track of the next available ID to 110 | use for a infraction. 111 | """ 112 | 113 | Guild.objects(_id=guild_id).update_one(inc__infraction_id=1) 114 | 115 | 116 | def all_rero_mappings() -> dict: 117 | g = get_guild() 118 | current = g.reaction_role_mapping 119 | return current 120 | 121 | 122 | def add_rero_mapping(mapping) -> None: 123 | g = get_guild() 124 | current = g.reaction_role_mapping 125 | the_key = list(mapping.keys())[0] 126 | current[str(the_key)] = mapping[the_key] 127 | g.reaction_role_mapping = current 128 | g.save() 129 | 130 | 131 | def append_rero_mapping(message_id, mapping) -> None: 132 | g = get_guild() 133 | current = g.reaction_role_mapping 134 | current[str(message_id)] = current[str(message_id)] | mapping 135 | g.reaction_role_mapping = current 136 | g.save() 137 | 138 | 139 | def get_rero_mapping(id) -> Optional[Any]: 140 | g = get_guild() 141 | if id in g.reaction_role_mapping: 142 | return g.reaction_role_mapping[id] 143 | else: 144 | return None 145 | 146 | 147 | def delete_rero_mapping(id) -> None: 148 | g = get_guild() 149 | if str(id) in g.reaction_role_mapping.keys(): 150 | g.reaction_role_mapping.pop(str(id)) 151 | g.save() 152 | 153 | 154 | def get_giveaway(_id: int) -> Giveaway: 155 | """ 156 | Return the Document representing a giveaway, whose ID (message ID) is given by `id` 157 | If the giveaway doesn't exist in the database, then None is returned. 158 | Parameters 159 | ---------- 160 | id : int 161 | The ID (message ID) of the giveaway 162 | 163 | Returns 164 | ------- 165 | Giveaway 166 | """ 167 | giveaway = Giveaway.objects(_id=_id).first() 168 | return giveaway 169 | 170 | 171 | def add_giveaway( 172 | id: int, 173 | channel: int, 174 | name: str, 175 | entries: list, 176 | winners: int, 177 | ended: bool = False, 178 | prev_winners=[]) -> None: 179 | """ 180 | Add a giveaway to the database. 181 | Parameters 182 | ---------- 183 | id : int 184 | The message ID of the giveaway 185 | channel : int 186 | The channel ID that the giveaway is in 187 | name : str 188 | The name of the giveaway. 189 | entries : list 190 | A list of user IDs who have entered (reacted to) the giveaway. 191 | winners : int 192 | The amount of winners that will be selected at the end of the giveaway. 193 | """ 194 | giveaway = Giveaway() 195 | giveaway._id = id 196 | giveaway.channel = channel 197 | giveaway.name = name 198 | giveaway.entries = entries 199 | giveaway.winners = winners 200 | giveaway.is_ended = ended 201 | giveaway.previous_winners = prev_winners 202 | giveaway.save() 203 | 204 | 205 | def add_raid_phrase(phrase: str) -> bool: 206 | existing = get_guild().raid_phrases.filter(word=phrase) 207 | if (len(existing) > 0): 208 | return False 209 | Guild.objects(_id=guild_id).update_one( 210 | push__raid_phrases=FilterWord(word=phrase, bypass=5, notify=True)) 211 | return True 212 | 213 | 214 | def remove_raid_phrase(phrase: str) -> None: 215 | Guild.objects(_id=guild_id).update_one( 216 | pull__raid_phrases__word=FilterWord(word=phrase).word) 217 | 218 | 219 | def set_spam_mode(mode) -> None: 220 | Guild.objects(_id=guild_id).update_one(set__ban_today_spam_accounts=mode) 221 | 222 | 223 | def add_filtered_word(fw: FilterWord) -> None: 224 | existing = get_guild().filter_words.filter(word=fw.word) 225 | if (len(existing) > 0): 226 | return False 227 | 228 | Guild.objects(_id=guild_id).update_one(push__filter_words=fw) 229 | return True 230 | 231 | 232 | def remove_filtered_word(word: str) -> int: 233 | return Guild.objects( 234 | _id=guild_id).update_one( 235 | pull__filter_words__word=FilterWord( 236 | word=word).word) 237 | 238 | 239 | def update_filtered_word(word: FilterWord) -> int: 240 | return Guild.objects( 241 | _id=guild_id, 242 | filter_words__word=word.word).update_one( 243 | set__filter_words__S=word) 244 | 245 | 246 | def add_whitelisted_guild(id: int) -> bool: 247 | g = Guild.objects(_id=guild_id) 248 | g2 = g.first() 249 | if id not in g2.filter_excluded_guilds: 250 | g.update_one(push__filter_excluded_guilds=id) 251 | return True 252 | return False 253 | 254 | 255 | def remove_whitelisted_guild(id: int) -> bool: 256 | g = Guild.objects(_id=guild_id) 257 | g2 = g.first() 258 | if id in g2.filter_excluded_guilds: 259 | g.update_one(pull__filter_excluded_guilds=id) 260 | return True 261 | return False 262 | 263 | 264 | def add_ignored_channel(id: int) -> bool: 265 | g = Guild.objects(_id=guild_id) 266 | g2 = g.first() 267 | if id not in g2.filter_excluded_channels: 268 | g.update_one(push__filter_excluded_channels=id) 269 | return True 270 | return False 271 | 272 | 273 | def remove_ignored_channel(id: int) -> bool: 274 | g = Guild.objects(_id=guild_id) 275 | g2 = g.first() 276 | if id in g2.filter_excluded_channels: 277 | g.update_one(pull__filter_excluded_channels=id) 278 | return True 279 | return False 280 | 281 | 282 | def add_ignored_channel_logging(id: int) -> bool: 283 | g = Guild.objects(_id=guild_id) 284 | g2 = g.first() 285 | if id not in g2.logging_excluded_channels: 286 | g.update_one(push__logging_excluded_channels=id) 287 | return True 288 | return False 289 | 290 | 291 | def remove_ignored_channel_logging(id: int) -> bool: 292 | g = Guild.objects(_id=guild_id) 293 | g2 = g.first() 294 | if id in g2.logging_excluded_channels: 295 | g.update_one(pull__logging_excluded_channels=id) 296 | return True 297 | return False 298 | 299 | 300 | def get_locked_channels() -> list: 301 | return get_guild().locked_channels 302 | 303 | 304 | def add_locked_channels(channel) -> None: 305 | Guild.objects(_id=guild_id).update_one(push__locked_channels=channel) 306 | 307 | 308 | def remove_locked_channels(channel) -> None: 309 | Guild.objects(_id=guild_id).update_one(pull__locked_channels=channel) 310 | 311 | 312 | def set_nsa_mapping(channel_id, webhooks) -> None: 313 | guild = Guild.objects(_id=guild_id).first() 314 | guild.nsa_mapping[str(channel_id)] = webhooks 315 | guild.save() 316 | -------------------------------------------------------------------------------- /bridget/cogs/memes.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import asyncio 3 | 4 | from io import BytesIO 5 | from discord import app_commands 6 | from discord.ext import commands 7 | 8 | from utils import Cog, send_error, send_success 9 | from utils.enums import PermissionLevel 10 | from utils.menus import Menu 11 | from utils.modals import TagModal, EditTagModal 12 | from utils.services import guild_service 13 | from utils.autocomplete import memes_autocomplete 14 | from cogs.tags import format_tag_page, prepare_tag_embed, prepare_tag_view 15 | 16 | 17 | class Memes(Cog): 18 | @app_commands.autocomplete(name=memes_autocomplete) 19 | @app_commands.command() 20 | async def meme(self, ctx: discord.Interaction, name: str, user_to_mention: discord.Member = None) -> None: 21 | """Send a meme 22 | 23 | Args: 24 | ctx (discord.Interaction): Context 25 | name (str): Name of the meme 26 | user_to_mention (discord.Member, optional): User to mention. Defaults to None. 27 | """ 28 | 29 | name = name.lower() 30 | tag = guild_service.get_meme(name) 31 | 32 | if tag is None: 33 | raise commands.BadArgument("That meme does not exist.") 34 | 35 | # run cooldown so tag can't be spammed 36 | # bucket = self.tag_cooldown.get_bucket(tag.name) 37 | # current = datetime.now().timestamp() 38 | # ratelimit only if the invoker is not a moderator 39 | # if bucket.update_rate_limit(current) and not (gatekeeper.has(ctx.guild, ctx.user, 5) or ctx.guild.get_role(guild_service.get_guild().role_sub_mod) in ctx.user.roles): 40 | # raise commands.BadArgument("That tag is on cooldown.") 41 | 42 | # if the Tag has an image, add it to the embed 43 | _file = tag.image.read() 44 | if _file is not None: 45 | _file = discord.File( 46 | BytesIO(_file), 47 | filename="image.gif" if tag.image.content_type == "image/gif" else "image.png") 48 | else: 49 | _file = discord.utils.MISSING 50 | 51 | if user_to_mention is not None: 52 | title = f"Hey {user_to_mention.mention}, have a look at this!" 53 | else: 54 | title = None 55 | 56 | await ctx.response.send_message(content=title, embed=prepare_tag_embed(tag), view=prepare_tag_view(tag), file=_file) 57 | 58 | @commands.guild_only() 59 | @commands.command(name="meme", aliases=["m"]) 60 | async def _tag(self, ctx: commands.Context, name: str) -> None: 61 | """Send a tag 62 | 63 | Args: 64 | ctx (commands.Context): Context 65 | name (str): Name of the tag 66 | """ 67 | 68 | name = name.lower() 69 | tag = guild_service.get_tag(name) 70 | 71 | if tag is None: 72 | raise commands.BadArgument("That tag does not exist.") 73 | 74 | # if the Tag has an image, add it to the embed 75 | _file = tag.image.read() 76 | if _file is not None: 77 | _file = discord.File( 78 | BytesIO(_file), 79 | filename="image.gif" if tag.image.content_type == "image/gif" else "image.png") 80 | else: 81 | _file = discord.utils.MISSING 82 | 83 | if ctx.message.reference is not None: 84 | title = f"Hey {ctx.message.reference.resolved.author.mention}, have a look at this!" 85 | await ctx.send(content=title, embed=prepare_tag_embed(tag), view=prepare_tag_view(tag), file=_file) 86 | else: 87 | await ctx.message.reply(embed=prepare_tag_embed(tag), view=prepare_tag_view(tag), file=_file, mention_author=False) 88 | 89 | 90 | @app_commands.command() 91 | async def memelist(self, ctx: discord.Interaction) -> None: 92 | """List all memes 93 | 94 | Args: 95 | ctx (discord.Interaction): Context 96 | """ 97 | 98 | _tags = sorted( 99 | guild_service.get_guild().memes, 100 | key=lambda tag: tag.name) 101 | 102 | if len(_tags) == 0: 103 | raise commands.BadArgument("There are no memes defined.") 104 | 105 | menu = Menu( 106 | ctx, 107 | _tags, 108 | per_page=12, 109 | page_formatter=format_tag_page, 110 | whisper=True) 111 | await menu.start() 112 | 113 | 114 | class MemesGroup(Cog, commands.GroupCog, group_name="memes"): 115 | @PermissionLevel.HELPER 116 | @app_commands.command() 117 | async def add(self, ctx: discord.Interaction, name: str, image: discord.Attachment = None) -> None: 118 | """Add a meme 119 | 120 | Args: 121 | ctx (discord.Interaction): Context 122 | name (str): Name of the meme 123 | image (discord.Attachment, optional): Meme image. Defaults to None. 124 | """ 125 | 126 | if not name.isalnum(): 127 | raise commands.BadArgument("Meme name must be alphanumeric.") 128 | 129 | if len(name.split()) > 1: 130 | raise commands.BadArgument( 131 | "Meme names can't be longer than 1 word.") 132 | 133 | if (guild_service.get_tag(name.lower())) is not None: 134 | raise commands.BadArgument("Meme with that name already exists.") 135 | 136 | content_type = None 137 | if image is not None: 138 | content_type = image.content_type 139 | if content_type not in [ 140 | "image/png", 141 | "image/jpeg", 142 | "image/gif", 143 | "image/webp"]: 144 | raise commands.BadArgument("Attached file was not an image!") 145 | 146 | if image.size > 8_000_000: 147 | raise commands.BadArgument("That image is too big!") 148 | 149 | image = await image.read() 150 | 151 | modal = TagModal(bot=self.bot, tag_name=name, author=ctx.user) 152 | await ctx.response.send_modal(modal) 153 | await modal.wait() 154 | 155 | tag = modal.tag 156 | if tag is None: 157 | return 158 | 159 | # did the user want to attach an image to this tag? 160 | if image is not None: 161 | tag.image.put(image, content_type=content_type) 162 | 163 | # store tag in database 164 | guild_service.add_meme(tag) 165 | 166 | _file = tag.image.read() 167 | if _file is not None: 168 | _file = discord.File( 169 | BytesIO(_file), 170 | filename="image.gif" if tag.image.content_type == "image/gif" else "image.png") 171 | 172 | followup = await ctx.followup.send("Added new meme!", file=_file or discord.utils.MISSING, embed=prepare_tag_embed(tag) or discord.utils.MISSING, view=prepare_tag_view(tag) or discord.utils.MISSING) 173 | await asyncio.sleep(5) 174 | await followup.delete() 175 | 176 | @PermissionLevel.HELPER 177 | @app_commands.autocomplete(name=memes_autocomplete) 178 | @app_commands.command() 179 | async def edit(self, ctx: discord.Interaction, name: str, image: discord.Attachment = None) -> None: 180 | """Edit a meme 181 | 182 | Args: 183 | ctx (discord.Interaction): Context 184 | name (str): Name of the meme 185 | image (discord.Attachment, optional): Meme image. Defaults to None. 186 | """ 187 | 188 | if len(name.split()) > 1: 189 | raise commands.BadArgument( 190 | "Meme names can't be longer than 1 word.") 191 | 192 | name = name.lower() 193 | tag = guild_service.get_meme(name) 194 | 195 | if tag is None: 196 | raise commands.BadArgument("That meme does not exist.") 197 | 198 | content_type = None 199 | if image is not None: 200 | # ensure the attached file is an image 201 | content_type = image.content_type 202 | if image.size > 8_000_000: 203 | raise commands.BadArgument("That image is too big!") 204 | 205 | image = await image.read() 206 | # save image bytes 207 | if tag.image is not None: 208 | tag.image.replace(image, content_type=content_type) 209 | else: 210 | tag.image.put(image, content_type=content_type) 211 | else: 212 | tag.image.delete() 213 | 214 | modal = EditTagModal(tag=tag, author=ctx.user) 215 | await ctx.response.send_modal(modal) 216 | await modal.wait() 217 | 218 | if not modal.edited: 219 | await send_error(ctx, "Meme edit was cancelled.", ephemeral=True) 220 | return 221 | 222 | tag = modal.tag 223 | 224 | # store tag in database 225 | guild_service.edit_meme(tag) 226 | 227 | _file = tag.image.read() 228 | if _file is not None: 229 | _file = discord.File( 230 | BytesIO(_file), 231 | filename="image.gif" if tag.image.content_type == "image/gif" else "image.png") 232 | 233 | followup = await ctx.followup.send("Edited meme!", file=_file or discord.utils.MISSING, embed=prepare_tag_embed(tag), view=prepare_tag_view(tag) or discord.utils.MISSING) 234 | await asyncio.sleep(5) 235 | await followup.delete() 236 | 237 | @PermissionLevel.HELPER 238 | @app_commands.autocomplete(name=memes_autocomplete) 239 | @app_commands.command() 240 | async def delete(self, ctx: discord.Interaction, name: str) -> None: 241 | """Delete a meme 242 | 243 | Args: 244 | ctx (discord.Interaction): Context 245 | name (str): Name of the meme 246 | """ 247 | 248 | name = name.lower() 249 | 250 | tag = guild_service.get_meme(name) 251 | if tag is None: 252 | raise commands.BadArgument("That meme does not exist.") 253 | 254 | if tag.image is not None: 255 | tag.image.delete() 256 | 257 | guild_service.remove_meme(name) 258 | await send_success(ctx, f"Deleted meme `{tag.name}`.", delete_after=5) 259 | -------------------------------------------------------------------------------- /bridget/cogs/native_actions_listeners.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import discord 3 | 4 | from discord.ext import commands 5 | from datetime import datetime 6 | from discord.utils import format_dt, escape_markdown 7 | from discord.enums import AutoModRuleActionType 8 | from bridget.model.infraction import Infraction 9 | 10 | from utils import Cog 11 | from utils.mod import add_kick_infraction, add_mute_infraction, add_ban_infraction, add_unban_infraction, add_unmute_infraction, notify_user_warn_noctx, prepare_warn_log, submit_public_log, submit_public_log_noctx 12 | from utils.services import guild_service, user_service 13 | from utils.utils import audit_logs_multi, get_warnpoints 14 | from utils.views import AutoModReportView 15 | 16 | async def warn(bot: commands.Bot, target_member: discord.Member, mod: discord.Member, points: int, reason: str): 17 | db_guild = guild_service.get_guild() 18 | 19 | infraction = Infraction( 20 | _id=db_guild.infraction_id, 21 | _type="WARN", 22 | mod_id=mod.id, 23 | mod_tag=str(mod), 24 | reason=escape_markdown(reason), 25 | punishment=str(points) 26 | ) 27 | 28 | guild_service.inc_infractionid() 29 | user_service.add_infraction(target_member.id, infraction) 30 | user_service.inc_points(target_member.id, points) 31 | 32 | db_user = user_service.get_user(target_member.id) 33 | cur_points = get_warnpoints(db_user) 34 | 35 | log = prepare_warn_log(mod, target_member, infraction) 36 | log.add_field(name="Current points", value=f"{cur_points}/10", inline=True) 37 | 38 | dmed = await notify_user_warn_noctx(target_member, mod, db_user, db_guild, cur_points, log) 39 | await submit_public_log_noctx(bot, db_guild, target_member, log, dmed) 40 | 41 | class StringWithFlags: 42 | def __init__(self, value): 43 | self.value = value 44 | 45 | def parse_string_with_flags(string): 46 | return StringWithFlags(string) 47 | 48 | class StringWithFlags: 49 | def __init__(self, value: str): 50 | self.value: str = value 51 | 52 | def parse_string_with_flags(string: str): 53 | return StringWithFlags(string) 54 | 55 | class NativeActionsListeners(Cog): 56 | @Cog.listener() 57 | async def on_member_remove(self, member: discord.Member) -> None: 58 | guild = member.guild 59 | audit_logs = [audit async for audit in guild.audit_logs(limit=1, action=discord.AuditLogAction.kick, after=member.joined_at)] 60 | if audit_logs and audit_logs[0].target == member: 61 | channel = member.guild.get_channel( 62 | guild_service.get_guild().channel_public) 63 | await channel.send(embed=await add_kick_infraction(member, audit_logs[0].user, "No reason." if audit_logs[0].reason is None else audit_logs[0].reason, guild_service.get_guild(), self.bot)) 64 | 65 | @Cog.listener() 66 | async def on_member_ban(self, guild: discord.Guild, member: discord.Member) -> None: 67 | user = user_service.get_user(member.id) 68 | user.last_ban_date = datetime.now().date() 69 | user.ban_count += 1 70 | user.is_banned = True 71 | user.save() 72 | 73 | audit_logs = [audit async for audit in guild.audit_logs(limit=1, action=discord.AuditLogAction.ban)] 74 | if audit_logs and audit_logs[0].target == member: 75 | channel = guild.get_channel( 76 | guild_service.get_guild().channel_public) 77 | await channel.send(embed=await add_ban_infraction(member, audit_logs[0].user, "No reason." if audit_logs[0].reason is None else audit_logs[0].reason, guild_service.get_guild(), self.bot)) 78 | 79 | @Cog.listener() 80 | async def on_member_unban(self, guild: discord.Guild, member: discord.User) -> None: 81 | audit_logs = [audit async for audit in guild.audit_logs(limit=1, action=discord.AuditLogAction.unban, after=member.created_at)] 82 | if audit_logs and audit_logs[0].target == member: 83 | channel = guild.get_channel( 84 | guild_service.get_guild().channel_public) 85 | await channel.send(embed=await add_unban_infraction(member, audit_logs[0].user, "No reason." if audit_logs[0].reason is None else audit_logs[0].reason, guild_service.get_guild(), self.bot)) 86 | 87 | @Cog.listener() 88 | async def on_member_update(self, before: discord.Member, after: discord.Member) -> None: 89 | if not before.is_timed_out() and after.is_timed_out(): 90 | channel = self.bot.get_channel( 91 | guild_service.get_guild().channel_public) 92 | # get reason from audit log 93 | audit_logs = await audit_logs_multi(before.guild, [ discord.AuditLogAction.member_update, discord.AuditLogAction.automod_timeout_member ], limit=1, after=after.joined_at) 94 | if audit_logs and audit_logs[0].target == after: 95 | print("hello") 96 | parser = argparse.ArgumentParser(exit_on_error=False, usage="") 97 | parser.add_argument("-w", "--warn", type=int, default=1) 98 | parser.add_argument("message", type=str, nargs='*') 99 | try: 100 | strparse = parser.parse_args(audit_logs[0].reason.split()) 101 | except SystemExit as e: 102 | await (await audit_logs[0].user.create_dm()).send("Your mute had an exception! The error was probably an invalid flag. Unmuting the user!") 103 | await after.edit(timed_out_until=None, reason="A SystemExit was triggered by argparse (probably broken flags)") 104 | return 105 | print(type(strparse)) 106 | await channel.send(embed=await add_mute_infraction(after, audit_logs[0].user, "No reason." if not ''.join(strparse.message) else ''.join(strparse.message), guild_service.get_guild(), self.bot)) 107 | print("before warn") 108 | await warn(self.bot, after, audit_logs[0].user, strparse.warn, "Automatic point addition") 109 | elif before.is_timed_out() and not after.is_timed_out(): 110 | channel = self.bot.get_channel( 111 | guild_service.get_guild().channel_public) 112 | # get reason from audit log 113 | audit_logs = await audit_logs_multi(before.guild, [ discord.AuditLogAction.member_update, discord.AuditLogAction.automod_timeout_member ], limit=1, after=after.joined_at) 114 | if audit_logs and audit_logs[0].target == after: 115 | await channel.send(embed=await add_unmute_infraction(after, audit_logs[0].user, "No reason." if audit_logs[0].reason is None else audit_logs[0].reason, guild_service.get_guild(), self.bot)) 116 | 117 | @commands.Cog.listener() 118 | async def on_automod_action(self, ctx: discord.AutoModAction) -> None: 119 | rule = await ctx.fetch_rule() 120 | member = ctx.guild.get_member(ctx.user_id) 121 | 122 | # filter with mod+ bypass 123 | if ctx.action.type == discord.AutoModRuleActionType.send_alert_message and rule.name.endswith('🚨'): 124 | print(ctx, rule) 125 | await automod_fancy_embed(self.bot, ctx, rule, member) 126 | elif ctx.action.type == AutoModRuleActionType.timeout: 127 | # check role and ban for raid phrase 128 | if guild_service.get_guild().role_memberplus not in [ x.id for x in member.roles ]: 129 | await ctx.guild.get_member(ctx.user_id).ban(reason="Raid phrase detected") 130 | 131 | 132 | async def automod_fancy_embed(bot: discord.BotIntegration, ctx: discord.AutoModAction, rule: discord.AutoModRule, member: discord.Member) -> None: 133 | # embed 134 | embed = discord.Embed(title="Filter word detected") 135 | embed.color = discord.Color.red() 136 | embed.set_thumbnail(url=member.display_avatar) 137 | embed.add_field( 138 | name="Member", value=f'{member} ({member.mention})', inline=True) 139 | embed.add_field( 140 | name="Channel", value=ctx.channel.mention, inline=True) 141 | embed.add_field(name="Message", value=ctx.content, inline=False) 142 | embed.add_field(name="Filtered word", value=ctx.matched_content, inline=True) 143 | embed.timestamp = datetime.now() 144 | embed.set_footer(text=f"{member}") 145 | embed.add_field( 146 | name="Join date", 147 | value=f"{format_dt(member.joined_at, style='F')} ({format_dt(member.joined_at, style='R')})", 148 | inline=True) 149 | embed.add_field( 150 | name="Created", 151 | value=f"{format_dt(member.created_at, style='F')} ({format_dt(member.created_at, style='R')})", 152 | inline=True) 153 | embed.add_field( 154 | name="Warn points", 155 | value=user_service.get_user(ctx.user_id).warn_points, 156 | inline=True) 157 | 158 | account_age = (datetime.now() - datetime.fromtimestamp(discord.utils.snowflake_time(member.id))).days 159 | 160 | 161 | if member.joined_at != None: 162 | guild_age = (datetime.now() - member.joined_at).days 163 | else: 164 | guild_age = account_age 165 | 166 | risk_factor = user_service.get_user(ctx.user_id).warn_points * 0.5 + min(len(user_service.get_infractions(member.id)), 10) * 0.3 + min((guild_age / 30), 10) * 0.1 + min(account_age / 60, 10) * 0.1 167 | 168 | embed.add_field(name="User risk factor (scale: 1-10)", 169 | value=f"{risk_factor} (warn points * 0.5 + infraction count (max 10) * 0.3 + days since guild join / 30 (max 10) * 0.1 + days since account creation / 60 (max 10) * 0.1)" 170 | ) 171 | 172 | reversed_roles = member.roles 173 | reversed_roles.reverse() 174 | 175 | roles = "" 176 | for role in reversed_roles[0:4]: 177 | if role != member.guild.default_role: 178 | roles += role.mention + " " 179 | roles = roles.strip() + "..." 180 | 181 | embed.add_field( 182 | name="Roles", value=roles if roles else "None", inline=False) 183 | 184 | # buttons 185 | view = AutoModReportView(member, bot) 186 | 187 | # send embed and buttons (roles=True to enable mod ping) 188 | channel = ctx.guild.get_channel(guild_service.get_guild().channel_reports) 189 | await channel.send(content=f"<@&{guild_service.get_guild().role_reportping}>", 190 | embed=embed, view=view, allowed_mentions=discord.AllowedMentions(everyone=False, roles=False, users=True) 191 | ) 192 | 193 | --------------------------------------------------------------------------------