[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: FalseDev\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Use commands '....'\n2. Do '....'\n3. See error\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"pip\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n"
  },
  {
    "path": ".github/workflows/black.yml",
    "content": "name: Lint\n\non: [push, pull_request]\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions/setup-python@v2\n      - uses: psf/black@stable\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\n#\n# ******** NOTE ********\n# We have attempted to detect the languages in your repository. Please check\n# the `language` matrix defined below to confirm you have the correct set of\n# supported CodeQL languages.\n#\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [ main ]\n  schedule:\n    - cron: '32 12 * * 3'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'python' ]\n        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]\n        # Learn more:\n        # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v2\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v1\n      with:\n        languages: ${{ matrix.language }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file.\n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n        # queries: ./path/to/local/query, your-org/your-repo/queries@main\n\n    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).\n    # If this step fails, then you should remove it and run the build manually (see below)\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v1\n\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 https://git.io/JvXDl\n\n    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines\n    #    and modify them (or add more) to build your code if your project\n    #    uses a compiled language\n\n    #- run: |\n    #   make bootstrap\n    #   make release\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v1\n"
  },
  {
    "path": ".github/workflows/isort.yml",
    "content": "name: Run isort\non:\n  - push\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions/setup-python@v2\n        with:\n          python-version: 3.9\n      - uses: jamescurtin/isort-action@master\n        with:\n            requirementsFiles: \"requirements.txt requirements-dev.txt\"\n"
  },
  {
    "path": ".gitignore",
    "content": "config.yaml\n.vim\n.env\n**/__pycache__\ntmp\ncache\n\n# Tortoise stuff\naerich.ini\nmigrations\n\n.vercel\n"
  },
  {
    "path": ".isort.cfg",
    "content": "[settings]\nmulti_line_output = 3\ninclude_trailing_comma = True\nforce_grid_wrap = 0\nuse_parentheses = True\nensure_newline_before_comments = True\nline_length = 88\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at thetechnopath1802@gmail.com. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 FalseDev\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n\t<img src=\"https://cdn.discordapp.com/attachments/770679803635433473/825250084589273118/circle-cropped4.png\" height=\"125px\" width=\"125px\" />\n</p>\n\n<p align='center'><a href = \"https://discord.gg/HXqXKdVBhs\" target = \"_blank\"><img src = \"https://discord.com/api/guilds/782517843820412948/embed.png\"></a></p>\n\n<h1 align=\"center\">Techstruck</h1>\n\n<h3><img src=\"https://cdn.discordapp.com/emojis/562008110412201986.png\" height=\"20px\"> • Info</h3>\n<ul>\n<li><a href=\"https://discord.gg/jhK3bpNkRH\">Tech Struck</a> is a discord server where developers, designers and just about everyone struck with curiosity on tech unite together as a community!</li>\n<li>This repository has Tech Struck server's custom bot along with webhook based announcement and <i>BrainFeed</i> senders for Tech Struck</li>\n</ul>\n\n<h3><img src=\"https://cdn.discordapp.com/attachments/770679803635433473/825245721951207454/802801495153967154.png\" height=\"20px\"> • I'd like to contribute</h3>\n<p>You may help by adding features to Tech Struck or fix bugs in the code. Here's how:</p>\n<ol>\n  <li>Fork the repository</li>\n  <li>Clone your fork: <code>git clone https://github.com/your-username/Tech-Struck.git</code></li>\n  <li>Create your feature branch: <code>git checkout -b my-new-feature</code></li>\n  <li>Commit your changes: <code>git commit -am 'uwu new feature'</code></li>\n  <li>Push to the branch: <code>git push origin my-new-feature</code></li>\n  <li>Submit a pull request</li>\n</ol>\n\n<h3><img src=\"https://cdn.discordapp.com/attachments/770679803635433473/825245805476184074/675395743044993053.png\" height=\"20px\"> • I found a bug!</h3>\n<ul><li>Please open an issue or even send a pull request with the fix to help up keet the bot and other bug free!</li></ul>\n\n<h3 align=\"center\"><a href=\"https://discord.gg/jhK3bpNkRH\"><img src=\"https://www.freepnglogos.com/uploads/discord-logo-png/discord-logo-logodownload-download-logotipos-1.png\" height=\"20px\"></a> <a href=\"https://discord.gg/jhK3bpNkRH\">Click me to join Tech Struck</a></h3>\n"
  },
  {
    "path": "api/dependencies.py",
    "content": "import hmac\nimport ssl\nfrom datetime import datetime\n\nimport asyncpg\nfrom aiohttp import ClientSession\nfrom fastapi import Header, HTTPException, Query, Request, status, templating\nfrom jose import jwt\n\nfrom config.common import config\nfrom config.webhook import webhook_config\n\nfrom .exceptions import CustomHTTPException\n\njinja = templating.Jinja2Templates(\"./public/templates/\")\n\n\ndef auth_dep(authorization: str = Header(...)):\n    if not hmac.compare_digest(authorization, webhook_config.authorization):\n        raise HTTPException(status.HTTP_401_UNAUTHORIZED)\n\n\nasync def aiohttp_session():\n    session = ClientSession(headers={\"Accept\": \"application/json\"})\n    try:\n        yield session\n    finally:\n        await session.close()\n\n\ndef state_check(request: Request, state: str = Query(...)) -> int:\n    try:\n        payload = jwt.decode(state, config.secret)\n    except jwt.JWTError:\n        raise CustomHTTPException(\n            jinja.TemplateResponse(\n                \"oauth_error.html\",\n                {\"request\": request, \"detail\": \"Invalid state\"},\n                status_code=status.HTTP_406_NOT_ACCEPTABLE,\n            )\n        )\n\n    expiry = datetime.fromisoformat(payload[\"expiry\"])\n    if datetime.now() > expiry:\n        raise CustomHTTPException(\n            jinja.TemplateResponse(\n                \"oauth_error.html\",\n                {\"request\": request, \"detail\": \"Expired link\"},\n                status_code=status.HTTP_406_NOT_ACCEPTABLE,\n            )\n        )\n\n    return payload[\"id\"]\n\n\nctx = ssl.create_default_context()\nctx.check_hostname = False\nctx.verify_mode = ssl.CERT_NONE\n\n\nasync def db_connection():\n    connection = await asyncpg.connect(config.database_uri, ssl=ctx)\n    try:\n        yield connection\n    finally:\n        await connection.close()\n"
  },
  {
    "path": "api/exceptions.py",
    "content": "class CustomHTTPException(Exception):\n    def __init__(self, response):\n        self.response = response\n"
  },
  {
    "path": "api/main.py",
    "content": "import sys\n\nfrom fastapi import FastAPI, Request\n\nfrom .exceptions import CustomHTTPException\nfrom .routers import oauth, webhooks\n\nif sys.version_info[1] < 7:\n    from backports.datetime_fromisoformat import MonkeyPatch\n\n    MonkeyPatch.patch_fromisoformat()\n\n\napp = FastAPI()\n\n\n@app.exception_handler(CustomHTTPException)\ndef custom_http_exception_handler(request: Request, exc: CustomHTTPException):\n    return exc.response\n\n\napp.include_router(oauth.router)\napp.include_router(webhooks.router)\n"
  },
  {
    "path": "api/routers/oauth.py",
    "content": "from datetime import datetime\nfrom urllib.parse import parse_qs\n\nimport asyncpg\nfrom aiohttp import ClientSession\nfrom fastapi import (\n    APIRouter,\n    Depends,\n    HTTPException,\n    Query,\n    Request,\n    status,\n    templating,\n)\nfrom jose import jwt\n\nfrom config.common import config\nfrom config.oauth import github_oauth_config, stack_oauth_config\n\nfrom ..dependencies import aiohttp_session, db_connection, jinja, state_check\n\nrouter = APIRouter(\n    prefix=\"/oauth\",\n)\n\n# {table} is the table name, {field} the field name\n# Hence this query is safe against sql injection type attacks\ninsert_or_update_template = \"\"\"\ninsert into {table} (id, {field}) values ($1, $2) on conflict (id) do update set {field}=$2\n\"\"\".strip()\n\nstack_sql_query = insert_or_update_template.format(\n    table=\"users\", field=\"stackoverflow_oauth_token\"\n)\ngithub_sql_query = insert_or_update_template.format(\n    table=\"users\", field=\"github_oauth_token\"\n)\n\n\n# TODO: Cache recently used jwt tokens until expiry and deny their usage\n# TODO: Serverless is stateless, hence use db caching\n\n\n@router.get(\"/stackexchange\")\nasync def stackexchange_oauth(\n    request: Request,\n    code: str = Query(...),\n    user_id: int = Depends(state_check),\n    db_conn: asyncpg.pool.Pool = Depends(db_connection),\n    session: ClientSession = Depends(aiohttp_session),\n):\n    \"\"\"Link account with stackexchange through OAuth2\"\"\"\n\n    res = await session.post(\n        \"https://stackoverflow.com/oauth/access_token/json\",\n        data={**stack_oauth_config.dict(), \"code\": code},\n    )\n    auth = await res.json()\n    if \"access_token\" not in auth:\n        return {k: v for k, v in auth.items() if k.startswith(\"error_\")}\n    await db_conn.execute(stack_sql_query, user_id, auth[\"access_token\"])\n\n    return jinja.TemplateResponse(\n        \"oauth_success.html\", {\"request\": request, \"oauth_provider\": \"Stackexchange\"}\n    )\n\n\n@router.get(\"/github\")\nasync def github_oauth(\n    request: Request,\n    code: str = Query(...),\n    user_id: int = Depends(state_check),\n    db_conn: asyncpg.pool.Pool = Depends(db_connection),\n    session: ClientSession = Depends(aiohttp_session),\n):\n    \"\"\"Link account with github through OAuth2\"\"\"\n    res = await session.post(\n        \"https://github.com/login/oauth/access_token\",\n        data={**github_oauth_config.dict(), \"code\": code},\n    )\n    auth = await res.json()\n    await db_conn.execute(github_sql_query, user_id, auth[\"access_token\"])\n\n    return jinja.TemplateResponse(\n        \"oauth_success.html\", {\"request\": request, \"oauth_provider\": \"Github\"}\n    )\n"
  },
  {
    "path": "api/routers/webhooks.py",
    "content": "import datetime\nimport json\nimport random\nfrom concurrent import futures\nfrom typing import Iterable, List\n\nfrom aiohttp import ClientSession\nfrom discord import AsyncWebhookAdapter, Color, Embed, RequestsWebhookAdapter, Webhook\nfrom fastapi import APIRouter, Depends\nfrom praw import Reddit\n\nfrom config.reddit import reddit_config\nfrom config.webhook import webhook_config\n\nfrom ..dependencies import aiohttp_session, auth_dep\n\nrouter = APIRouter(prefix=\"/webhooks\", dependencies=[Depends(auth_dep)])\n\nreddit = Reddit(\n    **reddit_config.dict(),\n    user_agent=\"TechStruck\",\n)\n\n\nREDDIT_ALLOWED_FORMATS = (\".jpg\", \".gif\", \".png\", \".jpeg\")\nSUBREDDITS = (\n    \"memes\",\n    \"meme\",\n    \"dankmeme\",\n    \"me_irl\",\n    \"dankmemes\",\n    \"showerthoughts\",\n    \"jokes\",\n    \"funny\",\n)\n\n\ndef send_meme(webhook: Webhook, subreddits: List[str]) -> bool:\n    meme_subreddit = reddit.subreddit(random.choice(subreddits))\n    meme = meme_subreddit.random()\n    if not any((meme.url.endswith(i) for i in REDDIT_ALLOWED_FORMATS)):\n        return False\n    embed = Embed(title=meme.title, color=Color.magenta())\n    embed.set_image(url=meme.url)\n    embed.set_footer(text=f\"\\U0001f44d {meme.ups} \\u2502 \\U0001f44e {meme.downs}\")\n    webhook.send(embed=embed)\n    return True\n\n\n# The subreddits arg exists although theres a\n# global so that in the future it can be\n# modified for multiple channels/servers\ndef send_memes(webhook: Webhook, subreddits: Iterable[str], quantity: int):\n    sent = 0\n    skipped = 0\n    with futures.ThreadPoolExecutor() as tp:\n        while sent < quantity:\n            results = [\n                tp.submit(send_meme, webhook, subreddits)\n                for _ in range(quantity - sent)\n            ]\n            new_sent = sum([r.result() for r in results])\n            skipped += (quantity - sent) - new_sent\n            sent += new_sent\n    return sent, skipped\n\n\n@router.get(\"/meme\")\ndef send_memes_route():\n    sent, skipped = send_memes(\n        Webhook.from_url(webhook_config.meme, adapter=RequestsWebhookAdapter()),\n        SUBREDDITS,\n        5,\n    )\n    return {\"sent\": sent, \"skipped\": skipped}\n\n\n@router.get(\"/git-tip\")\nasync def git_tip(session: ClientSession = Depends(aiohttp_session)):\n    tips_json_url = \"https://raw.githubusercontent.com/git-tips/tips/master/tips.json\"\n\n    async with session.get(tips_json_url) as res:\n        tips = json.loads(await res.text())\n\n    tip_no = (datetime.date.today() - datetime.date(2021, 1, 31)).days\n\n    tip = tips[tip_no]\n\n    await Webhook.from_url(\n        webhook_config.git_tips, adapter=AsyncWebhookAdapter(session)\n    ).send(\n        \"<@&804403893760688179>\",\n        embed=Embed(\n            title=tip[\"title\"],\n            description=\"```sh\\n\" + tip[\"tip\"] + \"```\",\n            color=Color.green(),\n        ).set_footer(text=\"Tip {}\".format(tip_no)),\n        avatar_url=\"https://upload.wikimedia.org/wikipedia/commons/thumb/3/3f/Git_icon.svg/2000px-Git_icon.svg.png\",\n    )\n    return {\"status\": \"success\"}\n"
  },
  {
    "path": "bot/__main__.py",
    "content": "import os\n\nfrom .bot import TechStruckBot\n\nos.environ.setdefault(\"JISHAKU_HIDE\", \"1\")\nos.environ.setdefault(\"JISHAKU_RETAIN\", \"1\")\nos.environ.setdefault(\"JISHAKU_NO_UNDERSCORE\", \"1\")\n\nif __name__ == \"__main__\":\n    from config.bot import bot_config\n    from tortoise_config import tortoise_config\n\n    bot = TechStruckBot(tortoise_config=tortoise_config)\n    bot.run(bot_config.bot_token)\n"
  },
  {
    "path": "bot/bot.py",
    "content": "import asyncio\nimport contextlib\nimport math\nimport re\nimport traceback\nfrom typing import Iterable\n\nfrom aiohttp import ClientSession\nfrom discord import (\n    AllowedMentions,\n    AsyncWebhookAdapter,\n    Color,\n    Embed,\n    Forbidden,\n    Intents,\n    Message,\n    NotFound,\n    TextChannel,\n    Webhook,\n    utils,\n)\nfrom discord.ext import commands, tasks\nfrom discord.http import HTTPClient\nfrom tortoise import Tortoise\n\nfrom config.bot import bot_config\nfrom models import GuildModel\n\n\nclass TechStruckBot(commands.Bot):\n    http: HTTPClient\n\n    def __init__(self, *, tortoise_config, load_extensions=True, loadjsk=True):\n        allowed_mentions = AllowedMentions(\n            users=True, replied_user=True, roles=False, everyone=False\n        )\n        super().__init__(\n            command_prefix=self.get_custom_prefix,\n            intents=Intents.all(),\n            allowed_mentions=allowed_mentions,\n            description=\"A bot by and for developers to integrate several tools into one place.\",\n            strip_after_prefix=True,\n        )\n        self.tortoise_config = tortoise_config\n        self.db_connected = False\n        self.prefix_cache = {}\n        self.connect_db.start()\n\n        if load_extensions:\n            self.load_extensions(\n                (\n                    \"bot.core\",\n                    \"bot.cogs.admin\",\n                    \"bot.cogs.thank\",\n                    \"bot.cogs.stackexchange\",\n                    \"bot.cogs.github\",\n                    \"bot.cogs.help_command\",\n                    \"bot.cogs.code_exec\",\n                    \"bot.cogs.fun\",\n                    \"bot.cogs.rtfm\",\n                    \"bot.cogs.joke\",\n                    \"bot.cogs.utils\",\n                    \"bot.cogs.brainfeed\",\n                    \"bot.cogs.packages\",\n                    \"bot.cogs.coc\",\n                )\n            )\n        if loadjsk:\n            self.load_extension(\"jishaku\")\n\n    @property\n    def session(self) -> ClientSession:\n        return self.http._HTTPClient__session  # type: ignore\n\n    @tasks.loop(seconds=0, count=1)\n    async def connect_db(self):\n        print(\"Connecting to db\")\n        await Tortoise.init(self.tortoise_config)\n        self.db_connected = True\n        print(\"Database connected\")\n\n    def load_extensions(self, extentions: Iterable[str]):\n        for ext in extentions:\n            try:\n                self.load_extension(ext)\n            except Exception as e:\n                traceback.print_exception(type(e), e, e.__traceback__)\n\n    async def on_message(self, msg: Message):\n        if msg.author.bot:\n            return\n        while not self.db_connected:\n            await asyncio.sleep(0.2)\n        user_id = self.user.id\n        if msg.content in (f\"<@{user_id}>\", f\"<@!{user_id}>\"):\n            return await msg.reply(\n                \"My prefix here is `{}`\".format(await self.fetch_prefix(msg))\n            )\n        await self.process_commands(msg)\n\n    async def on_command_error(\n        self, ctx: commands.Context, error: commands.CommandError\n    ):\n        if isinstance(error, commands.CommandNotFound):\n            return\n        if not isinstance(error, commands.CommandInvokeError):\n            title = \" \".join(\n                re.compile(r\"[A-Z][a-z]*\").findall(error.__class__.__name__)\n            )\n            return await ctx.send(\n                embed=Embed(title=title, description=str(error), color=Color.red())\n            )\n\n        # If we've reached here, the error wasn't expected\n        # Report to logs\n        embed = Embed(\n            title=\"Error\",\n            description=\"An unknown error has occurred and my developer has been notified of it.\",\n            color=Color.red(),\n        )\n        with contextlib.suppress(NotFound, Forbidden):\n            await ctx.send(embed=embed)\n\n        traceback_text = \"\".join(\n            traceback.format_exception(type(error), error, error.__traceback__)\n        )\n\n        length = len(traceback_text)\n        chunks = math.ceil(length / 1990)\n\n        traceback_texts = [\n            traceback_text[l * 1990 : (l + 1) * 1990] for l in range(chunks)\n        ]\n        traceback_embeds = [\n            Embed(\n                title=\"Traceback\",\n                description=(\"```py\\n\" + text + \"\\n```\"),\n                color=Color.red(),\n            )\n            for text in traceback_texts\n        ]\n\n        # Add message content\n        info_embed = Embed(\n            title=\"Message content\",\n            description=\"```\\n\" + utils.escape_markdown(ctx.message.content) + \"\\n```\",\n            color=Color.red(),\n        )\n        # Guild information\n        value = (\n            (\n                \"**Name**: {0.name}\\n\"\n                \"**ID**: {0.id}\\n\"\n                \"**Created**: {0.created_at}\\n\"\n                \"**Joined**: {0.me.joined_at}\\n\"\n                \"**Member count**: {0.member_count}\\n\"\n                \"**Permission integer**: {0.me.guild_permissions.value}\"\n            ).format(ctx.guild)\n            if ctx.guild\n            else \"None\"\n        )\n\n        info_embed.add_field(name=\"Guild\", value=value)\n        # Channel information\n        if isinstance(ctx.channel, TextChannel):\n            value = (\n                \"**Type**: TextChannel\\n\"\n                \"**Name**: {0.name}\\n\"\n                \"**ID**: {0.id}\\n\"\n                \"**Created**: {0.created_at}\\n\"\n                \"**Permission integer**: {1}\\n\"\n            ).format(ctx.channel, ctx.channel.permissions_for(ctx.guild.me).value)\n        else:\n            value = (\n                \"**Type**: DM\\n\" \"**ID**: {0.id}\\n\" \"**Created**: {0.created_at}\\n\"\n            ).format(ctx.channel)\n\n        info_embed.add_field(name=\"Channel\", value=value)\n\n        # User info\n        value = (\n            \"**Name**: {0}\\n\" \"**ID**: {0.id}\\n\" \"**Created**: {0.created_at}\\n\"\n        ).format(ctx.author)\n\n        info_embed.add_field(name=\"User\", value=value)\n\n        wh = Webhook.from_url(\n            bot_config.log_webhook, adapter=AsyncWebhookAdapter(self.session)\n        )\n        return await wh.send(embeds=[*traceback_embeds, info_embed])\n\n    async def get_custom_prefix(self, _, message: Message) -> str:\n        prefix = await self.fetch_prefix(message)\n        bot_id = self.user.id\n        prefixes = [prefix, f\"<@{bot_id}> \", f\"<@!{bot_id}> \"]\n\n        comp = re.compile(\n            \"^(\" + \"|\".join(re.escape(p) for p in prefixes) + \").*\", flags=re.I\n        )\n        match = comp.match(message.content)\n        if match is not None:\n            return match.group(1)\n        return prefix\n\n    async def fetch_prefix(self, message: Message) -> str:\n        # DMs/Group\n        if not message.guild:\n            return \".\"\n\n        guild_id = message.guild.id\n        # Get from cache\n        if guild_id in self.prefix_cache:\n            return self.prefix_cache[guild_id]\n        # Fetch from db\n        guild, _ = await GuildModel.get_or_create(id=guild_id)\n        self.prefix_cache[guild_id] = guild.prefix\n        return guild.prefix\n\n    async def on_ready(self):\n        print(\"Ready!\")\n"
  },
  {
    "path": "bot/cogs/admin.py",
    "content": "from discord.ext import commands\nfrom discord.utils import get\n\nfrom utils.embed import yaml_file_to_message\n\n\nclass Admin(commands.Cog):\n    def __init__(self, bot: commands.Bot):\n        self.bot = bot\n\n    async def _refresh(self, ctx: commands.Context, filename: str, channel_name: str):\n        target_channel = get(ctx.guild.text_channels, name=channel_name)\n        async for msg in target_channel.history():\n            if msg.author.id == self.bot.user.id:\n                target = msg\n        m, e, _ = yaml_file_to_message(filename)\n        await target.edit(message=m, embed=e)\n\n    @commands.group(name=\"refresh\", invoke_without_subcommand=True)\n    @commands.is_owner()\n    async def refresh(self, ctx: commands.Context):\n        await ctx.send_help()\n\n    @refresh.command(name=\"roles\")\n    async def refresh_roles(self, ctx: commands.Context):\n        await self._refresh(ctx, \"./yaml_embeds/roles.yaml\", \"\\U0001f3c5\\u2502roles\")\n\n    @refresh.command(name=\"rules\")\n    async def refresh_rules(self, ctx: commands.Context):\n        await self._refresh(ctx, \"./yaml_embeds/rules.yaml\", \"\\u2502rules\")\n\n\ndef setup(bot: commands.Bot):\n    bot.add_cog(Admin(bot))\n"
  },
  {
    "path": "bot/cogs/brainfeed.py",
    "content": "import asyncio\nfrom datetime import datetime\nfrom functools import cached_property\n\nfrom discord import Embed, Member, NotFound, Reaction, TextChannel\nfrom discord.ext import commands, flags  # type: ignore\nfrom discord.utils import get\n\nfrom bot.bot import TechStruckBot\nfrom bot.utils.embed_flag_input import dict_to_embed, embed_input\n\n\nclass UnknownBrainfeed(commands.CommandError):\n    def __str__(self) -> str:\n        return \"The BrainFeed with the requested ID was not found\"\n\n\nclass BrainFeed(commands.Cog):\n    \"\"\"BrainFeed related commands\"\"\"\n\n    def __init__(self, bot: TechStruckBot):\n        self.bot = bot\n        self.submission_channel_id = 824887130853474304\n\n    @flags.group(aliases=[\"bf\", \"brain\", \"feed\"], invoke_without_command=True)\n    async def brainfeed(self, ctx: commands.Context):\n        \"\"\"BrainFeed - the daily dose of knowledge\"\"\"\n        await ctx.send_help(self.brainfeed)  # type: ignore\n\n    @cached_property\n    def submission_channel(self) -> TextChannel:\n        return self.bot.get_channel(self.submission_channel_id)  # type: ignore\n\n    @embed_input(basic=True, image=True)\n    @brainfeed.command(aliases=[\"new\", \"submit\"], cls=flags.FlagCommand)\n    @commands.guild_only()\n    @commands.max_concurrency(1, per=commands.BucketType.user)\n    async def add(self, ctx: commands.Context, **kwargs):\n        \"\"\"Submit your brainfeed for approval and publishing\"\"\"\n        embed = dict_to_embed(kwargs)\n        embed.set_author(name=ctx.author.name, icon_url=str(ctx.author.avatar_url))\n        embed.timestamp = datetime.now()\n        msg = await ctx.send(embed=embed)\n        await msg.add_reaction(\"\\u2705\")\n        await msg.add_reaction(\"\\u274c\")\n\n        def check(r: Reaction, u: Member):\n            return (\n                u == ctx.author and r.emoji in (\"\\u2705\", \"\\u274c\") and r.message == msg\n            )\n\n        try:\n            r, _ = await self.bot.wait_for(\"reaction_add\", check=check, timeout=120)\n        except asyncio.TimeoutError:\n            return await msg.reply(\"Timeout!\")\n        if r.emoji == \"\\u274c\":\n            return await ctx.send(\"Cancelled!\")\n        await ctx.trigger_typing()\n        submission = await self.submission_channel.send(embed=embed)\n        metaembed = Embed(\n            title=\"Submission details\",\n            description=(\n                \"```\"\n                f\"User ID: {ctx.author.id}\\n\"\n                f\"User name: {ctx.author}\\n\"\n                f\"Channel ID: {ctx.channel.id}\\n\"\n                f\"Channel name: {ctx.channel}\\n\"\n                f\"Guild ID: {ctx.guild.id}\\n\"\n                f\"Guild name: {ctx.guild}\\n\"\n                \"```\"\n            ),\n        )\n        await submission.reply(embed=metaembed)\n        await ctx.send(f\"Submitted\\nSubmission ID: {submission.id}\")\n\n    async def get_submission(self, bf_id) -> Embed:\n        try:\n            msg = await self.submission_channel.fetch_message(bf_id)\n        except NotFound:\n            raise UnknownBrainfeed()\n\n        if not msg.embeds:\n            raise UnknownBrainfeed()\n\n        return msg.embeds[0]\n\n    @brainfeed.command(aliases=[\"show\"])\n    @commands.cooldown(1, 15, commands.BucketType.user)\n    async def view(self, ctx: commands.Context, id: int):\n        \"\"\"View a BrainFeed\"\"\"\n        embed = await self.get_submission(id)\n        await ctx.send(embed=embed)\n\n    @flags.add_flag(\"--in\", \"-i\", type=TextChannel, default=None)\n    @flags.add_flag(\"--webhook\", \"-wh\", action=\"store_true\", default=False)\n    @flags.add_flag(\"--webhook-name\", \"-wn\", default=\"BrainFeed\")\n    @flags.add_flag(\"--webhook-dispose\", \"-wd\", action=\"store_true\", default=False)\n    @brainfeed.command(aliases=[\"post\"], cls=flags.FlagCommand)\n    @commands.has_guild_permissions(administrator=True)\n    @commands.bot_has_guild_permissions(manage_webhooks=True, embed_links=True)\n    async def send(self, ctx: commands.Context, bf_id: int, **kwargs):\n        \"\"\"Publish a BrainFeed in your server\"\"\"\n        channel: TextChannel = ctx.channel  # type: ignore\n        if in_ := kwargs.pop(\"in\"):\n            channel = await in_\n\n        embed = await self.get_submission(bf_id)\n\n        if not kwargs.pop(\"webhook\"):\n            return await channel.send(embed=embed)\n\n        wh_name: str = kwargs.pop(\"webhook_name\")\n\n        webhook = get(\n            await channel.webhooks(), name=wh_name\n        ) or await channel.create_webhook(name=wh_name)\n\n        await webhook.send(embed=embed)\n        if kwargs.pop(\"webhook_dispose\"):\n            await webhook.delete()\n\n    @brainfeed.command(hidden=True)\n    @commands.is_owner()\n    async def approve(self, ctx: commands.Context, *, id: int):\n        try:\n            msg = await self.submission_channel.fetch_message(id)\n        except NotFound:\n            await ctx.send(\"Submission not found\")\n        else:\n            await msg.remove_reaction(\"\\u274c\", ctx.guild.me)\n            await msg.add_reaction(\"\\u2705\")\n            await ctx.send(\"Approved\")\n\n    @brainfeed.command(hidden=True)\n    @commands.is_owner()\n    async def deny(self, ctx: commands.Context, *, id: int):\n        try:\n            msg = await self.submission_channel.fetch_message(id)\n        except NotFound:\n            await ctx.send(\"Submission not found\")\n        else:\n            await msg.remove_reaction(\"\\u2705\", ctx.guild.me)\n            await msg.add_reaction(\"\\u274c\")\n            await ctx.send(\"Denied\")\n\n\ndef setup(bot: TechStruckBot):\n    bot.add_cog(BrainFeed(bot))\n"
  },
  {
    "path": "bot/cogs/coc.py",
    "content": "import asyncio\nimport re\nimport time\n\nimport aiohttp\n\nimport discord\nfrom discord.ext import commands\n\nfrom ..bot import TechStruckBot\n\ncoc_role = 862200819376717865  # Coc role in TCA\ncoc_channel = 862195507229360168  # Coc channel in TCA\ncoc_message = 862200700410527744\n\nURL_REGEX = re.compile(r\"https://www.codingame.com/clashofcode/clash/([0-9a-f]{39})\")\nAPI_URL = \"https://www.codingame.com/services/ClashOfCode/findClashByHandle\"\n\n\nclass ClashOfCode(commands.Cog):\n    def __init__(self, bot):\n        self.bot = bot\n        self.session = False\n        self.session_message_id: int = 0\n        self.session_users = []\n        self.previous_clash: int = 0\n\n    @commands.Cog.listener()\n    async def on_ready(self):\n        self.guild = self.bot.get_guild(681882711945641997)\n\n    @property\n    def role(self):\n        return self.guild.get_role(coc_role)\n\n    def em(self, mode, players):\n        embed = discord.Embed(title=\"**Clash started**\", color=discord.Color.random())\n        embed.add_field(name=\"Mode\", value=mode, inline=False)\n        embed.add_field(name=\"Players\", value=players)\n        return embed\n\n    @commands.Cog.listener()\n    async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent):\n        if payload.user_id == self.bot.user.id:\n            return\n\n        if self.session_message_id != 0:\n            if payload.message_id == self.session_message_id:\n                if payload.emoji.id == 859056281788743690:\n                    if payload.user_id not in self.session_users:\n                        self.session_users.append(payload.user_id)\n        if payload.message_id != coc_message:\n            return\n\n        if self.role in payload.member.roles:\n            return\n\n        await payload.member.add_roles(self.role)\n        try:\n            await payload.member.send(f\"Gave you the **{self.role.name}** role!\")\n        except discord.HTTPException:\n            pass\n\n    @commands.Cog.listener()\n    async def on_raw_reaction_remove(self, payload: discord.RawReactionActionEvent):\n        if payload.user_id == self.bot.user.id:\n            return\n\n        if self.session_message_id != 0:\n            if payload.message_id == self.session_message_id:\n                if payload.emoji.id == 859056281788743690:\n                    if payload.user_id in self.session_users:\n                        self.session_users.remove(payload.user_id)\n\n        if payload.message_id != coc_message:\n            return\n\n        member = self.guild.get_member(payload.user_id)\n        if self.role not in member.roles:\n            return\n\n        await member.remove_roles(self.role)\n        try:\n            await member.send(f\"Removed your **{self.role.name}** role!\")\n        except discord.HTTPException:\n            pass\n\n    @commands.group(name=\"clashofcode\", aliases=[\"coc\"])\n    @commands.check(lambda ctx: ctx.channel.id == coc_channel)\n    async def clash_of_code(self, ctx: commands.Context):\n        \"\"\"Clash of Code\"\"\"\n        if ctx.invoked_subcommand is None:\n            return await ctx.send_help(self.bot.get_command(\"coc\"))\n\n    @clash_of_code.group(aliases=[\"s\"])\n    @commands.check(lambda ctx: ctx.channel.id == coc_channel)\n    async def session(self, ctx: commands.Context):\n        \"\"\"Start or End a clash of code session\"\"\"\n        if ctx.invoked_subcommand is None:\n            if self.session_message_id == 0:\n                return await ctx.send_help(self.bot.get_command(\"coc session start\"))\n            return await ctx.send_help(self.bot.get_command(\"coc session end\"))\n\n    @session.command(name=\"start\", aliases=[\"s\"])\n    @commands.check(lambda ctx: ctx.channel.id == coc_channel)\n    async def session_start(self, ctx: commands.context):\n        \"\"\"Start a new coc session\"\"\"\n        if self.session_message_id != 0:\n            return await ctx.send(\n                f\"There is an active session right now.\\n\"\n                f\"Join by reacting to the pinned message or using `{ctx.prefix}coc session join`. Have fun!\"\n            )\n\n        pager = commands.Paginator(\n            prefix=f\"**Hey, {ctx.author.mention} is starting a coc session.\\n\"\n            f\"Use `{ctx.prefix}coc session join` or react to this message to join**\",\n            suffix=\"\",\n        )\n\n        for member in self.role.members:\n            if member != ctx.author:\n                if member.status != discord.Status.offline:\n                    pager.add_line(member.mention + \", \")\n\n        if not len(pager.pages):\n            return await ctx.send(\n                f\"{ctx.author.mention}, Nobody is online to play with <:pepe_sad:756087659281121312>\"\n            )\n\n        self.session = True\n        self.previous_clash = int(time.time())\n        self.session_users.append(ctx.author.id)\n\n        msg = await ctx.send(pager.pages[0])\n        self.session_message_id = msg.id\n        await msg.add_reaction(\"<:poggythumbsup:859056281788743690>\")\n\n        try:\n            await msg.pin()\n        except:\n            await ctx.send(\"Failed to pin message\")\n\n        while self.session_message_id != 0:\n            await asyncio.sleep(10)\n\n            if (\n                self.previous_clash + 1800 < int(time.time())\n                and self.session_message_id != 0\n            ):\n                await ctx.send(\"Clash session has been closed due to inactivity\")\n                try:\n                    await msg.unpin()\n                except:\n                    await ctx.send(\"Failed to unpin message\")\n\n                self.previous_clash = 0\n                self.session_users = []\n                self.session_message_id = 0\n                self.session = False\n                break\n\n    @session.command(name=\"join\", aliases=[\"j\"])\n    @commands.check(lambda ctx: ctx.channel.id == coc_channel)\n    async def session_join(self, ctx: commands.Context):\n        \"\"\"Join the current active coc session\"\"\"\n        if self.session_message_id == 0:\n            return await ctx.send(\n                f\"There is no active coc session at the moment.\\n\"\n                f\"Use `{ctx.prefix}coc session start` to start a coc session.\"\n            )\n        if ctx.author.id in self.session_users:\n            return await ctx.send(\n                \"You are already in the session. Have fun playing.\\n\"\n                f\"If you want to leave remove your reaction or use `{ctx.prefix}coc session leave`\"\n            )\n        self.session_users.append(ctx.author.id)\n        return await ctx.send(\"You have joined the session. Have fun playing\")\n\n    @session.command(name=\"leave\", aliases=[\"l\"])\n    @commands.check(lambda ctx: ctx.channel.id == coc_channel)\n    async def session_leave(self, ctx: commands.Context):\n        \"\"\"Leave the current active coc session\"\"\"\n        if self.session_message_id == 0:\n            return await ctx.send(\n                f\"There is no active coc session right now\"\n                f\"use `{ctx.prefix}coc session start` to start a coc session\"\n            )\n        if ctx.author.id not in self.session_users:\n            return await ctx.send(\n                \"You aren't in a clash of code session right now.\\n\"\n                f\"If you want to join react to session message or use `{ctx.prefix}coc session join`\"\n            )\n        self.session_users.remove(ctx.author.id)\n        return await ctx.send(\"You have left the session. No more pings for now.\")\n\n    @session.command(name=\"end\", aliases=[\"e\"])\n    @commands.check(lambda ctx: ctx.channel.id == coc_channel)\n    async def session_end(self, ctx: commands.context):\n        \"\"\"Ends the current coc session\"\"\"\n        if self.session_message_id == 0:\n            return await ctx.send(\"There is no active clash of code session.\")\n\n        try:\n            msg = await ctx.channel.fetch_message(self.session_message_id)\n            try:\n                await msg.unpin()\n            except:\n                await ctx.send(\"Failed to unpin message\")\n        except:\n            await ctx.send(\"Error while fetching message to unpin\")\n\n        self.previous_clash = 0\n        self.session_users = []\n        self.session_message_id = 0\n        self.session = False\n\n        return await ctx.send(\n            f\"Clash session has been closed by {ctx.author.mention}. See you later :wave:\"\n        )\n\n    @clash_of_code.command(name=\"invite\", aliases=[\"i\"])\n    @commands.has_any_role(\n        681895373454835749,  # Owner\n        580911082290282506,  # Admin perms\n        795145820210462771,  # Staff\n        726650418444107869,  # Official Helper\n        coc_role,\n    )\n    @commands.check(lambda ctx: ctx.channel.id == coc_channel)\n    @commands.cooldown(1, 60, commands.BucketType.channel)\n    async def coc_invite(self, ctx: commands.Context, *, url: str = None):\n        \"\"\"Mentions all the users with the `Clash Of Code` role that are in the current session.\"\"\"\n        await ctx.message.delete()\n        if self.session_message_id == 0:\n            ctx.command.reset_cooldown(ctx)\n            return await ctx.send(\n                \"No active Clash of Code session please create one to start playing\\n\"\n                f\"Use `{ctx.prefix}coc session start` to start a coc session <:smugcat:737943749929467975>\"\n            )\n\n        if ctx.author.id not in self.session_users:\n            ctx.command.reset_cooldown(ctx)\n            return await ctx.send(\n                \"You can't create a clash unless you participate in the session\\n\"\n                f\"Use `{ctx.prefix}coc session join` or react to the pinned message to join the coc session \"\n                \"<:smugcat:737943749929467975>\"\n            )\n\n        if url is None:\n            ctx.command.reset_cooldown(ctx)\n            return await ctx.send(\"You should provide a valid clash of code url\")\n\n        link = URL_REGEX.fullmatch(url)\n        if not link:\n            ctx.command.reset_cooldown(ctx)\n            return await ctx.send('Could not find any valid \"clashofcode\" url')\n\n        self.previous_clash = time.time()\n\n        id = link[1]\n\n        async with aiohttp.ClientSession() as session:\n            async with session.post(API_URL, json=[id]) as resp:\n                json = await resp.json()\n\n        pager = commands.Paginator(\n            prefix=\"\\n\".join(\n                [\n                    f\"**Hey, {ctx.author.mention} is hosting a Clash Of Code game!**\",\n                    f\"Mode{'s' if len(json['modes']) > 1 else ''}: {', '.join(json['modes'])}\",\n                    f\"Programming languages: {', '.join(json['programmingLanguages']) if json['programmingLanguages'] else 'All'}\",\n                    f\"Join here: {link[0]}\",\n                ]\n            ),\n            suffix=\"\",\n        )\n\n        for member_id in self.session_users:\n            if member_id != ctx.author.id:\n                member = self.bot.get_user(member_id)\n                pager.add_line(member.mention + \", \")\n\n        if not len(pager.pages):\n            return await ctx.send(\n                f\"{ctx.author.mention}, Nobody is online to play with <:pepe_sad:756087659281121312>\"\n            )\n\n        for page in pager.pages:\n            await ctx.send(page)\n\n        async with aiohttp.ClientSession() as session:\n            while not json[\"started\"]:\n                await asyncio.sleep(10)  # wait 10s to avoid flooding the API\n                async with session.post(API_URL, json=[id]) as resp:\n                    json = await resp.json()\n\n        players = len(json[\"players\"])\n        players_text = \", \".join(\n            [\n                p[\"codingamerNickname\"]\n                for p in sorted(json[\"players\"], key=lambda p: p[\"position\"])\n            ]\n        )\n        start_message = await ctx.send(embed=self.em(json[\"mode\"], players_text))\n\n        async with aiohttp.ClientSession() as session:\n            while not json[\"finished\"]:\n                await asyncio.sleep(10)  # wait 10s to avoid flooding the API\n                async with session.post(API_URL, json=[id]) as resp:\n                    json = await resp.json()\n\n                if len(json[\"players\"]) != players:\n                    players_text = \", \".join(\n                        [\n                            p[\"codingamerNickname\"]\n                            for p in sorted(\n                                json[\"players\"], key=lambda p: p[\"position\"]\n                            )\n                        ]\n                    )\n                    await start_message.edit(embed=self.em(json[\"mode\"], players_text))\n\n        embed = discord.Embed(\n            title=\"**Clash finished, here are the results**\",\n            color=discord.Color.random(),\n        )\n\n        for p in sorted(json[\"players\"], key=lambda p: p[\"rank\"]):\n            embed.add_field(\n                name=f\"{p['rank']}. {p['codingamerNickname']}\",\n                value=(\n                    f\"Code length: {p['criterion']}, \"\n                    if json[\"mode\"] == \"SHORTEST\"\n                    else \"\"\n                )\n                + f\"Score: {p['score']}%, Time: {p['duration'] // 60_000}:{p['duration'] // 1000 % 60:02}\",\n                inline=False,\n            )\n        await ctx.send(embed=embed)\n\n\ndef setup(bot: TechStruckBot):\n    bot.add_cog(ClashOfCode(bot=bot))\n"
  },
  {
    "path": "bot/cogs/code_exec.py",
    "content": "import re\n\nfrom discord import Color, Embed\nfrom discord.ext import commands\n\nfrom config.bot import bot_config\n\n\n# TODO: Move this into utils\nasync def create_guest_paste_bin(session, code):\n    res = await session.post(\n        \"https://pastebin.com/api/api_post.php\",\n        data={\n            \"api_dev_key\": bot_config.pastebin_api_key,\n            \"api_paste_code\": code,\n            \"api_paste_private\": 0,\n            \"api_paste_name\": \"output.txt\",\n            \"api_paste_expire_date\": \"1D\",\n            \"api_option\": \"paste\",\n        },\n    )\n    return await res.text()\n\n\nclass CodeExec(commands.Cog):\n    def __init__(self, bot: commands.Bot):\n        self.bot = bot\n        # TODO: Improve this further\n        self.regex = re.compile(r\"(\\w*)\\s*(?:```)(\\w*)?([\\s\\S]*)(?:```$)\")\n\n    @property\n    def session(self):\n        return self.bot.http._HTTPClient__session  # type: ignore\n\n    async def _run_code(self, *, lang: str, code: str):\n        res = await self.session.post(\n            \"https://emkc.org/api/v1/piston/execute\",\n            json={\"language\": lang, \"source\": code},\n        )\n        return await res.json()\n\n    @commands.command()\n    async def run(self, ctx: commands.Context, *, codeblock: str):\n        \"\"\"\n        Run code and get results instantly\n        **Note**: You must use codeblocks around the code\n        \"\"\"\n        matches = self.regex.findall(codeblock)\n        if not matches:\n            return await ctx.reply(\n                embed=Embed(\n                    title=\"Uh-oh\", description=\"Couldn't quite see your codeblock\"\n                )\n            )\n        lang = matches[0][0] or matches[0][1]\n        if not lang:\n            return await ctx.reply(\n                embed=Embed(\n                    title=\"Uh-oh\",\n                    description=\"Couldn't find the language hinted in the codeblock or before it\",\n                )\n            )\n        code = matches[0][2]\n        result = await self._run_code(lang=lang, code=code)\n\n        await self._send_result(ctx, result)\n\n    @commands.command()\n    async def runl(self, ctx: commands.Context, lang: str, *, code: str):\n        \"\"\"\n        Run a single line of code, **must** specify language as first argument\n        \"\"\"\n        result = await self._run_code(lang=lang, code=code)\n        await self._send_result(ctx, result)\n\n    async def _send_result(self, ctx: commands.Context, result: dict):\n        if \"message\" in result:\n            return await ctx.reply(\n                embed=Embed(\n                    title=\"Uh-oh\", description=result[\"message\"], color=Color.red()\n                )\n            )\n        output = result[\"output\"]\n        #        if len(output) > 2000:\n        #            url = await create_guest_paste_bin(self.session, output)\n        #            return await ctx.reply(\"Your output was too long, so here's the pastebin link \" + url)\n        embed = Embed(title=f\"Ran your {result['language']} code\", color=Color.green())\n        output = output[:500].strip()\n        shortened = len(output) > 500\n        lines = output.splitlines()\n        shortened = shortened or (len(lines) > 15)\n        output = \"\\n\".join(lines[:15])\n        output += shortened * \"\\n\\n**Output shortened**\"\n        embed.add_field(name=\"Output\", value=output or \"**<No output>**\")\n\n        await ctx.reply(embed=embed)\n\n\ndef setup(bot: commands.Bot):\n    bot.add_cog(CodeExec(bot))\n"
  },
  {
    "path": "bot/cogs/fun.py",
    "content": "import asyncio\n\nfrom discord import Color, Embed, Forbidden, Member, utils\nfrom discord.ext import commands\n\nfrom bot.bot import TechStruckBot\n\n\nclass Fun(commands.Cog):\n    \"\"\"Commands for fun and entertainment\"\"\"\n\n    def __init__(self, bot: TechStruckBot):\n        self.bot = bot\n\n    @commands.command()\n    async def beer(\n        self, ctx, user: Member = None, *, reason: commands.clean_content = None\n    ):\n        \"\"\"Have virtual beer with your friends/fellow members\"\"\"\n        if not user or user.id == ctx.author.id:\n            return await ctx.send(f\"{ctx.author.name}: paaaarty!:tada::beer:\")\n        if user.id == self.bot.user.id:\n            return await ctx.send(\"drinks beer with you* :beers:\")\n        if user.bot:\n            return await ctx.send(f\"lol {ctx.author.name}lol\")\n\n        beer_offer = f\"{user.name}, you got a :beer: offer from {ctx.author.name}\"\n        beer_offer = beer_offer + f\"\\n\\nReason: {reason}\" if reason else beer_offer\n        msg = await ctx.send(beer_offer)\n\n        def reaction_check(reaction, m):\n            return m.id == user.id and str(reaction.emoji) == \"🍻\"\n\n        try:\n            await msg.add_reaction(\"🍻\")\n            await self.bot.wait_for(\"reaction_add\", timeout=30.0, check=reaction_check)\n            await msg.edit(\n                content=f\"{user.name} and {ctx.author.name} are enjoying a lovely beer together :beers:\"\n            )\n        except asyncio.TimeoutError:\n            await msg.delete()\n            await ctx.send(\n                f\"well, doesn't seem like {user.name} wanted a beer with you {ctx.author.name} ;-;\"\n            )\n        except Forbidden:\n            beer_offer = f\"{user.name}, you got a :beer: from {ctx.author.name}\"\n            beer_offer = beer_offer + f\"\\n\\nReason: {reason}\" if reason else beer_offer\n            await msg.edit(content=beer_offer)\n\n    @commands.command()\n    async def beers(\n        self,\n        ctx: commands.Context,\n        members: commands.Greedy[Member],\n        *,\n        reason: commands.clean_content = None,\n    ):\n        \"\"\"Invite a bunch of people to have beer\"\"\"\n        if not members:\n            return await ctx.send(\"You can't have beer with no other person!\")\n        for member in members:\n            if member.bot:\n                return await ctx.send(\"Beer with bots isn't exactly a thing...\")\n\n        message = (\n            \", \".join(m.display_name for m in members)\n            + \"\\nYou have been invited for beer \\U0001f37b by \"\n            + ctx.author.display_name\n            + ((\" Reason: \" + reason) if reason else \"\")\n        )\n\n        msg = await ctx.send(message)\n        await msg.add_reaction(\"\\U0001f37b\")\n\n        def check(r, m):\n            return m in members and r.message == msg and str(r.emoji) == \"\\U0001f37b\"\n\n        while True:\n            try:\n                r, _ = await self.bot.wait_for(\"reaction_add\", check=check, timeout=60)\n            except asyncio.TimeoutError:\n                return await msg.edit(\n                    content=\"Ouch, looks like not everyone wants beer now...\"\n                )\n            else:\n                if set(\n                    m.id for m in await r.message.reactions[0].users().flatten()\n                ).issuperset(m.id for m in members):\n                    content = (\n                        \", \".join(\n                            utils.escape_mentions(m.display_name) for m in members\n                        )\n                        + \", \"\n                        + utils.escape_mentions(ctx.author.display_name)\n                        + \" enjoy a lovely beer together \\U0001f37b\"\n                    )\n\n                    return await msg.edit(content=content)\n\n    @commands.command()\n    async def beerparty(\n        self, ctx: commands.Context, *, reason: commands.clean_content = None\n    ):\n        \"\"\"Openly allow anyone to join and enjoy in a beer party\"\"\"\n        reason = (\"\\nReason: \" + reason) if reason else \"\"\n        msg = await ctx.send(f\"Open invite to a beer party! {reason}\")\n        await msg.add_reaction(\"\\U0001f37b\")\n        await asyncio.sleep(20)\n        users = (\n            await (await ctx.channel.fetch_message(msg.id))\n            .reactions[0]\n            .users()\n            .flatten()\n        )\n        await ctx.send(\n            \", \".join(\n                [\n                    utils.escape_mentions(u.display_name)\n                    for u in users + ([] if ctx.author in users else [ctx.author])\n                    if not u.bot\n                ]\n            )\n            + \" enjoy a lovely beer paaarty \\U0001f37b\"\n        )\n\n\ndef setup(bot: TechStruckBot):\n    bot.add_cog(Fun(bot))\n"
  },
  {
    "path": "bot/cogs/github.py",
    "content": "import datetime\nimport re\nfrom io import BytesIO\nfrom typing import Optional\nfrom urllib.parse import urlencode\n\nfrom cachetools import TTLCache\nfrom discord import Color, Embed, File, Forbidden, Member\nfrom discord.ext import commands\nfrom jose import jwt\nfrom reportlab.graphics import renderPM\nfrom svglib.svglib import svg2rlg\n\nfrom bot.utils.process_files import process_files\nfrom config.common import config\nfrom config.oauth import github_oauth_config\nfrom models import UserModel\n\n\nclass GithubNotLinkedError(commands.CommandError):\n    def __str__(self):\n        return \"Your github account hasn't been linked yet, please use the `linkgithub` command to do it\"\n\n\nclass InvalidTheme(commands.CommandError):\n    def __str__(self):\n        return \"Not a valid theme. List of all valid themes:- default, dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula\"\n\n\nclass Github(commands.Cog):\n    \"\"\"Commands related to Github\"\"\"\n\n    def __init__(self, bot: commands.Bot):\n        self.bot = bot\n        self.themes = \"default dark radical merko gruvbox tokyonight onedark cobalt synthwave highcontrast dracula\".split()\n        self.files_regex = re.compile(r\"\\s{0,}```\\w{0,}\\s{0,}\")\n        self.token_cache = TTLCache(maxsize=1000, ttl=600)\n\n    @property\n    def session(self):\n        return self.bot.http._HTTPClient__session  # type: ignore\n\n    async def cog_before_invoke(self, ctx: commands.Context):\n        if ctx.command == self.link_github:\n            return\n\n        token = self.token_cache.get(ctx.author.id)\n        if not token:\n            user = await UserModel.get_or_none(id=ctx.author.id)\n            if user is None or user.github_oauth_token is None:\n                raise GithubNotLinkedError()\n            token = user.github_oauth_token\n            self.token_cache[ctx.author.id] = token\n        ctx.gh_token = token  # type: ignore\n\n    @commands.command(name=\"linkgithub\", aliases=[\"lngithub\"])\n    async def link_github(self, ctx: commands.Context):\n        \"\"\"Link your Github account through OAuth2 to gain access to Github related commands\"\"\"\n        expiry = datetime.datetime.utcnow() + datetime.timedelta(seconds=120)\n        url = \"https://github.com/login/oauth/authorize?\" + urlencode(\n            {\n                \"client_id\": github_oauth_config.client_id,\n                \"scope\": \"gist\",\n                \"redirect_uri\": \"https://tech-struck.vercel.app/oauth/github\",\n                \"state\": jwt.encode(\n                    {\"id\": ctx.author.id, \"expiry\": str(expiry)}, config.secret\n                ),\n            }\n        )\n        try:\n            await ctx.author.send(\n                embed=Embed(\n                    title=\"Connect Github\",\n                    description=f\"Click [this]({url}) to link your github account. This link invalidates in 2 minutes\",\n                )\n            )\n        except Forbidden:\n            await ctx.send(\n                \"Your DMs are closed. Open them so I can send you the authorization link.\"\n            )\n\n    @commands.group(name=\"gist\", aliases=[\"gs\"], invoke_without_command=True)\n    async def gist(self, ctx: commands.Context):\n        \"\"\"Commands related to Github gists\"\"\"\n        await ctx.send_help(self.gist)\n\n    @gist.command(name=\"create\", aliases=[\"cr\"])\n    async def create_gist(self, ctx: commands.Context, *, inp: Optional[str] = None):\n        \"\"\"\n        Create gists from within discord\n\n        Three ways to specify the files:\n        -   Reply to a message with attachments\n        -   Send attachments along with the command\n        -   Use a filename and codeblock... format\n\n        Example:\n\n        filename.py\n        ```\n        # Codeblock with contents of filename.py\n        ```\n\n        filename2.txt\n        ```\n        Codeblock containing filename2.txt's contents\n        ```\n        \"\"\"\n\n        files, skipped = await process_files(ctx, inp)\n\n        req = await self.github_request(ctx, \"POST\", \"/gists\", json={\"files\": files})\n\n        res = await req.json()\n        # TODO: Make this more verbose to the user and log errors\n        embed = Embed(\n            title=\"Gist creation\",\n            description=res.get(\"html_url\", \"Something went wrong.\"),\n        )\n        embed.add_field(name=\"Files\", value=\"\\n\".join(files.keys()), inline=False)\n        if skipped:\n            embed.add_field(\n                name=\"Skipped files\", value=\"\\n\".join(skipped), inline=False\n            )\n        await ctx.send(embed=embed)\n\n    @gist.command(name=\"list\", aliases=[\"ls\"])\n    async def list_gist(self, ctx: commands.Context):\n        \"\"\"\n        List 10 gists made by you\n        \"\"\"\n        req = await self.github_request(ctx, \"GET\", \"/gists\")\n\n        gists = (await req.json())[:10]\n        embed = Embed(title=\"Your gists\", color=Color.green())\n        description = \"\\n\\n\".join(\n            [\n                \"`{0[id]}`\\n[{name}]({0[html_url]})\".format(\n                    gist, name=next(iter(gist[\"files\"]))\n                )\n                for gist in gists\n            ]\n        )\n        embed.description = description\n        await ctx.send(embed=embed)\n\n    @gist.command(\"delete\", aliases=[\"del\", \"rm\", \"remove\"])\n    async def delete_gist(self, ctx: commands.Context, *, gist_id: str):\n        \"\"\"\n        Delete a gist using its ID\n        You can get the ID from the list\n        \"\"\"\n        req = await self.github_request(ctx, \"DELETE\", \"/gists/{}\".format(gist_id))\n        if req.status == 204:\n            return await ctx.send(\"Deleted\")\n        if req.status == 404:\n            return await ctx.send(\"Not found\")\n        if req.status == 403:\n            return await ctx.send(\"Forbidden\")\n\n    @commands.command(name=\"githubsearch\", aliases=[\"ghsearch\", \"ghse\"])\n    async def github_search(self, ctx: commands.Context, *, term: str):\n        \"\"\"\n        Search through all public repositories in Github\n\n        Github search filters work here\n        eg `ghse user:FalseDev`\n        \"\"\"\n        # TODO: Docs\n\n        req = await self.github_request(\n            ctx, \"GET\", \"/search/repositories\", dict(q=term, per_page=5)\n        )\n\n        data = await req.json()\n        if not data[\"items\"]:\n            return await ctx.send(\n                embed=Embed(\n                    title=f\"Searched for {term}\",\n                    color=Color.red(),\n                    description=\"No results found\",\n                )\n            )\n\n        em = Embed(\n            title=f\"Searched for {term}\",\n            color=Color.green(),\n            description=\"\\n\\n\".join(\n                [\n                    \"[{0[owner][login]}/{0[name]}]({0[html_url]})\\n{0[stargazers_count]:,} :star:\\u2800{0[forks_count]} \\u2387\\u2800\\n{1}\".format(\n                        result, self.repo_desc_format(result)\n                    )\n                    for result in data[\"items\"]\n                ]\n            ),\n        )\n\n        await ctx.send(embed=em)\n\n    @commands.command(name=\"githubstats\", aliases=[\"ghstats\", \"ghst\"])\n    async def github_stats(\n        self, ctx: commands.Context, username: str = None, theme=\"radical\"\n    ):\n        \"\"\"View statistics about you/any Github user in various themes\"\"\"\n        theme = self.process_theme(theme)\n\n        url = \"https://github-readme-stats.codestackr.vercel.app/api\"\n\n        username = username or await self.get_gh_user(ctx)\n\n        file = await self.get_file_from_svg_url(\n            url,\n            params={\n                \"username\": username,\n                \"show_icons\": \"true\",\n                \"hide_border\": \"true\",\n                \"theme\": theme,\n            },\n            exclude=[b\"A++\", b\"A+\"],\n        )\n        await ctx.send(file=File(file, filename=\"stats.png\"))\n\n    @commands.command(name=\"githublanguages\", aliases=[\"ghlangs\", \"ghtoplangs\"])\n    async def github_top_languages(\n        self, ctx: commands.Context, username: str = None, theme: str = \"radical\"\n    ):\n        \"\"\"View language usage statistics for you/any github user in various themes\"\"\"\n\n        username = username or await self.get_gh_user(ctx)\n        theme = self.process_theme(theme)\n        url = \"https://github-readme-stats.codestackr.vercel.app/api/top-langs/\"\n\n        file = await self.get_file_from_svg_url(\n            url, params={\"username\": username, \"theme\": theme}\n        )\n        await ctx.send(file=File(file, filename=\"langs.png\"))\n\n    async def get_file_from_svg_url(\n        self, url: str, *, params={}, exclude=[], fmt=\"PNG\"\n    ):\n        res = await (await self.session.get(url, params=params)).content.read()\n        for i in exclude:\n            res = res.replace(\n                i, b\"\"\n            )  # removes everything that needs to be excluded (eg. the uncentered A+)\n        drawing = svg2rlg(BytesIO(res))\n        file = BytesIO(renderPM.drawToString(drawing, fmt=fmt))\n        return file\n\n    def process_theme(self, theme):\n        theme = theme.lower()\n        if theme not in self.themes:\n            raise InvalidTheme()\n        return theme\n\n    @staticmethod\n    def repo_desc_format(result):\n        description = result[\"description\"]\n        if not description:\n            return \"\"\n        return description if len(description) < 100 else (description[:100] + \"...\")\n\n    async def github_request(\n        self,\n        ctx: commands.Context,\n        req_type: str,\n        endpoint: str,\n        params: dict = None,\n        json: dict = None,\n    ):\n        return await self.session.request(\n            req_type,\n            f\"https://api.github.com{endpoint}\",\n            params=params,\n            json=json,\n            headers={\"Authorization\": f\"Bearer {ctx.gh_token}\"},\n        )\n\n    async def get_gh_user(self, ctx: commands.Context):\n        response = await (await self.github_request(ctx, \"GET\", \"/user\")).json()\n        return response.get(\"login\")\n\n\ndef setup(bot: commands.Bot):\n    bot.add_cog(Github(bot))\n"
  },
  {
    "path": "bot/cogs/help_command.py",
    "content": "import discord\nfrom discord.ext import commands\n\nbot_links = \"\"\"[Support](https://discord.gg/KgZRMch3b6)\\u2800\\\n[Github](https://github.com/FalseDev/Tech-struck)\\u2800\\\n[Suggestions](https://github.com/FalseDev/Tech-struck/issues)\"\"\"\n\n\nclass HelpCommand(commands.HelpCommand):\n    \"\"\"\n    An Embed help command\n    Based on https://gist.github.com/Rapptz/31a346ed1eb545ddeb0d451d81a60b3b\n    \"\"\"\n\n    COLOUR = discord.Colour.greyple()\n\n    def get_ending_note(self):\n        return \"Use {0}{1} [command] for more info on a command.\".format(\n            self.clean_prefix, self.invoked_with\n        )\n\n    def get_command_signature(self, command):\n        return \"{0.qualified_name} {0.signature}\".format(command)\n\n    async def send_bot_help(self, mapping):\n        embed = discord.Embed(title=\"Bot Commands\", colour=self.COLOUR)\n        description = self.context.bot.description\n        if description:\n            embed.description = description\n\n        for cog, cmds in mapping.items():\n            if cog is None:\n                continue\n            name = cog.qualified_name\n            filtered = await self.filter_commands(cmds, sort=True)\n            if filtered:\n                value = \"\\u2002\".join(f\"`{c.name}`\" for c in cmds)\n                if cog and cog.description:\n                    value = \"{0}\\n{1}\".format(cog.description, value)\n\n                embed.add_field(name=name, value=value)\n\n        embed.set_footer(text=self.get_ending_note())\n        self.add_support_server(embed)\n        await self.get_destination().send(embed=embed)\n\n    async def send_cog_help(self, cog):\n        embed = discord.Embed(\n            title=\"{0.qualified_name} Commands\".format(cog), colour=self.COLOUR\n        )\n        if cog.description:\n            embed.description = cog.description\n\n        filtered = await self.filter_commands(cog.get_commands(), sort=True)\n        for command in filtered:\n            embed.add_field(\n                name=command.qualified_name,\n                value=command.short_doc or \"...\",\n                inline=False,\n            )\n\n        embed.set_footer(text=self.get_ending_note())\n        self.add_support_server(embed)\n        await self.get_destination().send(embed=embed)\n\n    async def send_group_help(self, group):\n        embed = discord.Embed(title=group.qualified_name, colour=self.COLOUR)\n        if group.help:\n            embed.description = group.help\n\n        filtered = await self.filter_commands(group.commands, sort=True)\n        for command in filtered:\n            embed.add_field(\n                name=command.qualified_name,\n                value=command.short_doc or \"...\",\n                inline=False,\n            )\n\n        embed.set_footer(text=self.get_ending_note())\n        self.add_support_server(embed)\n        await self.get_destination().send(embed=embed)\n\n    def add_support_server(self, embed):\n        return embed.add_field(name=\"Links\", value=bot_links, inline=False)\n\n    async def send_command_help(self, command):\n        embed = discord.Embed(title=command.qualified_name, colour=self.COLOUR)\n        embed.add_field(name=\"Signatute\", value=self.get_command_signature(command))\n        if command.help:\n            embed.description = command.help\n\n        embed.set_footer(text=self.get_ending_note())\n        self.add_support_server(embed)\n        await self.get_destination().send(embed=embed)\n\n\ndef setup(bot: commands.Bot):\n    bot._default_help_command = bot.help_command\n    bot.help_command = HelpCommand()\n\n\ndef teardown(bot):\n    bot.help_command = bot._default_help_command\n"
  },
  {
    "path": "bot/cogs/joke.py",
    "content": "import asyncio\n\nfrom discord import Color, Embed, Message, RawReactionActionEvent, TextChannel, utils\nfrom discord.ext import commands\n\nfrom models import JokeModel, UserModel\n\njoke_format = \"\"\"**Setup**: {0.setup}\\n\n**End**: {0.end}\\n\n**Server**: {1.name} (`{1.id}`)\\n\n**Username**: {2} (`{2.id}`)\\n\nJoke ID: {0.id}\"\"\"\n\n\nclass Joke(commands.Cog):\n    \"\"\"Joke related commands\"\"\"\n\n    def __init__(self, bot: commands.Bot):\n        self.bot = bot\n\n    @commands.group(invoke_without_command=True)\n    async def joke(self, ctx: commands.Context):\n        \"\"\"Joke commands\"\"\"\n        await ctx.send_help(self.joke)\n\n    @joke.command()\n    @commands.cooldown(1, 60, type=commands.BucketType.user)\n    async def add(self, ctx: commands.Context):\n        \"\"\"Submit a joke that can then get approved and part of the collection\"\"\"\n        try:\n            setup = await self._get_input(\n                ctx,\n                \"Enter joke setup\",\n                \"Enter the question/setup to be done before answering/finishing the joke\",\n            )\n            end = await self._get_input(\n                ctx, \"Enter joke end\", \"Enter the text to be used to finish the joke\"\n            )\n        except asyncio.TimeoutError:\n            return await ctx.send(\"You didn't answer\")\n\n        await UserModel.get_or_create(id=ctx.author.id)\n        joke = await JokeModel.create(setup=setup, end=end, creator_id=ctx.author.id)\n\n        msg = await self.joke_entries_channel.send(\n            embed=Embed(\n                title=f\"Joke #{joke.id}\",\n                description=joke_format.format(joke, ctx.guild, ctx.author),\n                color=Color.dark_gold(),\n            )\n        )\n        await ctx.send(\"Your submission has been recorded!\")\n\n        await msg.add_reaction(\"\\u2705\")\n        await msg.add_reaction(\"\\u274e\")\n        await self.joke_entries_channel.send(\"<@&815237052639477792>\", delete_after=1)\n\n    @property\n    def joke_entries_channel(self) -> TextChannel:\n        return self.bot.get_channel(815237244218114058)\n\n    async def _get_input(self, ctx: commands.Context, title: str, description: str):\n        await ctx.send(\n            embed=Embed(title=title, description=description, color=Color.dark_blue())\n        )\n\n        def check(m: Message):\n            return m.author == ctx.author and m.channel == ctx.channel\n\n        res: Message = await self.bot.wait_for(\"message\", check=check, timeout=120)\n        return await commands.clean_content().convert(ctx, res.content)\n\n    @commands.Cog.listener(\"on_raw_reaction_add\")\n    @commands.Cog.listener(\"on_raw_reaction_remove\")\n    async def reaction_listener(self, payload: RawReactionActionEvent):\n        if payload.channel_id != 815237244218114058:\n            return\n        msg: Message = await self.joke_entries_channel.fetch_message(payload.message_id)\n\n        up_reaction = utils.get(msg.reactions, emoji=\"\\u2705\")\n        down_reaction = utils.get(msg.reactions, emoji=\"\\u274e\")\n        ups = (up_reaction and await up_reaction.users().flatten()) or []\n        # downs = (down_reaction and await up_reaction.users().flatten()) or []\n        # TODO: Add further stuff here for downvotes checking etc\n\n        embed = msg.embeds[0]\n        if len(ups) > 3:\n            await JokeModel.filter(id=int(embed.title[6:])).update(accepted=True)\n            embed.color = Color.green()\n            await msg.edit(embed=embed)\n\n\ndef setup(bot: commands.Bot):\n    bot.add_cog(Joke(bot))\n"
  },
  {
    "path": "bot/cogs/packages.py",
    "content": "from aiohttp import ContentTypeError\nfrom discord import Color, Embed\nfrom discord.ext.commands import Cog, Context, command\n\nfrom ..bot import TechStruckBot\n\nclass Packages(Cog):\n    \"\"\"Commands related to Package Search\"\"\"\n\n    def __init__(self, bot: TechStruckBot):\n        self.bot = bot\n\n    @property\n    def session(self):\n        return self.bot.session\n\n    async def get_package(self, url: str):\n        return await self.session.get(url=url)\n\n    @command(aliases=[\"pypi\"])\n    async def pypisearch(self, ctx: Context, arg: str):\n        \"\"\"Get info about a Python package directly from PyPi\"\"\"\n\n        res_raw = await self.get_package(f\"https://pypi.org/pypi/{arg}/json\")\n\n        try:\n            res_json = await res_raw.json()\n        except ContentTypeError:\n            return await ctx.send(\n                embed=Embed(\n                    description=\"No such package found in the search query.\",\n                    color=Color.blurple(),\n                )\n            )\n\n        res = res_json[\"info\"]\n\n        def getval(key):\n            return res[key] or \"Unknown\"\n\n        name = getval(\"name\")\n        author = getval(\"author\")\n        author_email = getval(\"author_email\")\n\n        description = getval(\"summary\")\n        home_page = getval(\"home_page\")\n\n        project_url = getval(\"project_url\")\n        version = getval(\"version\")\n        _license = getval(\"license\")\n\n        embed = Embed(\n            title=f\"{name} PyPi Stats\", description=description, color=Color.teal()\n        )\n\n        embed.add_field(name=\"Author\", value=author, inline=True)\n        embed.add_field(name=\"Author Email\", value=author_email, inline=True)\n\n        embed.add_field(name=\"Version\", value=version, inline=False)\n        embed.add_field(name=\"License\", value=_license, inline=True)\n\n        embed.add_field(name=\"Project Url\", value=project_url, inline=False)\n        embed.add_field(name=\"Home Page\", value=home_page)\n\n        embed.set_thumbnail(url=\"https://i.imgur.com/syDydkb.png\")\n\n        await ctx.send(embed=embed)\n\n    @command(aliases=[\"npm\"])\n    async def npmsearch(self, ctx: Context, arg: str):\n        \"\"\"Get info about a NPM package directly from the NPM Registry\"\"\"\n\n        res_raw = await self.get_package(f\"https://registry.npmjs.org/{arg}/\")\n\n        res_json = await res_raw.json()\n\n        if res_json.get(\"error\"):\n            return await ctx.send(\n                embed=Embed(\n                    description=\"No such package found in the search query.\",\n                    color=0xCC3534,\n                )\n            )\n\n        latest_version = res_json[\"dist-tags\"][\"latest\"]\n        latest_info = res_json[\"versions\"][latest_version]\n\n        def getval(*keys):\n            keys = list(keys)\n            val = latest_info.get(keys.pop(0)) or {}\n\n            if keys:\n                for i in keys:\n                    try:\n                        val = val.get(i)\n                    except TypeError:\n                        return \"Unknown\"\n\n            return val or \"Unknown\"\n\n        pkg_name = getval(\"name\")\n        description = getval(\"description\")\n\n        author = getval(\"author\", \"name\")\n        author_email = getval(\"author\", \"email\")\n\n        repository = (\n            getval(\"repository\", \"url\").removeprefix(\"git+\").removesuffix(\".git\")\n        )\n\n        homepage = getval(\"homepage\")\n        _license = getval(\"license\")\n\n        em = Embed(\n            title=f\"{pkg_name} NPM Stats\", description=description, color=0xCC3534\n        )\n\n        em.add_field(name=\"Author\", value=author, inline=True)\n        em.add_field(name=\"Author Email\", value=author_email, inline=True)\n\n        em.add_field(name=\"Latest Version\", value=latest_version, inline=False)\n        em.add_field(name=\"License\", value=_license, inline=True)\n\n        em.add_field(name=\"Repository\", value=repository, inline=False)\n        em.add_field(name=\"Homepage\", value=homepage, inline=True)\n\n        em.set_thumbnail(\n            url=\"https://upload.wikimedia.org/wikipedia/commons/thumb/d/db/Npm-logo.svg/800px-Npm-logo.svg.png\"\n        )\n\n        await ctx.send(embed=em)\n\n    @command(aliases=[\"crates\"])\n    async def crate(self, ctx: Context, arg: str):\n        \"\"\"Get info about a Rust package directly from the Crates.IO Registry\"\"\"\n\n        res_raw = await self.get_package(f\"https://crates.io/api/v1/crates/{arg}\")\n\n        res_json = await res_raw.json()\n\n        if res_json.get(\"errors\"):\n            return await ctx.send(\n                embed=Embed(\n                    description=\"No such package found in the search query.\",\n                    color=0xE03D29,\n                )\n            )\n        main_info = res_json[\"crate\"]\n        latest_info = res_json[\"versions\"][0]\n\n        def getmainval(key):\n            return main_info[key] or \"Unknown\"\n\n        def getversionvals(*keys):\n            keys = list(keys)\n            val = latest_info.get(keys.pop(0)) or {}\n\n            if keys:\n                for i in keys:\n                    try:\n                        val = val.get(i)\n                    except TypeError:\n                        return \"Unknown\"\n\n            return val or \"Unknown\"\n\n        pkg_name = getmainval(\"name\")\n        description = getmainval(\"description\")\n        downloads = getmainval(\"downloads\")\n\n        publisher = getversionvals(\"published_by\", \"name\")\n        latest_version = getversionvals(\"num\")\n        repository = getmainval(\"repository\")\n\n        homepage = getmainval(\"homepage\")\n        _license = getversionvals(\"license\")\n\n        em = Embed(\n            title=f\"{pkg_name} crates.io Stats\", description=description, color=0xE03D29\n        )\n\n        em.add_field(name=\"Published By\", value=publisher, inline=True)\n        em.add_field(name=\"Downloads\", value=\"{:,}\".format(downloads), inline=True)\n\n        em.add_field(name=\"Latest Version\", value=latest_version, inline=False)\n        em.add_field(name=\"License\", value=_license, inline=True)\n\n        em.add_field(name=\"Repository\", value=repository, inline=False)\n        em.add_field(name=\"Homepage\", value=homepage, inline=True)\n\n        em.set_thumbnail(\n            url=\"https://upload.wikimedia.org/wikipedia/commons/thumb/d/d5/Rust_programming_language_black_logo.svg/2048px-Rust_programming_language_black_logo.svg.png\"\n        )\n\n        await ctx.send(embed=em)\n\ndef setup(bot: TechStruckBot):\n    bot.add_cog(Packages(bot))\n"
  },
  {
    "path": "bot/cogs/quiz.py",
    "content": "from discord import Color, Embed, Message\nfrom discord.ext import commands\nfrom quizapi import create_quiz_api\n\nfrom config.bot import bot_config\n\n\nclass Quiz(commands.Cog):\n    def __init__(self, bot: commands.Bot):\n        self.bot = bot\n        self.session = create_quiz_api(bot_config.quiz_api_token, async_mode=True)\n\n    @commands.command()\n    async def startquiz(self, ctx: commands.Context):\n        await ctx.send(\"Collecting questions!\")\n        questions = await self.session.get_quiz(limit=5, category=\"linux\")\n        embed = Embed(title=\"Big Brain Time\", color=Color.darker_gray())\n\n        def check(m: Message):\n            return m.channel == ctx.channel\n\n        scoreboard = {}\n        for q in questions:\n            embed.clear_fields()\n            desc = q.description + \"\\n\" if q.description else \"\"\n            desc += \" \".join([\"`\" + t.name + \"`\" for t in q.tags]) + \"\\n\"\n            for i, a in enumerate(q.answers, 65):\n                desc += chr(i) + \") \" + a + \"\\n\"\n            embed.add_field(name=q.question, value=desc)\n            correct_answers = []\n            print(q.correct_answers)\n            for i in range(q.correct_answers.count(True)):\n                correct_answers.append(chr(65 + q.correct_answers.index(True)))\n                q.correct_answers.remove(True)\n                print(correct_answers)\n            await ctx.send(embed=embed)\n            unanswered = True\n            while unanswered:\n                try:\n                    resp = await self.bot.wait_for(\"message\", check=check, timeout=45)\n                except:\n                    return await ctx.send(\"No one answered\")\n                # await resp.delete()\n                if resp.content.upper() in (correct_answers):\n                    scoreboard[resp.author.id] = scoreboard.get(resp.author.id, 0) + 1\n                    unanswered = False\n        scores = \"\\n\".join(\n            [\n                f\"<@!{mid}>: {score}\"\n                for mid, score in sorted(scoreboard.items(), key=lambda i: i[1])\n            ]\n        )\n        await ctx.send(\n            embed=Embed(title=\"Results\", description=scores, color=Color.green())\n        )\n\n\ndef setup(bot: commands.Bot):\n    bot.add_cog(Quiz(bot))\n"
  },
  {
    "path": "bot/cogs/rtfm.py",
    "content": "import warnings\n\nimport aiohttp\nfrom discord import Color, Embed\nfrom discord.ext import commands, flags\n\nfrom bot.bot import TechStruckBot\nfrom bot.utils import fuzzy, rtfm\n\n\nclass RTFM(commands.Cog):\n    \"\"\"Search through manuals of several python modules and python itself\"\"\"\n\n    targets = {\n        \"python\": \"https://docs.python.org/3\",\n        \"discord.py\": \"https://discordpy.readthedocs.io/en/latest\",\n        \"numpy\": \"https://numpy.readthedocs.io/en/latest\",\n        \"pandas\": \"https://pandas.pydata.org/docs\",\n        \"pillow\": \"https://pillow.readthedocs.io/en/stable\",\n        \"imageio\": \"https://imageio.readthedocs.io/en/stable\",\n        \"requests\": \"https://requests.readthedocs.io/en/master\",\n        \"aiohttp\": \"https://docs.aiohttp.org/en/stable\",\n        \"django\": \"https://django.readthedocs.io/en/stable\",\n        \"flask\": \"https://flask.palletsprojects.com/en/1.1.x\",\n        \"praw\": \"https://praw.readthedocs.io/en/latest\",\n        \"apraw\": \"https://apraw.readthedocs.io/en/latest\",\n        \"asyncpg\": \"https://magicstack.github.io/asyncpg/current\",\n        \"aiosqlite\": \"https://aiosqlite.omnilib.dev/en/latest\",\n        \"sqlalchemy\": \"https://docs.sqlalchemy.org/en/14\",\n        \"tensorflow\": \"https://www.tensorflow.org/api_docs/python\",\n        \"matplotlib\": \"https://matplotlib.org/stable\",\n        \"seaborn\": \"https://seaborn.pydata.org\",\n        \"pygame\": \"https://www.pygame.org/docs\",\n        \"simplejson\": \"https://simplejson.readthedocs.io/en/latest\",\n        \"wikipedia\": \"https://wikipedia.readthedocs.io/en/latest\",\n    }\n\n    aliases = {\n        (\"py\", \"py3\", \"python3\", \"python\"): \"python\",\n        (\"dpy\", \"discord.py\", \"discordpy\"): \"discord.py\",\n        (\"np\", \"numpy\", \"num\"): \"numpy\",\n        (\"pd\", \"pandas\", \"panda\"): \"pandas\",\n        (\"pillow\", \"pil\"): \"pillow\",\n        (\"imageio\", \"imgio\", \"img\"): \"imageio\",\n        (\"requests\", \"req\"): \"requests\",\n        (\"aiohttp\", \"http\"): \"aiohttp\",\n        (\"django\", \"dj\"): \"django\",\n        (\"flask\", \"fl\"): \"flask\",\n        (\"reddit\", \"praw\", \"pr\"): \"praw\",\n        (\"asyncpraw\", \"apraw\", \"apr\"): \"apraw\",\n        (\"asyncpg\", \"pg\"): \"asyncpg\",\n        (\"aiosqlite\", \"sqlite\", \"sqlite3\", \"sqli\"): \"aiosqlite\",\n        (\"sqlalchemy\", \"sql\", \"alchemy\", \"alchem\"): \"sqlalchemy\",\n        (\"tensorflow\", \"tf\"): \"tensorflow\",\n        (\"matplotlib\", \"mpl\", \"plt\"): \"matplotlib\",\n        (\"seaborn\", \"sea\"): \"seaborn\",\n        (\"pygame\", \"pyg\", \"game\"): \"pygame\",\n        (\"simplejson\", \"sjson\", \"json\"): \"simplejson\",\n        (\"wiki\", \"wikipedia\"): \"wikipedia\",\n    }\n\n    url_overrides = {\n        \"tensorflow\": \"https://github.com/mr-ubik/tensorflow-intersphinx/raw/master/tf2_py_objects.inv\"\n    }\n\n    def __init__(self, bot: TechStruckBot) -> None:\n        self.bot = bot\n        self.cache = {}\n\n    @property\n    def session(self) -> aiohttp.ClientSession:\n        return self.bot.http._HTTPClient__session  # type: ignore\n\n    async def build(self, target) -> None:\n        url = self.targets[target]\n        req = await self.session.get(\n            self.url_overrides.get(target, url + \"/objects.inv\")\n        )\n        if req.status != 200:\n            warnings.warn(\n                Warning(\n                    f\"Received response with status code {req.status} when trying to build RTFM cache for {target} through {url}/objects.inv\"\n                )\n            )\n            raise commands.CommandError(\"Failed to build RTFM cache\")\n        self.cache[target] = rtfm.SphinxObjectFileReader(\n            await req.read()\n        ).parse_object_inv(url)\n\n    @commands.group(invoke_without_command=True)\n    async def rtfm(self, ctx: commands.Context, doc: str, *, term: str = None):\n        \"\"\"\n        Search through docs of a module/python\n        Args: target, term\n        \"\"\"\n        doc = doc.lower()\n        target = None\n        for aliases, target_name in self.aliases.items():\n            if doc in aliases:\n                target = target_name\n\n        if not target:\n            return await ctx.reply(\"Alias/target not found\")\n        if not term:\n            return await ctx.reply(self.targets[target])\n\n        cache = self.cache.get(target)\n        if not cache:\n            await ctx.trigger_typing()\n            await self.build(target)\n            cache = self.cache.get(target)\n\n        results = fuzzy.finder(term, list(cache.items()), key=lambda x: x[0], lazy=False)[:8]  # type: ignore\n\n        if not results:\n            return await ctx.reply(\"Couldn't find any results\")\n\n        await ctx.reply(\n            embed=Embed(\n                title=f\"Searched in {target}\",\n                description=\"\\n\".join([f\"[`{key}`]({url})\" for key, url in results]),\n                color=Color.dark_purple(),\n            )\n        )\n\n    @rtfm.command(name=\"list\")\n    async def list_targets(self, ctx: commands.Context):\n        \"\"\"List all the avaliable documentation search targets\"\"\"\n        aliases = {v: k for k, v in self.aliases.items()}\n        embed = Embed(title=\"RTFM list of avaliable modules\", color=Color.green())\n        embed.description = \"\\n\".join(\n            [\n                \"[{0}]({1}): {2}\".format(\n                    target,\n                    link,\n                    \"\\u2800\".join([f\"`{i}`\" for i in aliases[target] if i != target]),\n                )\n                for target, link in self.targets.items()\n            ]\n        )\n\n        await ctx.send(embed=embed)\n\n    @flags.add_flag(\"aliases\", nargs=\"+\")\n    @flags.add_flag(\"url\")\n    @flags.add_flag(\"name\")\n    @flags.add_flag(\"--override\", \"-o\")\n    @rtfm.command(name=\"add\", hidden=True, cls=flags.FlagCommand)\n    @commands.is_owner()\n    async def add_target(self, ctx: commands.Context, **kwargs):\n        print(kwargs)\n        name, url, aliases, override = (\n            kwargs.pop(\"name\"),\n            kwargs.pop(\"url\"),\n            kwargs.pop(\"aliases\"),\n            kwargs.pop(\"override\"),\n        )\n        print(name, url, aliases, override)\n        self.targets[name] = url\n        self.aliases[tuple(aliases)] = name\n        if override:\n            self.url_overrides[name] = override\n\n        await ctx.send(\n            \"RTFM target {name} added with aliases {aliases}\".format(\n                name=name, aliases=aliases\n            )\n        )\n\n\ndef setup(bot: TechStruckBot):\n    bot.add_cog(RTFM(bot))\n"
  },
  {
    "path": "bot/cogs/stackexchange.py",
    "content": "import datetime\nimport html\nimport json\nimport os\nimport traceback\nfrom typing import Optional\nfrom urllib.parse import urlencode\n\nfrom cachetools import TTLCache\nfrom discord import Color, Embed, Forbidden, Member\nfrom discord.ext import commands, flags, tasks\nfrom jose import jwt\n\nfrom bot.utils import fuzzy\nfrom config.common import config\nfrom config.oauth import stack_oauth_config\nfrom models import UserModel\n\nsearch_result_template = \"[View]({site[site_url]}/q/{q[question_id]})\\u2800\\u2800Score: {q[score]}\\u2800\\u2800Tags: {tags}\"\n\n\nclass StackExchangeNotLinkedError(commands.CommandError):\n    def __str__(self):\n        return \"Your stackexchange account hasn't been linked yet, please use the `linkstack` command to do it\"\n\n\nclass StackExchangeError(commands.CommandError):\n    pass\n\n\nclass Stackexchange(commands.Cog):\n    \"\"\"Commands related to the StackExchange network\"\"\"\n\n    def __init__(self, bot: commands.Bot):\n        self.bot = bot\n        self.ready = False\n        self.sites = None\n        self.token_cache = TTLCache(maxsize=1000, ttl=600)\n        self.load_sites.start()\n\n    @property\n    def session(self):\n        return self.bot.http._HTTPClient__session\n\n    @tasks.loop(count=1)\n    async def load_sites(self):\n        if os.path.isfile(\"cache/stackexchange_sites.json\"):\n            with open(\"cache/stackexchange_sites.json\") as f:\n                self.sites = json.load(f)\n        else:\n            try:\n                data = await self.stack_request(\n                    None,\n                    \"GET\",\n                    \"/sites\",\n                    params={\"pagesize\": \"500\", \"filter\": \"*Ids4-aWV*RW_UxCPr0D\"},\n                )\n            except Exception:\n                return traceback.print_exc()\n            else:\n                self.sites = data[\"items\"]\n                if not os.path.isdir(\"cache\"):\n                    os.mkdir(\"cache\")\n                with open(\"cache/stackexchange_sites.json\", \"w\") as f:\n                    json.dump(self.sites, f)\n\n        self.ready = True\n\n    async def cog_check(self, ctx: commands.Context):\n        if not self.ready:\n            raise StackExchangeError(\"Stackexchange commands are not ready yet\")\n        return True\n\n    async def cog_before_invoke(self, ctx: commands.Context):\n        if ctx.command == self.link_stackoverflow:\n            return\n\n        token = self.token_cache.get(ctx.author.id)\n        if not token:\n            user = await UserModel.get_or_none(id=ctx.author.id)\n            if user is None or user.stackoverflow_oauth_token is None:\n                raise StackExchangeNotLinkedError()\n\n            token = user.stackoverflow_oauth_token\n            self.token_cache[ctx.author.id] = token\n        ctx.stack_token = token  # type: ignore\n\n    @flags.add_flag(\"--site\", type=str, default=\"stackoverflow\")\n    @flags.command(\n        name=\"stackprofile\",\n        aliases=[\"stackpro\", \"stackacc\", \"stackaccount\"],\n    )\n    async def stack_profile(self, ctx: commands.Context, **kwargs):\n        \"\"\"Check your stackoverflow reputation\"\"\"\n        # TODO: Use a stackexchange filter here\n        # https://api.stackexchange.com/docs/filters\n        site = self.get_site(kwargs[\"site\"])\n        data = await self.stack_request(\n            ctx,\n            \"GET\",\n            \"/me\",\n            data={\n                \"site\": site[\"api_site_parameter\"],\n            },\n        )\n        if not data[\"items\"]:\n            return await ctx.send(\"You don't have an account in this site!\")\n        profile = data[\"items\"][0]\n        embed = Embed(title=site[\"name\"] + \" Profile\", color=0x0077CC)\n\n        embed.add_field(name=\"Username\", value=profile[\"display_name\"], inline=False)\n        embed.add_field(name=\"Reputation\", value=profile[\"reputation\"], inline=False)\n        embed.add_field(\n            name=\"Badges\",\n            value=\"\\U0001f947 {0[gold]} \\u2502 \\U0001f948 {0[silver]} \\u2502 \\U0001f949 {0[bronze]}\".format(\n                profile[\"badge_counts\"]\n            ),\n            inline=False,\n        )\n\n        embed.set_thumbnail(url=profile[\"profile_image\"])\n        await ctx.send(embed=embed)\n\n    @flags.add_flag(\"--site\", type=str, default=\"stackoverflow\")\n    @flags.add_flag(\"--tagged\", type=str, nargs=\"+\", default=[])\n    @flags.add_flag(\"term\", nargs=\"+\")\n    @flags.command(name=\"stacksearch\", aliases=[\"stackser\"])\n    async def stackexchange_search(self, ctx: commands.Context, **kwargs):\n        \"\"\"Search stackexchange for your question\"\"\"\n        term, sitename, tagged = (\n            \" \".join(kwargs[\"term\"]),\n            kwargs[\"site\"],\n            kwargs[\"tagged\"],\n        )\n\n        site = self.get_site(sitename)\n\n        data = await self.stack_request(\n            ctx,\n            \"GET\",\n            \"/search/excerpts\",\n            data={\n                \"site\": sitename,\n                \"sort\": \"relevance\",\n                \"q\": term,\n                \"tagged\": \";\".join(tagged),\n                \"pagesize\": 5,\n                \"filter\": \"ld-5YXYGN1SK1e\",\n            },\n        )\n        embed = Embed(title=f\"{site['name']} search\", color=Color.green())\n        embed.set_thumbnail(url=site[\"icon_url\"])\n        if data[\"items\"]:\n            for i, q in enumerate(data[\"items\"], 1):\n                tags = \"\\u2800\".join([\"`\" + t + \"`\" for t in q[\"tags\"]])\n                embed.add_field(\n                    name=str(i) + \" \" + html.unescape(q[\"title\"]),\n                    value=search_result_template.format(site=site, q=q, tags=tags),\n                    inline=False,\n                )\n        else:\n            embed.add_field(name=\"Oops\", value=\"Couldn't find any results\")\n        await ctx.send(embed=embed)\n\n    @commands.command(aliases=[\"stacksites\"])\n    async def stacksite(self, ctx: commands.Context, *, term: str):\n        \"\"\"Search through list of stackexchange sites and find relevant ones\"\"\"\n        sites = fuzzy.finder(term, self.sites, key=lambda s: s[\"name\"], lazy=False)[:5]  # type: ignore\n        embed = Embed(color=Color.blue())\n        description = \"\\n\".join(\n            [\"[`{0[name]}`]({0[site_url]})\".format(site) for site in sites]\n        )\n        embed.description = description\n        await ctx.send(embed=embed)\n\n    def get_site(self, sitename: str):\n        sitename = sitename.lower()\n        for site in self.sites:\n            if site[\"api_site_parameter\"] == sitename:\n                return site\n        raise StackExchangeError(f\"Invalid site {sitename} provided\")\n\n    async def stack_request(\n        self,\n        ctx: Optional[commands.Context],\n        method: str,\n        endpoint: str,\n        params: dict = {},\n        data: dict = {},\n    ):\n        data.update(stack_oauth_config.dict())\n        if ctx:\n            data[\"access_token\"] = (ctx.stack_token,)\n        res = await self.session.request(\n            method,\n            f\"https://api.stackexchange.com/2.2{endpoint}\",\n            params=params,\n            data=data,\n        )\n\n        data = await res.json()\n        if \"error_message\" in data:\n            raise StackExchangeError(data[\"error_message\"])\n        return data\n\n    @commands.command(name=\"linkstack\", aliases=[\"lnstack\"])\n    async def link_stackoverflow(self, ctx: commands.Context):\n        \"\"\"Link your stackoverflow account\"\"\"\n        expiry = datetime.datetime.utcnow() + datetime.timedelta(seconds=120)\n        url = \"https://stackoverflow.com/oauth/?\" + urlencode(\n            {\n                \"client_id\": stack_oauth_config.client_id,\n                \"scope\": \"no_expiry\",\n                \"redirect_uri\": \"https://tech-struck.vercel.app/oauth/stackexchange\",\n                \"state\": jwt.encode(\n                    {\"id\": ctx.author.id, \"expiry\": str(expiry)}, config.secret\n                ),\n            }\n        )\n        try:\n            await ctx.author.send(\n                embed=Embed(\n                    title=\"Connect Stackexchange\",\n                    description=f\"Click [this]({url}) to link your stackexchange account. This link invalidates in 2 minutes\",\n                    color=Color.blue(),\n                )\n            )\n        except Forbidden:\n            await ctx.send(\n                \"Your DMs (direct messages) are closed. Open them so I can send you a safe authorization link.\"\n            )\n\n\ndef setup(bot: commands.Bot):\n    bot.add_cog(Stackexchange(bot))\n"
  },
  {
    "path": "bot/cogs/thank.py",
    "content": "import asyncio\nfrom typing import Optional\n\nfrom discord import Color, Embed, Member, Reaction\nfrom discord.ext import commands\nfrom tortoise.functions import Count, Q\n\nfrom models import ThankModel, UserModel\n\ndelete_thank_message = \"\"\"**Thanked**: <@!{0.thanked_id}>\n**Thanker**: <@!{0.thanker_id}>\n**Description**: {0.description}\n**Time**: {0.time}\\n\nConfirmation required!\"\"\"\n\nthank_list_message = \"\"\"`{0.time:%D %T}` ID:`{0.id}`\nFrom: <@!{0.thanker_id}> ({0.thanker_id})\nDescription: {0.description}\\n\"\"\"\n\n\nclass Thank(commands.Cog):\n    \"\"\"Commands related to thanking members/helpers for help received\"\"\"\n\n    def __init__(self, bot: commands.Bot):\n        self.bot = bot\n\n    @commands.group(invoke_without_command=True, aliases=[\"thanks\", \"rep\"])\n    @commands.cooldown(5, 300, commands.BucketType.user)\n    async def thank(self, ctx: commands.Context, recv: Member, *, description: str):\n        \"\"\"Thank someone for their help with a description to show gratitude\"\"\"\n        des_len = len(description)\n        if des_len < 5 or des_len > 100:\n            return await ctx.send(\n                f\"Thank description must be between 5 and 100 characters, yours was {des_len}\"\n            )\n        if recv.id == ctx.author.id:\n            return await ctx.send(\n                embed=Embed(\n                    title=\"Bruh\",\n                    description=\"You can't thank yourselves\",\n                    color=Color.red(),\n                )\n            )\n        if recv.bot:\n            return await ctx.send(\n                embed=Embed(\n                    title=\"Bruh\", description=\"You can't thank a bot\", color=Color.red()\n                )\n            )\n        # TODO: Convert this to an expression (?) for efficiency\n        thanked, _ = await UserModel.get_or_create(id=recv.id)\n        thanker, _ = await UserModel.get_or_create(id=ctx.author.id)\n        await ThankModel.create(\n            thanker=thanker,\n            thanked=thanked,\n            description=description,\n            guild_id=ctx.guild.id,\n        )\n        await ctx.send(\n            embed=Embed(\n                description=f\"You thanked {recv.mention}!\", color=0x6EFFFF\n            )\n        )\n\n    @thank.command(name=\"stats\", aliases=[\"check\"])\n    async def thank_stats(\n        self, ctx: commands.Context, *, member: Optional[Member] = None\n    ):\n        \"\"\"View stats for thanks you've received and sent, in the current server and globally\"\"\"\n        member = member or ctx.author\n        sent_thanks = await ThankModel.filter(thanker__id=member.id).count()\n        recv_thanks = await ThankModel.filter(thanked__id=member.id).count()\n        server_sent_thanks = await ThankModel.filter(\n            thanker__id=member.id, guild__id=ctx.guild.id\n        ).count()\n        server_recv_thanks = await ThankModel.filter(\n            thanked__id=member.id, guild__id=ctx.guild.id\n        ).count()\n\n        embed = Embed(title=f\"Thank stats for: {member}\", color=Color.green())\n        embed.add_field(\n            name=\"Thanks received\",\n            value=\"Global: {}\\nThis server: {}\".format(recv_thanks, server_recv_thanks),\n        )\n        embed.add_field(\n            name=\"Thanks sent\",\n            value=\"Global: {}\\nThis server: {}\".format(sent_thanks, server_sent_thanks),\n        )\n        await ctx.send(embed=embed)\n\n    @thank.command(name=\"leaderboard\", aliases=[\"lb\"])\n    async def thank_leaderboard(self, ctx: commands.Context):\n        \"\"\"View a leaderboard of top helpers in the current server\"\"\"\n        await ctx.trigger_typing()\n        lb = (\n            await UserModel.annotate(\n                thank_count=Count(\"thanks\", _filter=Q(thanks__guild_id=ctx.guild.id))\n            )\n            .filter(thank_count__gt=0)\n            .order_by(\"-thank_count\")\n            .limit(5)\n        )\n        if not lb:\n            return await ctx.send(\n                embed=Embed(\n                    title=\"Oopsy\",\n                    description=\"There are no thanks here yet!\",\n                    color=Color.red(),\n                )\n            )\n        invis = \"\\u2800\"\n        embed = Embed(\n            title=\"LeaderBoard\",\n            color=Color.blue(),\n            description=\"\\n\\n\".join(\n                [\n                    f\"**{m.thank_count} Thanks**{invis * (4 - len(str(m.thank_count)))}<@!{m.id}>\"\n                    for m in lb\n                ]\n            ),\n        )\n        await ctx.send(embed=embed)\n\n    @thank.command(name=\"delete\")\n    @commands.has_guild_permissions(kick_members=True)\n    async def delete_thank(self, ctx: commands.Context, thank_id: int):\n        \"\"\"Remove an invalid/fake thank record\"\"\"\n        thank = await ThankModel.get_or_none(pk=thank_id, guild_id=ctx.guild.id)\n        if not thank:\n            return await ctx.send(\"Thank with given ID not found\")\n        msg = await ctx.send(\n            embed=Embed(\n                title=\"Delete thank\",\n                description=delete_thank_message.format(thank),\n            )\n        )\n        await msg.add_reaction(\"\\u2705\")\n        await msg.add_reaction(\"\\u274e\")\n\n        def check(r: Reaction, u: Member):\n            return u.id == ctx.author.id and str(r.emoji) in (\"\\u2705\", \"\\u274e\")\n\n        try:\n            r, _ = await self.bot.wait_for(\"reaction_add\", check=check)\n        except asyncio.TimeoutError:\n            return await ctx.reply(\"Cancelled.\")\n        if str(r.emoji) == \"\\u2705\":\n            await thank.delete()\n            return await ctx.reply(\"Deleted.\")\n        return await ctx.reply(\"Cancelled.\")\n\n    @thank.command(name=\"list\")\n    @commands.has_guild_permissions(kick_members=True)\n    async def list_thanks(self, ctx: commands.Context, member: Member):\n        \"\"\"List the most recent 10 thanks received by a user in the current server\"\"\"\n        thanks = (\n            await ThankModel.filter(thanked_id=member.id, guild_id=ctx.guild.id)\n            .order_by(\"-time\")\n            .limit(10)\n        )\n\n        await ctx.send(\n            embed=Embed(\n                title=\"Listing\",\n                description=\"\\n\".join([thank_list_message.format(t) for t in thanks]),\n                color=Color.dark_blue(),\n            )\n        )\n\n\ndef setup(bot: commands.Bot):\n    bot.add_cog(Thank(bot))\n"
  },
  {
    "path": "bot/cogs/utils.py",
    "content": "import sys\nimport os\nimport inspect\n\nfrom discord import Embed, Message, TextChannel\nfrom discord.ext import commands, flags\n\nfrom bot.bot import TechStruckBot\nfrom bot.utils.embed_flag_input import (\n    allowed_mentions_input,\n    dict_to_allowed_mentions,\n    dict_to_embed,\n    embed_input,\n    process_message_mentions,\n    webhook_input,\n)\n\nflags._converters.CONVERTERS[\"Message\"] = commands.MessageConverter().convert\n\n\nasync def maybe_await(coro):\n    if not coro:\n        return\n    return await coro\n\n\nclass Utils(commands.Cog):\n    \"\"\"Utility commands\"\"\"\n\n    def __init__(self, bot: TechStruckBot):\n        self.bot = bot\n\n    @embed_input(all=True)\n    @allowed_mentions_input()\n    @webhook_input()\n    @flags.add_flag(\"--channel\", \"--in\", type=TextChannel, default=None)\n    @flags.add_flag(\"--message\", \"--msg\", \"-m\", default=None)\n    @flags.add_flag(\"--edit\", \"-e\", type=Message, default=None)\n    @flags.command(\n        brief=\"Send an embed with any fields, in any channel, with command line like arguments\"\n    )\n    @commands.has_guild_permissions(administrator=True)\n    @commands.bot_has_permissions(manage_webhooks=True, embed_links=True)\n    async def embed(self, ctx: commands.Context, **kwargs):\n        \"\"\"\n        Send an embed and its fully customizable\n        Default mention settings:\n            Users:      Enabled\n            Roles:      Disabled\n            Everyone:   Disabled\n        \"\"\"\n        embed = dict_to_embed(kwargs, author=ctx.author)\n        allowed_mentions = dict_to_allowed_mentions(kwargs)\n        message = process_message_mentions(kwargs.pop(\"message\"))\n\n        if kwargs.pop(\"webhook\"):\n            if edit_message := kwargs.pop(\"edit\"):\n                edit_message.close()\n            username, avatar_url = kwargs.pop(\"webhook_username\"), kwargs.pop(\n                \"webhook_avatar\"\n            )\n            if kwargs.pop(\"webhook_auto_author\"):\n                username, avatar_url = (\n                    username or ctx.author.display_name,\n                    avatar_url or ctx.author.avatar_url,\n                )\n            target = kwargs.pop(\"channel\") or ctx.channel\n            if name := kwargs.pop(\"webhook_new_name\"):\n                wh = await target.create_webhook(name=name)\n            elif name := kwargs.pop(\"webhook_name\"):\n                try:\n                    wh = next(\n                        filter(\n                            lambda wh: wh.name.casefold() == name.casefold(),\n                            await target.webhooks(),\n                        )\n                    )\n                except StopIteration:\n                    return await ctx.send(\n                        \"No pre existing webhook found with given name\"\n                    )\n            else:\n                return await ctx.send(\"No valid webhook identifiers provided\")\n            await wh.send(\n                message,\n                embed=embed,\n                allowed_mentions=allowed_mentions,\n                username=username,\n                avatar_url=avatar_url,\n            )\n            if kwargs.pop(\"webhook_dispose\"):\n                await wh.delete()\n            return await ctx.message.add_reaction(\"\\u2705\")\n\n        if edit := await maybe_await(kwargs.pop(\"edit\")):\n            if edit.author != ctx.guild.me:\n                return await ctx.send(\n                    f\"The target message wasn't sent by me! It was sent by {edit.author}\"\n                )\n            await edit.edit(\n                content=message, embed=embed, allowed_mentions=allowed_mentions\n            )\n        else:\n            target = kwargs.pop(\"channel\") or ctx\n            await target.send(message, embed=embed, allowed_mentions=allowed_mentions)\n        await ctx.message.add_reaction(\"\\u2705\")\n\n    @commands.command()\n    async def rawembed(self, ctx: commands.Context):\n        ref = ctx.message.reference\n        if not ref or not ref.message_id:\n            return await ctx.send(\"Reply to an message with an embed\")\n        message = ref.cached_message or await ctx.channel.fetch_message(ref.message_id)\n\n        if not message.embeds:\n            return await ctx.send(\"Message had no embeds\")\n        em = message.embeds[0]\n        description = \"```\" + str(em.to_dict()) + \"```\"\n        embed = Embed(description=description)\n        await ctx.reply(embed=embed)\n\n    @commands.command()\n    async def source(self, ctx: commands.Context, *, command=None):\n        \"\"\"Get the source code of the bot or the provided command.\"\"\"\n        if command is None:\n            return await ctx.send(\n                embed=Embed(\n                    description=f\"My source can be found [here](https://github.com/TechStruck/TechStruck-Bot)!\",\n                    color=0x8ADCED\n            )\n        )\n\n        if command == \"help\":\n            src = type(self.bot.help_command)\n            module = src.__module__\n            filename = inspect.getsourcefile(src)\n        else:\n            cmd = self.bot.get_command(command)\n            if cmd is None:\n                return await ctx.send(\n                    embed=Embed(\n                        description=\"No such command found.\",\n                        color=0x8ADCED\n                    )\n                )\n            \n            src = cmd.callback.__code__\n            module = cmd.callback.__module__\n            filename = src.co_filename\n\n        lines, firstline = inspect.getsourcelines(src)\n        lines = len(lines)\n        location = (\n            module.replace(\".\", \"/\") + \".py\"\n            if module.startswith(\"discord\")\n            else os.path.relpath(filename).replace(r\"\\\\\", \"/\")\n        )\n\n        url = f\"https://github.com/TechStruck/TechStruck-Bot/blob/main/{location}#L{firstline}-L{firstline+lines-1}\"\n        await ctx.send(\n            embed=Embed(\n                description=f\"Source of {command} can be found [here]({url}).\",\n                color=0x8ADCED\n            )\n        )\n\n\ndef setup(bot: TechStruckBot):\n    bot.add_cog(Utils(bot))\n\n\ndef teardown(bot: TechStruckBot):\n    del sys.modules[\"bot.utils.embed_flag_input\"]\n"
  },
  {
    "path": "bot/core.py",
    "content": "import platform\nimport sys\n\nimport psutil\nfrom discord import Color, Embed, NotFound\nfrom discord import __version__ as discord_version\nfrom discord.ext import commands\n\nfrom models import GuildModel\n\nfrom .bot import TechStruckBot\n\n\nclass Common(commands.Cog):\n    def __init__(self, bot: TechStruckBot):\n        self.bot = bot\n\n    @commands.command(aliases=[\"latency\"])\n    async def ping(self, ctx: commands.Context):\n        \"\"\"Check latency of the bot\"\"\"\n        latency = str(round(self.bot.latency * 1000, 1))\n        await ctx.send(\n            embed=Embed(title=\"Pong!\", description=f\"{latency}ms\", color=Color.blue())\n        )\n\n    @commands.command(aliases=[\"statistics\"])\n    async def stats(self, ctx: commands.Context):\n        \"\"\"Stats of the bot\"\"\"\n        users = len(self.bot.users)\n        guilds = len(self.bot.guilds)\n\n        embed = Embed(color=Color.dark_green())\n        fields = (\n            (\"Guilds\", guilds),\n            (\"Users\", users),\n            (\"System\", platform.release()),\n            (\n                \"Memory\",\n                \"{:.4} MB\".format(psutil.Process().memory_info().rss / 1024 ** 2),\n            ),\n            (\"Python version\", \".\".join([str(v) for v in sys.version_info[:3]])),\n            (\"Discord version\", discord_version),\n        )\n        for name, value in fields:\n            embed.add_field(name=name, value=str(value), inline=False)\n\n        embed.set_thumbnail(url=str(ctx.guild.me.avatar_url))\n\n        await ctx.send(embed=embed)\n\n    @commands.command(aliases=[\"re\"])\n    async def redo(self, ctx: commands.Context):\n        \"\"\"Reply to a message to rerun it if its a command, helps when you've made typos\"\"\"\n        ref = ctx.message.reference\n        if ref is None or ref.message_id is None:\n            return\n        try:\n            message = await ctx.channel.fetch_message(ref.message_id)\n        except NotFound:\n            return await ctx.reply(\"Couldn't find that message\")\n        if message.author != ctx.author:\n            return\n        await self.bot.process_commands(message)\n\n    @commands.command()\n    @commands.guild_only()\n    @commands.has_guild_permissions(manage_guild=True)\n    async def setprefix(self, ctx: commands.Context, *, prefix: str):\n        \"\"\"Set a custom prefix for the current server\"\"\"\n        if len(prefix) > 10:\n            return await ctx.send(\"Prefix too long, must be within 10 characters!\")\n        self.bot.prefix_cache[ctx.guild.id] = prefix\n        await GuildModel.filter(id=ctx.guild.id).update(prefix=prefix)\n        await ctx.send(f\"My prefix has been updated to `{prefix}`\")\n\n    @commands.command()\n    async def prefix(self, ctx: commands.Context):\n        \"\"\"View current prefix of bot\"\"\"\n        await ctx.send(\n            f\"My prefix here is `\"\n            + (self.bot.prefix_cache[ctx.guild.id] if ctx.guild else \".\")\n            + \"`\"\n        )\n\n    @commands.command()\n    async def invite(self, ctx: commands.Context):\n        embed = Embed(\n            title=\"Invite me!\",\n            description=\"[Click here](https://discord.com/api/oauth2/authorize?client_id=790474885804982293&permissions=0&scope=bot%20applications.commands) to add me to your server with no extra role!\",\n            color=Color.green(),\n        )\n        await ctx.send(embed=embed)\n\n\ndef setup(bot: TechStruckBot):\n    bot.add_cog(Common(bot))\n"
  },
  {
    "path": "bot/utils/embed_flag_input.py",
    "content": "import functools\nimport re\nfrom typing import Dict, Iterable, TypeVar, Union\nfrom urllib import parse\n\nfrom discord import AllowedMentions, Embed, Member, User\nfrom discord.ext import commands, flags  # type: ignore\n\n_F = TypeVar(\n    \"_F\",\n)\n\n\nclass InvalidFieldArgs(commands.CommandError):\n    pass\n\n\nclass EmbeyEmbedError(commands.CommandError):\n    def __str__(self) -> str:\n        return \"The embed has no fields/attributes populated\"\n\n\nclass InvalidUrl(commands.CommandError):\n    def __init__(self, invalid_url: str, *, https_only: bool = False) -> None:\n        self.invalid_url = invalid_url\n        self.https_only = https_only\n\n    def __str__(self) -> str:\n        return \"The url entered (`%s`) is invalid.%s\" % (\n            self.invalid_url,\n            \"\\nThe url must be https\" if self.https_only else \"\",\n        )\n\n\nclass InvalidColor(commands.CommandError):\n    def __init__(self, value) -> None:\n        self.value = value\n\n    def __str__(self):\n        return \"%s isn't a valid color, eg: `#fff000`, `f0f0f0`\" % self.value\n\n\nclass UrlValidator:\n    def __init__(self, *, https_only=False) -> None:\n        self.https_only = https_only\n\n    def __call__(self, value):\n        url = parse.urlparse(value)\n        schemes = (\"https\",) if self.https_only else (\"http\", \"https\")\n        if url.scheme not in schemes or not url.hostname:\n            raise InvalidUrl(value, https_only=self.https_only)\n        return value\n\n\ndef colortype(value: str):\n    try:\n        return int(value.removeprefix(\"#\"), base=16)\n    except ValueError:\n        raise InvalidColor(value)\n\n\nurl_type = UrlValidator(https_only=True)\n\n\ndef process_message_mentions(message: str) -> str:\n    if not message:\n        return \"\"\n    for _type, _id in re.findall(r\"(role|user):(\\d{18})\", message):\n        message = message.replace(\n            _type + \":\" + _id, f\"<@!{_id}>\" if _type == \"user\" else f\"<@&{_id}>\"\n        )\n    for label in (\"mention\", \"ping\"):\n        for role in (\"everyone\", \"here\"):\n            message = message.replace(label + \":\" + role, f\"@{role}\")\n    return message\n\n\nclass FlagAdder:\n    def __init__(self, kwarg_map: Dict[str, Iterable], *, default_mode: bool = False):\n        self.kwarg_map = kwarg_map\n        self.default_mode = default_mode\n\n    def call(self, func: _F, **kwargs) -> _F:\n        if kwargs.pop(\"all\", False):\n            for flags in self.kwarg_map.values():\n                self.apply(flags=flags, func=func)\n            return func\n        kwargs = {**{k: self.default_mode for k in self.kwarg_map.keys()}, **kwargs}\n        for k, v in kwargs.items():\n            if v:\n                self.apply(flags=self.kwarg_map[k], func=func)\n        return func\n\n    def __call__(self, func=None, **kwargs):\n        if func is None:\n            return functools.partial(self.call, **kwargs)\n        return self.call(func, **kwargs)\n\n    def apply(self, *, flags: Iterable, func: _F) -> _F:\n        for flag in flags:\n            flag(func)\n        return func\n\n\nembed_input = FlagAdder(\n    {\n        \"basic\": (\n            flags.add_flag(\"--title\", \"-t\"),\n            flags.add_flag(\"--description\", \"-d\"),\n            flags.add_flag(\"--fields\", \"-f\", nargs=\"+\"),\n            flags.add_flag(\"--colour\", \"--color\", \"-c\", type=colortype),\n        ),\n        \"image\": (\n            flags.add_flag(\"--thumbnail\", \"-th\", type=url_type),\n            flags.add_flag(\"--image\", \"-i\", type=url_type),\n        ),\n        \"author\": (\n            flags.add_flag(\"--author-name\", \"--aname\", \"-an\"),\n            flags.add_flag(\"--auto-author\", \"-aa\", action=\"store_true\", default=False),\n            flags.add_flag(\"--author-url\", \"--aurl\", \"-au\", type=url_type),\n            flags.add_flag(\"--author-icon\", \"--aicon\", \"-ai\", type=url_type),\n        ),\n        \"footer\": (\n            flags.add_flag(\"--footer-icon\", \"-fi\", type=url_type),\n            flags.add_flag(\"--footer-text\", \"-ft\"),\n        ),\n    }\n)\n\n\nallowed_mentions_input = FlagAdder(\n    {\n        \"all\": (\n            flags.add_flag(\n                \"--everyone-mention\", \"-em\", default=False, action=\"store_true\"\n            ),\n            flags.add_flag(\n                \"--role-mentions\", \"-rm\", default=False, action=\"store_true\"\n            ),\n            flags.add_flag(\n                \"--user-mentions\", \"-um\", default=True, action=\"store_false\"\n            ),\n        )\n    },\n    default_mode=True,\n)\n\nwebhook_input = FlagAdder(\n    {\n        \"all\": (\n            flags.add_flag(\"--webhook\", \"-w\", action=\"store_true\", default=False),\n            flags.add_flag(\"--webhook-username\", \"-wun\", type=str, default=None),\n            flags.add_flag(\"--webhook-avatar\", \"-wav\", type=url_type, default=None),\n            flags.add_flag(\n                \"--webhook-auto-author\", \"-waa\", action=\"store_true\", default=False\n            ),\n            flags.add_flag(\"--webhook-new-name\", \"-wnn\", type=str, default=None),\n            flags.add_flag(\"--webhook-name\", \"-wn\", type=str, default=None),\n            flags.add_flag(\n                \"--webhook-dispose\", \"-wd\", action=\"store_true\", default=False\n            ),\n        )\n    },\n    default_mode=True,\n)\n\n\ndef dict_to_embed(data: Dict[str, str], author: Union[User, Member] = None):\n    embed = Embed()\n    for field in (\"title\", \"description\", \"colour\"):\n        if value := data.pop(field, None):\n            setattr(embed, field, value)\n    for field in \"thumbnail\", \"image\":\n        if value := data.pop(field, None):\n            getattr(embed, \"set_\" + field)(url=value)\n\n    if data.pop(\"auto_author\", False) and author:\n        embed.set_author(name=author.display_name, icon_url=str(author.avatar_url))\n    if \"author_name\" in data and data[\"author_name\"]:\n        kwargs = {}\n        if icon_url := data.pop(\"author_icon\", None):\n            kwargs[\"icon_url\"] = icon_url\n        if author_url := data.pop(\"author_url\", None):\n            kwargs[\"url\"] = author_url\n\n        embed.set_author(name=data.pop(\"author_name\"), **kwargs)\n\n    if \"footer_text\" in data and data[\"footer_text\"]:\n        kwargs = {}\n        if footer_icon := data.pop(\"footer_icon\", None):\n            kwargs[\"icon_url\"] = footer_icon\n\n        embed.set_footer(text=data.pop(\"footer_text\"), **kwargs)\n\n    fields = data.pop(\"fields\", []) or []\n    if len(fields) % 2 == 1:\n        raise InvalidFieldArgs(\n            \"Number of arguments for fields must be an even number, pairs of name and value\"\n        )\n\n    for name, value in zip(fields[::2], fields[1::2]):\n        embed.add_field(name=name, value=value, inline=False)\n\n    if embed.to_dict() == {\"type\": \"rich\"}:\n        raise EmbeyEmbedError()\n\n    return embed\n\n\ndef dict_to_allowed_mentions(data):\n    return AllowedMentions(\n        everyone=data.pop(\"everyone_mention\"),\n        roles=data.pop(\"role_mentions\"),\n        users=data.pop(\"user_mentions\"),\n    )\n"
  },
  {
    "path": "bot/utils/fuzzy.py",
    "content": "# -*- coding: utf-8 -*-\n\n\"\"\"\nThis Source Code Form is subject to the terms of the Mozilla Public\nLicense, v. 2.0. If a copy of the MPL was not distributed with this\nfile, You can obtain one at http://mozilla.org/MPL/2.0/.\n\"\"\"\n\n# help with: http://chairnerd.seatgeek.com/fuzzywuzzy-fuzzy-string-matching-in-python/\n\nimport heapq\nimport re\nfrom difflib import SequenceMatcher\n\n\ndef ratio(a, b):\n    m = SequenceMatcher(None, a, b)\n    return int(round(100 * m.ratio()))\n\n\ndef quick_ratio(a, b):\n    m = SequenceMatcher(None, a, b)\n    return int(round(100 * m.quick_ratio()))\n\n\ndef partial_ratio(a, b):\n    short, long = (a, b) if len(a) <= len(b) else (b, a)\n    m = SequenceMatcher(None, short, long)\n\n    blocks = m.get_matching_blocks()\n\n    scores = []\n    for i, j, n in blocks:\n        start = max(j - i, 0)\n        end = start + len(short)\n        o = SequenceMatcher(None, short, long[start:end])\n        r = o.ratio()\n\n        if 100 * r > 99:\n            return 100\n        scores.append(r)\n\n    return int(round(100 * max(scores)))\n\n\n_word_regex = re.compile(r\"\\W\", re.IGNORECASE)\n\n\ndef _sort_tokens(a):\n    a = _word_regex.sub(\" \", a).lower().strip()\n    return \" \".join(sorted(a.split()))\n\n\ndef token_sort_ratio(a, b):\n    a = _sort_tokens(a)\n    b = _sort_tokens(b)\n    return ratio(a, b)\n\n\ndef quick_token_sort_ratio(a, b):\n    a = _sort_tokens(a)\n    b = _sort_tokens(b)\n    return quick_ratio(a, b)\n\n\ndef partial_token_sort_ratio(a, b):\n    a = _sort_tokens(a)\n    b = _sort_tokens(b)\n    return partial_ratio(a, b)\n\n\ndef _extraction_generator(query, choices, scorer=quick_ratio, score_cutoff=0):\n    try:\n        for key, value in choices.items():\n            score = scorer(query, key)\n            if score >= score_cutoff:\n                yield (key, score, value)\n    except AttributeError:\n        for choice in choices:\n            score = scorer(query, choice)\n            if score >= score_cutoff:\n                yield (choice, score)\n\n\ndef extract(query, choices, *, scorer=quick_ratio, score_cutoff=0, limit=10):\n    it = _extraction_generator(query, choices, scorer, score_cutoff)\n    key = lambda t: t[1]\n    if limit is not None:\n        return heapq.nlargest(limit, it, key=key)\n    return sorted(it, key=key, reverse=True)\n\n\ndef extract_one(query, choices, *, scorer=quick_ratio, score_cutoff=0):\n    it = _extraction_generator(query, choices, scorer, score_cutoff)\n    key = lambda t: t[1]\n    try:\n        return max(it, key=key)\n    except:\n        # iterator could return nothing\n        return None\n\n\ndef extract_or_exact(query, choices, *, limit=None, scorer=quick_ratio, score_cutoff=0):\n    matches = extract(\n        query, choices, scorer=scorer, score_cutoff=score_cutoff, limit=limit\n    )\n    if len(matches) == 0:\n        return []\n\n    if len(matches) == 1:\n        return matches\n\n    top = matches[0][1]\n    second = matches[1][1]\n\n    # check if the top one is exact or more than 30% more correct than the top\n    if top == 100 or top > (second + 30):\n        return [matches[0]]\n\n    return matches\n\n\ndef extract_matches(query, choices, *, scorer=quick_ratio, score_cutoff=0):\n    matches = extract(\n        query, choices, scorer=scorer, score_cutoff=score_cutoff, limit=None\n    )\n    if len(matches) == 0:\n        return []\n\n    top_score = matches[0][1]\n    to_return = []\n    index = 0\n    while True:\n        try:\n            match = matches[index]\n        except IndexError:\n            break\n        else:\n            index += 1\n\n        if match[1] != top_score:\n            break\n\n        to_return.append(match)\n    return to_return\n\n\ndef finder(text, collection, *, key=None, lazy=True):\n    suggestions = []\n    text = str(text)\n    pat = \".*?\".join(map(re.escape, text))\n    regex = re.compile(pat, flags=re.IGNORECASE)\n    for item in collection:\n        to_search = key(item) if key else item\n        r = regex.search(to_search)\n        if r:\n            suggestions.append((len(r.group()), r.start(), item))\n\n    def sort_key(tup):\n        if key:\n            return tup[0], tup[1], key(tup[2])\n        return tup\n\n    if lazy:\n        return (z for _, _, z in sorted(suggestions, key=sort_key))\n    else:\n        return [z for _, _, z in sorted(suggestions, key=sort_key)]\n\n\ndef find(text, collection, *, key=None):\n    try:\n        return finder(text, collection, key=key, lazy=False)[0]\n    except IndexError:\n        return None\n"
  },
  {
    "path": "bot/utils/process_files.py",
    "content": "import re\nfrom typing import Dict, List, Tuple\n\nfrom discord.ext import commands\n\nfiles_pattern = re.compile(r\"\\s{0,}```\\w{0,}\\s{0,}\")\n\n\nclass NoValidFiles(commands.CommandError):\n    def __str__(self):\n        return \"None of the files were valid or no files were given\"\n\n\nasync def process_files(\n    ctx: commands.Context, inp: str\n) -> Tuple[Dict[str, Dict[str, str]], List[str]]:\n    files = {}\n\n    attachments = ctx.message.attachments.copy()\n    skipped = []\n    msg = ctx.message\n\n    # If the message was a reply\n    if msg.reference and msg.reference.message_id:\n        replied = msg.reference.cached_message or await msg.channel.fetch_message(\n            msg.reference.message_id\n        )\n        attachments.extend(replied.attachments)\n\n    if inp:\n        # TODO: Change this to something better\n        files_and_names = files_pattern.split(inp)[:-1]\n\n        # Dict comprehension to create the files 'object'\n        files = {\n            name: {\"content\": content + \"\\n\"}\n            for name, content in zip(files_and_names[0::2], files_and_names[1::2])\n        }\n\n    for attachment in attachments:\n        if attachment.size > 64 * 1024 or attachment.filename.endswith(\n            (\"jpg\", \"jpeg\", \"png\")\n        ):\n            skipped.append(attachment.filename)\n            continue\n        try:\n            b = (await attachment.read()).decode(\"utf-8\")\n        except UnicodeDecodeError:\n            skipped.append(attachment.filename)\n        else:\n            files[attachment.filename] = {\"content\": b}\n\n    if not files:\n        raise NoValidFiles()\n\n    return files, skipped\n"
  },
  {
    "path": "bot/utils/rtfm.py",
    "content": "import io\nimport os\nimport re\nimport zlib\n\n# Directly taken and modified from Rapptz/RoboDanny\n# https://github.com/Rapptz/RoboDanny/blob/715a5cf8545b94d61823f62db484be4fac1c95b1/cogs/api.py\n# This code is under the Mozilla Public License 2.0\n\n\nclass SphinxObjectFileReader:\n    # Inspired by Sphinx's InventoryFileReader\n    BUFSIZE = 16 * 1024\n\n    def __init__(self, buffer):\n        self.stream = io.BytesIO(buffer)\n\n    def readline(self):\n        return self.stream.readline().decode(\"utf-8\")\n\n    def skipline(self):\n        self.stream.readline()\n\n    def read_compressed_chunks(self):\n        decompressor = zlib.decompressobj()\n        while True:\n            chunk = self.stream.read(self.BUFSIZE)\n            if len(chunk) == 0:\n                break\n            yield decompressor.decompress(chunk)\n        yield decompressor.flush()\n\n    def read_compressed_lines(self):\n        buf = b\"\"\n        for chunk in self.read_compressed_chunks():\n            buf += chunk\n            pos = buf.find(b\"\\n\")\n            while pos != -1:\n                yield buf[:pos].decode(\"utf-8\")\n                buf = buf[pos + 1 :]\n                pos = buf.find(b\"\\n\")\n\n    def parse_object_inv(self, url):\n        # key: URL\n        # n.b.: key doesn't have `discord` or `discord.ext.commands` namespaces\n        result = {}\n\n        # first line is version info\n        inv_version = self.readline().rstrip()\n\n        if inv_version != \"# Sphinx inventory version 2\":\n            raise RuntimeError(\"Invalid objects.inv file version.\")\n\n        # next line is \"# Project: <name>\"\n        # then after that is \"# Version: <version>\"\n        projname = self.readline().rstrip()[11:]\n        version = self.readline().rstrip()[11:]\n\n        # next line says if it's a zlib header\n        line = self.readline()\n        if \"zlib\" not in line:\n            raise RuntimeError(\"Invalid objects.inv file, not z-lib compatible.\")\n\n        # This code mostly comes from the Sphinx repository.\n        entry_regex = re.compile(r\"(?x)(.+?)\\s+(\\S*:\\S*)\\s+(-?\\d+)\\s+(\\S+)\\s+(.*)\")\n        for line in self.read_compressed_lines():\n            match = entry_regex.match(line.rstrip())\n            if not match:\n                continue\n\n            name, directive, prio, location, dispname = match.groups()\n            domain, _, subdirective = directive.partition(\":\")\n            if directive == \"py:module\" and name in result:\n                # From the Sphinx Repository:\n                # due to a bug in 1.1 and below,\n                # two inventory entries are created\n                # for Python modules, and the first\n                # one is correct\n                continue\n\n            # Most documentation pages have a label\n            if directive == \"std:doc\":\n                subdirective = \"label\"\n\n            if location.endswith(\"$\"):\n                location = location[:-1] + name\n\n            key = name if dispname == \"-\" else dispname\n            prefix = f\"{subdirective}:\" if domain == \"std\" else \"\"\n\n            if projname == \"discord.py\":\n                key = key.replace(\"discord.ext.commands.\", \"\").replace(\"discord.\", \"\")\n\n            result[f\"{prefix}{key}\"] = os.path.join(url, location)\n\n        return result\n"
  },
  {
    "path": "bot.Dockerfile",
    "content": "FROM python:3.9\n\nWORKDIR /app\nENV PYTHONDONTWRITEBYTECODE=1\n\nCOPY requirements-bot.txt ./\nRUN pip install --no-cache-dir -r requirements-bot.txt\n\nCOPY . .\n\nCMD [ \"python\", \"-m\", \"bot\" ]\n"
  },
  {
    "path": "config/__init__.py",
    "content": ""
  },
  {
    "path": "config/bot.py",
    "content": "from pydantic import BaseSettings\n\n\nclass BotConfig(BaseSettings):\n    bot_token: str\n    quiz_api_token: str\n    log_webhook: str\n\n    class Config:\n        env_file = \".env\"\n\n\nbot_config = BotConfig()\n"
  },
  {
    "path": "config/common.py",
    "content": "from pydantic import BaseSettings, PostgresDsn\n\n\nclass Settings(BaseSettings):\n\n    secret: str\n    database_uri: PostgresDsn\n    no_ssl: bool = False\n\n    class Config:\n        env_file = \".env\"\n        fields = {\n            \"database_uri\": {\"env\": [\"database_uri\", \"database_url\", \"database\"]},\n            \"no_ssl\": {\"env\": \"database_no_ssl\"},\n            \"secret\": {\"env\": \"signing_secret\"},\n        }\n\n\nconfig = Settings()\n"
  },
  {
    "path": "config/oauth.py",
    "content": "from pydantic import BaseSettings\n\n\nclass StackOAuthConfig(BaseSettings):\n    client_id: str\n    client_secret: str\n    redirect_uri: str\n    key: str\n\n    class Config:\n        env_file = \".env\"\n        env_prefix = \"stackexchange_\"\n\n\nclass GithubOAuthConfig(BaseSettings):\n    client_id: str\n    client_secret: str\n    redirect_uri: str\n\n    class Config:\n        env_file = \".env\"\n        env_prefix = \"github_\"\n\n\nstack_oauth_config = StackOAuthConfig()\ngithub_oauth_config = GithubOAuthConfig()\n"
  },
  {
    "path": "config/reddit.py",
    "content": "from pydantic import BaseSettings\n\n\nclass RedditConfig(BaseSettings):\n    client_id: str\n    client_secret: str\n    username: str\n    password: str\n\n    class Config:\n        env_file = \".env\"\n        env_prefix = \"reddit_\"\n\n\nreddit_config = RedditConfig()\n"
  },
  {
    "path": "config/webhook.py",
    "content": "from pydantic import BaseSettings\n\n\nclass Webhooks(BaseSettings):\n    git_tips: str\n    meme: str\n    authorization: str\n\n    class Config:\n        env_file = \".env\"\n        env_prefix = \"webhook_url_\"\n        fields = {\"authorization\": {\"env\": \"authorization\"}}\n\n\nwebhook_config = Webhooks()\n"
  },
  {
    "path": "heroku.yml",
    "content": "build:\n    docker:\n        worker: bot.Dockerfile\n"
  },
  {
    "path": "models.py",
    "content": "from tortoise import Model, fields\n\n\nclass ThankModel(Model):\n    id = fields.IntField(pk=True)\n    guild = fields.ForeignKeyField(\n        model_name=\"main.GuildModel\",\n        related_name=\"all_thanks\",\n        description=\"Guild in which the user was thanked\",\n    )\n    thanker = fields.ForeignKeyField(\n        model_name=\"main.UserModel\",\n        related_name=\"sent_thanks\",\n        description=\"The member who sent the thanks\",\n    )\n    thanked = fields.ForeignKeyField(\n        model_name=\"main.UserModel\",\n        related_name=\"thanks\",\n        description=\"The member who was thanked\",\n    )\n    time = fields.DatetimeField(auto_now_add=True)\n    description = fields.CharField(max_length=100)\n\n    class Meta:\n        table = \"thanks\"\n        table_description = \"Represents a 'thank' given from one user to another\"\n\n\nclass GuildModel(Model):\n    id = fields.BigIntField(pk=True, description=\"Discord ID of the guild\")\n    all_thanks: fields.ForeignKeyRelation[ThankModel]\n    prefix = fields.CharField(\n        max_length=10, default=\".\", description=\"Custom prefix of the guild\"\n    )\n\n    class Meta:\n        table = \"guilds\"\n        table_description = \"Represents a discord guild's settings\"\n\n\nclass UserModel(Model):\n    id = fields.BigIntField(pk=True, description=\"Discord ID of the user\")\n    # External references\n    github_oauth_token = fields.CharField(\n        max_length=50, null=True, description=\"Github OAuth2 access token of the user\"\n    )\n    stackoverflow_oauth_token = fields.CharField(\n        max_length=50,\n        null=True,\n        description=\"Stackoverflow OAuth2 access token of the user\",\n    )\n\n    thanks: fields.ForeignKeyRelation[ThankModel]\n    sent_thanks: fields.ForeignKeyRelation[ThankModel]\n\n    class Meta:\n        table = \"users\"\n        table_description = \"Represents all users\"\n\n\nclass JokeModel(Model):\n    id = fields.IntField(pk=True, description=\"Joke ID\")\n\n    setup = fields.CharField(max_length=150, description=\"Joke setup\")\n    end = fields.CharField(max_length=150, description=\"Joke end\")\n    tags = fields.JSONField(default=[], description=\"List of tags\")\n\n    accepted = fields.BooleanField(\n        default=False, description=\"Whether the joke has been accepted in\"\n    )\n\n    creator = fields.ForeignKeyField(\n        model_name=\"main.UserModel\",\n        related_name=\"joke_submissions\",\n        description=\"User who submitted this Joke\",\n    )\n\n    class Meta:\n        table = \"jokes\"\n        table_description = \"User submitted jokes being collected\"\n"
  },
  {
    "path": "public/templates/404.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n\t<title>404</title>\n\t<style type=\"text/css\">\n\t@import url(\"https://fonts.googleapis.com/css?family=Bevan\");\n\n* {\n    padding: 0;\n    margin: 0;\n    box-sizing: border-box;\n}\n\nbody {\n    background: rgb(40,40,40);\n    overflow: hidden;\n}\n\np {\n    font-family: \"Bevan\", cursive;\n    font-size: 130px;\n    margin: 10vh 0 0;\n    text-align: center;\n    letter-spacing: 5px;\n    background-color: black;\n    color: transparent;\n    text-shadow: 2px 2px 3px rgba(255, 255, 255, 0.1);\n    -webkit-background-clip: text;\n    -moz-background-clip: text;\n    background-clip: text;\n\n\n}\n\ncode {\n    color: #bdbdbd;\n    text-align: center;\n    display: block;\n    font-size: 16px;\n    margin: 0 30px 25px;\n\n\n}\nspan {\n    color: #f0c674;\n}\n\ni {\n    color: #b5bd68;\n}\n\nem {\n    color: #b294bb;\n    font-style: unset;\n}\n\n b {\n    color: #81a2be;\n    font-weight: 500;\n}\n\n\n\na {\n    color: #8abeb7;\n    font-family: monospace;\n    font-size: 20px;\n    text-decoration: underline;\n    margin-top:10px;\n    display:inline-block\n}\n\n@media screen and (max-width: 880px) {\n    p {\n        font-size: 14vw;\n    }\n}\t\n\n\n\t</style>\n</head>\n\n<body>\n<p>HTTP: <font style=\"font-size: 1.2em;\">404</font></p>\n\n<code><span>this_page</span>.<em>not_found</em> = True</code>\n<code><span>if</span> <b>you_spelt_it_wrong</b>: \n\t<span>    try_again()</span></code>\n\n<code><span>elif <b>we_screwed_up</b>:</span>\n\t<em>    print</em>(<i>\"We're really sorry about that.\"</i>); return <span> redirect</span>(<em>url_for</em>(<i>\"home\"</i>))</code>\n\n<center><a href = \"/\">HOME</a></center>\n\n<script type=\"text/javascript\">\nfunction type(n, t) {\n    var str = document.getElementsByTagName(\"code\")[n].innerHTML.toString();\n    var i = 0;\n    document.getElementsByTagName(\"code\")[n].innerHTML = \"\";\n\n    setTimeout(function() {\n        var se = setInterval(function() {\n            i++;\n            document.getElementsByTagName(\"code\")[n].innerHTML =\n                str.slice(0, i) + \"|\";\n            if (i == str.length) {\t\n                clearInterval(se);\n                document.getElementsByTagName(\"code\")[n].innerHTML = str;\n            }\n        }, 10);\n    }, t);\n}\n\ntype(0, 0);\ntype(1, 600);\ntype(2, 1300);\n\t\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "public/templates/oauth_error.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n\t<title>Error!</title>\n\n<link rel=\"stylesheet\" type=\"text/css\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css\" />\n<link rel=\"stylesheet\" type=\"text/css\" href=\"https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css\" />\n<style type=\"text/css\">\n\n\tbody {\n\t  background-color: black;\n\t}\n\n    .success\n\t{\n\t\tborder:3px solid #fff;\n\t\theight:280px;\n        border-radius:20px;\n        background:#892cdc;\n\t}\n   .success_header\n   {\n\t   background:#52057b;/*rgba(255,102,0,1);*/\n\t   padding:20px;\n       border-radius:20px 20px 0px 0px;\n\t   \n   }\n   \n   .check\n   {\n\t   margin:0px auto;\n\t   width:50px;\n\t   height:50px;\n\t   border-radius:100%;\n\t   background:#fff;\n\t   text-align:center;\n   }\n   \n   .check i\n   {\n\t   vertical-align:middle;\n\t   line-height:50px;\n\t   font-size:30px;\n   }\n\n    .content \n    {\n        text-align:center;\n        color:white;\n    }\n\n    .content  h1\n    {\n        font-size:25px;\n        padding-top:25px;\n    }\n\n    .content a\n    {\n        width:200px;\n        height:35px;\n        color:#fff;\n        border-radius:30px;\n        padding:5px 10px;\n        background:#bc6ff1;\n        transition:all ease-in-out 0.3s;\n    }\n\n    .content a:hover\n    {\n        text-decoration:none;\n        background:#000;\n    }\n   \n</style>\n</head>\n<body>\n<div class=\"container\">\n   <div class=\"row\">\n      <div class=\"col-md-6 mx-auto mt-5\">\n         <div class=\"success\">\n            <div class=\"success_header\">\n               <div class=\"check\"><i class=\"fa fa-times\" aria-hidden=\"true\"></i></div>\n            </div>\n            <div class=\"content\">\n               <h1>Error</h1>\n               <p>{{detail}}</p>\n               <a href=\"#\">Go Back</a>\n            </div>\n         </div>\n      </div>\n   </div>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "public/templates/oauth_success.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n<title>Success!</title>\n\n<link rel=\"stylesheet\" type=\"text/css\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css\" />\n<link rel=\"stylesheet\" type=\"text/css\" href=\"https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css\" />\n<style type=\"text/css\">\n\n\tbody {\n\t  background-color: black;\n\t}\n\n    .success\n\t{\n\t\tborder:3px solid #fff;\n\t\theight:280px;\n        border-radius:20px;\n        background:#892cdc;\n\t}\n   .success_header\n   {\n\t   background:#52057b;/*rgba(255,102,0,1);*/\n\t   padding:20px;\n       border-radius:20px 20px 0px 0px;\n\t   \n   }\n   \n   .check\n   {\n\t   margin:0px auto;\n\t   width:50px;\n\t   height:50px;\n\t   border-radius:100%;\n\t   background:#fff;\n\t   text-align:center;\n   }\n   \n   .check i\n   {\n\t   vertical-align:middle;\n\t   line-height:50px;\n\t   font-size:30px;\n   }\n\n    .content \n    {\n        text-align:center;\n        color:white;\n    }\n\n    .content  h1\n    {\n        font-size:25px;\n        padding-top:25px;\n    }\n\n    .content a\n    {\n        width:200px;\n        height:35px;\n        color:#fff;\n        border-radius:30px;\n        padding:5px 10px;\n        background:#bc6ff1;\n        transition:all ease-in-out 0.3s;\n    }\n\n    .content a:hover\n    {\n        text-decoration:none;\n        background:#000;\n    }\n   \n</style>\n</head>\n<body>\n<div class=\"container\">\n   <div class=\"row\">\n      <div class=\"col-md-6 mx-auto mt-5\">\n         <div class=\"success\">\n            <div class=\"success_header\">\n               <div class=\"check\"><i class=\"fa fa-check\" aria-hidden=\"true\"></i></div>\n            </div>\n            <div class=\"content\">\n               <h1>Success</h1>\n               <p>Your {{oauth_provider}} has been linked.</p>\n               <a href=\"#\">Go Back</a>\n            </div>\n         </div>\n      </div>\n   </div>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "requirements-bot.txt",
    "content": "aerich==0.5.0\ncachetools==4.2.1\ndiscord.py==1.7.1\ndiscord-flags==2.1.1\njishaku==1.20.0.220\npsutil==5.8.0\npython-dotenv==0.17.0\npython-jose==3.2.0\nPyYAML==5.4.1\nsvglib==1.1.0\ntortoise-orm[asyncpg]==0.16.21\n\n"
  },
  {
    "path": "requirements-dev.txt",
    "content": "black==20.8b1\nisort==5.8.0\n"
  },
  {
    "path": "requirements.txt",
    "content": "asyncpg==0.22.0\nbackports-datetime-fromisoformat==1.0.0; python_version < '3.7'\ndiscord.py==1.7.1\nfastapi==0.65.2\nJinja2==2.11.3\npraw==7.2.0\npython-dotenv==0.17.0\npython-jose==3.2.0\nrequests==2.25.1\nasync-exit-stack; python_version < '3.7'\nasync-generator; python_version < '3.7'\n\n"
  },
  {
    "path": "tortoise_config.py",
    "content": "import ssl\n\nfrom config import common\n\n# TODO: Yet to find a fix for this\nctx = ssl.create_default_context()\nctx.check_hostname = False\nctx.verify_mode = ssl.CERT_NONE\n\ndatabase_uri = common.config.database_uri\n\ntortoise_config = {\n    \"connections\": {\n        \"default\": {\n            \"engine\": \"tortoise.backends.asyncpg\",\n            \"credentials\": {\n                \"database\": database_uri.path[1:],\n                \"host\": database_uri.host,\n                \"password\": database_uri.password,\n                \"port\": database_uri.port or 5432,\n                \"user\": database_uri.user,\n                \"ssl\": ctx if common.config.no_ssl else None,\n            },\n        }\n    },\n    \"apps\": {\n        \"main\": {\"models\": [\"models\", \"aerich.models\"], \"default_connection\": \"default\"}\n    },\n}\n"
  },
  {
    "path": "utils/db_backup.py",
    "content": "import asyncio\nimport pickle\nfrom datetime import datetime\n\nimport asyncpg\n\nfrom config.common import config\n\n\nasync def backup():\n    conn = await asyncpg.connect(str(config.database_uri))\n    tables = (\"users\", \"thanks\", \"guilds\", \"jokes\")\n    data = {\n        field: [dict(rec) for rec in await conn.fetch(\"SELECT * FROM {}\".format(field))]\n        for field in tables\n    }\n    return data\n\n\ndef main():\n    data = asyncio.get_event_loop().run_until_complete(backup())\n    filename = \"backup-{:%d-%m-%y-%H:%M}.pickle\".format(datetime.now())\n    with open(filename, \"wb\") as f:\n        pickle.dump(data, f)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "utils/embed.py",
    "content": "import datetime\n\nimport yaml\nfrom discord import Color, Embed, File\n\n\ndef build_embed(embed_data, add_timestamp=False):\n    embed = Embed(\n        title=embed_data.get(\"title\"),\n        description=embed_data.get(\"description\"),\n        color=embed_data.get(\"color\", Color.green()),\n    )\n\n    if \"thumbnail\" in embed_data:\n        embed.set_thumbnail(url=embed_data[\"thumbnail\"])\n\n    if \"image\" in embed_data:\n        embed.set_image(url=embed_data[\"image\"])\n\n    if \"author\" in embed_data:\n        embed.set_author(**embed_data[\"author\"])\n\n    if \"footer\" in embed_data:\n        embed.set_footer(**embed_data[\"footer\"])\n\n    if add_timestamp or embed_data.get(\"add_timestamp\", False):\n        embed.timestamp = datetime.datetime.utcnow()\n\n    for f in embed_data.get(\"fields\", []):\n        f.setdefault(\"inline\", False)\n        embed.add_field(**f)\n\n    return embed\n\n\ndef bot_type_converter(data, add_timestamp=False):\n    text = data.get(\"text\")\n    embed_data = data.get(\"embed\")\n    file_names = data.get(\"files\", [])\n\n    embed = None\n\n    if embed_data:\n        embed = build_embed(embed_data)\n\n    return text, embed, [File(fn) for fn in file_names]\n\n\ndef webhook_type_converter(data, add_timestamp=False):\n    messages_data = data\n    outputs = []\n    for message_data in messages_data.get(\"messages\", []):\n        embeds_data = message_data.get(\"embeds\", [])\n        embeds = [build_embed(embed_data) for embed_data in embeds_data]\n        outputs.append(\n            (\n                message_data.get(\"text\"),\n                embeds,\n                [File(fn) for fn in message_data.get(\"files\", [])] or None,\n            )\n        )\n\n    return outputs, messages_data.get(\"username\"), messages_data.get(\"avatar_url\")\n\n\ndef yaml_file_to_message(filename: str, **kwargs):\n    with open(filename) as f:\n        data = yaml.load(f, yaml.Loader)\n    if data[\"type\"] == \"bot\":\n        return bot_type_converter(data, **kwargs)\n    if data[\"type\"] == \"webhook\":\n        return webhook_type_converter(data, **kwargs)\n    raise RuntimeError(\"Incompatible type\")\n"
  },
  {
    "path": "utils/webhook.py",
    "content": "from typing import Optional\n\nfrom discord import RequestsWebhookAdapter, Webhook\n\nfrom .embed import yaml_file_to_message\n\n\ndef make_webhook(url: str, adapter=RequestsWebhookAdapter()):\n    return Webhook.from_url(url, adapter=adapter)\n\n\ndef send_from_yaml(\n    *, webhook: Webhook, filename: str, text: Optional[str] = None, **kwargs\n):\n    messages, username, avatar_url = yaml_file_to_message(filename)\n    kwargs.setdefault(\"username\", username)\n    kwargs.setdefault(\"avatar_url\", avatar_url)\n    return [\n        webhook.send(message[0] or text, embeds=message[1], files=message[2], **kwargs)\n        for message in messages\n    ]\n"
  },
  {
    "path": "vercel.json",
    "content": "{\n  \"functions\": {\n    \"api/main.py\": { \"maxDuration\": 10 }\n  },\n  \"routes\": [{ \"src\": \"/(.*)\", \"dest\": \"/api/main\" }]\n}\n"
  }
]