Skip to content

Commit 7c29cf5

Browse files
authored
Merge pull request #1 from community-network/multiple_servers
support multiple discord servers and use same data for the same user …
2 parents 5de241e + 6f6df60 commit 7c29cf5

22 files changed

+815
-117
lines changed

.env.template

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
DISCORD_BOT_TOKEN = ""
2+
POSTGRES_USER = kdrole_bot
3+
POSTGRES_PASSWORD = ""
4+
POSTGRES_DB = kdrole_bot
5+
DB_HOST = localhost

alembic/env.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
from logging.config import fileConfig
22

3-
from sqlalchemy import engine_from_config
3+
from sqlalchemy import URL, engine_from_config
44
from sqlalchemy import pool
55

66
from alembic import context
77

88
# add db items to autogenerate the migrations
9+
from config import load_config
910
from database import connection
10-
from database.dto import users # noqa: F401
11+
from database.dto import users, kd_roles # noqa: F401
1112

1213
# this is the Alembic Config object, which provides
1314
# access to the values within the .ini file in use.
@@ -18,8 +19,17 @@
1819
if config.config_file_name is not None:
1920
fileConfig(config.config_file_name)
2021

21-
url = "sqlite:///app.db"
22-
22+
env_config = load_config()
23+
24+
uri = URL.create(
25+
drivername="postgresql",
26+
username=env_config.db.postgres_user,
27+
password=env_config.db.postgres_password,
28+
host=env_config.db.db_host,
29+
port=env_config.db.db_port,
30+
database=env_config.db.postgres_db,
31+
)
32+
url = uri.render_as_string(hide_password=False).replace("%", "%%")
2333
config.set_main_option("sqlalchemy.url", url)
2434

2535
# add your model's MetaData object here

alembic/versions/0acc0de9083c_create_initial_tables.py renamed to alembic/versions/3c93bb5d292f_create_initial_tables.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""create initial tables
22
3-
Revision ID: 0acc0de9083c
3+
Revision ID: 3c93bb5d292f
44
Revises:
5-
Create Date: 2026-02-21 12:07:53.092683
5+
Create Date: 2026-02-21 19:24:26.563016
66
77
"""
88
from typing import Sequence, Union
@@ -12,7 +12,7 @@
1212

1313

1414
# revision identifiers, used by Alembic.
15-
revision: str = '0acc0de9083c'
15+
revision: str = '3c93bb5d292f'
1616
down_revision: Union[str, Sequence[str], None] = None
1717
branch_labels: Union[str, Sequence[str], None] = None
1818
depends_on: Union[str, Sequence[str], None] = None
@@ -21,16 +21,26 @@
2121
def upgrade() -> None:
2222
"""Upgrade schema."""
2323
# ### commands auto generated by Alembic - please adjust! ###
24+
op.create_table('kd_roles',
25+
sa.Column('server_id', sa.BigInteger(), nullable=False),
26+
sa.Column('role_id', sa.BigInteger(), nullable=False),
27+
sa.Column('kd_amount', sa.Float(), nullable=False),
28+
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
29+
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
30+
sa.PrimaryKeyConstraint('server_id', 'role_id'),
31+
sa.UniqueConstraint('server_id', 'role_id', 'kd_amount')
32+
)
2433
op.create_table('users',
34+
sa.Column('server_id', sa.BigInteger(), nullable=False),
2535
sa.Column('discord_id', sa.BigInteger(), nullable=False),
2636
sa.Column('username', sa.String(), nullable=False),
2737
sa.Column('player_id', sa.BigInteger(), nullable=False),
2838
sa.Column('kdr_role_id', sa.BigInteger(), nullable=True),
2939
sa.Column('user_id', sa.BigInteger(), nullable=False),
30-
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
31-
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
32-
sa.PrimaryKeyConstraint('discord_id'),
33-
sa.UniqueConstraint('discord_id', 'player_id', 'user_id')
40+
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
41+
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
42+
sa.PrimaryKeyConstraint('server_id', 'discord_id'),
43+
sa.UniqueConstraint('server_id', 'discord_id', 'player_id', 'user_id')
3444
)
3545
# ### end Alembic commands ###
3646

@@ -39,4 +49,5 @@ def downgrade() -> None:
3949
"""Downgrade schema."""
4050
# ### commands auto generated by Alembic - please adjust! ###
4151
op.drop_table('users')
52+
op.drop_table('kd_roles')
4253
# ### end Alembic commands ###

api/gametools.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import json
2-
from typing import TypedDict
2+
from typing import TypeVar, TypedDict, overload
33

44
import aiohttp
55
from discord import Interaction
66

77
from database.dto.users import User
88
from dto.kdr import KDR
9+
from dto.user_servers import UserServers
910

1011
ENDPOINT = "https://api.gametools.network/"
1112
NEEDED_FIELDS = ["human_kills_total", "deaths_total"]
@@ -15,6 +16,11 @@
1516
}
1617

1718

19+
class ServerResult(TypedDict):
20+
user: UserServers
21+
gamemodes: dict[str, KDR]
22+
23+
1824
class Result(TypedDict):
1925
user: User
2026
gamemodes: dict[str, KDR]
@@ -23,7 +29,14 @@ class Result(TypedDict):
2329
class GametoolsApi:
2430
session: aiohttp.ClientSession
2531

26-
async def read_stats(self, users: list[User], data: dict) -> list[Result]:
32+
@overload
33+
async def read_stats(
34+
self, users: list[UserServers], data: dict
35+
) -> list[ServerResult]: ...
36+
@overload
37+
async def read_stats(self, users: list[User], data: dict) -> list[Result]: ...
38+
39+
async def read_stats(self, users, data):
2740
db_users = {str(user.player_id): user for user in users}
2841
results = []
2942
for player in data.get("playerStats", []):
@@ -90,6 +103,7 @@ async def find_player(self, interaction: Interaction, username: str) -> list[Use
90103
players = res.get("results", [])
91104
return [
92105
User(
106+
server_id=interaction.guild_id,
93107
discord_id=interaction.user.id,
94108
username=username,
95109
player_id=int(player.get("personaId", "")),
@@ -117,7 +131,7 @@ async def get_stats(self, user: User):
117131
result = await r.json()
118132
return await self.read_stats([user], result)
119133

120-
async def get_multiple_stats(self, multiple_users: list[User]):
134+
async def get_multiple_stats(self, multiple_users: list[UserServers]):
121135
payload = [
122136
{"player_id": user.player_id, "user_id": user.user_id, "platform": "pc"}
123137
for user in multiple_users

bot.py

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
"""discord api connection"""
22

33
import asyncio
4+
import collections
45
import logging
56
import os
67
import discord
78
from discord.ext import commands, tasks
8-
from sqlalchemy import select
99
from api.gametools import GametoolsApi
1010
from config import load_config
1111
from database.connection import DatabaseSingleton
1212
from database.dto.users import User
1313
from logger import setup_logger
1414

15+
from utils.kd_roles import get_all_kd_roles
16+
from utils.user_servers import fetch_user_servers
17+
1518
env_config = load_config()
1619

1720
logger = logging.getLogger("bot")
@@ -24,7 +27,7 @@ class KDRBot(commands.AutoShardedBot):
2427
def __init__(self, *args, **kwargs):
2528
self.logger = logger
2629
self.config = env_config
27-
self.db = DatabaseSingleton(env_config.bot.db_url)
30+
self.db = DatabaseSingleton(env_config.db)
2831
self.gametools_api = GametoolsApi()
2932
super().__init__(*args, **kwargs)
3033

@@ -48,22 +51,27 @@ async def update_kdr(self):
4851
from utils.role_management import RoleManagement # against circular import
4952

5053
async with self.db.create_session() as session:
51-
guild = await bot.fetch_guild(bot.config.bot.server_id)
52-
if guild is None:
53-
return
54-
55-
stmt = select(User)
56-
res = await session.execute(stmt)
57-
chunked_users = res.scalars().partitions(10)
58-
for chunk in chunked_users:
59-
stats = await self.gametools_api.get_multiple_stats(list(chunk))
54+
server_kd_roles = await get_all_kd_roles(session)
55+
user_servers = await fetch_user_servers(session)
56+
for chunk in user_servers:
57+
stats = await self.gametools_api.get_multiple_stats(chunk)
6058
for stat in stats:
61-
kdr_role_id = await RoleManagement().update_kdr_role(
62-
self, stat, guild
63-
)
64-
user = stat["user"]
65-
if user.kdr_role_id != kdr_role_id:
66-
await user.update_kdr(session, kdr_role_id)
59+
if isinstance(stat["user"], User):
60+
continue
61+
for server in stat["user"].servers:
62+
user = stat["user"]
63+
kdr_role_id = await RoleManagement().update_kdr_role(
64+
self,
65+
user.to_user(server),
66+
stat["gamemodes"],
67+
server_kd_roles.get(
68+
server.server_id, collections.OrderedDict({})
69+
),
70+
)
71+
if server.kdr_role_id != kdr_role_id:
72+
await User(discord_id=server.discord_id).update_kdr(
73+
session, kdr_role_id
74+
)
6775
logger.info("Done updating KDR roles!")
6876

6977

@@ -72,6 +80,30 @@ async def update_kdr(self):
7280
bot = KDRBot(command_prefix="!", intents=intents)
7381

7482

83+
@bot.event
84+
async def on_command_error(ctx, error):
85+
"""dont give a error if a command doesn't exist"""
86+
if isinstance(error, commands.CommandNotFound):
87+
return
88+
elif isinstance(error, commands.MissingRequiredArgument):
89+
return
90+
elif isinstance(error, commands.MissingRole):
91+
return
92+
elif isinstance(error, commands.MissingPermissions):
93+
embed = discord.Embed(
94+
color=0xE74C3C, description="Your not allowed to use this command"
95+
)
96+
await ctx.send(embed=embed)
97+
elif isinstance(error, commands.NoPrivateMessage):
98+
embed = discord.Embed(
99+
color=0xE74C3C,
100+
description="This command can only be used within a community, not in DM",
101+
)
102+
await ctx.send(embed=embed)
103+
else:
104+
raise error
105+
106+
75107
@bot.event
76108
async def on_ready():
77109
"""After bot is logged into discord"""

0 commit comments

Comments
 (0)