Skip to content

Commit 706d132

Browse files
committed
Add bot code
1 parent fb85568 commit 706d132

22 files changed

+1106
-1
lines changed

.github/workflows/codeql.yml

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: "CodeQL"
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
pull_request:
9+
branches:
10+
- main
11+
schedule:
12+
- cron: '36 7 * * 0'
13+
14+
jobs:
15+
Analyze:
16+
name: Analyze
17+
runs-on: ubuntu-latest
18+
permissions:
19+
actions: read
20+
contents: read
21+
security-events: write
22+
23+
steps:
24+
- name: Checkout Repository
25+
uses: actions/checkout@v4
26+
- name: Set up Python 3.11
27+
id: setup-python
28+
uses: actions/setup-python@v4
29+
with:
30+
python-version: '3.11'
31+
- name: Set up Poetry
32+
uses: Gr1N/setup-poetry@v8
33+
- name: Cache Poetry
34+
id: cache-poetry
35+
uses: actions/cache@v3
36+
with:
37+
path: ~/.cache/pypoetry/virtualenvs
38+
key: ${{ runner.os }}-codeql-python-${{ hashFiles('**/poetry.lock') }}
39+
- name: Install Poetry Dependencies
40+
if: steps.cache-poetry.outputs.cache-hit != 'true'
41+
run: |
42+
poetry install
43+
- name: Initialize CodeQL
44+
uses: github/codeql-action/init@v2
45+
with:
46+
languages: python
47+
setup-python-dependencies: false
48+
- name: Perform CodeQL Analysis
49+
uses: github/codeql-action/analyze@v2
50+
with:
51+
upload: true

.github/workflows/lint.yml

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: Lint
2+
on:
3+
push:
4+
branches:
5+
- main
6+
7+
pull_request:
8+
branches:
9+
- main
10+
11+
jobs:
12+
Analyze:
13+
runs-on: ubuntu-latest
14+
15+
strategy:
16+
fail-fast: false
17+
matrix:
18+
version: [3.9, '3.10', '3.11']
19+
20+
steps:
21+
- name: Checkout Repository
22+
uses: actions/checkout@v4
23+
24+
- name: Set up Python ${{ matrix.version }}
25+
id: setup-python
26+
uses: actions/setup-python@v4
27+
with:
28+
python-version: ${{ matrix.version }}
29+
30+
- name: Set up Poetry
31+
uses: Gr1N/setup-poetry@v8
32+
33+
- name: Cache Poetry
34+
id: cache-poetry
35+
uses: actions/cache@v3
36+
with:
37+
path: ~/.cache/pypoetry/virtualenvs
38+
key: ${{ runner.os }}-poetry-lint-${{ matrix.version }}-${{ hashFiles('**/poetry.lock') }}
39+
40+
- name: Install Poetry Dependencies
41+
if: steps.cache-poetry.outputs.cache-hit != 'true'
42+
run: |
43+
poetry install --with dev
44+
45+
- name: Run Pyright
46+
run: |
47+
poetry run pyright bot
48+
49+
- name: Run Ruff
50+
run: |
51+
poetry run ruff bot

.github/workflows/release.yml

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Release
2+
on:
3+
push:
4+
branches:
5+
- main
6+
jobs:
7+
Release:
8+
runs-on: ubuntu-latest
9+
if: contains(github.event.head_commit.message, '#major') || contains(github.event.head_commit.message, '#minor') || contains(github.event.head_commit.message, '#patch')
10+
steps:
11+
- uses: actions/checkout@v4
12+
with:
13+
fetch-depth: '0'
14+
15+
- name: Bump version and push tag
16+
uses: anothrNick/[email protected]
17+
id: tag_version
18+
env:
19+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
20+
WITH_V: true
21+
RELEASE_BRANCHES: main
22+
23+
- name: Release New Version
24+
uses: ncipollo/release-action@v1
25+
with:
26+
bodyFile: "changelog.md"
27+
token: ${{ secrets.PAT_TOKEN }}
28+
tag: ${{ steps.tag_version.outputs.new_tag }}
29+
name: ${{ steps.tag_version.outputs.new_tag }}

README.md

+60
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,62 @@
11
# rodhaj
22
proto-repo for discord app
3+
4+
**later down the road, pls do not push to main branch directly**
5+
6+
## Stuff that needs to be done
7+
8+
- [x] Paginators
9+
- [ ] R. Danny migrations or asyncpg-trek
10+
- [ ] The features
11+
12+
## Getting Started
13+
14+
### Preface on Slash Commands
15+
16+
Unlike other frameworks, discord.py does not automatically sync slash commands (if you want to learn more why, see [this and why Noelle is heavily against it](https://github.com/No767/Zoee#preface-on-slash-commands-and-syncing)). So the way to sync is by using an prefixed commands, which is [Umbra's Sync Command](https://about.abstractumbra.dev/discord.py/2023/01/29/sync-command-example.html). More than likely you'll need to read up on how this slash command works in order to get started. In short, you'll probably want to sync your test bot to the guild instead (as demostrated here):
17+
18+
```
19+
# Replace 1235 with your guild id
20+
r>sync 1235
21+
```
22+
23+
24+
### Setup Instructions
25+
26+
You must have these installed:
27+
28+
- Poetry
29+
- Python
30+
- Git
31+
- PostgreSQL
32+
33+
In order to run pg in a docker container, spin up the docker compose file
34+
located in the root of the repo (`sudo docker compose up -d`).
35+
36+
1. Clone the repo or use it as a template.
37+
2. Copy over the ENV file template to the `bot` directory
38+
39+
```bash
40+
cp envs/dev.env bot/.env
41+
```
42+
3. Install the dependencies
43+
44+
```bash
45+
poetry install
46+
```
47+
48+
4. Configure the settings in the ENV (note that configuring the postgres uri is required)
49+
50+
5. Run the bot
51+
52+
```bash
53+
poetry run python bot/launcher.py
54+
```
55+
56+
6. Once your bot is running, sync the commands to your guild. You might have to wait a while because the syncing process usually takes some time. Once completed, you should now have the `CommandTree` synced to that guild.
57+
58+
```
59+
# Replace 12345 with your guild id
60+
r>sync 12345
61+
```
62+
7. Now go ahead and play around with the default commands. Add your own, delete some, do whatever you want now.

bot/cogs/__init__.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from pkgutil import iter_modules
2+
from typing import Literal, NamedTuple
3+
4+
5+
class VersionInfo(NamedTuple):
6+
major: int
7+
minor: int
8+
micro: int
9+
releaselevel: Literal["alpha", "beta", "final"]
10+
11+
def __str__(self) -> str:
12+
return f"{self.major}.{self.minor}.{self.micro}-{self.releaselevel}"
13+
14+
15+
EXTENSIONS = [module.name for module in iter_modules(__path__, f"{__package__}.")]
16+
VERSION: VersionInfo = VersionInfo(major=0, minor=1, micro=0, releaselevel="alpha")

bot/cogs/dev_tools.py

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from typing import Literal, Optional
2+
3+
import discord
4+
from cogs import EXTENSIONS
5+
from discord.ext import commands
6+
from discord.ext.commands import Context, Greedy
7+
8+
from rodhaj import Rodhaj
9+
10+
11+
class DevTools(commands.Cog, command_attrs=dict(hidden=True)):
12+
"""Tools for developing RodHaj"""
13+
14+
def __init__(self, bot: Rodhaj):
15+
self.bot = bot
16+
17+
# Umbra's sync command
18+
# To learn more about it, see the link below (and ?tag ass on the dpy server):
19+
# https://about.abstractumbra.dev/discord.py/2023/01/29/sync-command-example.html
20+
@commands.guild_only()
21+
@commands.is_owner()
22+
@commands.command(name="sync", hidden=True)
23+
async def sync(
24+
self,
25+
ctx: Context,
26+
guilds: Greedy[discord.Object],
27+
spec: Optional[Literal["~", "*", "^"]] = None,
28+
) -> None:
29+
"""Performs a sync of the tree. This will sync, copy globally, or clear the tree.
30+
31+
Args:
32+
ctx (Context): Context of the command
33+
guilds (Greedy[discord.Object]): Which guilds to sync to. Greedily accepts a number of guilds
34+
spec (Optional[Literal["~", "*", "^"], optional): Specs to sync.
35+
"""
36+
await ctx.defer()
37+
if not guilds:
38+
if spec == "~":
39+
synced = await self.bot.tree.sync(guild=ctx.guild)
40+
elif spec == "*":
41+
self.bot.tree.copy_global_to(guild=ctx.guild) # type: ignore
42+
synced = await self.bot.tree.sync(guild=ctx.guild)
43+
elif spec == "^":
44+
self.bot.tree.clear_commands(guild=ctx.guild)
45+
await self.bot.tree.sync(guild=ctx.guild)
46+
synced = []
47+
else:
48+
synced = await self.bot.tree.sync()
49+
50+
await ctx.send(
51+
f"Synced {len(synced)} commands {'globally' if spec is None else 'to the current guild.'}"
52+
)
53+
return
54+
55+
ret = 0
56+
for guild in guilds:
57+
try:
58+
await self.bot.tree.sync(guild=guild)
59+
except discord.HTTPException:
60+
pass
61+
else:
62+
ret += 1
63+
64+
await ctx.send(f"Synced the tree to {ret}/{len(guilds)}.")
65+
66+
@commands.guild_only()
67+
@commands.is_owner()
68+
@commands.command(name="reload-all", hidden=True)
69+
async def reload_all(self, ctx: commands.Context) -> None:
70+
"""Reloads all cogs. Used in production to not produce any downtime"""
71+
if not hasattr(self.bot, "uptime"):
72+
await ctx.send("Bot + exts must be up and loaded before doing this")
73+
return
74+
75+
for extension in EXTENSIONS:
76+
await self.bot.reload_extension(extension)
77+
await ctx.send("Successfully reloaded all extensions live")
78+
79+
80+
async def setup(bot: Rodhaj):
81+
await bot.add_cog(DevTools(bot))

bot/cogs/meta.py

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import datetime
2+
import itertools
3+
import platform
4+
5+
import discord
6+
import psutil
7+
import pygit2
8+
from discord import app_commands
9+
from discord.ext import commands
10+
from discord.utils import format_dt
11+
from libs.utils import Embed, human_timedelta
12+
13+
from rodhaj import Rodhaj
14+
15+
16+
# A cog houses a category of commands
17+
# Unlike djs, think of commands being stored as a category,
18+
# which the cog is that category
19+
class Meta(commands.Cog):
20+
def __init__(self, bot: Rodhaj) -> None:
21+
self.bot = bot
22+
self.process = psutil.Process()
23+
24+
def get_bot_uptime(self, *, brief: bool = False) -> str:
25+
return human_timedelta(
26+
self.bot.uptime, accuracy=None, brief=brief, suffix=False
27+
)
28+
29+
def format_commit(self, commit: pygit2.Commit) -> str:
30+
short, _, _ = commit.message.partition("\n")
31+
short_sha2 = commit.hex[0:6]
32+
commit_tz = datetime.timezone(
33+
datetime.timedelta(minutes=commit.commit_time_offset)
34+
)
35+
commit_time = datetime.datetime.fromtimestamp(commit.commit_time).astimezone(
36+
commit_tz
37+
)
38+
39+
# [`hash`](url) message (offset)
40+
offset = format_dt(commit_time.astimezone(datetime.timezone.utc), "R")
41+
return f"[`{short_sha2}`](https://github.com/transprogrammer/rodhaj/commit/{commit.hex}) {short} ({offset})"
42+
43+
def get_last_commits(self, count: int = 5):
44+
repo = pygit2.Repository(".git")
45+
commits = list(
46+
itertools.islice(
47+
repo.walk(repo.head.target, pygit2.GIT_SORT_TOPOLOGICAL), count
48+
)
49+
)
50+
return "\n".join(self.format_commit(c) for c in commits)
51+
52+
@app_commands.command(name="about")
53+
async def about(self, interaction: discord.Interaction) -> None:
54+
"""Shows some stats for Rodhaj"""
55+
total_members = 0
56+
total_unique = len(self.bot.users)
57+
58+
for guild in self.bot.guilds:
59+
total_members += guild.member_count or 0
60+
61+
# For Kumiko, it's done differently
62+
# R. Danny's way of doing it is probably close enough anyways
63+
memory_usage = self.process.memory_full_info().uss / 1024**2
64+
cpu_usage = self.process.cpu_percent() / psutil.cpu_count()
65+
66+
revisions = self.get_last_commits()
67+
embed = Embed()
68+
embed.set_author(name=self.bot.user.name, icon_url=self.bot.user.display_avatar.url) # type: ignore
69+
embed.title = "About Me"
70+
embed.description = f"Latest Changes:\n {revisions}"
71+
embed.set_footer(
72+
text=f"Made with discord.py v{discord.__version__}",
73+
icon_url="https://cdn.discordapp.com/emojis/596577034537402378.png?size=128",
74+
)
75+
embed.add_field(name="Servers Count", value=len(self.bot.guilds))
76+
embed.add_field(
77+
name="User Count", value=f"{total_members} total\n{total_unique} unique"
78+
)
79+
embed.add_field(
80+
name="Process", value=f"{memory_usage:.2f} MiB\n{cpu_usage:.2f}% CPU"
81+
)
82+
embed.add_field(name="Python Version", value=platform.python_version())
83+
embed.add_field(name="Version", value=str(self.bot.version))
84+
embed.add_field(name="Uptime", value=self.get_bot_uptime(brief=True))
85+
await interaction.response.send_message(embed=embed)
86+
87+
@app_commands.command(name="uptime")
88+
async def uptime(self, interaction: discord.Interaction) -> None:
89+
"""Displays the bot's uptime"""
90+
uptime_message = f"Uptime: {self.get_bot_uptime()}"
91+
await interaction.response.send_message(uptime_message)
92+
93+
@app_commands.command(name="version")
94+
async def version(self, interaction: discord.Interaction) -> None:
95+
"""Displays the current build version"""
96+
version_message = f"Version: {self.bot.version}"
97+
await interaction.response.send_message(version_message)
98+
99+
100+
async def setup(bot: Rodhaj) -> None:
101+
await bot.add_cog(Meta(bot))

0 commit comments

Comments
 (0)