Full Code of TechStruck/TechStruck-Bot for AI

main 514519896d31 cached
57 files
137.9 KB
33.8k tokens
247 symbols
1 requests
Download .txt
Repository: TechStruck/TechStruck-Bot
Branch: main
Commit: 514519896d31
Files: 57
Total size: 137.9 KB

Directory structure:
gitextract_f5y6cz_r/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── black.yml
│       ├── codeql-analysis.yml
│       └── isort.yml
├── .gitignore
├── .isort.cfg
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── api/
│   ├── dependencies.py
│   ├── exceptions.py
│   ├── main.py
│   └── routers/
│       ├── oauth.py
│       └── webhooks.py
├── bot/
│   ├── __main__.py
│   ├── bot.py
│   ├── cogs/
│   │   ├── admin.py
│   │   ├── brainfeed.py
│   │   ├── coc.py
│   │   ├── code_exec.py
│   │   ├── fun.py
│   │   ├── github.py
│   │   ├── help_command.py
│   │   ├── joke.py
│   │   ├── packages.py
│   │   ├── quiz.py
│   │   ├── rtfm.py
│   │   ├── stackexchange.py
│   │   ├── thank.py
│   │   └── utils.py
│   ├── core.py
│   └── utils/
│       ├── embed_flag_input.py
│       ├── fuzzy.py
│       ├── process_files.py
│       └── rtfm.py
├── bot.Dockerfile
├── config/
│   ├── __init__.py
│   ├── bot.py
│   ├── common.py
│   ├── oauth.py
│   ├── reddit.py
│   └── webhook.py
├── heroku.yml
├── models.py
├── public/
│   └── templates/
│       ├── 404.html
│       ├── oauth_error.html
│       └── oauth_success.html
├── requirements-bot.txt
├── requirements-dev.txt
├── requirements.txt
├── tortoise_config.py
├── utils/
│   ├── db_backup.py
│   ├── embed.py
│   └── webhook.py
└── vercel.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: FalseDev

---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Use commands '....'
2. Do '....'
3. See error

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Additional context**
Add any other context about the problem here.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: "pip"
    directory: "/"
    schedule:
      interval: "daily"


================================================
FILE: .github/workflows/black.yml
================================================
name: Lint

on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-python@v2
      - uses: psf/black@stable


================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"

on:
  push:
    branches: [ main ]
  pull_request:
    # The branches below must be a subset of the branches above
    branches: [ main ]
  schedule:
    - cron: '32 12 * * 3'

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest

    strategy:
      fail-fast: false
      matrix:
        language: [ 'python' ]
        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
        # Learn more:
        # 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

    steps:
    - name: Checkout repository
      uses: actions/checkout@v2

    # Initializes the CodeQL tools for scanning.
    - name: Initialize CodeQL
      uses: github/codeql-action/init@v1
      with:
        languages: ${{ matrix.language }}
        # If you wish to specify custom queries, you can do so here or in a config file.
        # By default, queries listed here will override any specified in a config file.
        # Prefix the list here with "+" to use these queries and those in the config file.
        # queries: ./path/to/local/query, your-org/your-repo/queries@main

    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
    # If this step fails, then you should remove it and run the build manually (see below)
    - name: Autobuild
      uses: github/codeql-action/autobuild@v1

    # ℹ️ Command-line programs to run using the OS shell.
    # 📚 https://git.io/JvXDl

    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
    #    and modify them (or add more) to build your code if your project
    #    uses a compiled language

    #- run: |
    #   make bootstrap
    #   make release

    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v1


================================================
FILE: .github/workflows/isort.yml
================================================
name: Run isort
on:
  - push

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-python@v2
        with:
          python-version: 3.9
      - uses: jamescurtin/isort-action@master
        with:
            requirementsFiles: "requirements.txt requirements-dev.txt"


================================================
FILE: .gitignore
================================================
config.yaml
.vim
.env
**/__pycache__
tmp
cache

# Tortoise stuff
aerich.ini
migrations

.vercel


================================================
FILE: .isort.cfg
================================================
[settings]
multi_line_output = 3
include_trailing_comma = True
force_grid_wrap = 0
use_parentheses = True
ensure_newline_before_comments = True
line_length = 88


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.

## Our Standards

Examples of behavior that contributes to creating a positive environment
include:

* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or
 advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
 address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
 professional setting

## Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.

## Scope

This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at thetechnopath1802@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html

[homepage]: https://www.contributor-covenant.org

For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2021 FalseDev

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<p align="center">
	<img src="https://cdn.discordapp.com/attachments/770679803635433473/825250084589273118/circle-cropped4.png" height="125px" width="125px" />
</p>

<p align='center'><a href = "https://discord.gg/HXqXKdVBhs" target = "_blank"><img src = "https://discord.com/api/guilds/782517843820412948/embed.png"></a></p>

<h1 align="center">Techstruck</h1>

<h3><img src="https://cdn.discordapp.com/emojis/562008110412201986.png" height="20px"> • Info</h3>
<ul>
<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>
<li>This repository has Tech Struck server's custom bot along with webhook based announcement and <i>BrainFeed</i> senders for Tech Struck</li>
</ul>

<h3><img src="https://cdn.discordapp.com/attachments/770679803635433473/825245721951207454/802801495153967154.png" height="20px"> • I'd like to contribute</h3>
<p>You may help by adding features to Tech Struck or fix bugs in the code. Here's how:</p>
<ol>
  <li>Fork the repository</li>
  <li>Clone your fork: <code>git clone https://github.com/your-username/Tech-Struck.git</code></li>
  <li>Create your feature branch: <code>git checkout -b my-new-feature</code></li>
  <li>Commit your changes: <code>git commit -am 'uwu new feature'</code></li>
  <li>Push to the branch: <code>git push origin my-new-feature</code></li>
  <li>Submit a pull request</li>
</ol>

<h3><img src="https://cdn.discordapp.com/attachments/770679803635433473/825245805476184074/675395743044993053.png" height="20px"> • I found a bug!</h3>
<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>

<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>


================================================
FILE: api/dependencies.py
================================================
import hmac
import ssl
from datetime import datetime

import asyncpg
from aiohttp import ClientSession
from fastapi import Header, HTTPException, Query, Request, status, templating
from jose import jwt

from config.common import config
from config.webhook import webhook_config

from .exceptions import CustomHTTPException

jinja = templating.Jinja2Templates("./public/templates/")


def auth_dep(authorization: str = Header(...)):
    if not hmac.compare_digest(authorization, webhook_config.authorization):
        raise HTTPException(status.HTTP_401_UNAUTHORIZED)


async def aiohttp_session():
    session = ClientSession(headers={"Accept": "application/json"})
    try:
        yield session
    finally:
        await session.close()


def state_check(request: Request, state: str = Query(...)) -> int:
    try:
        payload = jwt.decode(state, config.secret)
    except jwt.JWTError:
        raise CustomHTTPException(
            jinja.TemplateResponse(
                "oauth_error.html",
                {"request": request, "detail": "Invalid state"},
                status_code=status.HTTP_406_NOT_ACCEPTABLE,
            )
        )

    expiry = datetime.fromisoformat(payload["expiry"])
    if datetime.now() > expiry:
        raise CustomHTTPException(
            jinja.TemplateResponse(
                "oauth_error.html",
                {"request": request, "detail": "Expired link"},
                status_code=status.HTTP_406_NOT_ACCEPTABLE,
            )
        )

    return payload["id"]


ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE


async def db_connection():
    connection = await asyncpg.connect(config.database_uri, ssl=ctx)
    try:
        yield connection
    finally:
        await connection.close()


================================================
FILE: api/exceptions.py
================================================
class CustomHTTPException(Exception):
    def __init__(self, response):
        self.response = response


================================================
FILE: api/main.py
================================================
import sys

from fastapi import FastAPI, Request

from .exceptions import CustomHTTPException
from .routers import oauth, webhooks

if sys.version_info[1] < 7:
    from backports.datetime_fromisoformat import MonkeyPatch

    MonkeyPatch.patch_fromisoformat()


app = FastAPI()


@app.exception_handler(CustomHTTPException)
def custom_http_exception_handler(request: Request, exc: CustomHTTPException):
    return exc.response


app.include_router(oauth.router)
app.include_router(webhooks.router)


================================================
FILE: api/routers/oauth.py
================================================
from datetime import datetime
from urllib.parse import parse_qs

import asyncpg
from aiohttp import ClientSession
from fastapi import (
    APIRouter,
    Depends,
    HTTPException,
    Query,
    Request,
    status,
    templating,
)
from jose import jwt

from config.common import config
from config.oauth import github_oauth_config, stack_oauth_config

from ..dependencies import aiohttp_session, db_connection, jinja, state_check

router = APIRouter(
    prefix="/oauth",
)

# {table} is the table name, {field} the field name
# Hence this query is safe against sql injection type attacks
insert_or_update_template = """
insert into {table} (id, {field}) values ($1, $2) on conflict (id) do update set {field}=$2
""".strip()

stack_sql_query = insert_or_update_template.format(
    table="users", field="stackoverflow_oauth_token"
)
github_sql_query = insert_or_update_template.format(
    table="users", field="github_oauth_token"
)


# TODO: Cache recently used jwt tokens until expiry and deny their usage
# TODO: Serverless is stateless, hence use db caching


@router.get("/stackexchange")
async def stackexchange_oauth(
    request: Request,
    code: str = Query(...),
    user_id: int = Depends(state_check),
    db_conn: asyncpg.pool.Pool = Depends(db_connection),
    session: ClientSession = Depends(aiohttp_session),
):
    """Link account with stackexchange through OAuth2"""

    res = await session.post(
        "https://stackoverflow.com/oauth/access_token/json",
        data={**stack_oauth_config.dict(), "code": code},
    )
    auth = await res.json()
    if "access_token" not in auth:
        return {k: v for k, v in auth.items() if k.startswith("error_")}
    await db_conn.execute(stack_sql_query, user_id, auth["access_token"])

    return jinja.TemplateResponse(
        "oauth_success.html", {"request": request, "oauth_provider": "Stackexchange"}
    )


@router.get("/github")
async def github_oauth(
    request: Request,
    code: str = Query(...),
    user_id: int = Depends(state_check),
    db_conn: asyncpg.pool.Pool = Depends(db_connection),
    session: ClientSession = Depends(aiohttp_session),
):
    """Link account with github through OAuth2"""
    res = await session.post(
        "https://github.com/login/oauth/access_token",
        data={**github_oauth_config.dict(), "code": code},
    )
    auth = await res.json()
    await db_conn.execute(github_sql_query, user_id, auth["access_token"])

    return jinja.TemplateResponse(
        "oauth_success.html", {"request": request, "oauth_provider": "Github"}
    )


================================================
FILE: api/routers/webhooks.py
================================================
import datetime
import json
import random
from concurrent import futures
from typing import Iterable, List

from aiohttp import ClientSession
from discord import AsyncWebhookAdapter, Color, Embed, RequestsWebhookAdapter, Webhook
from fastapi import APIRouter, Depends
from praw import Reddit

from config.reddit import reddit_config
from config.webhook import webhook_config

from ..dependencies import aiohttp_session, auth_dep

router = APIRouter(prefix="/webhooks", dependencies=[Depends(auth_dep)])

reddit = Reddit(
    **reddit_config.dict(),
    user_agent="TechStruck",
)


REDDIT_ALLOWED_FORMATS = (".jpg", ".gif", ".png", ".jpeg")
SUBREDDITS = (
    "memes",
    "meme",
    "dankmeme",
    "me_irl",
    "dankmemes",
    "showerthoughts",
    "jokes",
    "funny",
)


def send_meme(webhook: Webhook, subreddits: List[str]) -> bool:
    meme_subreddit = reddit.subreddit(random.choice(subreddits))
    meme = meme_subreddit.random()
    if not any((meme.url.endswith(i) for i in REDDIT_ALLOWED_FORMATS)):
        return False
    embed = Embed(title=meme.title, color=Color.magenta())
    embed.set_image(url=meme.url)
    embed.set_footer(text=f"\U0001f44d {meme.ups} \u2502 \U0001f44e {meme.downs}")
    webhook.send(embed=embed)
    return True


# The subreddits arg exists although theres a
# global so that in the future it can be
# modified for multiple channels/servers
def send_memes(webhook: Webhook, subreddits: Iterable[str], quantity: int):
    sent = 0
    skipped = 0
    with futures.ThreadPoolExecutor() as tp:
        while sent < quantity:
            results = [
                tp.submit(send_meme, webhook, subreddits)
                for _ in range(quantity - sent)
            ]
            new_sent = sum([r.result() for r in results])
            skipped += (quantity - sent) - new_sent
            sent += new_sent
    return sent, skipped


@router.get("/meme")
def send_memes_route():
    sent, skipped = send_memes(
        Webhook.from_url(webhook_config.meme, adapter=RequestsWebhookAdapter()),
        SUBREDDITS,
        5,
    )
    return {"sent": sent, "skipped": skipped}


@router.get("/git-tip")
async def git_tip(session: ClientSession = Depends(aiohttp_session)):
    tips_json_url = "https://raw.githubusercontent.com/git-tips/tips/master/tips.json"

    async with session.get(tips_json_url) as res:
        tips = json.loads(await res.text())

    tip_no = (datetime.date.today() - datetime.date(2021, 1, 31)).days

    tip = tips[tip_no]

    await Webhook.from_url(
        webhook_config.git_tips, adapter=AsyncWebhookAdapter(session)
    ).send(
        "<@&804403893760688179>",
        embed=Embed(
            title=tip["title"],
            description="```sh\n" + tip["tip"] + "```",
            color=Color.green(),
        ).set_footer(text="Tip {}".format(tip_no)),
        avatar_url="https://upload.wikimedia.org/wikipedia/commons/thumb/3/3f/Git_icon.svg/2000px-Git_icon.svg.png",
    )
    return {"status": "success"}


================================================
FILE: bot/__main__.py
================================================
import os

from .bot import TechStruckBot

os.environ.setdefault("JISHAKU_HIDE", "1")
os.environ.setdefault("JISHAKU_RETAIN", "1")
os.environ.setdefault("JISHAKU_NO_UNDERSCORE", "1")

if __name__ == "__main__":
    from config.bot import bot_config
    from tortoise_config import tortoise_config

    bot = TechStruckBot(tortoise_config=tortoise_config)
    bot.run(bot_config.bot_token)


================================================
FILE: bot/bot.py
================================================
import asyncio
import contextlib
import math
import re
import traceback
from typing import Iterable

from aiohttp import ClientSession
from discord import (
    AllowedMentions,
    AsyncWebhookAdapter,
    Color,
    Embed,
    Forbidden,
    Intents,
    Message,
    NotFound,
    TextChannel,
    Webhook,
    utils,
)
from discord.ext import commands, tasks
from discord.http import HTTPClient
from tortoise import Tortoise

from config.bot import bot_config
from models import GuildModel


class TechStruckBot(commands.Bot):
    http: HTTPClient

    def __init__(self, *, tortoise_config, load_extensions=True, loadjsk=True):
        allowed_mentions = AllowedMentions(
            users=True, replied_user=True, roles=False, everyone=False
        )
        super().__init__(
            command_prefix=self.get_custom_prefix,
            intents=Intents.all(),
            allowed_mentions=allowed_mentions,
            description="A bot by and for developers to integrate several tools into one place.",
            strip_after_prefix=True,
        )
        self.tortoise_config = tortoise_config
        self.db_connected = False
        self.prefix_cache = {}
        self.connect_db.start()

        if load_extensions:
            self.load_extensions(
                (
                    "bot.core",
                    "bot.cogs.admin",
                    "bot.cogs.thank",
                    "bot.cogs.stackexchange",
                    "bot.cogs.github",
                    "bot.cogs.help_command",
                    "bot.cogs.code_exec",
                    "bot.cogs.fun",
                    "bot.cogs.rtfm",
                    "bot.cogs.joke",
                    "bot.cogs.utils",
                    "bot.cogs.brainfeed",
                    "bot.cogs.packages",
                    "bot.cogs.coc",
                )
            )
        if loadjsk:
            self.load_extension("jishaku")

    @property
    def session(self) -> ClientSession:
        return self.http._HTTPClient__session  # type: ignore

    @tasks.loop(seconds=0, count=1)
    async def connect_db(self):
        print("Connecting to db")
        await Tortoise.init(self.tortoise_config)
        self.db_connected = True
        print("Database connected")

    def load_extensions(self, extentions: Iterable[str]):
        for ext in extentions:
            try:
                self.load_extension(ext)
            except Exception as e:
                traceback.print_exception(type(e), e, e.__traceback__)

    async def on_message(self, msg: Message):
        if msg.author.bot:
            return
        while not self.db_connected:
            await asyncio.sleep(0.2)
        user_id = self.user.id
        if msg.content in (f"<@{user_id}>", f"<@!{user_id}>"):
            return await msg.reply(
                "My prefix here is `{}`".format(await self.fetch_prefix(msg))
            )
        await self.process_commands(msg)

    async def on_command_error(
        self, ctx: commands.Context, error: commands.CommandError
    ):
        if isinstance(error, commands.CommandNotFound):
            return
        if not isinstance(error, commands.CommandInvokeError):
            title = " ".join(
                re.compile(r"[A-Z][a-z]*").findall(error.__class__.__name__)
            )
            return await ctx.send(
                embed=Embed(title=title, description=str(error), color=Color.red())
            )

        # If we've reached here, the error wasn't expected
        # Report to logs
        embed = Embed(
            title="Error",
            description="An unknown error has occurred and my developer has been notified of it.",
            color=Color.red(),
        )
        with contextlib.suppress(NotFound, Forbidden):
            await ctx.send(embed=embed)

        traceback_text = "".join(
            traceback.format_exception(type(error), error, error.__traceback__)
        )

        length = len(traceback_text)
        chunks = math.ceil(length / 1990)

        traceback_texts = [
            traceback_text[l * 1990 : (l + 1) * 1990] for l in range(chunks)
        ]
        traceback_embeds = [
            Embed(
                title="Traceback",
                description=("```py\n" + text + "\n```"),
                color=Color.red(),
            )
            for text in traceback_texts
        ]

        # Add message content
        info_embed = Embed(
            title="Message content",
            description="```\n" + utils.escape_markdown(ctx.message.content) + "\n```",
            color=Color.red(),
        )
        # Guild information
        value = (
            (
                "**Name**: {0.name}\n"
                "**ID**: {0.id}\n"
                "**Created**: {0.created_at}\n"
                "**Joined**: {0.me.joined_at}\n"
                "**Member count**: {0.member_count}\n"
                "**Permission integer**: {0.me.guild_permissions.value}"
            ).format(ctx.guild)
            if ctx.guild
            else "None"
        )

        info_embed.add_field(name="Guild", value=value)
        # Channel information
        if isinstance(ctx.channel, TextChannel):
            value = (
                "**Type**: TextChannel\n"
                "**Name**: {0.name}\n"
                "**ID**: {0.id}\n"
                "**Created**: {0.created_at}\n"
                "**Permission integer**: {1}\n"
            ).format(ctx.channel, ctx.channel.permissions_for(ctx.guild.me).value)
        else:
            value = (
                "**Type**: DM\n" "**ID**: {0.id}\n" "**Created**: {0.created_at}\n"
            ).format(ctx.channel)

        info_embed.add_field(name="Channel", value=value)

        # User info
        value = (
            "**Name**: {0}\n" "**ID**: {0.id}\n" "**Created**: {0.created_at}\n"
        ).format(ctx.author)

        info_embed.add_field(name="User", value=value)

        wh = Webhook.from_url(
            bot_config.log_webhook, adapter=AsyncWebhookAdapter(self.session)
        )
        return await wh.send(embeds=[*traceback_embeds, info_embed])

    async def get_custom_prefix(self, _, message: Message) -> str:
        prefix = await self.fetch_prefix(message)
        bot_id = self.user.id
        prefixes = [prefix, f"<@{bot_id}> ", f"<@!{bot_id}> "]

        comp = re.compile(
            "^(" + "|".join(re.escape(p) for p in prefixes) + ").*", flags=re.I
        )
        match = comp.match(message.content)
        if match is not None:
            return match.group(1)
        return prefix

    async def fetch_prefix(self, message: Message) -> str:
        # DMs/Group
        if not message.guild:
            return "."

        guild_id = message.guild.id
        # Get from cache
        if guild_id in self.prefix_cache:
            return self.prefix_cache[guild_id]
        # Fetch from db
        guild, _ = await GuildModel.get_or_create(id=guild_id)
        self.prefix_cache[guild_id] = guild.prefix
        return guild.prefix

    async def on_ready(self):
        print("Ready!")


================================================
FILE: bot/cogs/admin.py
================================================
from discord.ext import commands
from discord.utils import get

from utils.embed import yaml_file_to_message


class Admin(commands.Cog):
    def __init__(self, bot: commands.Bot):
        self.bot = bot

    async def _refresh(self, ctx: commands.Context, filename: str, channel_name: str):
        target_channel = get(ctx.guild.text_channels, name=channel_name)
        async for msg in target_channel.history():
            if msg.author.id == self.bot.user.id:
                target = msg
        m, e, _ = yaml_file_to_message(filename)
        await target.edit(message=m, embed=e)

    @commands.group(name="refresh", invoke_without_subcommand=True)
    @commands.is_owner()
    async def refresh(self, ctx: commands.Context):
        await ctx.send_help()

    @refresh.command(name="roles")
    async def refresh_roles(self, ctx: commands.Context):
        await self._refresh(ctx, "./yaml_embeds/roles.yaml", "\U0001f3c5\u2502roles")

    @refresh.command(name="rules")
    async def refresh_rules(self, ctx: commands.Context):
        await self._refresh(ctx, "./yaml_embeds/rules.yaml", "\u2502rules")


def setup(bot: commands.Bot):
    bot.add_cog(Admin(bot))


================================================
FILE: bot/cogs/brainfeed.py
================================================
import asyncio
from datetime import datetime
from functools import cached_property

from discord import Embed, Member, NotFound, Reaction, TextChannel
from discord.ext import commands, flags  # type: ignore
from discord.utils import get

from bot.bot import TechStruckBot
from bot.utils.embed_flag_input import dict_to_embed, embed_input


class UnknownBrainfeed(commands.CommandError):
    def __str__(self) -> str:
        return "The BrainFeed with the requested ID was not found"


class BrainFeed(commands.Cog):
    """BrainFeed related commands"""

    def __init__(self, bot: TechStruckBot):
        self.bot = bot
        self.submission_channel_id = 824887130853474304

    @flags.group(aliases=["bf", "brain", "feed"], invoke_without_command=True)
    async def brainfeed(self, ctx: commands.Context):
        """BrainFeed - the daily dose of knowledge"""
        await ctx.send_help(self.brainfeed)  # type: ignore

    @cached_property
    def submission_channel(self) -> TextChannel:
        return self.bot.get_channel(self.submission_channel_id)  # type: ignore

    @embed_input(basic=True, image=True)
    @brainfeed.command(aliases=["new", "submit"], cls=flags.FlagCommand)
    @commands.guild_only()
    @commands.max_concurrency(1, per=commands.BucketType.user)
    async def add(self, ctx: commands.Context, **kwargs):
        """Submit your brainfeed for approval and publishing"""
        embed = dict_to_embed(kwargs)
        embed.set_author(name=ctx.author.name, icon_url=str(ctx.author.avatar_url))
        embed.timestamp = datetime.now()
        msg = await ctx.send(embed=embed)
        await msg.add_reaction("\u2705")
        await msg.add_reaction("\u274c")

        def check(r: Reaction, u: Member):
            return (
                u == ctx.author and r.emoji in ("\u2705", "\u274c") and r.message == msg
            )

        try:
            r, _ = await self.bot.wait_for("reaction_add", check=check, timeout=120)
        except asyncio.TimeoutError:
            return await msg.reply("Timeout!")
        if r.emoji == "\u274c":
            return await ctx.send("Cancelled!")
        await ctx.trigger_typing()
        submission = await self.submission_channel.send(embed=embed)
        metaembed = Embed(
            title="Submission details",
            description=(
                "```"
                f"User ID: {ctx.author.id}\n"
                f"User name: {ctx.author}\n"
                f"Channel ID: {ctx.channel.id}\n"
                f"Channel name: {ctx.channel}\n"
                f"Guild ID: {ctx.guild.id}\n"
                f"Guild name: {ctx.guild}\n"
                "```"
            ),
        )
        await submission.reply(embed=metaembed)
        await ctx.send(f"Submitted\nSubmission ID: {submission.id}")

    async def get_submission(self, bf_id) -> Embed:
        try:
            msg = await self.submission_channel.fetch_message(bf_id)
        except NotFound:
            raise UnknownBrainfeed()

        if not msg.embeds:
            raise UnknownBrainfeed()

        return msg.embeds[0]

    @brainfeed.command(aliases=["show"])
    @commands.cooldown(1, 15, commands.BucketType.user)
    async def view(self, ctx: commands.Context, id: int):
        """View a BrainFeed"""
        embed = await self.get_submission(id)
        await ctx.send(embed=embed)

    @flags.add_flag("--in", "-i", type=TextChannel, default=None)
    @flags.add_flag("--webhook", "-wh", action="store_true", default=False)
    @flags.add_flag("--webhook-name", "-wn", default="BrainFeed")
    @flags.add_flag("--webhook-dispose", "-wd", action="store_true", default=False)
    @brainfeed.command(aliases=["post"], cls=flags.FlagCommand)
    @commands.has_guild_permissions(administrator=True)
    @commands.bot_has_guild_permissions(manage_webhooks=True, embed_links=True)
    async def send(self, ctx: commands.Context, bf_id: int, **kwargs):
        """Publish a BrainFeed in your server"""
        channel: TextChannel = ctx.channel  # type: ignore
        if in_ := kwargs.pop("in"):
            channel = await in_

        embed = await self.get_submission(bf_id)

        if not kwargs.pop("webhook"):
            return await channel.send(embed=embed)

        wh_name: str = kwargs.pop("webhook_name")

        webhook = get(
            await channel.webhooks(), name=wh_name
        ) or await channel.create_webhook(name=wh_name)

        await webhook.send(embed=embed)
        if kwargs.pop("webhook_dispose"):
            await webhook.delete()

    @brainfeed.command(hidden=True)
    @commands.is_owner()
    async def approve(self, ctx: commands.Context, *, id: int):
        try:
            msg = await self.submission_channel.fetch_message(id)
        except NotFound:
            await ctx.send("Submission not found")
        else:
            await msg.remove_reaction("\u274c", ctx.guild.me)
            await msg.add_reaction("\u2705")
            await ctx.send("Approved")

    @brainfeed.command(hidden=True)
    @commands.is_owner()
    async def deny(self, ctx: commands.Context, *, id: int):
        try:
            msg = await self.submission_channel.fetch_message(id)
        except NotFound:
            await ctx.send("Submission not found")
        else:
            await msg.remove_reaction("\u2705", ctx.guild.me)
            await msg.add_reaction("\u274c")
            await ctx.send("Denied")


def setup(bot: TechStruckBot):
    bot.add_cog(BrainFeed(bot))


================================================
FILE: bot/cogs/coc.py
================================================
import asyncio
import re
import time

import aiohttp

import discord
from discord.ext import commands

from ..bot import TechStruckBot

coc_role = 862200819376717865  # Coc role in TCA
coc_channel = 862195507229360168  # Coc channel in TCA
coc_message = 862200700410527744

URL_REGEX = re.compile(r"https://www.codingame.com/clashofcode/clash/([0-9a-f]{39})")
API_URL = "https://www.codingame.com/services/ClashOfCode/findClashByHandle"


class ClashOfCode(commands.Cog):
    def __init__(self, bot):
        self.bot = bot
        self.session = False
        self.session_message_id: int = 0
        self.session_users = []
        self.previous_clash: int = 0

    @commands.Cog.listener()
    async def on_ready(self):
        self.guild = self.bot.get_guild(681882711945641997)

    @property
    def role(self):
        return self.guild.get_role(coc_role)

    def em(self, mode, players):
        embed = discord.Embed(title="**Clash started**", color=discord.Color.random())
        embed.add_field(name="Mode", value=mode, inline=False)
        embed.add_field(name="Players", value=players)
        return embed

    @commands.Cog.listener()
    async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent):
        if payload.user_id == self.bot.user.id:
            return

        if self.session_message_id != 0:
            if payload.message_id == self.session_message_id:
                if payload.emoji.id == 859056281788743690:
                    if payload.user_id not in self.session_users:
                        self.session_users.append(payload.user_id)
        if payload.message_id != coc_message:
            return

        if self.role in payload.member.roles:
            return

        await payload.member.add_roles(self.role)
        try:
            await payload.member.send(f"Gave you the **{self.role.name}** role!")
        except discord.HTTPException:
            pass

    @commands.Cog.listener()
    async def on_raw_reaction_remove(self, payload: discord.RawReactionActionEvent):
        if payload.user_id == self.bot.user.id:
            return

        if self.session_message_id != 0:
            if payload.message_id == self.session_message_id:
                if payload.emoji.id == 859056281788743690:
                    if payload.user_id in self.session_users:
                        self.session_users.remove(payload.user_id)

        if payload.message_id != coc_message:
            return

        member = self.guild.get_member(payload.user_id)
        if self.role not in member.roles:
            return

        await member.remove_roles(self.role)
        try:
            await member.send(f"Removed your **{self.role.name}** role!")
        except discord.HTTPException:
            pass

    @commands.group(name="clashofcode", aliases=["coc"])
    @commands.check(lambda ctx: ctx.channel.id == coc_channel)
    async def clash_of_code(self, ctx: commands.Context):
        """Clash of Code"""
        if ctx.invoked_subcommand is None:
            return await ctx.send_help(self.bot.get_command("coc"))

    @clash_of_code.group(aliases=["s"])
    @commands.check(lambda ctx: ctx.channel.id == coc_channel)
    async def session(self, ctx: commands.Context):
        """Start or End a clash of code session"""
        if ctx.invoked_subcommand is None:
            if self.session_message_id == 0:
                return await ctx.send_help(self.bot.get_command("coc session start"))
            return await ctx.send_help(self.bot.get_command("coc session end"))

    @session.command(name="start", aliases=["s"])
    @commands.check(lambda ctx: ctx.channel.id == coc_channel)
    async def session_start(self, ctx: commands.context):
        """Start a new coc session"""
        if self.session_message_id != 0:
            return await ctx.send(
                f"There is an active session right now.\n"
                f"Join by reacting to the pinned message or using `{ctx.prefix}coc session join`. Have fun!"
            )

        pager = commands.Paginator(
            prefix=f"**Hey, {ctx.author.mention} is starting a coc session.\n"
            f"Use `{ctx.prefix}coc session join` or react to this message to join**",
            suffix="",
        )

        for member in self.role.members:
            if member != ctx.author:
                if member.status != discord.Status.offline:
                    pager.add_line(member.mention + ", ")

        if not len(pager.pages):
            return await ctx.send(
                f"{ctx.author.mention}, Nobody is online to play with <:pepe_sad:756087659281121312>"
            )

        self.session = True
        self.previous_clash = int(time.time())
        self.session_users.append(ctx.author.id)

        msg = await ctx.send(pager.pages[0])
        self.session_message_id = msg.id
        await msg.add_reaction("<:poggythumbsup:859056281788743690>")

        try:
            await msg.pin()
        except:
            await ctx.send("Failed to pin message")

        while self.session_message_id != 0:
            await asyncio.sleep(10)

            if (
                self.previous_clash + 1800 < int(time.time())
                and self.session_message_id != 0
            ):
                await ctx.send("Clash session has been closed due to inactivity")
                try:
                    await msg.unpin()
                except:
                    await ctx.send("Failed to unpin message")

                self.previous_clash = 0
                self.session_users = []
                self.session_message_id = 0
                self.session = False
                break

    @session.command(name="join", aliases=["j"])
    @commands.check(lambda ctx: ctx.channel.id == coc_channel)
    async def session_join(self, ctx: commands.Context):
        """Join the current active coc session"""
        if self.session_message_id == 0:
            return await ctx.send(
                f"There is no active coc session at the moment.\n"
                f"Use `{ctx.prefix}coc session start` to start a coc session."
            )
        if ctx.author.id in self.session_users:
            return await ctx.send(
                "You are already in the session. Have fun playing.\n"
                f"If you want to leave remove your reaction or use `{ctx.prefix}coc session leave`"
            )
        self.session_users.append(ctx.author.id)
        return await ctx.send("You have joined the session. Have fun playing")

    @session.command(name="leave", aliases=["l"])
    @commands.check(lambda ctx: ctx.channel.id == coc_channel)
    async def session_leave(self, ctx: commands.Context):
        """Leave the current active coc session"""
        if self.session_message_id == 0:
            return await ctx.send(
                f"There is no active coc session right now"
                f"use `{ctx.prefix}coc session start` to start a coc session"
            )
        if ctx.author.id not in self.session_users:
            return await ctx.send(
                "You aren't in a clash of code session right now.\n"
                f"If you want to join react to session message or use `{ctx.prefix}coc session join`"
            )
        self.session_users.remove(ctx.author.id)
        return await ctx.send("You have left the session. No more pings for now.")

    @session.command(name="end", aliases=["e"])
    @commands.check(lambda ctx: ctx.channel.id == coc_channel)
    async def session_end(self, ctx: commands.context):
        """Ends the current coc session"""
        if self.session_message_id == 0:
            return await ctx.send("There is no active clash of code session.")

        try:
            msg = await ctx.channel.fetch_message(self.session_message_id)
            try:
                await msg.unpin()
            except:
                await ctx.send("Failed to unpin message")
        except:
            await ctx.send("Error while fetching message to unpin")

        self.previous_clash = 0
        self.session_users = []
        self.session_message_id = 0
        self.session = False

        return await ctx.send(
            f"Clash session has been closed by {ctx.author.mention}. See you later :wave:"
        )

    @clash_of_code.command(name="invite", aliases=["i"])
    @commands.has_any_role(
        681895373454835749,  # Owner
        580911082290282506,  # Admin perms
        795145820210462771,  # Staff
        726650418444107869,  # Official Helper
        coc_role,
    )
    @commands.check(lambda ctx: ctx.channel.id == coc_channel)
    @commands.cooldown(1, 60, commands.BucketType.channel)
    async def coc_invite(self, ctx: commands.Context, *, url: str = None):
        """Mentions all the users with the `Clash Of Code` role that are in the current session."""
        await ctx.message.delete()
        if self.session_message_id == 0:
            ctx.command.reset_cooldown(ctx)
            return await ctx.send(
                "No active Clash of Code session please create one to start playing\n"
                f"Use `{ctx.prefix}coc session start` to start a coc session <:smugcat:737943749929467975>"
            )

        if ctx.author.id not in self.session_users:
            ctx.command.reset_cooldown(ctx)
            return await ctx.send(
                "You can't create a clash unless you participate in the session\n"
                f"Use `{ctx.prefix}coc session join` or react to the pinned message to join the coc session "
                "<:smugcat:737943749929467975>"
            )

        if url is None:
            ctx.command.reset_cooldown(ctx)
            return await ctx.send("You should provide a valid clash of code url")

        link = URL_REGEX.fullmatch(url)
        if not link:
            ctx.command.reset_cooldown(ctx)
            return await ctx.send('Could not find any valid "clashofcode" url')

        self.previous_clash = time.time()

        id = link[1]

        async with aiohttp.ClientSession() as session:
            async with session.post(API_URL, json=[id]) as resp:
                json = await resp.json()

        pager = commands.Paginator(
            prefix="\n".join(
                [
                    f"**Hey, {ctx.author.mention} is hosting a Clash Of Code game!**",
                    f"Mode{'s' if len(json['modes']) > 1 else ''}: {', '.join(json['modes'])}",
                    f"Programming languages: {', '.join(json['programmingLanguages']) if json['programmingLanguages'] else 'All'}",
                    f"Join here: {link[0]}",
                ]
            ),
            suffix="",
        )

        for member_id in self.session_users:
            if member_id != ctx.author.id:
                member = self.bot.get_user(member_id)
                pager.add_line(member.mention + ", ")

        if not len(pager.pages):
            return await ctx.send(
                f"{ctx.author.mention}, Nobody is online to play with <:pepe_sad:756087659281121312>"
            )

        for page in pager.pages:
            await ctx.send(page)

        async with aiohttp.ClientSession() as session:
            while not json["started"]:
                await asyncio.sleep(10)  # wait 10s to avoid flooding the API
                async with session.post(API_URL, json=[id]) as resp:
                    json = await resp.json()

        players = len(json["players"])
        players_text = ", ".join(
            [
                p["codingamerNickname"]
                for p in sorted(json["players"], key=lambda p: p["position"])
            ]
        )
        start_message = await ctx.send(embed=self.em(json["mode"], players_text))

        async with aiohttp.ClientSession() as session:
            while not json["finished"]:
                await asyncio.sleep(10)  # wait 10s to avoid flooding the API
                async with session.post(API_URL, json=[id]) as resp:
                    json = await resp.json()

                if len(json["players"]) != players:
                    players_text = ", ".join(
                        [
                            p["codingamerNickname"]
                            for p in sorted(
                                json["players"], key=lambda p: p["position"]
                            )
                        ]
                    )
                    await start_message.edit(embed=self.em(json["mode"], players_text))

        embed = discord.Embed(
            title="**Clash finished, here are the results**",
            color=discord.Color.random(),
        )

        for p in sorted(json["players"], key=lambda p: p["rank"]):
            embed.add_field(
                name=f"{p['rank']}. {p['codingamerNickname']}",
                value=(
                    f"Code length: {p['criterion']}, "
                    if json["mode"] == "SHORTEST"
                    else ""
                )
                + f"Score: {p['score']}%, Time: {p['duration'] // 60_000}:{p['duration'] // 1000 % 60:02}",
                inline=False,
            )
        await ctx.send(embed=embed)


def setup(bot: TechStruckBot):
    bot.add_cog(ClashOfCode(bot=bot))


================================================
FILE: bot/cogs/code_exec.py
================================================
import re

from discord import Color, Embed
from discord.ext import commands

from config.bot import bot_config


# TODO: Move this into utils
async def create_guest_paste_bin(session, code):
    res = await session.post(
        "https://pastebin.com/api/api_post.php",
        data={
            "api_dev_key": bot_config.pastebin_api_key,
            "api_paste_code": code,
            "api_paste_private": 0,
            "api_paste_name": "output.txt",
            "api_paste_expire_date": "1D",
            "api_option": "paste",
        },
    )
    return await res.text()


class CodeExec(commands.Cog):
    def __init__(self, bot: commands.Bot):
        self.bot = bot
        # TODO: Improve this further
        self.regex = re.compile(r"(\w*)\s*(?:```)(\w*)?([\s\S]*)(?:```$)")

    @property
    def session(self):
        return self.bot.http._HTTPClient__session  # type: ignore

    async def _run_code(self, *, lang: str, code: str):
        res = await self.session.post(
            "https://emkc.org/api/v1/piston/execute",
            json={"language": lang, "source": code},
        )
        return await res.json()

    @commands.command()
    async def run(self, ctx: commands.Context, *, codeblock: str):
        """
        Run code and get results instantly
        **Note**: You must use codeblocks around the code
        """
        matches = self.regex.findall(codeblock)
        if not matches:
            return await ctx.reply(
                embed=Embed(
                    title="Uh-oh", description="Couldn't quite see your codeblock"
                )
            )
        lang = matches[0][0] or matches[0][1]
        if not lang:
            return await ctx.reply(
                embed=Embed(
                    title="Uh-oh",
                    description="Couldn't find the language hinted in the codeblock or before it",
                )
            )
        code = matches[0][2]
        result = await self._run_code(lang=lang, code=code)

        await self._send_result(ctx, result)

    @commands.command()
    async def runl(self, ctx: commands.Context, lang: str, *, code: str):
        """
        Run a single line of code, **must** specify language as first argument
        """
        result = await self._run_code(lang=lang, code=code)
        await self._send_result(ctx, result)

    async def _send_result(self, ctx: commands.Context, result: dict):
        if "message" in result:
            return await ctx.reply(
                embed=Embed(
                    title="Uh-oh", description=result["message"], color=Color.red()
                )
            )
        output = result["output"]
        #        if len(output) > 2000:
        #            url = await create_guest_paste_bin(self.session, output)
        #            return await ctx.reply("Your output was too long, so here's the pastebin link " + url)
        embed = Embed(title=f"Ran your {result['language']} code", color=Color.green())
        output = output[:500].strip()
        shortened = len(output) > 500
        lines = output.splitlines()
        shortened = shortened or (len(lines) > 15)
        output = "\n".join(lines[:15])
        output += shortened * "\n\n**Output shortened**"
        embed.add_field(name="Output", value=output or "**<No output>**")

        await ctx.reply(embed=embed)


def setup(bot: commands.Bot):
    bot.add_cog(CodeExec(bot))


================================================
FILE: bot/cogs/fun.py
================================================
import asyncio

from discord import Color, Embed, Forbidden, Member, utils
from discord.ext import commands

from bot.bot import TechStruckBot


class Fun(commands.Cog):
    """Commands for fun and entertainment"""

    def __init__(self, bot: TechStruckBot):
        self.bot = bot

    @commands.command()
    async def beer(
        self, ctx, user: Member = None, *, reason: commands.clean_content = None
    ):
        """Have virtual beer with your friends/fellow members"""
        if not user or user.id == ctx.author.id:
            return await ctx.send(f"{ctx.author.name}: paaaarty!:tada::beer:")
        if user.id == self.bot.user.id:
            return await ctx.send("drinks beer with you* :beers:")
        if user.bot:
            return await ctx.send(f"lol {ctx.author.name}lol")

        beer_offer = f"{user.name}, you got a :beer: offer from {ctx.author.name}"
        beer_offer = beer_offer + f"\n\nReason: {reason}" if reason else beer_offer
        msg = await ctx.send(beer_offer)

        def reaction_check(reaction, m):
            return m.id == user.id and str(reaction.emoji) == "🍻"

        try:
            await msg.add_reaction("🍻")
            await self.bot.wait_for("reaction_add", timeout=30.0, check=reaction_check)
            await msg.edit(
                content=f"{user.name} and {ctx.author.name} are enjoying a lovely beer together :beers:"
            )
        except asyncio.TimeoutError:
            await msg.delete()
            await ctx.send(
                f"well, doesn't seem like {user.name} wanted a beer with you {ctx.author.name} ;-;"
            )
        except Forbidden:
            beer_offer = f"{user.name}, you got a :beer: from {ctx.author.name}"
            beer_offer = beer_offer + f"\n\nReason: {reason}" if reason else beer_offer
            await msg.edit(content=beer_offer)

    @commands.command()
    async def beers(
        self,
        ctx: commands.Context,
        members: commands.Greedy[Member],
        *,
        reason: commands.clean_content = None,
    ):
        """Invite a bunch of people to have beer"""
        if not members:
            return await ctx.send("You can't have beer with no other person!")
        for member in members:
            if member.bot:
                return await ctx.send("Beer with bots isn't exactly a thing...")

        message = (
            ", ".join(m.display_name for m in members)
            + "\nYou have been invited for beer \U0001f37b by "
            + ctx.author.display_name
            + ((" Reason: " + reason) if reason else "")
        )

        msg = await ctx.send(message)
        await msg.add_reaction("\U0001f37b")

        def check(r, m):
            return m in members and r.message == msg and str(r.emoji) == "\U0001f37b"

        while True:
            try:
                r, _ = await self.bot.wait_for("reaction_add", check=check, timeout=60)
            except asyncio.TimeoutError:
                return await msg.edit(
                    content="Ouch, looks like not everyone wants beer now..."
                )
            else:
                if set(
                    m.id for m in await r.message.reactions[0].users().flatten()
                ).issuperset(m.id for m in members):
                    content = (
                        ", ".join(
                            utils.escape_mentions(m.display_name) for m in members
                        )
                        + ", "
                        + utils.escape_mentions(ctx.author.display_name)
                        + " enjoy a lovely beer together \U0001f37b"
                    )

                    return await msg.edit(content=content)

    @commands.command()
    async def beerparty(
        self, ctx: commands.Context, *, reason: commands.clean_content = None
    ):
        """Openly allow anyone to join and enjoy in a beer party"""
        reason = ("\nReason: " + reason) if reason else ""
        msg = await ctx.send(f"Open invite to a beer party! {reason}")
        await msg.add_reaction("\U0001f37b")
        await asyncio.sleep(20)
        users = (
            await (await ctx.channel.fetch_message(msg.id))
            .reactions[0]
            .users()
            .flatten()
        )
        await ctx.send(
            ", ".join(
                [
                    utils.escape_mentions(u.display_name)
                    for u in users + ([] if ctx.author in users else [ctx.author])
                    if not u.bot
                ]
            )
            + " enjoy a lovely beer paaarty \U0001f37b"
        )


def setup(bot: TechStruckBot):
    bot.add_cog(Fun(bot))


================================================
FILE: bot/cogs/github.py
================================================
import datetime
import re
from io import BytesIO
from typing import Optional
from urllib.parse import urlencode

from cachetools import TTLCache
from discord import Color, Embed, File, Forbidden, Member
from discord.ext import commands
from jose import jwt
from reportlab.graphics import renderPM
from svglib.svglib import svg2rlg

from bot.utils.process_files import process_files
from config.common import config
from config.oauth import github_oauth_config
from models import UserModel


class GithubNotLinkedError(commands.CommandError):
    def __str__(self):
        return "Your github account hasn't been linked yet, please use the `linkgithub` command to do it"


class InvalidTheme(commands.CommandError):
    def __str__(self):
        return "Not a valid theme. List of all valid themes:- default, dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontrast, dracula"


class Github(commands.Cog):
    """Commands related to Github"""

    def __init__(self, bot: commands.Bot):
        self.bot = bot
        self.themes = "default dark radical merko gruvbox tokyonight onedark cobalt synthwave highcontrast dracula".split()
        self.files_regex = re.compile(r"\s{0,}```\w{0,}\s{0,}")
        self.token_cache = TTLCache(maxsize=1000, ttl=600)

    @property
    def session(self):
        return self.bot.http._HTTPClient__session  # type: ignore

    async def cog_before_invoke(self, ctx: commands.Context):
        if ctx.command == self.link_github:
            return

        token = self.token_cache.get(ctx.author.id)
        if not token:
            user = await UserModel.get_or_none(id=ctx.author.id)
            if user is None or user.github_oauth_token is None:
                raise GithubNotLinkedError()
            token = user.github_oauth_token
            self.token_cache[ctx.author.id] = token
        ctx.gh_token = token  # type: ignore

    @commands.command(name="linkgithub", aliases=["lngithub"])
    async def link_github(self, ctx: commands.Context):
        """Link your Github account through OAuth2 to gain access to Github related commands"""
        expiry = datetime.datetime.utcnow() + datetime.timedelta(seconds=120)
        url = "https://github.com/login/oauth/authorize?" + urlencode(
            {
                "client_id": github_oauth_config.client_id,
                "scope": "gist",
                "redirect_uri": "https://tech-struck.vercel.app/oauth/github",
                "state": jwt.encode(
                    {"id": ctx.author.id, "expiry": str(expiry)}, config.secret
                ),
            }
        )
        try:
            await ctx.author.send(
                embed=Embed(
                    title="Connect Github",
                    description=f"Click [this]({url}) to link your github account. This link invalidates in 2 minutes",
                )
            )
        except Forbidden:
            await ctx.send(
                "Your DMs are closed. Open them so I can send you the authorization link."
            )

    @commands.group(name="gist", aliases=["gs"], invoke_without_command=True)
    async def gist(self, ctx: commands.Context):
        """Commands related to Github gists"""
        await ctx.send_help(self.gist)

    @gist.command(name="create", aliases=["cr"])
    async def create_gist(self, ctx: commands.Context, *, inp: Optional[str] = None):
        """
        Create gists from within discord

        Three ways to specify the files:
        -   Reply to a message with attachments
        -   Send attachments along with the command
        -   Use a filename and codeblock... format

        Example:

        filename.py
        ```
        # Codeblock with contents of filename.py
        ```

        filename2.txt
        ```
        Codeblock containing filename2.txt's contents
        ```
        """

        files, skipped = await process_files(ctx, inp)

        req = await self.github_request(ctx, "POST", "/gists", json={"files": files})

        res = await req.json()
        # TODO: Make this more verbose to the user and log errors
        embed = Embed(
            title="Gist creation",
            description=res.get("html_url", "Something went wrong."),
        )
        embed.add_field(name="Files", value="\n".join(files.keys()), inline=False)
        if skipped:
            embed.add_field(
                name="Skipped files", value="\n".join(skipped), inline=False
            )
        await ctx.send(embed=embed)

    @gist.command(name="list", aliases=["ls"])
    async def list_gist(self, ctx: commands.Context):
        """
        List 10 gists made by you
        """
        req = await self.github_request(ctx, "GET", "/gists")

        gists = (await req.json())[:10]
        embed = Embed(title="Your gists", color=Color.green())
        description = "\n\n".join(
            [
                "`{0[id]}`\n[{name}]({0[html_url]})".format(
                    gist, name=next(iter(gist["files"]))
                )
                for gist in gists
            ]
        )
        embed.description = description
        await ctx.send(embed=embed)

    @gist.command("delete", aliases=["del", "rm", "remove"])
    async def delete_gist(self, ctx: commands.Context, *, gist_id: str):
        """
        Delete a gist using its ID
        You can get the ID from the list
        """
        req = await self.github_request(ctx, "DELETE", "/gists/{}".format(gist_id))
        if req.status == 204:
            return await ctx.send("Deleted")
        if req.status == 404:
            return await ctx.send("Not found")
        if req.status == 403:
            return await ctx.send("Forbidden")

    @commands.command(name="githubsearch", aliases=["ghsearch", "ghse"])
    async def github_search(self, ctx: commands.Context, *, term: str):
        """
        Search through all public repositories in Github

        Github search filters work here
        eg `ghse user:FalseDev`
        """
        # TODO: Docs

        req = await self.github_request(
            ctx, "GET", "/search/repositories", dict(q=term, per_page=5)
        )

        data = await req.json()
        if not data["items"]:
            return await ctx.send(
                embed=Embed(
                    title=f"Searched for {term}",
                    color=Color.red(),
                    description="No results found",
                )
            )

        em = Embed(
            title=f"Searched for {term}",
            color=Color.green(),
            description="\n\n".join(
                [
                    "[{0[owner][login]}/{0[name]}]({0[html_url]})\n{0[stargazers_count]:,} :star:\u2800{0[forks_count]} \u2387\u2800\n{1}".format(
                        result, self.repo_desc_format(result)
                    )
                    for result in data["items"]
                ]
            ),
        )

        await ctx.send(embed=em)

    @commands.command(name="githubstats", aliases=["ghstats", "ghst"])
    async def github_stats(
        self, ctx: commands.Context, username: str = None, theme="radical"
    ):
        """View statistics about you/any Github user in various themes"""
        theme = self.process_theme(theme)

        url = "https://github-readme-stats.codestackr.vercel.app/api"

        username = username or await self.get_gh_user(ctx)

        file = await self.get_file_from_svg_url(
            url,
            params={
                "username": username,
                "show_icons": "true",
                "hide_border": "true",
                "theme": theme,
            },
            exclude=[b"A++", b"A+"],
        )
        await ctx.send(file=File(file, filename="stats.png"))

    @commands.command(name="githublanguages", aliases=["ghlangs", "ghtoplangs"])
    async def github_top_languages(
        self, ctx: commands.Context, username: str = None, theme: str = "radical"
    ):
        """View language usage statistics for you/any github user in various themes"""

        username = username or await self.get_gh_user(ctx)
        theme = self.process_theme(theme)
        url = "https://github-readme-stats.codestackr.vercel.app/api/top-langs/"

        file = await self.get_file_from_svg_url(
            url, params={"username": username, "theme": theme}
        )
        await ctx.send(file=File(file, filename="langs.png"))

    async def get_file_from_svg_url(
        self, url: str, *, params={}, exclude=[], fmt="PNG"
    ):
        res = await (await self.session.get(url, params=params)).content.read()
        for i in exclude:
            res = res.replace(
                i, b""
            )  # removes everything that needs to be excluded (eg. the uncentered A+)
        drawing = svg2rlg(BytesIO(res))
        file = BytesIO(renderPM.drawToString(drawing, fmt=fmt))
        return file

    def process_theme(self, theme):
        theme = theme.lower()
        if theme not in self.themes:
            raise InvalidTheme()
        return theme

    @staticmethod
    def repo_desc_format(result):
        description = result["description"]
        if not description:
            return ""
        return description if len(description) < 100 else (description[:100] + "...")

    async def github_request(
        self,
        ctx: commands.Context,
        req_type: str,
        endpoint: str,
        params: dict = None,
        json: dict = None,
    ):
        return await self.session.request(
            req_type,
            f"https://api.github.com{endpoint}",
            params=params,
            json=json,
            headers={"Authorization": f"Bearer {ctx.gh_token}"},
        )

    async def get_gh_user(self, ctx: commands.Context):
        response = await (await self.github_request(ctx, "GET", "/user")).json()
        return response.get("login")


def setup(bot: commands.Bot):
    bot.add_cog(Github(bot))


================================================
FILE: bot/cogs/help_command.py
================================================
import discord
from discord.ext import commands

bot_links = """[Support](https://discord.gg/KgZRMch3b6)\u2800\
[Github](https://github.com/FalseDev/Tech-struck)\u2800\
[Suggestions](https://github.com/FalseDev/Tech-struck/issues)"""


class HelpCommand(commands.HelpCommand):
    """
    An Embed help command
    Based on https://gist.github.com/Rapptz/31a346ed1eb545ddeb0d451d81a60b3b
    """

    COLOUR = discord.Colour.greyple()

    def get_ending_note(self):
        return "Use {0}{1} [command] for more info on a command.".format(
            self.clean_prefix, self.invoked_with
        )

    def get_command_signature(self, command):
        return "{0.qualified_name} {0.signature}".format(command)

    async def send_bot_help(self, mapping):
        embed = discord.Embed(title="Bot Commands", colour=self.COLOUR)
        description = self.context.bot.description
        if description:
            embed.description = description

        for cog, cmds in mapping.items():
            if cog is None:
                continue
            name = cog.qualified_name
            filtered = await self.filter_commands(cmds, sort=True)
            if filtered:
                value = "\u2002".join(f"`{c.name}`" for c in cmds)
                if cog and cog.description:
                    value = "{0}\n{1}".format(cog.description, value)

                embed.add_field(name=name, value=value)

        embed.set_footer(text=self.get_ending_note())
        self.add_support_server(embed)
        await self.get_destination().send(embed=embed)

    async def send_cog_help(self, cog):
        embed = discord.Embed(
            title="{0.qualified_name} Commands".format(cog), colour=self.COLOUR
        )
        if cog.description:
            embed.description = cog.description

        filtered = await self.filter_commands(cog.get_commands(), sort=True)
        for command in filtered:
            embed.add_field(
                name=command.qualified_name,
                value=command.short_doc or "...",
                inline=False,
            )

        embed.set_footer(text=self.get_ending_note())
        self.add_support_server(embed)
        await self.get_destination().send(embed=embed)

    async def send_group_help(self, group):
        embed = discord.Embed(title=group.qualified_name, colour=self.COLOUR)
        if group.help:
            embed.description = group.help

        filtered = await self.filter_commands(group.commands, sort=True)
        for command in filtered:
            embed.add_field(
                name=command.qualified_name,
                value=command.short_doc or "...",
                inline=False,
            )

        embed.set_footer(text=self.get_ending_note())
        self.add_support_server(embed)
        await self.get_destination().send(embed=embed)

    def add_support_server(self, embed):
        return embed.add_field(name="Links", value=bot_links, inline=False)

    async def send_command_help(self, command):
        embed = discord.Embed(title=command.qualified_name, colour=self.COLOUR)
        embed.add_field(name="Signatute", value=self.get_command_signature(command))
        if command.help:
            embed.description = command.help

        embed.set_footer(text=self.get_ending_note())
        self.add_support_server(embed)
        await self.get_destination().send(embed=embed)


def setup(bot: commands.Bot):
    bot._default_help_command = bot.help_command
    bot.help_command = HelpCommand()


def teardown(bot):
    bot.help_command = bot._default_help_command


================================================
FILE: bot/cogs/joke.py
================================================
import asyncio

from discord import Color, Embed, Message, RawReactionActionEvent, TextChannel, utils
from discord.ext import commands

from models import JokeModel, UserModel

joke_format = """**Setup**: {0.setup}\n
**End**: {0.end}\n
**Server**: {1.name} (`{1.id}`)\n
**Username**: {2} (`{2.id}`)\n
Joke ID: {0.id}"""


class Joke(commands.Cog):
    """Joke related commands"""

    def __init__(self, bot: commands.Bot):
        self.bot = bot

    @commands.group(invoke_without_command=True)
    async def joke(self, ctx: commands.Context):
        """Joke commands"""
        await ctx.send_help(self.joke)

    @joke.command()
    @commands.cooldown(1, 60, type=commands.BucketType.user)
    async def add(self, ctx: commands.Context):
        """Submit a joke that can then get approved and part of the collection"""
        try:
            setup = await self._get_input(
                ctx,
                "Enter joke setup",
                "Enter the question/setup to be done before answering/finishing the joke",
            )
            end = await self._get_input(
                ctx, "Enter joke end", "Enter the text to be used to finish the joke"
            )
        except asyncio.TimeoutError:
            return await ctx.send("You didn't answer")

        await UserModel.get_or_create(id=ctx.author.id)
        joke = await JokeModel.create(setup=setup, end=end, creator_id=ctx.author.id)

        msg = await self.joke_entries_channel.send(
            embed=Embed(
                title=f"Joke #{joke.id}",
                description=joke_format.format(joke, ctx.guild, ctx.author),
                color=Color.dark_gold(),
            )
        )
        await ctx.send("Your submission has been recorded!")

        await msg.add_reaction("\u2705")
        await msg.add_reaction("\u274e")
        await self.joke_entries_channel.send("<@&815237052639477792>", delete_after=1)

    @property
    def joke_entries_channel(self) -> TextChannel:
        return self.bot.get_channel(815237244218114058)

    async def _get_input(self, ctx: commands.Context, title: str, description: str):
        await ctx.send(
            embed=Embed(title=title, description=description, color=Color.dark_blue())
        )

        def check(m: Message):
            return m.author == ctx.author and m.channel == ctx.channel

        res: Message = await self.bot.wait_for("message", check=check, timeout=120)
        return await commands.clean_content().convert(ctx, res.content)

    @commands.Cog.listener("on_raw_reaction_add")
    @commands.Cog.listener("on_raw_reaction_remove")
    async def reaction_listener(self, payload: RawReactionActionEvent):
        if payload.channel_id != 815237244218114058:
            return
        msg: Message = await self.joke_entries_channel.fetch_message(payload.message_id)

        up_reaction = utils.get(msg.reactions, emoji="\u2705")
        down_reaction = utils.get(msg.reactions, emoji="\u274e")
        ups = (up_reaction and await up_reaction.users().flatten()) or []
        # downs = (down_reaction and await up_reaction.users().flatten()) or []
        # TODO: Add further stuff here for downvotes checking etc

        embed = msg.embeds[0]
        if len(ups) > 3:
            await JokeModel.filter(id=int(embed.title[6:])).update(accepted=True)
            embed.color = Color.green()
            await msg.edit(embed=embed)


def setup(bot: commands.Bot):
    bot.add_cog(Joke(bot))


================================================
FILE: bot/cogs/packages.py
================================================
from aiohttp import ContentTypeError
from discord import Color, Embed
from discord.ext.commands import Cog, Context, command

from ..bot import TechStruckBot

class Packages(Cog):
    """Commands related to Package Search"""

    def __init__(self, bot: TechStruckBot):
        self.bot = bot

    @property
    def session(self):
        return self.bot.session

    async def get_package(self, url: str):
        return await self.session.get(url=url)

    @command(aliases=["pypi"])
    async def pypisearch(self, ctx: Context, arg: str):
        """Get info about a Python package directly from PyPi"""

        res_raw = await self.get_package(f"https://pypi.org/pypi/{arg}/json")

        try:
            res_json = await res_raw.json()
        except ContentTypeError:
            return await ctx.send(
                embed=Embed(
                    description="No such package found in the search query.",
                    color=Color.blurple(),
                )
            )

        res = res_json["info"]

        def getval(key):
            return res[key] or "Unknown"

        name = getval("name")
        author = getval("author")
        author_email = getval("author_email")

        description = getval("summary")
        home_page = getval("home_page")

        project_url = getval("project_url")
        version = getval("version")
        _license = getval("license")

        embed = Embed(
            title=f"{name} PyPi Stats", description=description, color=Color.teal()
        )

        embed.add_field(name="Author", value=author, inline=True)
        embed.add_field(name="Author Email", value=author_email, inline=True)

        embed.add_field(name="Version", value=version, inline=False)
        embed.add_field(name="License", value=_license, inline=True)

        embed.add_field(name="Project Url", value=project_url, inline=False)
        embed.add_field(name="Home Page", value=home_page)

        embed.set_thumbnail(url="https://i.imgur.com/syDydkb.png")

        await ctx.send(embed=embed)

    @command(aliases=["npm"])
    async def npmsearch(self, ctx: Context, arg: str):
        """Get info about a NPM package directly from the NPM Registry"""

        res_raw = await self.get_package(f"https://registry.npmjs.org/{arg}/")

        res_json = await res_raw.json()

        if res_json.get("error"):
            return await ctx.send(
                embed=Embed(
                    description="No such package found in the search query.",
                    color=0xCC3534,
                )
            )

        latest_version = res_json["dist-tags"]["latest"]
        latest_info = res_json["versions"][latest_version]

        def getval(*keys):
            keys = list(keys)
            val = latest_info.get(keys.pop(0)) or {}

            if keys:
                for i in keys:
                    try:
                        val = val.get(i)
                    except TypeError:
                        return "Unknown"

            return val or "Unknown"

        pkg_name = getval("name")
        description = getval("description")

        author = getval("author", "name")
        author_email = getval("author", "email")

        repository = (
            getval("repository", "url").removeprefix("git+").removesuffix(".git")
        )

        homepage = getval("homepage")
        _license = getval("license")

        em = Embed(
            title=f"{pkg_name} NPM Stats", description=description, color=0xCC3534
        )

        em.add_field(name="Author", value=author, inline=True)
        em.add_field(name="Author Email", value=author_email, inline=True)

        em.add_field(name="Latest Version", value=latest_version, inline=False)
        em.add_field(name="License", value=_license, inline=True)

        em.add_field(name="Repository", value=repository, inline=False)
        em.add_field(name="Homepage", value=homepage, inline=True)

        em.set_thumbnail(
            url="https://upload.wikimedia.org/wikipedia/commons/thumb/d/db/Npm-logo.svg/800px-Npm-logo.svg.png"
        )

        await ctx.send(embed=em)

    @command(aliases=["crates"])
    async def crate(self, ctx: Context, arg: str):
        """Get info about a Rust package directly from the Crates.IO Registry"""

        res_raw = await self.get_package(f"https://crates.io/api/v1/crates/{arg}")

        res_json = await res_raw.json()

        if res_json.get("errors"):
            return await ctx.send(
                embed=Embed(
                    description="No such package found in the search query.",
                    color=0xE03D29,
                )
            )
        main_info = res_json["crate"]
        latest_info = res_json["versions"][0]

        def getmainval(key):
            return main_info[key] or "Unknown"

        def getversionvals(*keys):
            keys = list(keys)
            val = latest_info.get(keys.pop(0)) or {}

            if keys:
                for i in keys:
                    try:
                        val = val.get(i)
                    except TypeError:
                        return "Unknown"

            return val or "Unknown"

        pkg_name = getmainval("name")
        description = getmainval("description")
        downloads = getmainval("downloads")

        publisher = getversionvals("published_by", "name")
        latest_version = getversionvals("num")
        repository = getmainval("repository")

        homepage = getmainval("homepage")
        _license = getversionvals("license")

        em = Embed(
            title=f"{pkg_name} crates.io Stats", description=description, color=0xE03D29
        )

        em.add_field(name="Published By", value=publisher, inline=True)
        em.add_field(name="Downloads", value="{:,}".format(downloads), inline=True)

        em.add_field(name="Latest Version", value=latest_version, inline=False)
        em.add_field(name="License", value=_license, inline=True)

        em.add_field(name="Repository", value=repository, inline=False)
        em.add_field(name="Homepage", value=homepage, inline=True)

        em.set_thumbnail(
            url="https://upload.wikimedia.org/wikipedia/commons/thumb/d/d5/Rust_programming_language_black_logo.svg/2048px-Rust_programming_language_black_logo.svg.png"
        )

        await ctx.send(embed=em)

def setup(bot: TechStruckBot):
    bot.add_cog(Packages(bot))


================================================
FILE: bot/cogs/quiz.py
================================================
from discord import Color, Embed, Message
from discord.ext import commands
from quizapi import create_quiz_api

from config.bot import bot_config


class Quiz(commands.Cog):
    def __init__(self, bot: commands.Bot):
        self.bot = bot
        self.session = create_quiz_api(bot_config.quiz_api_token, async_mode=True)

    @commands.command()
    async def startquiz(self, ctx: commands.Context):
        await ctx.send("Collecting questions!")
        questions = await self.session.get_quiz(limit=5, category="linux")
        embed = Embed(title="Big Brain Time", color=Color.darker_gray())

        def check(m: Message):
            return m.channel == ctx.channel

        scoreboard = {}
        for q in questions:
            embed.clear_fields()
            desc = q.description + "\n" if q.description else ""
            desc += " ".join(["`" + t.name + "`" for t in q.tags]) + "\n"
            for i, a in enumerate(q.answers, 65):
                desc += chr(i) + ") " + a + "\n"
            embed.add_field(name=q.question, value=desc)
            correct_answers = []
            print(q.correct_answers)
            for i in range(q.correct_answers.count(True)):
                correct_answers.append(chr(65 + q.correct_answers.index(True)))
                q.correct_answers.remove(True)
                print(correct_answers)
            await ctx.send(embed=embed)
            unanswered = True
            while unanswered:
                try:
                    resp = await self.bot.wait_for("message", check=check, timeout=45)
                except:
                    return await ctx.send("No one answered")
                # await resp.delete()
                if resp.content.upper() in (correct_answers):
                    scoreboard[resp.author.id] = scoreboard.get(resp.author.id, 0) + 1
                    unanswered = False
        scores = "\n".join(
            [
                f"<@!{mid}>: {score}"
                for mid, score in sorted(scoreboard.items(), key=lambda i: i[1])
            ]
        )
        await ctx.send(
            embed=Embed(title="Results", description=scores, color=Color.green())
        )


def setup(bot: commands.Bot):
    bot.add_cog(Quiz(bot))


================================================
FILE: bot/cogs/rtfm.py
================================================
import warnings

import aiohttp
from discord import Color, Embed
from discord.ext import commands, flags

from bot.bot import TechStruckBot
from bot.utils import fuzzy, rtfm


class RTFM(commands.Cog):
    """Search through manuals of several python modules and python itself"""

    targets = {
        "python": "https://docs.python.org/3",
        "discord.py": "https://discordpy.readthedocs.io/en/latest",
        "numpy": "https://numpy.readthedocs.io/en/latest",
        "pandas": "https://pandas.pydata.org/docs",
        "pillow": "https://pillow.readthedocs.io/en/stable",
        "imageio": "https://imageio.readthedocs.io/en/stable",
        "requests": "https://requests.readthedocs.io/en/master",
        "aiohttp": "https://docs.aiohttp.org/en/stable",
        "django": "https://django.readthedocs.io/en/stable",
        "flask": "https://flask.palletsprojects.com/en/1.1.x",
        "praw": "https://praw.readthedocs.io/en/latest",
        "apraw": "https://apraw.readthedocs.io/en/latest",
        "asyncpg": "https://magicstack.github.io/asyncpg/current",
        "aiosqlite": "https://aiosqlite.omnilib.dev/en/latest",
        "sqlalchemy": "https://docs.sqlalchemy.org/en/14",
        "tensorflow": "https://www.tensorflow.org/api_docs/python",
        "matplotlib": "https://matplotlib.org/stable",
        "seaborn": "https://seaborn.pydata.org",
        "pygame": "https://www.pygame.org/docs",
        "simplejson": "https://simplejson.readthedocs.io/en/latest",
        "wikipedia": "https://wikipedia.readthedocs.io/en/latest",
    }

    aliases = {
        ("py", "py3", "python3", "python"): "python",
        ("dpy", "discord.py", "discordpy"): "discord.py",
        ("np", "numpy", "num"): "numpy",
        ("pd", "pandas", "panda"): "pandas",
        ("pillow", "pil"): "pillow",
        ("imageio", "imgio", "img"): "imageio",
        ("requests", "req"): "requests",
        ("aiohttp", "http"): "aiohttp",
        ("django", "dj"): "django",
        ("flask", "fl"): "flask",
        ("reddit", "praw", "pr"): "praw",
        ("asyncpraw", "apraw", "apr"): "apraw",
        ("asyncpg", "pg"): "asyncpg",
        ("aiosqlite", "sqlite", "sqlite3", "sqli"): "aiosqlite",
        ("sqlalchemy", "sql", "alchemy", "alchem"): "sqlalchemy",
        ("tensorflow", "tf"): "tensorflow",
        ("matplotlib", "mpl", "plt"): "matplotlib",
        ("seaborn", "sea"): "seaborn",
        ("pygame", "pyg", "game"): "pygame",
        ("simplejson", "sjson", "json"): "simplejson",
        ("wiki", "wikipedia"): "wikipedia",
    }

    url_overrides = {
        "tensorflow": "https://github.com/mr-ubik/tensorflow-intersphinx/raw/master/tf2_py_objects.inv"
    }

    def __init__(self, bot: TechStruckBot) -> None:
        self.bot = bot
        self.cache = {}

    @property
    def session(self) -> aiohttp.ClientSession:
        return self.bot.http._HTTPClient__session  # type: ignore

    async def build(self, target) -> None:
        url = self.targets[target]
        req = await self.session.get(
            self.url_overrides.get(target, url + "/objects.inv")
        )
        if req.status != 200:
            warnings.warn(
                Warning(
                    f"Received response with status code {req.status} when trying to build RTFM cache for {target} through {url}/objects.inv"
                )
            )
            raise commands.CommandError("Failed to build RTFM cache")
        self.cache[target] = rtfm.SphinxObjectFileReader(
            await req.read()
        ).parse_object_inv(url)

    @commands.group(invoke_without_command=True)
    async def rtfm(self, ctx: commands.Context, doc: str, *, term: str = None):
        """
        Search through docs of a module/python
        Args: target, term
        """
        doc = doc.lower()
        target = None
        for aliases, target_name in self.aliases.items():
            if doc in aliases:
                target = target_name

        if not target:
            return await ctx.reply("Alias/target not found")
        if not term:
            return await ctx.reply(self.targets[target])

        cache = self.cache.get(target)
        if not cache:
            await ctx.trigger_typing()
            await self.build(target)
            cache = self.cache.get(target)

        results = fuzzy.finder(term, list(cache.items()), key=lambda x: x[0], lazy=False)[:8]  # type: ignore

        if not results:
            return await ctx.reply("Couldn't find any results")

        await ctx.reply(
            embed=Embed(
                title=f"Searched in {target}",
                description="\n".join([f"[`{key}`]({url})" for key, url in results]),
                color=Color.dark_purple(),
            )
        )

    @rtfm.command(name="list")
    async def list_targets(self, ctx: commands.Context):
        """List all the avaliable documentation search targets"""
        aliases = {v: k for k, v in self.aliases.items()}
        embed = Embed(title="RTFM list of avaliable modules", color=Color.green())
        embed.description = "\n".join(
            [
                "[{0}]({1}): {2}".format(
                    target,
                    link,
                    "\u2800".join([f"`{i}`" for i in aliases[target] if i != target]),
                )
                for target, link in self.targets.items()
            ]
        )

        await ctx.send(embed=embed)

    @flags.add_flag("aliases", nargs="+")
    @flags.add_flag("url")
    @flags.add_flag("name")
    @flags.add_flag("--override", "-o")
    @rtfm.command(name="add", hidden=True, cls=flags.FlagCommand)
    @commands.is_owner()
    async def add_target(self, ctx: commands.Context, **kwargs):
        print(kwargs)
        name, url, aliases, override = (
            kwargs.pop("name"),
            kwargs.pop("url"),
            kwargs.pop("aliases"),
            kwargs.pop("override"),
        )
        print(name, url, aliases, override)
        self.targets[name] = url
        self.aliases[tuple(aliases)] = name
        if override:
            self.url_overrides[name] = override

        await ctx.send(
            "RTFM target {name} added with aliases {aliases}".format(
                name=name, aliases=aliases
            )
        )


def setup(bot: TechStruckBot):
    bot.add_cog(RTFM(bot))


================================================
FILE: bot/cogs/stackexchange.py
================================================
import datetime
import html
import json
import os
import traceback
from typing import Optional
from urllib.parse import urlencode

from cachetools import TTLCache
from discord import Color, Embed, Forbidden, Member
from discord.ext import commands, flags, tasks
from jose import jwt

from bot.utils import fuzzy
from config.common import config
from config.oauth import stack_oauth_config
from models import UserModel

search_result_template = "[View]({site[site_url]}/q/{q[question_id]})\u2800\u2800Score: {q[score]}\u2800\u2800Tags: {tags}"


class StackExchangeNotLinkedError(commands.CommandError):
    def __str__(self):
        return "Your stackexchange account hasn't been linked yet, please use the `linkstack` command to do it"


class StackExchangeError(commands.CommandError):
    pass


class Stackexchange(commands.Cog):
    """Commands related to the StackExchange network"""

    def __init__(self, bot: commands.Bot):
        self.bot = bot
        self.ready = False
        self.sites = None
        self.token_cache = TTLCache(maxsize=1000, ttl=600)
        self.load_sites.start()

    @property
    def session(self):
        return self.bot.http._HTTPClient__session

    @tasks.loop(count=1)
    async def load_sites(self):
        if os.path.isfile("cache/stackexchange_sites.json"):
            with open("cache/stackexchange_sites.json") as f:
                self.sites = json.load(f)
        else:
            try:
                data = await self.stack_request(
                    None,
                    "GET",
                    "/sites",
                    params={"pagesize": "500", "filter": "*Ids4-aWV*RW_UxCPr0D"},
                )
            except Exception:
                return traceback.print_exc()
            else:
                self.sites = data["items"]
                if not os.path.isdir("cache"):
                    os.mkdir("cache")
                with open("cache/stackexchange_sites.json", "w") as f:
                    json.dump(self.sites, f)

        self.ready = True

    async def cog_check(self, ctx: commands.Context):
        if not self.ready:
            raise StackExchangeError("Stackexchange commands are not ready yet")
        return True

    async def cog_before_invoke(self, ctx: commands.Context):
        if ctx.command == self.link_stackoverflow:
            return

        token = self.token_cache.get(ctx.author.id)
        if not token:
            user = await UserModel.get_or_none(id=ctx.author.id)
            if user is None or user.stackoverflow_oauth_token is None:
                raise StackExchangeNotLinkedError()

            token = user.stackoverflow_oauth_token
            self.token_cache[ctx.author.id] = token
        ctx.stack_token = token  # type: ignore

    @flags.add_flag("--site", type=str, default="stackoverflow")
    @flags.command(
        name="stackprofile",
        aliases=["stackpro", "stackacc", "stackaccount"],
    )
    async def stack_profile(self, ctx: commands.Context, **kwargs):
        """Check your stackoverflow reputation"""
        # TODO: Use a stackexchange filter here
        # https://api.stackexchange.com/docs/filters
        site = self.get_site(kwargs["site"])
        data = await self.stack_request(
            ctx,
            "GET",
            "/me",
            data={
                "site": site["api_site_parameter"],
            },
        )
        if not data["items"]:
            return await ctx.send("You don't have an account in this site!")
        profile = data["items"][0]
        embed = Embed(title=site["name"] + " Profile", color=0x0077CC)

        embed.add_field(name="Username", value=profile["display_name"], inline=False)
        embed.add_field(name="Reputation", value=profile["reputation"], inline=False)
        embed.add_field(
            name="Badges",
            value="\U0001f947 {0[gold]} \u2502 \U0001f948 {0[silver]} \u2502 \U0001f949 {0[bronze]}".format(
                profile["badge_counts"]
            ),
            inline=False,
        )

        embed.set_thumbnail(url=profile["profile_image"])
        await ctx.send(embed=embed)

    @flags.add_flag("--site", type=str, default="stackoverflow")
    @flags.add_flag("--tagged", type=str, nargs="+", default=[])
    @flags.add_flag("term", nargs="+")
    @flags.command(name="stacksearch", aliases=["stackser"])
    async def stackexchange_search(self, ctx: commands.Context, **kwargs):
        """Search stackexchange for your question"""
        term, sitename, tagged = (
            " ".join(kwargs["term"]),
            kwargs["site"],
            kwargs["tagged"],
        )

        site = self.get_site(sitename)

        data = await self.stack_request(
            ctx,
            "GET",
            "/search/excerpts",
            data={
                "site": sitename,
                "sort": "relevance",
                "q": term,
                "tagged": ";".join(tagged),
                "pagesize": 5,
                "filter": "ld-5YXYGN1SK1e",
            },
        )
        embed = Embed(title=f"{site['name']} search", color=Color.green())
        embed.set_thumbnail(url=site["icon_url"])
        if data["items"]:
            for i, q in enumerate(data["items"], 1):
                tags = "\u2800".join(["`" + t + "`" for t in q["tags"]])
                embed.add_field(
                    name=str(i) + " " + html.unescape(q["title"]),
                    value=search_result_template.format(site=site, q=q, tags=tags),
                    inline=False,
                )
        else:
            embed.add_field(name="Oops", value="Couldn't find any results")
        await ctx.send(embed=embed)

    @commands.command(aliases=["stacksites"])
    async def stacksite(self, ctx: commands.Context, *, term: str):
        """Search through list of stackexchange sites and find relevant ones"""
        sites = fuzzy.finder(term, self.sites, key=lambda s: s["name"], lazy=False)[:5]  # type: ignore
        embed = Embed(color=Color.blue())
        description = "\n".join(
            ["[`{0[name]}`]({0[site_url]})".format(site) for site in sites]
        )
        embed.description = description
        await ctx.send(embed=embed)

    def get_site(self, sitename: str):
        sitename = sitename.lower()
        for site in self.sites:
            if site["api_site_parameter"] == sitename:
                return site
        raise StackExchangeError(f"Invalid site {sitename} provided")

    async def stack_request(
        self,
        ctx: Optional[commands.Context],
        method: str,
        endpoint: str,
        params: dict = {},
        data: dict = {},
    ):
        data.update(stack_oauth_config.dict())
        if ctx:
            data["access_token"] = (ctx.stack_token,)
        res = await self.session.request(
            method,
            f"https://api.stackexchange.com/2.2{endpoint}",
            params=params,
            data=data,
        )

        data = await res.json()
        if "error_message" in data:
            raise StackExchangeError(data["error_message"])
        return data

    @commands.command(name="linkstack", aliases=["lnstack"])
    async def link_stackoverflow(self, ctx: commands.Context):
        """Link your stackoverflow account"""
        expiry = datetime.datetime.utcnow() + datetime.timedelta(seconds=120)
        url = "https://stackoverflow.com/oauth/?" + urlencode(
            {
                "client_id": stack_oauth_config.client_id,
                "scope": "no_expiry",
                "redirect_uri": "https://tech-struck.vercel.app/oauth/stackexchange",
                "state": jwt.encode(
                    {"id": ctx.author.id, "expiry": str(expiry)}, config.secret
                ),
            }
        )
        try:
            await ctx.author.send(
                embed=Embed(
                    title="Connect Stackexchange",
                    description=f"Click [this]({url}) to link your stackexchange account. This link invalidates in 2 minutes",
                    color=Color.blue(),
                )
            )
        except Forbidden:
            await ctx.send(
                "Your DMs (direct messages) are closed. Open them so I can send you a safe authorization link."
            )


def setup(bot: commands.Bot):
    bot.add_cog(Stackexchange(bot))


================================================
FILE: bot/cogs/thank.py
================================================
import asyncio
from typing import Optional

from discord import Color, Embed, Member, Reaction
from discord.ext import commands
from tortoise.functions import Count, Q

from models import ThankModel, UserModel

delete_thank_message = """**Thanked**: <@!{0.thanked_id}>
**Thanker**: <@!{0.thanker_id}>
**Description**: {0.description}
**Time**: {0.time}\n
Confirmation required!"""

thank_list_message = """`{0.time:%D %T}` ID:`{0.id}`
From: <@!{0.thanker_id}> ({0.thanker_id})
Description: {0.description}\n"""


class Thank(commands.Cog):
    """Commands related to thanking members/helpers for help received"""

    def __init__(self, bot: commands.Bot):
        self.bot = bot

    @commands.group(invoke_without_command=True, aliases=["thanks", "rep"])
    @commands.cooldown(5, 300, commands.BucketType.user)
    async def thank(self, ctx: commands.Context, recv: Member, *, description: str):
        """Thank someone for their help with a description to show gratitude"""
        des_len = len(description)
        if des_len < 5 or des_len > 100:
            return await ctx.send(
                f"Thank description must be between 5 and 100 characters, yours was {des_len}"
            )
        if recv.id == ctx.author.id:
            return await ctx.send(
                embed=Embed(
                    title="Bruh",
                    description="You can't thank yourselves",
                    color=Color.red(),
                )
            )
        if recv.bot:
            return await ctx.send(
                embed=Embed(
                    title="Bruh", description="You can't thank a bot", color=Color.red()
                )
            )
        # TODO: Convert this to an expression (?) for efficiency
        thanked, _ = await UserModel.get_or_create(id=recv.id)
        thanker, _ = await UserModel.get_or_create(id=ctx.author.id)
        await ThankModel.create(
            thanker=thanker,
            thanked=thanked,
            description=description,
            guild_id=ctx.guild.id,
        )
        await ctx.send(
            embed=Embed(
                description=f"You thanked {recv.mention}!", color=0x6EFFFF
            )
        )

    @thank.command(name="stats", aliases=["check"])
    async def thank_stats(
        self, ctx: commands.Context, *, member: Optional[Member] = None
    ):
        """View stats for thanks you've received and sent, in the current server and globally"""
        member = member or ctx.author
        sent_thanks = await ThankModel.filter(thanker__id=member.id).count()
        recv_thanks = await ThankModel.filter(thanked__id=member.id).count()
        server_sent_thanks = await ThankModel.filter(
            thanker__id=member.id, guild__id=ctx.guild.id
        ).count()
        server_recv_thanks = await ThankModel.filter(
            thanked__id=member.id, guild__id=ctx.guild.id
        ).count()

        embed = Embed(title=f"Thank stats for: {member}", color=Color.green())
        embed.add_field(
            name="Thanks received",
            value="Global: {}\nThis server: {}".format(recv_thanks, server_recv_thanks),
        )
        embed.add_field(
            name="Thanks sent",
            value="Global: {}\nThis server: {}".format(sent_thanks, server_sent_thanks),
        )
        await ctx.send(embed=embed)

    @thank.command(name="leaderboard", aliases=["lb"])
    async def thank_leaderboard(self, ctx: commands.Context):
        """View a leaderboard of top helpers in the current server"""
        await ctx.trigger_typing()
        lb = (
            await UserModel.annotate(
                thank_count=Count("thanks", _filter=Q(thanks__guild_id=ctx.guild.id))
            )
            .filter(thank_count__gt=0)
            .order_by("-thank_count")
            .limit(5)
        )
        if not lb:
            return await ctx.send(
                embed=Embed(
                    title="Oopsy",
                    description="There are no thanks here yet!",
                    color=Color.red(),
                )
            )
        invis = "\u2800"
        embed = Embed(
            title="LeaderBoard",
            color=Color.blue(),
            description="\n\n".join(
                [
                    f"**{m.thank_count} Thanks**{invis * (4 - len(str(m.thank_count)))}<@!{m.id}>"
                    for m in lb
                ]
            ),
        )
        await ctx.send(embed=embed)

    @thank.command(name="delete")
    @commands.has_guild_permissions(kick_members=True)
    async def delete_thank(self, ctx: commands.Context, thank_id: int):
        """Remove an invalid/fake thank record"""
        thank = await ThankModel.get_or_none(pk=thank_id, guild_id=ctx.guild.id)
        if not thank:
            return await ctx.send("Thank with given ID not found")
        msg = await ctx.send(
            embed=Embed(
                title="Delete thank",
                description=delete_thank_message.format(thank),
            )
        )
        await msg.add_reaction("\u2705")
        await msg.add_reaction("\u274e")

        def check(r: Reaction, u: Member):
            return u.id == ctx.author.id and str(r.emoji) in ("\u2705", "\u274e")

        try:
            r, _ = await self.bot.wait_for("reaction_add", check=check)
        except asyncio.TimeoutError:
            return await ctx.reply("Cancelled.")
        if str(r.emoji) == "\u2705":
            await thank.delete()
            return await ctx.reply("Deleted.")
        return await ctx.reply("Cancelled.")

    @thank.command(name="list")
    @commands.has_guild_permissions(kick_members=True)
    async def list_thanks(self, ctx: commands.Context, member: Member):
        """List the most recent 10 thanks received by a user in the current server"""
        thanks = (
            await ThankModel.filter(thanked_id=member.id, guild_id=ctx.guild.id)
            .order_by("-time")
            .limit(10)
        )

        await ctx.send(
            embed=Embed(
                title="Listing",
                description="\n".join([thank_list_message.format(t) for t in thanks]),
                color=Color.dark_blue(),
            )
        )


def setup(bot: commands.Bot):
    bot.add_cog(Thank(bot))


================================================
FILE: bot/cogs/utils.py
================================================
import sys
import os
import inspect

from discord import Embed, Message, TextChannel
from discord.ext import commands, flags

from bot.bot import TechStruckBot
from bot.utils.embed_flag_input import (
    allowed_mentions_input,
    dict_to_allowed_mentions,
    dict_to_embed,
    embed_input,
    process_message_mentions,
    webhook_input,
)

flags._converters.CONVERTERS["Message"] = commands.MessageConverter().convert


async def maybe_await(coro):
    if not coro:
        return
    return await coro


class Utils(commands.Cog):
    """Utility commands"""

    def __init__(self, bot: TechStruckBot):
        self.bot = bot

    @embed_input(all=True)
    @allowed_mentions_input()
    @webhook_input()
    @flags.add_flag("--channel", "--in", type=TextChannel, default=None)
    @flags.add_flag("--message", "--msg", "-m", default=None)
    @flags.add_flag("--edit", "-e", type=Message, default=None)
    @flags.command(
        brief="Send an embed with any fields, in any channel, with command line like arguments"
    )
    @commands.has_guild_permissions(administrator=True)
    @commands.bot_has_permissions(manage_webhooks=True, embed_links=True)
    async def embed(self, ctx: commands.Context, **kwargs):
        """
        Send an embed and its fully customizable
        Default mention settings:
            Users:      Enabled
            Roles:      Disabled
            Everyone:   Disabled
        """
        embed = dict_to_embed(kwargs, author=ctx.author)
        allowed_mentions = dict_to_allowed_mentions(kwargs)
        message = process_message_mentions(kwargs.pop("message"))

        if kwargs.pop("webhook"):
            if edit_message := kwargs.pop("edit"):
                edit_message.close()
            username, avatar_url = kwargs.pop("webhook_username"), kwargs.pop(
                "webhook_avatar"
            )
            if kwargs.pop("webhook_auto_author"):
                username, avatar_url = (
                    username or ctx.author.display_name,
                    avatar_url or ctx.author.avatar_url,
                )
            target = kwargs.pop("channel") or ctx.channel
            if name := kwargs.pop("webhook_new_name"):
                wh = await target.create_webhook(name=name)
            elif name := kwargs.pop("webhook_name"):
                try:
                    wh = next(
                        filter(
                            lambda wh: wh.name.casefold() == name.casefold(),
                            await target.webhooks(),
                        )
                    )
                except StopIteration:
                    return await ctx.send(
                        "No pre existing webhook found with given name"
                    )
            else:
                return await ctx.send("No valid webhook identifiers provided")
            await wh.send(
                message,
                embed=embed,
                allowed_mentions=allowed_mentions,
                username=username,
                avatar_url=avatar_url,
            )
            if kwargs.pop("webhook_dispose"):
                await wh.delete()
            return await ctx.message.add_reaction("\u2705")

        if edit := await maybe_await(kwargs.pop("edit")):
            if edit.author != ctx.guild.me:
                return await ctx.send(
                    f"The target message wasn't sent by me! It was sent by {edit.author}"
                )
            await edit.edit(
                content=message, embed=embed, allowed_mentions=allowed_mentions
            )
        else:
            target = kwargs.pop("channel") or ctx
            await target.send(message, embed=embed, allowed_mentions=allowed_mentions)
        await ctx.message.add_reaction("\u2705")

    @commands.command()
    async def rawembed(self, ctx: commands.Context):
        ref = ctx.message.reference
        if not ref or not ref.message_id:
            return await ctx.send("Reply to an message with an embed")
        message = ref.cached_message or await ctx.channel.fetch_message(ref.message_id)

        if not message.embeds:
            return await ctx.send("Message had no embeds")
        em = message.embeds[0]
        description = "```" + str(em.to_dict()) + "```"
        embed = Embed(description=description)
        await ctx.reply(embed=embed)

    @commands.command()
    async def source(self, ctx: commands.Context, *, command=None):
        """Get the source code of the bot or the provided command."""
        if command is None:
            return await ctx.send(
                embed=Embed(
                    description=f"My source can be found [here](https://github.com/TechStruck/TechStruck-Bot)!",
                    color=0x8ADCED
            )
        )

        if command == "help":
            src = type(self.bot.help_command)
            module = src.__module__
            filename = inspect.getsourcefile(src)
        else:
            cmd = self.bot.get_command(command)
            if cmd is None:
                return await ctx.send(
                    embed=Embed(
                        description="No such command found.",
                        color=0x8ADCED
                    )
                )
            
            src = cmd.callback.__code__
            module = cmd.callback.__module__
            filename = src.co_filename

        lines, firstline = inspect.getsourcelines(src)
        lines = len(lines)
        location = (
            module.replace(".", "/") + ".py"
            if module.startswith("discord")
            else os.path.relpath(filename).replace(r"\\", "/")
        )

        url = f"https://github.com/TechStruck/TechStruck-Bot/blob/main/{location}#L{firstline}-L{firstline+lines-1}"
        await ctx.send(
            embed=Embed(
                description=f"Source of {command} can be found [here]({url}).",
                color=0x8ADCED
            )
        )


def setup(bot: TechStruckBot):
    bot.add_cog(Utils(bot))


def teardown(bot: TechStruckBot):
    del sys.modules["bot.utils.embed_flag_input"]


================================================
FILE: bot/core.py
================================================
import platform
import sys

import psutil
from discord import Color, Embed, NotFound
from discord import __version__ as discord_version
from discord.ext import commands

from models import GuildModel

from .bot import TechStruckBot


class Common(commands.Cog):
    def __init__(self, bot: TechStruckBot):
        self.bot = bot

    @commands.command(aliases=["latency"])
    async def ping(self, ctx: commands.Context):
        """Check latency of the bot"""
        latency = str(round(self.bot.latency * 1000, 1))
        await ctx.send(
            embed=Embed(title="Pong!", description=f"{latency}ms", color=Color.blue())
        )

    @commands.command(aliases=["statistics"])
    async def stats(self, ctx: commands.Context):
        """Stats of the bot"""
        users = len(self.bot.users)
        guilds = len(self.bot.guilds)

        embed = Embed(color=Color.dark_green())
        fields = (
            ("Guilds", guilds),
            ("Users", users),
            ("System", platform.release()),
            (
                "Memory",
                "{:.4} MB".format(psutil.Process().memory_info().rss / 1024 ** 2),
            ),
            ("Python version", ".".join([str(v) for v in sys.version_info[:3]])),
            ("Discord version", discord_version),
        )
        for name, value in fields:
            embed.add_field(name=name, value=str(value), inline=False)

        embed.set_thumbnail(url=str(ctx.guild.me.avatar_url))

        await ctx.send(embed=embed)

    @commands.command(aliases=["re"])
    async def redo(self, ctx: commands.Context):
        """Reply to a message to rerun it if its a command, helps when you've made typos"""
        ref = ctx.message.reference
        if ref is None or ref.message_id is None:
            return
        try:
            message = await ctx.channel.fetch_message(ref.message_id)
        except NotFound:
            return await ctx.reply("Couldn't find that message")
        if message.author != ctx.author:
            return
        await self.bot.process_commands(message)

    @commands.command()
    @commands.guild_only()
    @commands.has_guild_permissions(manage_guild=True)
    async def setprefix(self, ctx: commands.Context, *, prefix: str):
        """Set a custom prefix for the current server"""
        if len(prefix) > 10:
            return await ctx.send("Prefix too long, must be within 10 characters!")
        self.bot.prefix_cache[ctx.guild.id] = prefix
        await GuildModel.filter(id=ctx.guild.id).update(prefix=prefix)
        await ctx.send(f"My prefix has been updated to `{prefix}`")

    @commands.command()
    async def prefix(self, ctx: commands.Context):
        """View current prefix of bot"""
        await ctx.send(
            f"My prefix here is `"
            + (self.bot.prefix_cache[ctx.guild.id] if ctx.guild else ".")
            + "`"
        )

    @commands.command()
    async def invite(self, ctx: commands.Context):
        embed = Embed(
            title="Invite me!",
            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!",
            color=Color.green(),
        )
        await ctx.send(embed=embed)


def setup(bot: TechStruckBot):
    bot.add_cog(Common(bot))


================================================
FILE: bot/utils/embed_flag_input.py
================================================
import functools
import re
from typing import Dict, Iterable, TypeVar, Union
from urllib import parse

from discord import AllowedMentions, Embed, Member, User
from discord.ext import commands, flags  # type: ignore

_F = TypeVar(
    "_F",
)


class InvalidFieldArgs(commands.CommandError):
    pass


class EmbeyEmbedError(commands.CommandError):
    def __str__(self) -> str:
        return "The embed has no fields/attributes populated"


class InvalidUrl(commands.CommandError):
    def __init__(self, invalid_url: str, *, https_only: bool = False) -> None:
        self.invalid_url = invalid_url
        self.https_only = https_only

    def __str__(self) -> str:
        return "The url entered (`%s`) is invalid.%s" % (
            self.invalid_url,
            "\nThe url must be https" if self.https_only else "",
        )


class InvalidColor(commands.CommandError):
    def __init__(self, value) -> None:
        self.value = value

    def __str__(self):
        return "%s isn't a valid color, eg: `#fff000`, `f0f0f0`" % self.value


class UrlValidator:
    def __init__(self, *, https_only=False) -> None:
        self.https_only = https_only

    def __call__(self, value):
        url = parse.urlparse(value)
        schemes = ("https",) if self.https_only else ("http", "https")
        if url.scheme not in schemes or not url.hostname:
            raise InvalidUrl(value, https_only=self.https_only)
        return value


def colortype(value: str):
    try:
        return int(value.removeprefix("#"), base=16)
    except ValueError:
        raise InvalidColor(value)


url_type = UrlValidator(https_only=True)


def process_message_mentions(message: str) -> str:
    if not message:
        return ""
    for _type, _id in re.findall(r"(role|user):(\d{18})", message):
        message = message.replace(
            _type + ":" + _id, f"<@!{_id}>" if _type == "user" else f"<@&{_id}>"
        )
    for label in ("mention", "ping"):
        for role in ("everyone", "here"):
            message = message.replace(label + ":" + role, f"@{role}")
    return message


class FlagAdder:
    def __init__(self, kwarg_map: Dict[str, Iterable], *, default_mode: bool = False):
        self.kwarg_map = kwarg_map
        self.default_mode = default_mode

    def call(self, func: _F, **kwargs) -> _F:
        if kwargs.pop("all", False):
            for flags in self.kwarg_map.values():
                self.apply(flags=flags, func=func)
            return func
        kwargs = {**{k: self.default_mode for k in self.kwarg_map.keys()}, **kwargs}
        for k, v in kwargs.items():
            if v:
                self.apply(flags=self.kwarg_map[k], func=func)
        return func

    def __call__(self, func=None, **kwargs):
        if func is None:
            return functools.partial(self.call, **kwargs)
        return self.call(func, **kwargs)

    def apply(self, *, flags: Iterable, func: _F) -> _F:
        for flag in flags:
            flag(func)
        return func


embed_input = FlagAdder(
    {
        "basic": (
            flags.add_flag("--title", "-t"),
            flags.add_flag("--description", "-d"),
            flags.add_flag("--fields", "-f", nargs="+"),
            flags.add_flag("--colour", "--color", "-c", type=colortype),
        ),
        "image": (
            flags.add_flag("--thumbnail", "-th", type=url_type),
            flags.add_flag("--image", "-i", type=url_type),
        ),
        "author": (
            flags.add_flag("--author-name", "--aname", "-an"),
            flags.add_flag("--auto-author", "-aa", action="store_true", default=False),
            flags.add_flag("--author-url", "--aurl", "-au", type=url_type),
            flags.add_flag("--author-icon", "--aicon", "-ai", type=url_type),
        ),
        "footer": (
            flags.add_flag("--footer-icon", "-fi", type=url_type),
            flags.add_flag("--footer-text", "-ft"),
        ),
    }
)


allowed_mentions_input = FlagAdder(
    {
        "all": (
            flags.add_flag(
                "--everyone-mention", "-em", default=False, action="store_true"
            ),
            flags.add_flag(
                "--role-mentions", "-rm", default=False, action="store_true"
            ),
            flags.add_flag(
                "--user-mentions", "-um", default=True, action="store_false"
            ),
        )
    },
    default_mode=True,
)

webhook_input = FlagAdder(
    {
        "all": (
            flags.add_flag("--webhook", "-w", action="store_true", default=False),
            flags.add_flag("--webhook-username", "-wun", type=str, default=None),
            flags.add_flag("--webhook-avatar", "-wav", type=url_type, default=None),
            flags.add_flag(
                "--webhook-auto-author", "-waa", action="store_true", default=False
            ),
            flags.add_flag("--webhook-new-name", "-wnn", type=str, default=None),
            flags.add_flag("--webhook-name", "-wn", type=str, default=None),
            flags.add_flag(
                "--webhook-dispose", "-wd", action="store_true", default=False
            ),
        )
    },
    default_mode=True,
)


def dict_to_embed(data: Dict[str, str], author: Union[User, Member] = None):
    embed = Embed()
    for field in ("title", "description", "colour"):
        if value := data.pop(field, None):
            setattr(embed, field, value)
    for field in "thumbnail", "image":
        if value := data.pop(field, None):
            getattr(embed, "set_" + field)(url=value)

    if data.pop("auto_author", False) and author:
        embed.set_author(name=author.display_name, icon_url=str(author.avatar_url))
    if "author_name" in data and data["author_name"]:
        kwargs = {}
        if icon_url := data.pop("author_icon", None):
            kwargs["icon_url"] = icon_url
        if author_url := data.pop("author_url", None):
            kwargs["url"] = author_url

        embed.set_author(name=data.pop("author_name"), **kwargs)

    if "footer_text" in data and data["footer_text"]:
        kwargs = {}
        if footer_icon := data.pop("footer_icon", None):
            kwargs["icon_url"] = footer_icon

        embed.set_footer(text=data.pop("footer_text"), **kwargs)

    fields = data.pop("fields", []) or []
    if len(fields) % 2 == 1:
        raise InvalidFieldArgs(
            "Number of arguments for fields must be an even number, pairs of name and value"
        )

    for name, value in zip(fields[::2], fields[1::2]):
        embed.add_field(name=name, value=value, inline=False)

    if embed.to_dict() == {"type": "rich"}:
        raise EmbeyEmbedError()

    return embed


def dict_to_allowed_mentions(data):
    return AllowedMentions(
        everyone=data.pop("everyone_mention"),
        roles=data.pop("role_mentions"),
        users=data.pop("user_mentions"),
    )


================================================
FILE: bot/utils/fuzzy.py
================================================
# -*- coding: utf-8 -*-

"""
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""

# help with: http://chairnerd.seatgeek.com/fuzzywuzzy-fuzzy-string-matching-in-python/

import heapq
import re
from difflib import SequenceMatcher


def ratio(a, b):
    m = SequenceMatcher(None, a, b)
    return int(round(100 * m.ratio()))


def quick_ratio(a, b):
    m = SequenceMatcher(None, a, b)
    return int(round(100 * m.quick_ratio()))


def partial_ratio(a, b):
    short, long = (a, b) if len(a) <= len(b) else (b, a)
    m = SequenceMatcher(None, short, long)

    blocks = m.get_matching_blocks()

    scores = []
    for i, j, n in blocks:
        start = max(j - i, 0)
        end = start + len(short)
        o = SequenceMatcher(None, short, long[start:end])
        r = o.ratio()

        if 100 * r > 99:
            return 100
        scores.append(r)

    return int(round(100 * max(scores)))


_word_regex = re.compile(r"\W", re.IGNORECASE)


def _sort_tokens(a):
    a = _word_regex.sub(" ", a).lower().strip()
    return " ".join(sorted(a.split()))


def token_sort_ratio(a, b):
    a = _sort_tokens(a)
    b = _sort_tokens(b)
    return ratio(a, b)


def quick_token_sort_ratio(a, b):
    a = _sort_tokens(a)
    b = _sort_tokens(b)
    return quick_ratio(a, b)


def partial_token_sort_ratio(a, b):
    a = _sort_tokens(a)
    b = _sort_tokens(b)
    return partial_ratio(a, b)


def _extraction_generator(query, choices, scorer=quick_ratio, score_cutoff=0):
    try:
        for key, value in choices.items():
            score = scorer(query, key)
            if score >= score_cutoff:
                yield (key, score, value)
    except AttributeError:
        for choice in choices:
            score = scorer(query, choice)
            if score >= score_cutoff:
                yield (choice, score)


def extract(query, choices, *, scorer=quick_ratio, score_cutoff=0, limit=10):
    it = _extraction_generator(query, choices, scorer, score_cutoff)
    key = lambda t: t[1]
    if limit is not None:
        return heapq.nlargest(limit, it, key=key)
    return sorted(it, key=key, reverse=True)


def extract_one(query, choices, *, scorer=quick_ratio, score_cutoff=0):
    it = _extraction_generator(query, choices, scorer, score_cutoff)
    key = lambda t: t[1]
    try:
        return max(it, key=key)
    except:
        # iterator could return nothing
        return None


def extract_or_exact(query, choices, *, limit=None, scorer=quick_ratio, score_cutoff=0):
    matches = extract(
        query, choices, scorer=scorer, score_cutoff=score_cutoff, limit=limit
    )
    if len(matches) == 0:
        return []

    if len(matches) == 1:
        return matches

    top = matches[0][1]
    second = matches[1][1]

    # check if the top one is exact or more than 30% more correct than the top
    if top == 100 or top > (second + 30):
        return [matches[0]]

    return matches


def extract_matches(query, choices, *, scorer=quick_ratio, score_cutoff=0):
    matches = extract(
        query, choices, scorer=scorer, score_cutoff=score_cutoff, limit=None
    )
    if len(matches) == 0:
        return []

    top_score = matches[0][1]
    to_return = []
    index = 0
    while True:
        try:
            match = matches[index]
        except IndexError:
            break
        else:
            index += 1

        if match[1] != top_score:
            break

        to_return.append(match)
    return to_return


def finder(text, collection, *, key=None, lazy=True):
    suggestions = []
    text = str(text)
    pat = ".*?".join(map(re.escape, text))
    regex = re.compile(pat, flags=re.IGNORECASE)
    for item in collection:
        to_search = key(item) if key else item
        r = regex.search(to_search)
        if r:
            suggestions.append((len(r.group()), r.start(), item))

    def sort_key(tup):
        if key:
            return tup[0], tup[1], key(tup[2])
        return tup

    if lazy:
        return (z for _, _, z in sorted(suggestions, key=sort_key))
    else:
        return [z for _, _, z in sorted(suggestions, key=sort_key)]


def find(text, collection, *, key=None):
    try:
        return finder(text, collection, key=key, lazy=False)[0]
    except IndexError:
        return None


================================================
FILE: bot/utils/process_files.py
================================================
import re
from typing import Dict, List, Tuple

from discord.ext import commands

files_pattern = re.compile(r"\s{0,}```\w{0,}\s{0,}")


class NoValidFiles(commands.CommandError):
    def __str__(self):
        return "None of the files were valid or no files were given"


async def process_files(
    ctx: commands.Context, inp: str
) -> Tuple[Dict[str, Dict[str, str]], List[str]]:
    files = {}

    attachments = ctx.message.attachments.copy()
    skipped = []
    msg = ctx.message

    # If the message was a reply
    if msg.reference and msg.reference.message_id:
        replied = msg.reference.cached_message or await msg.channel.fetch_message(
            msg.reference.message_id
        )
        attachments.extend(replied.attachments)

    if inp:
        # TODO: Change this to something better
        files_and_names = files_pattern.split(inp)[:-1]

        # Dict comprehension to create the files 'object'
        files = {
            name: {"content": content + "\n"}
            for name, content in zip(files_and_names[0::2], files_and_names[1::2])
        }

    for attachment in attachments:
        if attachment.size > 64 * 1024 or attachment.filename.endswith(
            ("jpg", "jpeg", "png")
        ):
            skipped.append(attachment.filename)
            continue
        try:
            b = (await attachment.read()).decode("utf-8")
        except UnicodeDecodeError:
            skipped.append(attachment.filename)
        else:
            files[attachment.filename] = {"content": b}

    if not files:
        raise NoValidFiles()

    return files, skipped


================================================
FILE: bot/utils/rtfm.py
================================================
import io
import os
import re
import zlib

# Directly taken and modified from Rapptz/RoboDanny
# https://github.com/Rapptz/RoboDanny/blob/715a5cf8545b94d61823f62db484be4fac1c95b1/cogs/api.py
# This code is under the Mozilla Public License 2.0


class SphinxObjectFileReader:
    # Inspired by Sphinx's InventoryFileReader
    BUFSIZE = 16 * 1024

    def __init__(self, buffer):
        self.stream = io.BytesIO(buffer)

    def readline(self):
        return self.stream.readline().decode("utf-8")

    def skipline(self):
        self.stream.readline()

    def read_compressed_chunks(self):
        decompressor = zlib.decompressobj()
        while True:
            chunk = self.stream.read(self.BUFSIZE)
            if len(chunk) == 0:
                break
            yield decompressor.decompress(chunk)
        yield decompressor.flush()

    def read_compressed_lines(self):
        buf = b""
        for chunk in self.read_compressed_chunks():
            buf += chunk
            pos = buf.find(b"\n")
            while pos != -1:
                yield buf[:pos].decode("utf-8")
                buf = buf[pos + 1 :]
                pos = buf.find(b"\n")

    def parse_object_inv(self, url):
        # key: URL
        # n.b.: key doesn't have `discord` or `discord.ext.commands` namespaces
        result = {}

        # first line is version info
        inv_version = self.readline().rstrip()

        if inv_version != "# Sphinx inventory version 2":
            raise RuntimeError("Invalid objects.inv file version.")

        # next line is "# Project: <name>"
        # then after that is "# Version: <version>"
        projname = self.readline().rstrip()[11:]
        version = self.readline().rstrip()[11:]

        # next line says if it's a zlib header
        line = self.readline()
        if "zlib" not in line:
            raise RuntimeError("Invalid objects.inv file, not z-lib compatible.")

        # This code mostly comes from the Sphinx repository.
        entry_regex = re.compile(r"(?x)(.+?)\s+(\S*:\S*)\s+(-?\d+)\s+(\S+)\s+(.*)")
        for line in self.read_compressed_lines():
            match = entry_regex.match(line.rstrip())
            if not match:
                continue

            name, directive, prio, location, dispname = match.groups()
            domain, _, subdirective = directive.partition(":")
            if directive == "py:module" and name in result:
                # From the Sphinx Repository:
                # due to a bug in 1.1 and below,
                # two inventory entries are created
                # for Python modules, and the first
                # one is correct
                continue

            # Most documentation pages have a label
            if directive == "std:doc":
                subdirective = "label"

            if location.endswith("$"):
                location = location[:-1] + name

            key = name if dispname == "-" else dispname
            prefix = f"{subdirective}:" if domain == "std" else ""

            if projname == "discord.py":
                key = key.replace("discord.ext.commands.", "").replace("discord.", "")

            result[f"{prefix}{key}"] = os.path.join(url, location)

        return result


================================================
FILE: bot.Dockerfile
================================================
FROM python:3.9

WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1

COPY requirements-bot.txt ./
RUN pip install --no-cache-dir -r requirements-bot.txt

COPY . .

CMD [ "python", "-m", "bot" ]


================================================
FILE: config/__init__.py
================================================


================================================
FILE: config/bot.py
================================================
from pydantic import BaseSettings


class BotConfig(BaseSettings):
    bot_token: str
    quiz_api_token: str
    log_webhook: str

    class Config:
        env_file = ".env"


bot_config = BotConfig()


================================================
FILE: config/common.py
================================================
from pydantic import BaseSettings, PostgresDsn


class Settings(BaseSettings):

    secret: str
    database_uri: PostgresDsn
    no_ssl: bool = False

    class Config:
        env_file = ".env"
        fields = {
            "database_uri": {"env": ["database_uri", "database_url", "database"]},
            "no_ssl": {"env": "database_no_ssl"},
            "secret": {"env": "signing_secret"},
        }


config = Settings()


================================================
FILE: config/oauth.py
================================================
from pydantic import BaseSettings


class StackOAuthConfig(BaseSettings):
    client_id: str
    client_secret: str
    redirect_uri: str
    key: str

    class Config:
        env_file = ".env"
        env_prefix = "stackexchange_"


class GithubOAuthConfig(BaseSettings):
    client_id: str
    client_secret: str
    redirect_uri: str

    class Config:
        env_file = ".env"
        env_prefix = "github_"


stack_oauth_config = StackOAuthConfig()
github_oauth_config = GithubOAuthConfig()


================================================
FILE: config/reddit.py
================================================
from pydantic import BaseSettings


class RedditConfig(BaseSettings):
    client_id: str
    client_secret: str
    username: str
    password: str

    class Config:
        env_file = ".env"
        env_prefix = "reddit_"


reddit_config = RedditConfig()


================================================
FILE: config/webhook.py
================================================
from pydantic import BaseSettings


class Webhooks(BaseSettings):
    git_tips: str
    meme: str
    authorization: str

    class Config:
        env_file = ".env"
        env_prefix = "webhook_url_"
        fields = {"authorization": {"env": "authorization"}}


webhook_config = Webhooks()


================================================
FILE: heroku.yml
================================================
build:
    docker:
        worker: bot.Dockerfile


================================================
FILE: models.py
================================================
from tortoise import Model, fields


class ThankModel(Model):
    id = fields.IntField(pk=True)
    guild = fields.ForeignKeyField(
        model_name="main.GuildModel",
        related_name="all_thanks",
        description="Guild in which the user was thanked",
    )
    thanker = fields.ForeignKeyField(
        model_name="main.UserModel",
        related_name="sent_thanks",
        description="The member who sent the thanks",
    )
    thanked = fields.ForeignKeyField(
        model_name="main.UserModel",
        related_name="thanks",
        description="The member who was thanked",
    )
    time = fields.DatetimeField(auto_now_add=True)
    description = fields.CharField(max_length=100)

    class Meta:
        table = "thanks"
        table_description = "Represents a 'thank' given from one user to another"


class GuildModel(Model):
    id = fields.BigIntField(pk=True, description="Discord ID of the guild")
    all_thanks: fields.ForeignKeyRelation[ThankModel]
    prefix = fields.CharField(
        max_length=10, default=".", description="Custom prefix of the guild"
    )

    class Meta:
        table = "guilds"
        table_description = "Represents a discord guild's settings"


class UserModel(Model):
    id = fields.BigIntField(pk=True, description="Discord ID of the user")
    # External references
    github_oauth_token = fields.CharField(
        max_length=50, null=True, description="Github OAuth2 access token of the user"
    )
    stackoverflow_oauth_token = fields.CharField(
        max_length=50,
        null=True,
        description="Stackoverflow OAuth2 access token of the user",
    )

    thanks: fields.ForeignKeyRelation[ThankModel]
    sent_thanks: fields.ForeignKeyRelation[ThankModel]

    class Meta:
        table = "users"
        table_description = "Represents all users"


class JokeModel(Model):
    id = fields.IntField(pk=True, description="Joke ID")

    setup = fields.CharField(max_length=150, description="Joke setup")
    end = fields.CharField(max_length=150, description="Joke end")
    tags = fields.JSONField(default=[], description="List of tags")

    accepted = fields.BooleanField(
        default=False, description="Whether the joke has been accepted in"
    )

    creator = fields.ForeignKeyField(
        model_name="main.UserModel",
        related_name="joke_submissions",
        description="User who submitted this Joke",
    )

    class Meta:
        table = "jokes"
        table_description = "User submitted jokes being collected"


================================================
FILE: public/templates/404.html
================================================
<!DOCTYPE html>
<html>
<head>
	<title>404</title>
	<style type="text/css">
	@import url("https://fonts.googleapis.com/css?family=Bevan");

* {
    padding: 0;
    margin: 0;
    box-sizing: border-box;
}

body {
    background: rgb(40,40,40);
    overflow: hidden;
}

p {
    font-family: "Bevan", cursive;
    font-size: 130px;
    margin: 10vh 0 0;
    text-align: center;
    letter-spacing: 5px;
    background-color: black;
    color: transparent;
    text-shadow: 2px 2px 3px rgba(255, 255, 255, 0.1);
    -webkit-background-clip: text;
    -moz-background-clip: text;
    background-clip: text;


}

code {
    color: #bdbdbd;
    text-align: center;
    display: block;
    font-size: 16px;
    margin: 0 30px 25px;


}
span {
    color: #f0c674;
}

i {
    color: #b5bd68;
}

em {
    color: #b294bb;
    font-style: unset;
}

 b {
    color: #81a2be;
    font-weight: 500;
}



a {
    color: #8abeb7;
    font-family: monospace;
    font-size: 20px;
    text-decoration: underline;
    margin-top:10px;
    display:inline-block
}

@media screen and (max-width: 880px) {
    p {
        font-size: 14vw;
    }
}	


	</style>
</head>

<body>
<p>HTTP: <font style="font-size: 1.2em;">404</font></p>

<code><span>this_page</span>.<em>not_found</em> = True</code>
<code><span>if</span> <b>you_spelt_it_wrong</b>: 
	<span>    try_again()</span></code>

<code><span>elif <b>we_screwed_up</b>:</span>
	<em>    print</em>(<i>"We're really sorry about that."</i>); return <span> redirect</span>(<em>url_for</em>(<i>"home"</i>))</code>

<center><a href = "/">HOME</a></center>

<script type="text/javascript">
function type(n, t) {
    var str = document.getElementsByTagName("code")[n].innerHTML.toString();
    var i = 0;
    document.getElementsByTagName("code")[n].innerHTML = "";

    setTimeout(function() {
        var se = setInterval(function() {
            i++;
            document.getElementsByTagName("code")[n].innerHTML =
                str.slice(0, i) + "|";
            if (i == str.length) {	
                clearInterval(se);
                document.getElementsByTagName("code")[n].innerHTML = str;
            }
        }, 10);
    }, t);
}

type(0, 0);
type(1, 600);
type(2, 1300);
	
</script>

</body>
</html>


================================================
FILE: public/templates/oauth_error.html
================================================
<!DOCTYPE html>
<html>
<head>
	<title>Error!</title>

<link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" />
<style type="text/css">

	body {
	  background-color: black;
	}

    .success
	{
		border:3px solid #fff;
		height:280px;
        border-radius:20px;
        background:#892cdc;
	}
   .success_header
   {
	   background:#52057b;/*rgba(255,102,0,1);*/
	   padding:20px;
       border-radius:20px 20px 0px 0px;
	   
   }
   
   .check
   {
	   margin:0px auto;
	   width:50px;
	   height:50px;
	   border-radius:100%;
	   background:#fff;
	   text-align:center;
   }
   
   .check i
   {
	   vertical-align:middle;
	   line-height:50px;
	   font-size:30px;
   }

    .content 
    {
        text-align:center;
        color:white;
    }

    .content  h1
    {
        font-size:25px;
        padding-top:25px;
    }

    .content a
    {
        width:200px;
        height:35px;
        color:#fff;
        border-radius:30px;
        padding:5px 10px;
        background:#bc6ff1;
        transition:all ease-in-out 0.3s;
    }

    .content a:hover
    {
        text-decoration:none;
        background:#000;
    }
   
</style>
</head>
<body>
<div class="container">
   <div class="row">
      <div class="col-md-6 mx-auto mt-5">
         <div class="success">
            <div class="success_header">
               <div class="check"><i class="fa fa-times" aria-hidden="true"></i></div>
            </div>
            <div class="content">
               <h1>Error</h1>
               <p>{{detail}}</p>
               <a href="#">Go Back</a>
            </div>
         </div>
      </div>
   </div>
</div>
</body>
</html>


================================================
FILE: public/templates/oauth_success.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Success!</title>

<link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" />
<style type="text/css">

	body {
	  background-color: black;
	}

    .success
	{
		border:3px solid #fff;
		height:280px;
        border-radius:20px;
        background:#892cdc;
	}
   .success_header
   {
	   background:#52057b;/*rgba(255,102,0,1);*/
	   padding:20px;
       border-radius:20px 20px 0px 0px;
	   
   }
   
   .check
   {
	   margin:0px auto;
	   width:50px;
	   height:50px;
	   border-radius:100%;
	   background:#fff;
	   text-align:center;
   }
   
   .check i
   {
	   vertical-align:middle;
	   line-height:50px;
	   font-size:30px;
   }

    .content 
    {
        text-align:center;
        color:white;
    }

    .content  h1
    {
        font-size:25px;
        padding-top:25px;
    }

    .content a
    {
        width:200px;
        height:35px;
        color:#fff;
        border-radius:30px;
        padding:5px 10px;
        background:#bc6ff1;
        transition:all ease-in-out 0.3s;
    }

    .content a:hover
    {
        text-decoration:none;
        background:#000;
    }
   
</style>
</head>
<body>
<div class="container">
   <div class="row">
      <div class="col-md-6 mx-auto mt-5">
         <div class="success">
            <div class="success_header">
               <div class="check"><i class="fa fa-check" aria-hidden="true"></i></div>
            </div>
            <div class="content">
               <h1>Success</h1>
               <p>Your {{oauth_provider}} has been linked.</p>
               <a href="#">Go Back</a>
            </div>
         </div>
      </div>
   </div>
</div>
</body>
</html>


================================================
FILE: requirements-bot.txt
================================================
aerich==0.5.0
cachetools==4.2.1
discord.py==1.7.1
discord-flags==2.1.1
jishaku==1.20.0.220
psutil==5.8.0
python-dotenv==0.17.0
python-jose==3.2.0
PyYAML==5.4.1
svglib==1.1.0
tortoise-orm[asyncpg]==0.16.21



================================================
FILE: requirements-dev.txt
================================================
black==20.8b1
isort==5.8.0


================================================
FILE: requirements.txt
================================================
asyncpg==0.22.0
backports-datetime-fromisoformat==1.0.0; python_version < '3.7'
discord.py==1.7.1
fastapi==0.65.2
Jinja2==2.11.3
praw==7.2.0
python-dotenv==0.17.0
python-jose==3.2.0
requests==2.25.1
async-exit-stack; python_version < '3.7'
async-generator; python_version < '3.7'



================================================
FILE: tortoise_config.py
================================================
import ssl

from config import common

# TODO: Yet to find a fix for this
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE

database_uri = common.config.database_uri

tortoise_config = {
    "connections": {
        "default": {
            "engine": "tortoise.backends.asyncpg",
            "credentials": {
                "database": database_uri.path[1:],
                "host": database_uri.host,
                "password": database_uri.password,
                "port": database_uri.port or 5432,
                "user": database_uri.user,
                "ssl": ctx if common.config.no_ssl else None,
            },
        }
    },
    "apps": {
        "main": {"models": ["models", "aerich.models"], "default_connection": "default"}
    },
}


================================================
FILE: utils/db_backup.py
================================================
import asyncio
import pickle
from datetime import datetime

import asyncpg

from config.common import config


async def backup():
    conn = await asyncpg.connect(str(config.database_uri))
    tables = ("users", "thanks", "guilds", "jokes")
    data = {
        field: [dict(rec) for rec in await conn.fetch("SELECT * FROM {}".format(field))]
        for field in tables
    }
    return data


def main():
    data = asyncio.get_event_loop().run_until_complete(backup())
    filename = "backup-{:%d-%m-%y-%H:%M}.pickle".format(datetime.now())
    with open(filename, "wb") as f:
        pickle.dump(data, f)


if __name__ == "__main__":
    main()


================================================
FILE: utils/embed.py
================================================
import datetime

import yaml
from discord import Color, Embed, File


def build_embed(embed_data, add_timestamp=False):
    embed = Embed(
        title=embed_data.get("title"),
        description=embed_data.get("description"),
        color=embed_data.get("color", Color.green()),
    )

    if "thumbnail" in embed_data:
        embed.set_thumbnail(url=embed_data["thumbnail"])

    if "image" in embed_data:
        embed.set_image(url=embed_data["image"])

    if "author" in embed_data:
        embed.set_author(**embed_data["author"])

    if "footer" in embed_data:
        embed.set_footer(**embed_data["footer"])

    if add_timestamp or embed_data.get("add_timestamp", False):
        embed.timestamp = datetime.datetime.utcnow()

    for f in embed_data.get("fields", []):
        f.setdefault("inline", False)
        embed.add_field(**f)

    return embed


def bot_type_converter(data, add_timestamp=False):
    text = data.get("text")
    embed_data = data.get("embed")
    file_names = data.get("files", [])

    embed = None

    if embed_data:
        embed = build_embed(embed_data)

    return text, embed, [File(fn) for fn in file_names]


def webhook_type_converter(data, add_timestamp=False):
    messages_data = data
    outputs = []
    for message_data in messages_data.get("messages", []):
        embeds_data = message_data.get("embeds", [])
        embeds = [build_embed(embed_data) for embed_data in embeds_data]
        outputs.append(
            (
                message_data.get("text"),
                embeds,
                [File(fn) for fn in message_data.get("files", [])] or None,
            )
        )

    return outputs, messages_data.get("username"), messages_data.get("avatar_url")


def yaml_file_to_message(filename: str, **kwargs):
    with open(filename) as f:
        data = yaml.load(f, yaml.Loader)
    if data["type"] == "bot":
        return bot_type_converter(data, **kwargs)
    if data["type"] == "webhook":
        return webhook_type_converter(data, **kwargs)
    raise RuntimeError("Incompatible type")


================================================
FILE: utils/webhook.py
================================================
from typing import Optional

from discord import RequestsWebhookAdapter, Webhook

from .embed import yaml_file_to_message


def make_webhook(url: str, adapter=RequestsWebhookAdapter()):
    return Webhook.from_url(url, adapter=adapter)


def send_from_yaml(
    *, webhook: Webhook, filename: str, text: Optional[str] = None, **kwargs
):
    messages, username, avatar_url = yaml_file_to_message(filename)
    kwargs.setdefault("username", username)
    kwargs.setdefault("avatar_url", avatar_url)
    return [
        webhook.send(message[0] or text, embeds=message[1], files=message[2], **kwargs)
        for message in messages
    ]


================================================
FILE: vercel.json
================================================
{
  "functions": {
    "api/main.py": { "maxDuration": 10 }
  },
  "routes": [{ "src": "/(.*)", "dest": "/api/main" }]
}
Download .txt
gitextract_f5y6cz_r/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── black.yml
│       ├── codeql-analysis.yml
│       └── isort.yml
├── .gitignore
├── .isort.cfg
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── api/
│   ├── dependencies.py
│   ├── exceptions.py
│   ├── main.py
│   └── routers/
│       ├── oauth.py
│       └── webhooks.py
├── bot/
│   ├── __main__.py
│   ├── bot.py
│   ├── cogs/
│   │   ├── admin.py
│   │   ├── brainfeed.py
│   │   ├── coc.py
│   │   ├── code_exec.py
│   │   ├── fun.py
│   │   ├── github.py
│   │   ├── help_command.py
│   │   ├── joke.py
│   │   ├── packages.py
│   │   ├── quiz.py
│   │   ├── rtfm.py
│   │   ├── stackexchange.py
│   │   ├── thank.py
│   │   └── utils.py
│   ├── core.py
│   └── utils/
│       ├── embed_flag_input.py
│       ├── fuzzy.py
│       ├── process_files.py
│       └── rtfm.py
├── bot.Dockerfile
├── config/
│   ├── __init__.py
│   ├── bot.py
│   ├── common.py
│   ├── oauth.py
│   ├── reddit.py
│   └── webhook.py
├── heroku.yml
├── models.py
├── public/
│   └── templates/
│       ├── 404.html
│       ├── oauth_error.html
│       └── oauth_success.html
├── requirements-bot.txt
├── requirements-dev.txt
├── requirements.txt
├── tortoise_config.py
├── utils/
│   ├── db_backup.py
│   ├── embed.py
│   └── webhook.py
└── vercel.json
Download .txt
SYMBOL INDEX (247 symbols across 34 files)

FILE: api/dependencies.py
  function auth_dep (line 18) | def auth_dep(authorization: str = Header(...)):
  function aiohttp_session (line 23) | async def aiohttp_session():
  function state_check (line 31) | def state_check(request: Request, state: str = Query(...)) -> int:
  function db_connection (line 61) | async def db_connection():

FILE: api/exceptions.py
  class CustomHTTPException (line 1) | class CustomHTTPException(Exception):
    method __init__ (line 2) | def __init__(self, response):

FILE: api/main.py
  function custom_http_exception_handler (line 18) | def custom_http_exception_handler(request: Request, exc: CustomHTTPExcep...

FILE: api/routers/oauth.py
  function stackexchange_oauth (line 45) | async def stackexchange_oauth(
  function github_oauth (line 69) | async def github_oauth(

FILE: api/routers/webhooks.py
  function send_meme (line 38) | def send_meme(webhook: Webhook, subreddits: List[str]) -> bool:
  function send_memes (line 53) | def send_memes(webhook: Webhook, subreddits: Iterable[str], quantity: int):
  function send_memes_route (line 69) | def send_memes_route():
  function git_tip (line 79) | async def git_tip(session: ClientSession = Depends(aiohttp_session)):

FILE: bot/bot.py
  class TechStruckBot (line 30) | class TechStruckBot(commands.Bot):
    method __init__ (line 33) | def __init__(self, *, tortoise_config, load_extensions=True, loadjsk=T...
    method session (line 72) | def session(self) -> ClientSession:
    method connect_db (line 76) | async def connect_db(self):
    method load_extensions (line 82) | def load_extensions(self, extentions: Iterable[str]):
    method on_message (line 89) | async def on_message(self, msg: Message):
    method on_command_error (line 101) | async def on_command_error(
    method get_custom_prefix (line 192) | async def get_custom_prefix(self, _, message: Message) -> str:
    method fetch_prefix (line 205) | async def fetch_prefix(self, message: Message) -> str:
    method on_ready (line 219) | async def on_ready(self):

FILE: bot/cogs/admin.py
  class Admin (line 7) | class Admin(commands.Cog):
    method __init__ (line 8) | def __init__(self, bot: commands.Bot):
    method _refresh (line 11) | async def _refresh(self, ctx: commands.Context, filename: str, channel...
    method refresh (line 21) | async def refresh(self, ctx: commands.Context):
    method refresh_roles (line 25) | async def refresh_roles(self, ctx: commands.Context):
    method refresh_rules (line 29) | async def refresh_rules(self, ctx: commands.Context):
  function setup (line 33) | def setup(bot: commands.Bot):

FILE: bot/cogs/brainfeed.py
  class UnknownBrainfeed (line 13) | class UnknownBrainfeed(commands.CommandError):
    method __str__ (line 14) | def __str__(self) -> str:
  class BrainFeed (line 18) | class BrainFeed(commands.Cog):
    method __init__ (line 21) | def __init__(self, bot: TechStruckBot):
    method brainfeed (line 26) | async def brainfeed(self, ctx: commands.Context):
    method submission_channel (line 31) | def submission_channel(self) -> TextChannel:
    method add (line 38) | async def add(self, ctx: commands.Context, **kwargs):
    method get_submission (line 76) | async def get_submission(self, bf_id) -> Embed:
    method view (line 89) | async def view(self, ctx: commands.Context, id: int):
    method send (line 101) | async def send(self, ctx: commands.Context, bf_id: int, **kwargs):
    method approve (line 124) | async def approve(self, ctx: commands.Context, *, id: int):
    method deny (line 136) | async def deny(self, ctx: commands.Context, *, id: int):
  function setup (line 147) | def setup(bot: TechStruckBot):

FILE: bot/cogs/coc.py
  class ClashOfCode (line 20) | class ClashOfCode(commands.Cog):
    method __init__ (line 21) | def __init__(self, bot):
    method on_ready (line 29) | async def on_ready(self):
    method role (line 33) | def role(self):
    method em (line 36) | def em(self, mode, players):
    method on_raw_reaction_add (line 43) | async def on_raw_reaction_add(self, payload: discord.RawReactionAction...
    method on_raw_reaction_remove (line 65) | async def on_raw_reaction_remove(self, payload: discord.RawReactionAct...
    method clash_of_code (line 90) | async def clash_of_code(self, ctx: commands.Context):
    method session (line 97) | async def session(self, ctx: commands.Context):
    method session_start (line 106) | async def session_start(self, ctx: commands.context):
    method session_join (line 164) | async def session_join(self, ctx: commands.Context):
    method session_leave (line 181) | async def session_leave(self, ctx: commands.Context):
    method session_end (line 198) | async def session_end(self, ctx: commands.context):
    method coc_invite (line 231) | async def coc_invite(self, ctx: commands.Context, *, url: str = None):
  function setup (line 342) | def setup(bot: TechStruckBot):

FILE: bot/cogs/code_exec.py
  function create_guest_paste_bin (line 10) | async def create_guest_paste_bin(session, code):
  class CodeExec (line 25) | class CodeExec(commands.Cog):
    method __init__ (line 26) | def __init__(self, bot: commands.Bot):
    method session (line 32) | def session(self):
    method _run_code (line 35) | async def _run_code(self, *, lang: str, code: str):
    method run (line 43) | async def run(self, ctx: commands.Context, *, codeblock: str):
    method runl (line 69) | async def runl(self, ctx: commands.Context, lang: str, *, code: str):
    method _send_result (line 76) | async def _send_result(self, ctx: commands.Context, result: dict):
  function setup (line 99) | def setup(bot: commands.Bot):

FILE: bot/cogs/fun.py
  class Fun (line 9) | class Fun(commands.Cog):
    method __init__ (line 12) | def __init__(self, bot: TechStruckBot):
    method beer (line 16) | async def beer(
    method beers (line 51) | async def beers(
    method beerparty (line 101) | async def beerparty(
  function setup (line 127) | def setup(bot: TechStruckBot):

FILE: bot/cogs/github.py
  class GithubNotLinkedError (line 20) | class GithubNotLinkedError(commands.CommandError):
    method __str__ (line 21) | def __str__(self):
  class InvalidTheme (line 25) | class InvalidTheme(commands.CommandError):
    method __str__ (line 26) | def __str__(self):
  class Github (line 30) | class Github(commands.Cog):
    method __init__ (line 33) | def __init__(self, bot: commands.Bot):
    method session (line 40) | def session(self):
    method cog_before_invoke (line 43) | async def cog_before_invoke(self, ctx: commands.Context):
    method link_github (line 57) | async def link_github(self, ctx: commands.Context):
    method gist (line 83) | async def gist(self, ctx: commands.Context):
    method create_gist (line 88) | async def create_gist(self, ctx: commands.Context, *, inp: Optional[st...
    method list_gist (line 128) | async def list_gist(self, ctx: commands.Context):
    method delete_gist (line 148) | async def delete_gist(self, ctx: commands.Context, *, gist_id: str):
    method github_search (line 162) | async def github_search(self, ctx: commands.Context, *, term: str):
    method github_stats (line 201) | async def github_stats(
    method github_top_languages (line 224) | async def github_top_languages(
    method get_file_from_svg_url (line 238) | async def get_file_from_svg_url(
    method process_theme (line 250) | def process_theme(self, theme):
    method repo_desc_format (line 257) | def repo_desc_format(result):
    method github_request (line 263) | async def github_request(
    method get_gh_user (line 279) | async def get_gh_user(self, ctx: commands.Context):
  function setup (line 284) | def setup(bot: commands.Bot):

FILE: bot/cogs/help_command.py
  class HelpCommand (line 9) | class HelpCommand(commands.HelpCommand):
    method get_ending_note (line 17) | def get_ending_note(self):
    method get_command_signature (line 22) | def get_command_signature(self, command):
    method send_bot_help (line 25) | async def send_bot_help(self, mapping):
    method send_cog_help (line 47) | async def send_cog_help(self, cog):
    method send_group_help (line 66) | async def send_group_help(self, group):
    method add_support_server (line 83) | def add_support_server(self, embed):
    method send_command_help (line 86) | async def send_command_help(self, command):
  function setup (line 97) | def setup(bot: commands.Bot):
  function teardown (line 102) | def teardown(bot):

FILE: bot/cogs/joke.py
  class Joke (line 15) | class Joke(commands.Cog):
    method __init__ (line 18) | def __init__(self, bot: commands.Bot):
    method joke (line 22) | async def joke(self, ctx: commands.Context):
    method add (line 28) | async def add(self, ctx: commands.Context):
    method joke_entries_channel (line 59) | def joke_entries_channel(self) -> TextChannel:
    method _get_input (line 62) | async def _get_input(self, ctx: commands.Context, title: str, descript...
    method reaction_listener (line 75) | async def reaction_listener(self, payload: RawReactionActionEvent):
  function setup (line 93) | def setup(bot: commands.Bot):

FILE: bot/cogs/packages.py
  class Packages (line 7) | class Packages(Cog):
    method __init__ (line 10) | def __init__(self, bot: TechStruckBot):
    method session (line 14) | def session(self):
    method get_package (line 17) | async def get_package(self, url: str):
    method pypisearch (line 21) | async def pypisearch(self, ctx: Context, arg: str):
    method npmsearch (line 70) | async def npmsearch(self, ctx: Context, arg: str):
    method crate (line 134) | async def crate(self, ctx: Context, arg: str):
  function setup (line 197) | def setup(bot: TechStruckBot):

FILE: bot/cogs/quiz.py
  class Quiz (line 8) | class Quiz(commands.Cog):
    method __init__ (line 9) | def __init__(self, bot: commands.Bot):
    method startquiz (line 14) | async def startquiz(self, ctx: commands.Context):
  function setup (line 58) | def setup(bot: commands.Bot):

FILE: bot/cogs/rtfm.py
  class RTFM (line 11) | class RTFM(commands.Cog):
    method __init__ (line 66) | def __init__(self, bot: TechStruckBot) -> None:
    method session (line 71) | def session(self) -> aiohttp.ClientSession:
    method build (line 74) | async def build(self, target) -> None:
    method rtfm (line 91) | async def rtfm(self, ctx: commands.Context, doc: str, *, term: str = N...
    method list_targets (line 127) | async def list_targets(self, ctx: commands.Context):
    method add_target (line 150) | async def add_target(self, ctx: commands.Context, **kwargs):
  function setup (line 171) | def setup(bot: TechStruckBot):

FILE: bot/cogs/stackexchange.py
  class StackExchangeNotLinkedError (line 22) | class StackExchangeNotLinkedError(commands.CommandError):
    method __str__ (line 23) | def __str__(self):
  class StackExchangeError (line 27) | class StackExchangeError(commands.CommandError):
  class Stackexchange (line 31) | class Stackexchange(commands.Cog):
    method __init__ (line 34) | def __init__(self, bot: commands.Bot):
    method session (line 42) | def session(self):
    method load_sites (line 46) | async def load_sites(self):
    method cog_check (line 69) | async def cog_check(self, ctx: commands.Context):
    method cog_before_invoke (line 74) | async def cog_before_invoke(self, ctx: commands.Context):
    method stack_profile (line 93) | async def stack_profile(self, ctx: commands.Context, **kwargs):
    method stackexchange_search (line 128) | async def stackexchange_search(self, ctx: commands.Context, **kwargs):
    method stacksite (line 166) | async def stacksite(self, ctx: commands.Context, *, term: str):
    method get_site (line 176) | def get_site(self, sitename: str):
    method stack_request (line 183) | async def stack_request(
    method link_stackoverflow (line 207) | async def link_stackoverflow(self, ctx: commands.Context):
  function setup (line 234) | def setup(bot: commands.Bot):

FILE: bot/cogs/thank.py
  class Thank (line 21) | class Thank(commands.Cog):
    method __init__ (line 24) | def __init__(self, bot: commands.Bot):
    method thank (line 29) | async def thank(self, ctx: commands.Context, recv: Member, *, descript...
    method thank_stats (line 66) | async def thank_stats(
    method thank_leaderboard (line 92) | async def thank_leaderboard(self, ctx: commands.Context):
    method delete_thank (line 126) | async def delete_thank(self, ctx: commands.Context, thank_id: int):
    method list_thanks (line 154) | async def list_thanks(self, ctx: commands.Context, member: Member):
  function setup (line 171) | def setup(bot: commands.Bot):

FILE: bot/cogs/utils.py
  function maybe_await (line 21) | async def maybe_await(coro):
  class Utils (line 27) | class Utils(commands.Cog):
    method __init__ (line 30) | def __init__(self, bot: TechStruckBot):
    method embed (line 44) | async def embed(self, ctx: commands.Context, **kwargs):
    method rawembed (line 109) | async def rawembed(self, ctx: commands.Context):
    method source (line 123) | async def source(self, ctx: commands.Context, *, command=None):
  function setup (line 168) | def setup(bot: TechStruckBot):
  function teardown (line 172) | def teardown(bot: TechStruckBot):

FILE: bot/core.py
  class Common (line 14) | class Common(commands.Cog):
    method __init__ (line 15) | def __init__(self, bot: TechStruckBot):
    method ping (line 19) | async def ping(self, ctx: commands.Context):
    method stats (line 27) | async def stats(self, ctx: commands.Context):
    method redo (line 52) | async def redo(self, ctx: commands.Context):
    method setprefix (line 68) | async def setprefix(self, ctx: commands.Context, *, prefix: str):
    method prefix (line 77) | async def prefix(self, ctx: commands.Context):
    method invite (line 86) | async def invite(self, ctx: commands.Context):
  function setup (line 95) | def setup(bot: TechStruckBot):

FILE: bot/utils/embed_flag_input.py
  class InvalidFieldArgs (line 14) | class InvalidFieldArgs(commands.CommandError):
  class EmbeyEmbedError (line 18) | class EmbeyEmbedError(commands.CommandError):
    method __str__ (line 19) | def __str__(self) -> str:
  class InvalidUrl (line 23) | class InvalidUrl(commands.CommandError):
    method __init__ (line 24) | def __init__(self, invalid_url: str, *, https_only: bool = False) -> N...
    method __str__ (line 28) | def __str__(self) -> str:
  class InvalidColor (line 35) | class InvalidColor(commands.CommandError):
    method __init__ (line 36) | def __init__(self, value) -> None:
    method __str__ (line 39) | def __str__(self):
  class UrlValidator (line 43) | class UrlValidator:
    method __init__ (line 44) | def __init__(self, *, https_only=False) -> None:
    method __call__ (line 47) | def __call__(self, value):
  function colortype (line 55) | def colortype(value: str):
  function process_message_mentions (line 65) | def process_message_mentions(message: str) -> str:
  class FlagAdder (line 78) | class FlagAdder:
    method __init__ (line 79) | def __init__(self, kwarg_map: Dict[str, Iterable], *, default_mode: bo...
    method call (line 83) | def call(self, func: _F, **kwargs) -> _F:
    method __call__ (line 94) | def __call__(self, func=None, **kwargs):
    method apply (line 99) | def apply(self, *, flags: Iterable, func: _F) -> _F:
  function dict_to_embed (line 168) | def dict_to_embed(data: Dict[str, str], author: Union[User, Member] = No...
  function dict_to_allowed_mentions (line 210) | def dict_to_allowed_mentions(data):

FILE: bot/utils/fuzzy.py
  function ratio (line 16) | def ratio(a, b):
  function quick_ratio (line 21) | def quick_ratio(a, b):
  function partial_ratio (line 26) | def partial_ratio(a, b):
  function _sort_tokens (line 49) | def _sort_tokens(a):
  function token_sort_ratio (line 54) | def token_sort_ratio(a, b):
  function quick_token_sort_ratio (line 60) | def quick_token_sort_ratio(a, b):
  function partial_token_sort_ratio (line 66) | def partial_token_sort_ratio(a, b):
  function _extraction_generator (line 72) | def _extraction_generator(query, choices, scorer=quick_ratio, score_cuto...
  function extract (line 85) | def extract(query, choices, *, scorer=quick_ratio, score_cutoff=0, limit...
  function extract_one (line 93) | def extract_one(query, choices, *, scorer=quick_ratio, score_cutoff=0):
  function extract_or_exact (line 103) | def extract_or_exact(query, choices, *, limit=None, scorer=quick_ratio, ...
  function extract_matches (line 123) | def extract_matches(query, choices, *, scorer=quick_ratio, score_cutoff=0):
  function finder (line 148) | def finder(text, collection, *, key=None, lazy=True):
  function find (line 170) | def find(text, collection, *, key=None):

FILE: bot/utils/process_files.py
  class NoValidFiles (line 9) | class NoValidFiles(commands.CommandError):
    method __str__ (line 10) | def __str__(self):
  function process_files (line 14) | async def process_files(

FILE: bot/utils/rtfm.py
  class SphinxObjectFileReader (line 11) | class SphinxObjectFileReader:
    method __init__ (line 15) | def __init__(self, buffer):
    method readline (line 18) | def readline(self):
    method skipline (line 21) | def skipline(self):
    method read_compressed_chunks (line 24) | def read_compressed_chunks(self):
    method read_compressed_lines (line 33) | def read_compressed_lines(self):
    method parse_object_inv (line 43) | def parse_object_inv(self, url):

FILE: config/bot.py
  class BotConfig (line 4) | class BotConfig(BaseSettings):
    class Config (line 9) | class Config:

FILE: config/common.py
  class Settings (line 4) | class Settings(BaseSettings):
    class Config (line 10) | class Config:

FILE: config/oauth.py
  class StackOAuthConfig (line 4) | class StackOAuthConfig(BaseSettings):
    class Config (line 10) | class Config:
  class GithubOAuthConfig (line 15) | class GithubOAuthConfig(BaseSettings):
    class Config (line 20) | class Config:

FILE: config/reddit.py
  class RedditConfig (line 4) | class RedditConfig(BaseSettings):
    class Config (line 10) | class Config:

FILE: config/webhook.py
  class Webhooks (line 4) | class Webhooks(BaseSettings):
    class Config (line 9) | class Config:

FILE: models.py
  class ThankModel (line 4) | class ThankModel(Model):
    class Meta (line 24) | class Meta:
  class GuildModel (line 29) | class GuildModel(Model):
    class Meta (line 36) | class Meta:
  class UserModel (line 41) | class UserModel(Model):
    class Meta (line 56) | class Meta:
  class JokeModel (line 61) | class JokeModel(Model):
    class Meta (line 78) | class Meta:

FILE: utils/db_backup.py
  function backup (line 10) | async def backup():
  function main (line 20) | def main():

FILE: utils/embed.py
  function build_embed (line 7) | def build_embed(embed_data, add_timestamp=False):
  function bot_type_converter (line 36) | def bot_type_converter(data, add_timestamp=False):
  function webhook_type_converter (line 49) | def webhook_type_converter(data, add_timestamp=False):
  function yaml_file_to_message (line 66) | def yaml_file_to_message(filename: str, **kwargs):

FILE: utils/webhook.py
  function make_webhook (line 8) | def make_webhook(url: str, adapter=RequestsWebhookAdapter()):
  function send_from_yaml (line 12) | def send_from_yaml(
Condensed preview — 57 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (151K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 432,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: FalseDev\n\n---\n\n**Describ"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 467,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**Is"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 106,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: \"pip\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n"
  },
  {
    "path": ".github/workflows/black.yml",
    "chars": 193,
    "preview": "name: Lint\n\non: [push, pull_request]\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "chars": 2346,
    "preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
  },
  {
    "path": ".github/workflows/isort.yml",
    "chars": 330,
    "preview": "name: Run isort\non:\n  - push\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n   "
  },
  {
    "path": ".gitignore",
    "chars": 96,
    "preview": "config.yaml\n.vim\n.env\n**/__pycache__\ntmp\ncache\n\n# Tortoise stuff\naerich.ini\nmigrations\n\n.vercel\n"
  },
  {
    "path": ".isort.cfg",
    "chars": 161,
    "preview": "[settings]\nmulti_line_output = 3\ninclude_trailing_comma = True\nforce_grid_wrap = 0\nuse_parentheses = True\nensure_newline"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 3359,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
  },
  {
    "path": "LICENSE",
    "chars": 1065,
    "preview": "MIT License\n\nCopyright (c) 2021 FalseDev\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
  },
  {
    "path": "README.md",
    "chars": 2028,
    "preview": "<p align=\"center\">\n\t<img src=\"https://cdn.discordapp.com/attachments/770679803635433473/825250084589273118/circle-croppe"
  },
  {
    "path": "api/dependencies.py",
    "chars": 1793,
    "preview": "import hmac\nimport ssl\nfrom datetime import datetime\n\nimport asyncpg\nfrom aiohttp import ClientSession\nfrom fastapi impo"
  },
  {
    "path": "api/exceptions.py",
    "chars": 105,
    "preview": "class CustomHTTPException(Exception):\n    def __init__(self, response):\n        self.response = response\n"
  },
  {
    "path": "api/main.py",
    "chars": 498,
    "preview": "import sys\n\nfrom fastapi import FastAPI, Request\n\nfrom .exceptions import CustomHTTPException\nfrom .routers import oauth"
  },
  {
    "path": "api/routers/oauth.py",
    "chars": 2568,
    "preview": "from datetime import datetime\nfrom urllib.parse import parse_qs\n\nimport asyncpg\nfrom aiohttp import ClientSession\nfrom f"
  },
  {
    "path": "api/routers/webhooks.py",
    "chars": 2990,
    "preview": "import datetime\nimport json\nimport random\nfrom concurrent import futures\nfrom typing import Iterable, List\n\nfrom aiohttp"
  },
  {
    "path": "bot/__main__.py",
    "chars": 389,
    "preview": "import os\n\nfrom .bot import TechStruckBot\n\nos.environ.setdefault(\"JISHAKU_HIDE\", \"1\")\nos.environ.setdefault(\"JISHAKU_RET"
  },
  {
    "path": "bot/bot.py",
    "chars": 7080,
    "preview": "import asyncio\nimport contextlib\nimport math\nimport re\nimport traceback\nfrom typing import Iterable\n\nfrom aiohttp import"
  },
  {
    "path": "bot/cogs/admin.py",
    "chars": 1176,
    "preview": "from discord.ext import commands\nfrom discord.utils import get\n\nfrom utils.embed import yaml_file_to_message\n\n\nclass Adm"
  },
  {
    "path": "bot/cogs/brainfeed.py",
    "chars": 5469,
    "preview": "import asyncio\nfrom datetime import datetime\nfrom functools import cached_property\n\nfrom discord import Embed, Member, N"
  },
  {
    "path": "bot/cogs/coc.py",
    "chars": 13209,
    "preview": "import asyncio\nimport re\nimport time\n\nimport aiohttp\n\nimport discord\nfrom discord.ext import commands\n\nfrom ..bot import"
  },
  {
    "path": "bot/cogs/code_exec.py",
    "chars": 3416,
    "preview": "import re\n\nfrom discord import Color, Embed\nfrom discord.ext import commands\n\nfrom config.bot import bot_config\n\n\n# TODO"
  },
  {
    "path": "bot/cogs/fun.py",
    "chars": 4664,
    "preview": "import asyncio\n\nfrom discord import Color, Embed, Forbidden, Member, utils\nfrom discord.ext import commands\n\nfrom bot.bo"
  },
  {
    "path": "bot/cogs/github.py",
    "chars": 9929,
    "preview": "import datetime\nimport re\nfrom io import BytesIO\nfrom typing import Optional\nfrom urllib.parse import urlencode\n\nfrom ca"
  },
  {
    "path": "bot/cogs/help_command.py",
    "chars": 3580,
    "preview": "import discord\nfrom discord.ext import commands\n\nbot_links = \"\"\"[Support](https://discord.gg/KgZRMch3b6)\\u2800\\\n[Github]"
  },
  {
    "path": "bot/cogs/joke.py",
    "chars": 3464,
    "preview": "import asyncio\n\nfrom discord import Color, Embed, Message, RawReactionActionEvent, TextChannel, utils\nfrom discord.ext i"
  },
  {
    "path": "bot/cogs/packages.py",
    "chars": 6413,
    "preview": "from aiohttp import ContentTypeError\nfrom discord import Color, Embed\nfrom discord.ext.commands import Cog, Context, com"
  },
  {
    "path": "bot/cogs/quiz.py",
    "chars": 2229,
    "preview": "from discord import Color, Embed, Message\nfrom discord.ext import commands\nfrom quizapi import create_quiz_api\n\nfrom con"
  },
  {
    "path": "bot/cogs/rtfm.py",
    "chars": 6333,
    "preview": "import warnings\n\nimport aiohttp\nfrom discord import Color, Embed\nfrom discord.ext import commands, flags\n\nfrom bot.bot i"
  },
  {
    "path": "bot/cogs/stackexchange.py",
    "chars": 8366,
    "preview": "import datetime\nimport html\nimport json\nimport os\nimport traceback\nfrom typing import Optional\nfrom urllib.parse import "
  },
  {
    "path": "bot/cogs/thank.py",
    "chars": 6258,
    "preview": "import asyncio\nfrom typing import Optional\n\nfrom discord import Color, Embed, Member, Reaction\nfrom discord.ext import c"
  },
  {
    "path": "bot/cogs/utils.py",
    "chars": 6100,
    "preview": "import sys\nimport os\nimport inspect\n\nfrom discord import Embed, Message, TextChannel\nfrom discord.ext import commands, f"
  },
  {
    "path": "bot/core.py",
    "chars": 3361,
    "preview": "import platform\nimport sys\n\nimport psutil\nfrom discord import Color, Embed, NotFound\nfrom discord import __version__ as "
  },
  {
    "path": "bot/utils/embed_flag_input.py",
    "chars": 6853,
    "preview": "import functools\nimport re\nfrom typing import Dict, Iterable, TypeVar, Union\nfrom urllib import parse\n\nfrom discord impo"
  },
  {
    "path": "bot/utils/fuzzy.py",
    "chars": 4401,
    "preview": "# -*- coding: utf-8 -*-\n\n\"\"\"\nThis Source Code Form is subject to the terms of the Mozilla Public\nLicense, v. 2.0. If a c"
  },
  {
    "path": "bot/utils/process_files.py",
    "chars": 1607,
    "preview": "import re\nfrom typing import Dict, List, Tuple\n\nfrom discord.ext import commands\n\nfiles_pattern = re.compile(r\"\\s{0,}```"
  },
  {
    "path": "bot/utils/rtfm.py",
    "chars": 3235,
    "preview": "import io\nimport os\nimport re\nimport zlib\n\n# Directly taken and modified from Rapptz/RoboDanny\n# https://github.com/Rapp"
  },
  {
    "path": "bot.Dockerfile",
    "chars": 186,
    "preview": "FROM python:3.9\n\nWORKDIR /app\nENV PYTHONDONTWRITEBYTECODE=1\n\nCOPY requirements-bot.txt ./\nRUN pip install --no-cache-dir"
  },
  {
    "path": "config/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "config/bot.py",
    "chars": 203,
    "preview": "from pydantic import BaseSettings\n\n\nclass BotConfig(BaseSettings):\n    bot_token: str\n    quiz_api_token: str\n    log_we"
  },
  {
    "path": "config/common.py",
    "chars": 429,
    "preview": "from pydantic import BaseSettings, PostgresDsn\n\n\nclass Settings(BaseSettings):\n\n    secret: str\n    database_uri: Postgr"
  },
  {
    "path": "config/oauth.py",
    "chars": 499,
    "preview": "from pydantic import BaseSettings\n\n\nclass StackOAuthConfig(BaseSettings):\n    client_id: str\n    client_secret: str\n    "
  },
  {
    "path": "config/reddit.py",
    "chars": 257,
    "preview": "from pydantic import BaseSettings\n\n\nclass RedditConfig(BaseSettings):\n    client_id: str\n    client_secret: str\n    user"
  },
  {
    "path": "config/webhook.py",
    "chars": 293,
    "preview": "from pydantic import BaseSettings\n\n\nclass Webhooks(BaseSettings):\n    git_tips: str\n    meme: str\n    authorization: str"
  },
  {
    "path": "heroku.yml",
    "chars": 50,
    "preview": "build:\n    docker:\n        worker: bot.Dockerfile\n"
  },
  {
    "path": "models.py",
    "chars": 2529,
    "preview": "from tortoise import Model, fields\n\n\nclass ThankModel(Model):\n    id = fields.IntField(pk=True)\n    guild = fields.Forei"
  },
  {
    "path": "public/templates/404.html",
    "chars": 2236,
    "preview": "<!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/cs"
  },
  {
    "path": "public/templates/oauth_error.html",
    "chars": 1831,
    "preview": "<!DOCTYPE html>\n<html>\n<head>\n\t<title>Error!</title>\n\n<link rel=\"stylesheet\" type=\"text/css\" href=\"https://stackpath.boo"
  },
  {
    "path": "public/templates/oauth_success.html",
    "chars": 1864,
    "preview": "<!DOCTYPE html>\n<html>\n<head>\n<title>Success!</title>\n\n<link rel=\"stylesheet\" type=\"text/css\" href=\"https://stackpath.bo"
  },
  {
    "path": "requirements-bot.txt",
    "chars": 206,
    "preview": "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=="
  },
  {
    "path": "requirements-dev.txt",
    "chars": 27,
    "preview": "black==20.8b1\nisort==5.8.0\n"
  },
  {
    "path": "requirements.txt",
    "chars": 281,
    "preview": "asyncpg==0.22.0\nbackports-datetime-fromisoformat==1.0.0; python_version < '3.7'\ndiscord.py==1.7.1\nfastapi==0.65.2\nJinja2"
  },
  {
    "path": "tortoise_config.py",
    "chars": 799,
    "preview": "import ssl\n\nfrom config import common\n\n# TODO: Yet to find a fix for this\nctx = ssl.create_default_context()\nctx.check_h"
  },
  {
    "path": "utils/db_backup.py",
    "chars": 650,
    "preview": "import asyncio\nimport pickle\nfrom datetime import datetime\n\nimport asyncpg\n\nfrom config.common import config\n\n\nasync def"
  },
  {
    "path": "utils/embed.py",
    "chars": 2068,
    "preview": "import datetime\n\nimport yaml\nfrom discord import Color, Embed, File\n\n\ndef build_embed(embed_data, add_timestamp=False):\n"
  },
  {
    "path": "utils/webhook.py",
    "chars": 637,
    "preview": "from typing import Optional\n\nfrom discord import RequestsWebhookAdapter, Webhook\n\nfrom .embed import yaml_file_to_messag"
  },
  {
    "path": "vercel.json",
    "chars": 121,
    "preview": "{\n  \"functions\": {\n    \"api/main.py\": { \"maxDuration\": 10 }\n  },\n  \"routes\": [{ \"src\": \"/(.*)\", \"dest\": \"/api/main\" }]\n}"
  }
]

About this extraction

This page contains the full source code of the TechStruck/TechStruck-Bot GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 57 files (137.9 KB), approximately 33.8k tokens, and a symbol index with 247 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!