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" }]
}
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
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.