Profile schema and discord hook
This commit is contained in:
@@ -0,0 +1 @@
|
||||
from .profiles import setup
|
||||
|
||||
80
data/profiles.sql
Normal file
80
data/profiles.sql
Normal file
@@ -0,0 +1,80 @@
|
||||
BEGIN;
|
||||
|
||||
-- User and Community Profiles {{{
|
||||
|
||||
INSERT INTO version_history (component, from_version, to_version, author) VALUES ('PROFILES', 0, 1, 'Initial Creation');
|
||||
|
||||
|
||||
CREATE TABLE user_profiles(
|
||||
profileid INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
nickname TEXT,
|
||||
timezone TEXT,
|
||||
locale_hint TEXT,
|
||||
locale TEXT,
|
||||
avatar TEXT,
|
||||
migrated INTEGER REFERENCES user_profiles (profileid) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE TRIGGER user_profiles_timestamp BEFORE UPDATE ON user_profiles
|
||||
FOR EACH ROW EXECUTE FUNCTION update_timestamp_column();
|
||||
|
||||
CREATE TABLE profiles_discord(
|
||||
linkid INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
profileid INTEGER NOT NULL REFERENCES user_profiles (profileid) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
userid BIGINT NOT NULL,
|
||||
linked_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX profiles_discord_profileid ON profiles_discord (profileid);
|
||||
CREATE UNIQUE INDEX profiles_discord_userid ON profiles_discord (userid);
|
||||
|
||||
CREATE TABLE profiles_twitch(
|
||||
linkid INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
profileid INTEGER NOT NULL REFERENCES user_profiles (profileid) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
userid TEXT NOT NULL,
|
||||
linked_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX profiles_twitch_profileid ON profiles_twitch (profileid);
|
||||
CREATE UNIQUE INDEX profiles_twitch_userid ON profiles_twitch (userid);
|
||||
|
||||
|
||||
CREATE TABLE communities(
|
||||
communityid INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
migrated INTEGER REFERENCES user_profiles (profileid) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE TRIGGER communities_timestamp BEFORE UPDATE ON communities
|
||||
FOR EACH ROW EXECUTE FUNCTION update_timestamp_column();
|
||||
|
||||
CREATE TABLE communities_discord(
|
||||
linkid INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
guildid BIGINT NOT NULL,
|
||||
communityid INTEGER NOT NULL REFERENCES communities (communityid) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
linked_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX communities_discord_communityid ON communities_discord (communityid);
|
||||
CREATE UNIQUE INDEX communities_discord_guildid ON communities_discord (guildid);
|
||||
|
||||
CREATE TABLE communities_twitch(
|
||||
linkid INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
channelid TEXT NOT NULL,
|
||||
communityid INTEGER NOT NULL REFERENCES communities (communityid) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
linked_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX communities_twitch_communityid ON communities_twitch (communityid);
|
||||
CREATE UNIQUE INDEX communities_twitch_channelid ON communities_twitch (channelid);
|
||||
|
||||
CREATE TABLE community_members(
|
||||
memberid INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
communityid INTEGER NOT NULL REFERENCES communities (communityid) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
profileid INTEGER NOT NULL REFERENCES user_profiles (profileid) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE UNIQUE INDEX community_members_communityid_profileid ON community_members (communityid, profileid);
|
||||
|
||||
-- TODO: Consider 'networks' of multiple communities.
|
||||
-- }}}
|
||||
COMMIT;
|
||||
@@ -0,0 +1,5 @@
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from .discord import setup
|
||||
|
||||
81
profiles/data.py
Normal file
81
profiles/data.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from data import Registry, Table, RowModel
|
||||
from data.columns import String, Integer, Timestamp
|
||||
|
||||
|
||||
class UserProfile(RowModel):
|
||||
_tablename_ = 'user_profiles'
|
||||
_cache_ = {}
|
||||
|
||||
profileid = Integer(primary=True)
|
||||
nickname = String()
|
||||
timezone = String()
|
||||
locale_hint = String()
|
||||
locale = String()
|
||||
avatar = String()
|
||||
migrated = Integer()
|
||||
created_at = Timestamp()
|
||||
last_seen = Timestamp()
|
||||
_timestamp = Timestamp()
|
||||
|
||||
|
||||
class DiscordProfileLink(RowModel):
|
||||
_tablename_ = 'profiles_discord'
|
||||
_cache_ = {}
|
||||
|
||||
linkid = Integer()
|
||||
profileid = Integer()
|
||||
userid = Integer(primary=True)
|
||||
linked_at = Timestamp()
|
||||
|
||||
|
||||
class TwitchProfileLink(RowModel):
|
||||
_tablename_ = 'profiles_twitch'
|
||||
_cache_ = {}
|
||||
|
||||
linkid = Integer()
|
||||
profileid = Integer()
|
||||
userid = String(primary=True)
|
||||
linked_at = Timestamp()
|
||||
|
||||
|
||||
class Community(RowModel):
|
||||
_tablename_ = 'communities'
|
||||
_cache_ = {}
|
||||
|
||||
communityid = Integer(primary=True)
|
||||
migrated = Integer()
|
||||
created_at = Timestamp()
|
||||
last_seen = Timestamp()
|
||||
_timestamp = Timestamp()
|
||||
|
||||
|
||||
class DiscordCommunityLink(RowModel):
|
||||
_tablename_ = 'communities_discord'
|
||||
_cache_ = {}
|
||||
|
||||
linkid = Integer()
|
||||
guildid = Integer(primary=True)
|
||||
communityid = Integer()
|
||||
linked_at = Timestamp()
|
||||
|
||||
|
||||
class TwitchCommunityLink(RowModel):
|
||||
_tablename_ = 'communities_twitch'
|
||||
_cache_ = {}
|
||||
|
||||
linkid = Integer()
|
||||
channelid = String(primary=True)
|
||||
communityid = Integer()
|
||||
linked_at = Timestamp()
|
||||
|
||||
|
||||
class ProfilesData(Registry):
|
||||
VERSION = ('PROFILES', 1)
|
||||
|
||||
user_profiles = UserProfile.table
|
||||
profiles_discord = DiscordProfileLink.table
|
||||
profiles_twitch = TwitchProfileLink.table
|
||||
|
||||
communities = Community.table
|
||||
communities_discord = DiscordCommunityLink.table
|
||||
communities_twitch = TwitchCommunityLink.table
|
||||
@@ -0,0 +1,5 @@
|
||||
from .. import logger
|
||||
|
||||
async def setup(bot):
|
||||
from .cog import ProfilesCog
|
||||
await bot.add_cog(ProfilesCog(bot))
|
||||
|
||||
89
profiles/discord/cog.py
Normal file
89
profiles/discord/cog.py
Normal file
@@ -0,0 +1,89 @@
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
|
||||
import discord
|
||||
from discord.ext import commands as cmds
|
||||
from discord import app_commands as appcmds
|
||||
|
||||
from meta import LionBot, LionCog, LionContext
|
||||
from meta.logger import log_wrap
|
||||
from utils.lib import utc_now
|
||||
|
||||
from ..data import ProfilesData, UserProfile, DiscordProfileLink, Community, DiscordCommunityLink
|
||||
from ..profiles import ProfilesRegistry
|
||||
|
||||
class ProfilesCog(LionCog):
|
||||
def __init__(self, bot: LionBot):
|
||||
self.bot = bot
|
||||
|
||||
self.data = bot.db.load_registry(ProfilesData())
|
||||
self.profiles = ProfilesRegistry(self.data)
|
||||
|
||||
async def cog_load(self):
|
||||
await self.data.init()
|
||||
await self.bot.version_check(*self.data.VERSION)
|
||||
await self.profiles.init()
|
||||
|
||||
async def bot_check_once(self, ctx: LionContext):
|
||||
profile = await self.fetch_profile(ctx.author, interaction=ctx.interaction, touch=True)
|
||||
ctx.profile = profile
|
||||
if ctx.guild:
|
||||
community = await self.fetch_community(ctx.guild, interaction=ctx.interaction, touch=True)
|
||||
ctx.community = community
|
||||
else:
|
||||
ctx.community = None
|
||||
return True
|
||||
|
||||
@log_wrap(isolate=True, action="Fetch Profile")
|
||||
async def fetch_profile(
|
||||
self,
|
||||
user: discord.User | discord.Member,
|
||||
interaction: Optional[discord.Interaction] = None,
|
||||
touch: bool = False,
|
||||
) -> UserProfile:
|
||||
"""
|
||||
Fetch or create the profile for the given user.
|
||||
"""
|
||||
async with self.bot.db.connection() as conn:
|
||||
self.bot.db.conn = conn
|
||||
async with conn.transaction():
|
||||
profile = await self.profiles.get_profile_discord(user.id)
|
||||
if profile is None:
|
||||
# Create a new profile
|
||||
# Then link with discord
|
||||
args = {
|
||||
'nickname': user.display_name,
|
||||
}
|
||||
if interaction:
|
||||
if interaction.locale:
|
||||
args['locale_hint'] = interaction.locale.language_code
|
||||
if user.avatar:
|
||||
args['avatar'] = user.avatar.url
|
||||
profile = await UserProfile.create(**args)
|
||||
await DiscordProfileLink.create(profileid=profile.profileid, userid=user.id)
|
||||
elif touch:
|
||||
await profile.update(last_seen=utc_now())
|
||||
|
||||
return profile
|
||||
|
||||
@log_wrap(isolate=True, action="Fetch Community")
|
||||
async def fetch_community(
|
||||
self,
|
||||
guild: discord.Guild,
|
||||
interaction: Optional[discord.Interaction] = None,
|
||||
touch: bool = False,
|
||||
) -> Community:
|
||||
"""
|
||||
Fetch or create the community for the given guild.
|
||||
"""
|
||||
async with self.bot.db.connection() as conn:
|
||||
self.bot.db.conn = conn
|
||||
async with conn.transaction():
|
||||
comm = await self.profiles.get_community_discord(guild.id)
|
||||
if comm is None:
|
||||
# Create a new Community and link to Discord
|
||||
comm = await Community.create()
|
||||
await DiscordCommunityLink.create(guildid=guild.id, communityid=comm.communityid)
|
||||
elif touch:
|
||||
await comm.update(last_seen=utc_now())
|
||||
return comm
|
||||
46
profiles/profiles.py
Normal file
46
profiles/profiles.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from typing import Optional
|
||||
from .data import (
|
||||
ProfilesData,
|
||||
UserProfile,
|
||||
DiscordProfileLink,
|
||||
TwitchProfileLink,
|
||||
Community,
|
||||
DiscordCommunityLink,
|
||||
TwitchCommunityLink,
|
||||
)
|
||||
|
||||
|
||||
class ProfilesRegistry:
|
||||
VERSION = ProfilesData.VERSION
|
||||
|
||||
def __init__(self, data: ProfilesData):
|
||||
self.data = data
|
||||
|
||||
async def init(self):
|
||||
await self.data.init()
|
||||
|
||||
async def get_profile(self, profileid: int) -> Optional[UserProfile]:
|
||||
return await UserProfile.fetch(profileid)
|
||||
|
||||
async def get_profile_discord(self, userid: int) -> Optional[UserProfile]:
|
||||
link = await DiscordProfileLink.fetch(userid)
|
||||
if link:
|
||||
return await UserProfile.fetch(link.profileid)
|
||||
|
||||
async def get_profile_twitch(self, userid: str) -> Optional[UserProfile]:
|
||||
link = await TwitchProfileLink.fetch(userid)
|
||||
if link:
|
||||
return await UserProfile.fetch(link.profileid)
|
||||
|
||||
async def get_community(self, communityid: int) -> Optional[Community]:
|
||||
return await Community.fetch(communityid)
|
||||
|
||||
async def get_community_discord(self, guildid: int) -> Optional[Community]:
|
||||
link = await DiscordCommunityLink.fetch(guildid)
|
||||
if link:
|
||||
return await Community.fetch(link.communityid)
|
||||
|
||||
async def get_community_twitch(self, channelid: str) -> Optional[Community]:
|
||||
link = await TwitchCommunityLink.fetch(channelid)
|
||||
if link:
|
||||
return await Community.fetch(link.communityid)
|
||||
Reference in New Issue
Block a user