diff --git a/.gitignore b/.gitignore
index 4356796..0199a21 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ venv
domains.txt
geckodriver.log
targets.json
+*.kdev4
\ No newline at end of file
diff --git a/.idea/dataSources.local.xml b/.idea/dataSources.local.xml
index 4316a9e..a4eba69 100644
--- a/.idea/dataSources.local.xml
+++ b/.idea/dataSources.local.xml
@@ -9,7 +9,10 @@
no-auth
-
+
+
+
+
diff --git a/.idea/dataSources/28efee07-d306-4126-bf69-01008b4887e2.xml b/.idea/dataSources/28efee07-d306-4126-bf69-01008b4887e2.xml
index 36e4d02..69d87da 100644
--- a/.idea/dataSources/28efee07-d306-4126-bf69-01008b4887e2.xml
+++ b/.idea/dataSources/28efee07-d306-4126-bf69-01008b4887e2.xml
@@ -1316,305 +1316,339 @@
-
+
+
-
-
-
-
+
+
+
+
INTEGER|0s
1
1
-
+
CHAR(32)|0s
2
-
+
VARCHAR(2000)|0s
1
3
-
+
VARCHAR(4096)|0s
4
-
+
VARCHAR(4096)|0s
5
-
+
FLOAT|0s
1
6
-
+
FLOAT|0s
1
7
-
+
VARCHAR(7)|0s
1
8
-
+
JSON|0s
1
9
-
+
BOOLEAN|0s
1
10
-
+
BOOLEAN|0s
1
11
-
+
JSON|0s
1
12
-
+
created_by
entry_id
students
-
+
entry_id
1
-
+
CHAR(32)|0s
1
1
-
+
VARCHAR(7)|0s
1
2
-
+
BIGINT|0s
1
3
-
+
FLOAT|0s
1
4
-
+
entry_id
1
1
-
+
student_id
1
1
-
+
entry_id
1
sqlite_autoindex_banned_1
-
+
student_id
sqlite_autoindex_banned_2
-
+
INTEGER|0s
1
1
-
+
VARCHAR(64)|0s
1
2
-
+
BIGINT|0s
1
3
-
+
VARCHAR(7)|0s
1
4
-
+
VARCHAR(32)|0s
1
5
-
+
code
1
1
-
+
id
1
-
+
code
sqlite_autoindex_codes_1
-
- TEXT|0s
- 1
-
-
- TEXT|0s
- 2
-
-
- TEXT|0s
- 3
-
-
- INT|0s
- 4
-
-
- TEXT|0s
- 5
-
-
+
CHAR(32)|0s
1
1
-
+
BIGINT|0s
1
2
-
- BIGINT|0s
- 1
+
+ TEXT|0s
3
-
- BIGINT|0s
+
+ FLOAT|0s
+ 1
4
-
+
+ FLOAT|0s
+ 5
+
+
entry_id
1
1
-
- id
- 1
- 1
-
-
+
entry_id
1
- sqlite_autoindex_starboard_1
-
-
- id
- sqlite_autoindex_starboard_2
+ sqlite_autoindex_jimmy_bans_1
+
+ TEXT|0s
+ 1
+
+
+ TEXT|0s
+ 2
+
+
+ TEXT|0s
+ 3
+
+
+ INT|0s
+ 4
+
+
+ TEXT|0s
+ 5
+
CHAR(32)|0s
1
1
- VARCHAR(7)|0s
+ BIGINT|0s
1
2
-
+
BIGINT|0s
1
3
-
- VARCHAR(32)|0s
- 1
+
+ BIGINT|0s
4
-
+
entry_id
1
1
-
+
id
1
1
-
- user_id
- 1
- 1
-
-
+
entry_id
1
- sqlite_autoindex_students_1
+ sqlite_autoindex_starboard_1
-
+
id
- sqlite_autoindex_students_2
+ sqlite_autoindex_starboard_2
-
- user_id
- sqlite_autoindex_students_3
-
-
+
CHAR(32)|0s
1
1
-
- VARCHAR(128)|0s
+
+ VARCHAR(7)|0s
1
2
-
- VARCHAR(128)|0s
+
+ BIGINT|0s
1
3
-
- BOOLEAN|0s
+
+ VARCHAR(32)|0s
1
4
-
- FLOAT|0s
- 1
- 5
-
-
- INTEGER|0s
- 6
-
-
- TEXT|0s
- 7
-
-
- BOOLEAN|0s
- 1
- 8
-
-
+
entry_id
1
1
-
+
+ id
+ 1
+ 1
+
+
+ user_id
+ 1
+ 1
+
+
+ entry_id
+ 1
+ sqlite_autoindex_students_1
+
+
+ id
+ sqlite_autoindex_students_2
+
+
+ user_id
+ sqlite_autoindex_students_3
+
+
+ CHAR(32)|0s
+ 1
+ 1
+
+
+ VARCHAR(128)|0s
+ 1
+ 2
+
+
+ VARCHAR(128)|0s
+ 1
+ 3
+
+
+ BOOLEAN|0s
+ 1
+ 4
+
+
+ FLOAT|0s
+ 1
+ 5
+
+
+ INTEGER|0s
+ 6
+
+
+ TEXT|0s
+ 7
+
+
+ BOOLEAN|0s
+ 1
+ 8
+
+
+ entry_id
+ 1
+ 1
+
+
entry_id
1
sqlite_autoindex_uptime_1
diff --git a/config.example.py b/config.example.py
index 4e1d5d3..cba7c31 100644
--- a/config.example.py
+++ b/config.example.py
@@ -13,6 +13,7 @@ import os
import discord
# The IDs of guilds the bot should be in; used to determine where to make slash commands
+# If you put multiple IDs in here, the first one should be your "primary" server.
guilds = [994710566612500550]
# Email & email password for the email verification system
@@ -46,7 +47,15 @@ HTTP_HOST = "127.0.0.1"
HTTP_PORT = 3762
# You can also just fully turn the web server off
-WEB_SERVER = False
+WEB_SERVER = True # change this to False to disable it
+
+# Or change uvicorn settings (see: https://www.uvicorn.org/settings/)
+# Note that passing `host` or `port` will raise an error, as those are configured above.
+UVICORN_CONFIG = {
+ "log_level": "error",
+ "access_log": False,
+ "lifespan": "off"
+}
# Only change this if you want to test changes to the bot without sending too much traffic to discord.
# Connect modes:
diff --git a/main.py b/main.py
index 2c24704..147f9b1 100644
--- a/main.py
+++ b/main.py
@@ -1,61 +1,11 @@
+import asyncio
+
import discord
from discord.ext import commands
-from asyncio import Lock
import config
from datetime import datetime, timezone, timedelta
-from utils import registry, console, get_or_none, JimmyBans
-from web.server import app
-import uvicorn
-
-
-intents = discord.Intents.default()
-intents += discord.Intents.messages
-intents += discord.Intents.message_content
-intents += discord.Intents.members
-intents += discord.Intents.presences
-
-
-extensions = [
- "jishaku",
- "cogs.verify",
- "cogs.mod",
- "cogs.events",
- "cogs.assignments",
- "cogs.timetable",
- "cogs.other",
- "cogs.starboard",
- "cogs.uptime",
-]
-
-
-class Bot(commands.Bot):
- def __init__(self):
- super().__init__(
- command_prefix=self.get_prefix,
- debug_guilds=config.guilds,
- allowed_mentions=discord.AllowedMentions.none(),
- intents=intents,
- )
- self.training_lock = Lock()
- self.started_at = datetime.now(tz=timezone.utc)
- self.bans = JimmyBans()
- for ext in extensions:
- try:
- bot.load_extension(ext)
- except discord.ExtensionFailed as e:
- console.log(f"[red]Failed to load extension {ext}: {e}")
- else:
- console.log(f"Loaded extension [green]{ext}")
-
- app.state.bot = self
- config = uvicorn.Config(
- app,
- port=3762
- )
-
-
-bot = Bot()
-bot.loop.run_until_complete(registry.create_all())
+from utils import console, get_or_none, JimmyBans
+from utils.client import bot
@bot.listen()
@@ -101,6 +51,9 @@ async def on_application_command(ctx: discord.ApplicationContext):
@bot.event
async def on_ready():
console.log("Logged in as", bot.user)
+ if getattr(config, "CONNECT_MODE", None) == 1:
+ console.log("Bot is now ready and exit target 1 is set, shutting down.")
+ await bot.close()
@bot.slash_command()
@@ -135,4 +88,36 @@ async def check_not_banned(ctx: discord.ApplicationContext | commands.Context):
if __name__ == "__main__":
console.log("Starting...")
bot.started_at = discord.utils.utcnow()
+
+ if getattr(config, "WEB_SERVER", True):
+ from web.server import app
+ import uvicorn
+
+ http_config = uvicorn.Config(
+ app,
+ host=getattr(config, "HTTP_HOST", "127.0.0.1"),
+ port=getattr(config, "HTTP_PORT", 3762),
+ lifespan="off",
+ access_log=False,
+ **getattr(config, "UVICORN_CONFIG", {})
+ )
+ server = uvicorn.Server(http_config)
+ console.log("Starting web server...")
+ loop = bot.loop
+ http_server_task = loop.create_task(server.serve())
+ bot.web = {
+ "server": server,
+ "config": http_config,
+ "task": http_server_task,
+ }
+
bot.run(config.token)
+ if hasattr(bot, "web"):
+ console.log("Cancelling web task...")
+ bot.web["task"].cancel()
+ console.log("Shutting down web server...")
+ try:
+ bot.web["task"].result()
+ except asyncio.CancelledError:
+ pass
+ console.log("Web server closed.")
diff --git a/requirements.txt b/requirements.txt
index 932f735..80bc4e4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -13,4 +13,3 @@ aiofiles==22.1.0
httpx==0.23.0
fastapi==0.92.0
uvicorn==0.20.0
-bcrypt==4.0.1
\ No newline at end of file
diff --git a/utils/client.py b/utils/client.py
new file mode 100644
index 0000000..533fe6a
--- /dev/null
+++ b/utils/client.py
@@ -0,0 +1,64 @@
+import discord
+import config
+from asyncio import Lock
+from discord.ext import commands
+from datetime import datetime, timezone
+
+
+__all__ = ("Bot", 'bot')
+
+
+# noinspection PyAbstractClass
+class Bot(commands.Bot):
+ def __init__(self, intents: discord.Intents, guilds: list[int], extensions: list[str]):
+ from .db import JimmyBans, registry
+ from .console import console
+ super().__init__(
+ command_prefix=self.get_prefix,
+ debug_guilds=guilds,
+ allowed_mentions=discord.AllowedMentions.none(),
+ intents=intents,
+ )
+ self.loop.run_until_complete(registry.create_all())
+ self.training_lock = Lock()
+ self.started_at = datetime.now(tz=timezone.utc)
+ self.bans = JimmyBans()
+ self.console = console
+ for ext in extensions:
+ try:
+ self.load_extension(ext)
+ except discord.ExtensionFailed as e:
+ console.log(f"[red]Failed to load extension {ext}: {e}")
+ if getattr(config, "dev", False):
+ console.print_exception()
+ else:
+ console.log(f"Loaded extension [green]{ext}")
+
+ if getattr(config, "CONNECT_MODE", None) == 2:
+ async def connect(self, *, reconnect: bool = True) -> None:
+ self.console.log("Exit target 2 reached, shutting down (not connecting to discord).")
+ return
+
+
+try:
+ from config import intents as _intents
+except ImportError:
+ _intents = discord.Intents.all()
+
+try:
+ from config import extensions as _extensions
+except ImportError:
+ _extensions = [
+ "jishaku",
+ "cogs.verify",
+ "cogs.mod",
+ "cogs.events",
+ "cogs.assignments",
+ "cogs.timetable",
+ "cogs.other",
+ "cogs.starboard",
+ "cogs.uptime",
+ ]
+
+
+bot = Bot(_intents, config.guilds, _extensions)
diff --git a/utils/db.py b/utils/db.py
index e34a7b9..52e1e1c 100644
--- a/utils/db.py
+++ b/utils/db.py
@@ -83,6 +83,9 @@ class Student(orm.Model):
id: str
user_id: int
name: str
+ access_token: str | None
+ ip_info: dict | None
+ access_token_hash: str | None
class BannedStudentID(orm.Model):
diff --git a/web/server.py b/web/server.py
index 7b56036..88cfbd1 100644
--- a/web/server.py
+++ b/web/server.py
@@ -1,11 +1,13 @@
import discord
import os
import httpx
-from datetime import datetime, timedelta, timezone
+from datetime import datetime, timezone
from hashlib import sha512
+
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse, RedirectResponse
-from utils.db import Student, get_or_none
+from utils import Student, get_or_none, VerifyCode, console, BannedStudentID
+from config import guilds
try:
from config import OAUTH_ID, OAUTH_SECRET, OAUTH_REDIRECT_URI
@@ -37,9 +39,10 @@ def ping():
"ping": "pong",
"online": app.state.bot.is_ready(),
"latency": app.state.bot.latency,
- "uptime": bot_started
+ "uptime": bot_started.total_seconds()
}
+
@app.get("/auth")
async def authenticate(req: Request, code: str = None, state: str = None):
if not (code and state) or state not in app.state.states:
@@ -94,7 +97,7 @@ async def authenticate(req: Request, code: str = None, state: str = None):
user = response.json()
# Now we need to fetch the student from the database
- student = await get_or_none(Student, discord_id=user["id"])
+ student = await get_or_none(Student, user_id=user["id"])
if not student:
raise HTTPException(
status_code=404,
@@ -125,15 +128,69 @@ async def authenticate(req: Request, code: str = None, state: str = None):
"/",
status_code=307,
headers={
- "Cache-Control": "max-age=86400"
+ "Cache-Control": "max-age=604800"
}
)
- # set the cookie for at most 86400 seconds - expire after that
+ # set the cookie for at most 604800 seconds - expire after that
response.set_cookie(
"token",
token,
- max_age=86400,
- same_site="strict",
+ max_age=604800,
+ samesite="strict",
httponly=True,
)
return response
+
+
+@app.get("/verify/{code}")
+async def verify(code: str):
+ guild = app.state.bot.get_guild(guilds[0])
+ if not guild:
+ raise HTTPException(
+ status_code=503,
+ detail="Not ready."
+ )
+
+ # First, we need to fetch the code from the database
+ verify_code = await get_or_none(VerifyCode, code=code)
+ if not verify_code:
+ raise HTTPException(
+ status_code=404,
+ detail="Code not found."
+ )
+
+ # Now we need to fetch the student from the database
+ student = await get_or_none(Student, user_id=verify_code.bind)
+ if student:
+ raise HTTPException(
+ status_code=400,
+ detail="Already verified."
+ )
+
+ ban = await get_or_none(BannedStudentID, student_id=verify_code.student_id)
+ if ban is not None:
+ return await guild.kick(
+ reason=f"Attempted to verify with banned student ID {ban.student_id}"
+ f" (originally associated with account {ban.associated_account})"
+ )
+ await Student.objects.create(
+ id=verify_code.student_id, user_id=verify_code.bind, name=verify_code.name
+ )
+ await verify_code.delete()
+ role = discord.utils.find(lambda r: r.name.lower() == "verified", guild.roles)
+ member = await guild.fetch_member(verify_code.bind)
+ if role and role < guild.me.top_role:
+ await member.add_roles(role, reason="Verified")
+ try:
+ await member.edit(nick=f"{verify_code.name}", reason="Verified")
+ except discord.HTTPException:
+ pass
+
+ # And delete the code
+ await verify_code.delete()
+
+ console.log(f"[green]{verify_code.bind} verified ({verify_code.bing}/{verify_code.student_id})")
+
+ return {
+ "message": "Successfully verified."
+ }