rewrite (core): Split and refactor Lion and config.
This commit is contained in:
@@ -150,9 +150,9 @@ class BabelCog(LionCog):
|
|||||||
async def cog_load(self):
|
async def cog_load(self):
|
||||||
if not self.bot.core:
|
if not self.bot.core:
|
||||||
raise ValueError("CoreCog must be loaded first!")
|
raise ValueError("CoreCog must be loaded first!")
|
||||||
self.bot.core.guild_settings.attach(LocaleSettings.ForceLocale)
|
self.bot.core.guild_config.register_model_setting(LocaleSettings.ForceLocale)
|
||||||
self.bot.core.guild_settings.attach(LocaleSettings.GuildLocale)
|
self.bot.core.guild_config.register_model_setting(LocaleSettings.GuildLocale)
|
||||||
self.bot.core.user_settings.attach(LocaleSettings.UserLocale)
|
self.bot.core.user_config.register_model_setting(LocaleSettings.UserLocale)
|
||||||
|
|
||||||
async def cog_unload(self):
|
async def cog_unload(self):
|
||||||
pass
|
pass
|
||||||
@@ -180,12 +180,12 @@ class BabelCog(LionCog):
|
|||||||
"""
|
"""
|
||||||
locale = None
|
locale = None
|
||||||
if ctx.guild:
|
if ctx.guild:
|
||||||
forced = ctx.alion.guild_settings['force_locale'].value
|
forced = ctx.lguild.config.get('force_locale').value
|
||||||
guild_locale = ctx.alion.guild_settings['guild_locale'].value
|
guild_locale = ctx.lguild.config.get('guild_locale').value
|
||||||
if forced:
|
if forced:
|
||||||
locale = guild_locale
|
locale = guild_locale
|
||||||
|
|
||||||
locale = locale or ctx.alion.user_settings['user_locale'].value
|
locale = locale or ctx.luser.config.get('user_locale').value
|
||||||
if ctx.interaction:
|
if ctx.interaction:
|
||||||
locale = locale or ctx.interaction.locale.value
|
locale = locale or ctx.interaction.locale.value
|
||||||
if ctx.guild:
|
if ctx.guild:
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ from settings.groups import SettingGroup
|
|||||||
|
|
||||||
from .data import CoreData
|
from .data import CoreData
|
||||||
from .lion import Lions
|
from .lion import Lions
|
||||||
from .guild_settings import GuildSettings
|
from .lion_guild import GuildConfig
|
||||||
from .user_settings import UserSettings
|
from .lion_member import MemberConfig
|
||||||
|
from .lion_user import UserConfig
|
||||||
|
|
||||||
|
|
||||||
class CoreCog(LionCog):
|
class CoreCog(LionCog):
|
||||||
@@ -20,7 +21,7 @@ class CoreCog(LionCog):
|
|||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.data = CoreData()
|
self.data = CoreData()
|
||||||
bot.db.load_registry(self.data)
|
bot.db.load_registry(self.data)
|
||||||
self.lions = Lions(bot)
|
self.lions = Lions(bot, self.data)
|
||||||
|
|
||||||
self.app_config: Optional[CoreData.AppConfig] = None
|
self.app_config: Optional[CoreData.AppConfig] = None
|
||||||
self.bot_config: Optional[CoreData.BotConfig] = None
|
self.bot_config: Optional[CoreData.BotConfig] = None
|
||||||
@@ -35,19 +36,13 @@ class CoreCog(LionCog):
|
|||||||
|
|
||||||
# Some ModelSetting registries
|
# Some ModelSetting registries
|
||||||
# These are for more convenient direct access
|
# These are for more convenient direct access
|
||||||
self.guild_settings = GuildSettings
|
self.guild_config = GuildConfig
|
||||||
self.user_settings = UserSettings
|
self.user_config = UserConfig
|
||||||
|
self.member_config = MemberConfig
|
||||||
|
|
||||||
self.app_cmd_cache: list[discord.app_commands.AppCommand] = []
|
self.app_cmd_cache: list[discord.app_commands.AppCommand] = []
|
||||||
self.cmd_name_cache: dict[str, discord.app_commands.AppCommand] = {}
|
self.cmd_name_cache: dict[str, discord.app_commands.AppCommand] = {}
|
||||||
|
|
||||||
async def bot_check_once(self, ctx: LionContext): # type: ignore
|
|
||||||
lion = await self.lions.fetch(ctx.guild.id if ctx.guild else 0, ctx.author.id)
|
|
||||||
if ctx.guild:
|
|
||||||
await lion.touch_discord_models(ctx.author) # type: ignore # Type checker doesn't recognise guard
|
|
||||||
ctx.alion = lion
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def cog_load(self):
|
async def cog_load(self):
|
||||||
# Fetch (and possibly create) core data rows.
|
# Fetch (and possibly create) core data rows.
|
||||||
conn = await self.bot.db.get_connection()
|
conn = await self.bot.db.get_connection()
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
from settings.groups import ModelSettings, SettingDotDict
|
|
||||||
from .data import CoreData
|
|
||||||
|
|
||||||
|
|
||||||
class GuildSettings(ModelSettings):
|
|
||||||
_settings = SettingDotDict()
|
|
||||||
model = CoreData.Guild
|
|
||||||
209
src/core/lion.py
209
src/core/lion.py
@@ -1,153 +1,96 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
from cachetools import LRUCache
|
from cachetools import LRUCache
|
||||||
|
import datetime
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
from meta import LionCog, LionBot, LionContext
|
from meta import LionCog, LionBot, LionContext
|
||||||
from settings import InteractiveSetting
|
|
||||||
from utils.lib import utc_now
|
|
||||||
from data import WeakCache
|
from data import WeakCache
|
||||||
|
|
||||||
from .data import CoreData
|
from .data import CoreData
|
||||||
|
|
||||||
from .user_settings import UserSettings
|
from .lion_guild import LionGuild
|
||||||
from .guild_settings import GuildSettings
|
from .lion_user import LionUser
|
||||||
|
from .lion_member import LionMember
|
||||||
|
|
||||||
class Lion:
|
|
||||||
"""
|
|
||||||
A Lion is a high level representation of a Member in the LionBot paradigm.
|
|
||||||
|
|
||||||
All members interacted with by the application should be available as Lions.
|
|
||||||
It primarily provides an interface to the User and Member data.
|
|
||||||
Lion also provides centralised access to various Member properties and methods,
|
|
||||||
that would normally be served by other cogs.
|
|
||||||
|
|
||||||
Many Lion methods may only be used when the required cogs and extensions are loaded.
|
|
||||||
A Lion may exist without a Bot instance or a Member in cache,
|
|
||||||
although the functionality available will be more limited.
|
|
||||||
|
|
||||||
There is no guarantee that a corresponding discord Member actually exists.
|
|
||||||
"""
|
|
||||||
__slots__ = ('bot', 'data', 'user_data', 'guild_data', '_member', '__weakref__')
|
|
||||||
|
|
||||||
def __init__(self, bot: LionBot, data: CoreData.Member, user_data: CoreData.User, guild_data: CoreData.Guild):
|
|
||||||
self.bot = bot
|
|
||||||
self.data = data
|
|
||||||
self.user_data = user_data
|
|
||||||
self.guild_data = guild_data
|
|
||||||
|
|
||||||
self._member: Optional[discord.Member] = None
|
|
||||||
|
|
||||||
# Data properties
|
|
||||||
|
|
||||||
@property
|
|
||||||
def key(self):
|
|
||||||
return (self.data.guildid, self.data.userid)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def guildid(self):
|
|
||||||
return self.data.guildid
|
|
||||||
|
|
||||||
@property
|
|
||||||
def userid(self):
|
|
||||||
return self.data.userid
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get(cls, guildid, userid):
|
|
||||||
return cls._cache_.get((guildid, userid), None)
|
|
||||||
|
|
||||||
# ModelSettings interfaces
|
|
||||||
@property
|
|
||||||
def guild_settings(self):
|
|
||||||
return GuildSettings(self.guildid, self.guild_data, bot=self.bot)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def user_settings(self):
|
|
||||||
return UserSettings(self.userid, self.user_data, bot=self.bot)
|
|
||||||
|
|
||||||
# Setting interfaces
|
|
||||||
# Each of these return an initialised member setting
|
|
||||||
|
|
||||||
@property
|
|
||||||
def timezone(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
def locale(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Time utilities
|
|
||||||
@property
|
|
||||||
def now(self):
|
|
||||||
"""
|
|
||||||
Returns current time-zone aware time for the member.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Discord data cache
|
|
||||||
async def touch_discord_models(self, member: discord.Member):
|
|
||||||
"""
|
|
||||||
Update the stored discord data from the given user or member object.
|
|
||||||
Intended to be used when we get member data from events that may not be available in cache.
|
|
||||||
"""
|
|
||||||
# Can we do these in one query?
|
|
||||||
if member.guild and (self.guild_data.name != member.guild.name):
|
|
||||||
await self.guild_data.update(name=member.guild.name)
|
|
||||||
|
|
||||||
avatar_key = member.avatar.key if member.avatar else None
|
|
||||||
await self.user_data.update(avatar_hash=avatar_key, name=member.name, last_seen=utc_now())
|
|
||||||
|
|
||||||
if member.display_name != self.data.display_name:
|
|
||||||
await self.data.update(display_name=member.display_name)
|
|
||||||
|
|
||||||
async def get_member(self) -> Optional[discord.Member]:
|
|
||||||
"""
|
|
||||||
Retrieve the member object for this Lion, if possible.
|
|
||||||
|
|
||||||
If the guild or member cannot be retrieved, returns None.
|
|
||||||
"""
|
|
||||||
guild = self.bot.get_guild(self.guildid)
|
|
||||||
if guild is not None:
|
|
||||||
member = guild.get_member(self.userid)
|
|
||||||
if member is None:
|
|
||||||
try:
|
|
||||||
member = await guild.fetch_member(self.userid)
|
|
||||||
except discord.HTTPException:
|
|
||||||
pass
|
|
||||||
return member
|
|
||||||
|
|
||||||
|
|
||||||
class Lions(LionCog):
|
class Lions(LionCog):
|
||||||
def __init__(self, bot: LionBot):
|
def __init__(self, bot: LionBot, data: CoreData):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
self.data = data
|
||||||
|
|
||||||
# Full Lions cache
|
# Caches
|
||||||
# Don't expire Lions with strong references
|
# Using WeakCache so strong references stay consistent
|
||||||
self._cache_: WeakCache[tuple[int, int], 'Lion'] = WeakCache(LRUCache(5000))
|
self.lion_guilds = WeakCache(LRUCache(2500))
|
||||||
|
self.lion_users = WeakCache(LRUCache(2000))
|
||||||
|
self.lion_members = WeakCache(LRUCache(5000))
|
||||||
|
|
||||||
self._settings_: dict[str, InteractiveSetting] = {}
|
async def bot_check_once(self, ctx: LionContext):
|
||||||
|
|
||||||
async def fetch(self, guildid, userid) -> Lion:
|
|
||||||
"""
|
"""
|
||||||
Fetch or create the given Member.
|
Insert the high-level Lion objects into context before command execution.
|
||||||
If the guild or user row doesn't exist, also creates it.
|
|
||||||
Relies on the core cog existing, to retrieve the core data.
|
Creates the objects if they do not already exist.
|
||||||
|
Updates relevant saved data from the Discord models,
|
||||||
|
and updates last seen for the LionUser (for data lifetime).
|
||||||
"""
|
"""
|
||||||
# TODO: Find a way to reduce this to one query, while preserving cache
|
if ctx.guild:
|
||||||
lion = self._cache_.get((guildid, userid))
|
# TODO: Consider doing all updates in one query, maybe with a View trigger on Member
|
||||||
if lion is None:
|
lmember = ctx.lmember = await self.fetch_member(ctx.guild.id, ctx.author.id, ctx.author)
|
||||||
if self.bot.core:
|
await lmember.touch_discord_model(ctx.author)
|
||||||
data = self.bot.core.data
|
|
||||||
|
ctx.luser = lmember.luser
|
||||||
|
await ctx.luser.touch_discord_model(ctx.author, seen=True)
|
||||||
|
|
||||||
|
ctx.lguild = lmember.lguild
|
||||||
|
await ctx.lguild.touch_discord_model(ctx.guild)
|
||||||
|
|
||||||
|
ctx.alion = lmember
|
||||||
else:
|
else:
|
||||||
raise ValueError("Cannot fetch Lion before core module is attached.")
|
ctx.lmember = ctx.alion = None
|
||||||
|
ctx.lguild = None
|
||||||
|
luser = ctx.luser = await self.fetch_user(ctx.author.id, ctx.author)
|
||||||
|
|
||||||
guild = await data.Guild.fetch_or_create(guildid)
|
await luser.touch_discord_model(ctx.author)
|
||||||
user = await data.User.fetch_or_create(userid)
|
|
||||||
member = await data.Member.fetch_or_create(guildid, userid)
|
|
||||||
lion = Lion(self.bot, member, user, guild)
|
|
||||||
self._cache_[(guildid, userid)] = lion
|
|
||||||
return lion
|
|
||||||
|
|
||||||
def add_model_setting(self, setting: InteractiveSetting):
|
ctx.alion = luser
|
||||||
self._settings_[setting.__class__.__name__] = setting
|
return True
|
||||||
return setting
|
|
||||||
|
async def fetch_user(self, userid, user: Optional[discord.User] = None) -> LionUser:
|
||||||
|
"""
|
||||||
|
Fetch the given LionUser, hitting cache if possible.
|
||||||
|
|
||||||
|
Creates the LionUser if it does not exist.
|
||||||
|
"""
|
||||||
|
if (luser := self.lion_users.get(userid, None)) is None:
|
||||||
|
data = await self.data.User.fetch_or_create(userid)
|
||||||
|
luser = LionUser(self.bot, data, user=user)
|
||||||
|
self.lion_users[userid] = luser
|
||||||
|
return luser
|
||||||
|
|
||||||
|
async def fetch_guild(self, guildid, guild: Optional[discord.Guild] = None) -> LionGuild:
|
||||||
|
"""
|
||||||
|
Fetch the given LionGuild, hitting cache if possible.
|
||||||
|
|
||||||
|
Creates the LionGuild if it does not exist.
|
||||||
|
"""
|
||||||
|
if (lguild := self.lion_guilds.get(guildid, None)) is None:
|
||||||
|
data = await self.data.Guild.fetch_or_create(guildid)
|
||||||
|
lguild = LionGuild(self.bot, data, guild=guild)
|
||||||
|
self.lion_guilds[guildid] = lguild
|
||||||
|
return lguild
|
||||||
|
|
||||||
|
async def fetch_member(self, guildid, userid, member: Optional[discord.Member] = None) -> LionMember:
|
||||||
|
"""
|
||||||
|
Fetch the given LionMember, using cache for data if possible.
|
||||||
|
|
||||||
|
|
||||||
|
Creates the LionGuild, LionUser, and LionMember if they do not already exist.
|
||||||
|
"""
|
||||||
|
# TODO: Can we do this more efficiently with one query, while keeping cache? Multiple joins?
|
||||||
|
key = (guildid, userid)
|
||||||
|
if (lmember := self.lion_members.get(key, None)) is None:
|
||||||
|
lguild = await self.fetch_guild(guildid, member.guild if member is not None else None)
|
||||||
|
luser = await self.fetch_user(userid, member)
|
||||||
|
data = await self.data.Member.fetch_or_create(guildid, userid)
|
||||||
|
lmember = LionMember(self.bot, data, lguild, luser, member)
|
||||||
|
self.lion_members[key] = lmember
|
||||||
|
return lmember
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
from settings.groups import ModelSettings, SettingDotDict
|
|
||||||
from .data import CoreData
|
|
||||||
|
|
||||||
|
|
||||||
class UserSettings(ModelSettings):
|
|
||||||
_settings = SettingDotDict()
|
|
||||||
model = CoreData.User
|
|
||||||
2
src/gui
2
src/gui
Submodule src/gui updated: 4f3d5740a3...2beff63ddc
@@ -8,7 +8,9 @@ from discord.ext.commands import Context
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .LionBot import LionBot
|
from .LionBot import LionBot
|
||||||
from core.lion import Lion
|
from core.lion_member import LionMember
|
||||||
|
from core.lion_user import LionUser
|
||||||
|
from core.lion_guild import LionGuild
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -45,7 +47,10 @@ class LionContext(Context['LionBot']):
|
|||||||
Extends Context to add Lion-specific methods and attributes.
|
Extends Context to add Lion-specific methods and attributes.
|
||||||
Also adds several contextual wrapped utilities for simpler user during command invocation.
|
Also adds several contextual wrapped utilities for simpler user during command invocation.
|
||||||
"""
|
"""
|
||||||
alion: 'Lion'
|
luser: 'LionUser'
|
||||||
|
lguild: 'LionGuild'
|
||||||
|
lmember: 'LionMember'
|
||||||
|
alion: 'LionUser | LionMember'
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
parts = {}
|
parts = {}
|
||||||
|
|||||||
@@ -415,7 +415,7 @@ class Economy(LionCog):
|
|||||||
).on_conflict(ignore=True)
|
).on_conflict(ignore=True)
|
||||||
else:
|
else:
|
||||||
# With only one target, we can take a simpler path, and make better use of local caches.
|
# With only one target, we can take a simpler path, and make better use of local caches.
|
||||||
await self.bot.core.lions.fetch(ctx.guild.id, target.id)
|
await self.bot.core.lions.fetch_member(ctx.guild.id, target.id)
|
||||||
# Now we are certain these members have a database row
|
# Now we are certain these members have a database row
|
||||||
|
|
||||||
# Perform the appropriate action
|
# Perform the appropriate action
|
||||||
@@ -889,7 +889,7 @@ class Economy(LionCog):
|
|||||||
|
|
||||||
t = self.bot.translator.t
|
t = self.bot.translator.t
|
||||||
Member = self.bot.core.data.Member
|
Member = self.bot.core.data.Member
|
||||||
target_lion = await self.bot.core.lions.fetch(ctx.guild.id, target.id)
|
target_lion = await self.bot.core.lions.fetch_member(ctx.guild.id, target.id)
|
||||||
|
|
||||||
# TODO: Add a "Send thanks" button to the DM?
|
# TODO: Add a "Send thanks" button to the DM?
|
||||||
# Alternative flow could be waiting until the target user presses accept
|
# Alternative flow could be waiting until the target user presses accept
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from babel.translator import LazyStr
|
|||||||
from ..data import ShopData
|
from ..data import ShopData
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.lion import Lion
|
from core.lion_member import LionMember
|
||||||
|
|
||||||
|
|
||||||
class ShopCog(LionCog):
|
class ShopCog(LionCog):
|
||||||
@@ -65,7 +65,7 @@ class Customer:
|
|||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.data = shop_data
|
self.data = shop_data
|
||||||
|
|
||||||
self.lion: 'Lion' = lion
|
self.lion: 'LionMember' = lion
|
||||||
|
|
||||||
# A list of InventoryItems held by this customer
|
# A list of InventoryItems held by this customer
|
||||||
self.inventory = inventory
|
self.inventory = inventory
|
||||||
@@ -84,7 +84,7 @@ class Customer:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def fetch(cls, bot: LionBot, shop_data: ShopData, guildid: int, userid: int):
|
async def fetch(cls, bot: LionBot, shop_data: ShopData, guildid: int, userid: int):
|
||||||
lion = await bot.core.lions.fetch(guildid, userid)
|
lion = await bot.core.lions.fetch_member(guildid, userid)
|
||||||
inventory = await shop_data.MemberInventoryInfo.fetch_inventory_info(guildid, userid)
|
inventory = await shop_data.MemberInventoryInfo.fetch_inventory_info(guildid, userid)
|
||||||
return cls(bot, shop_data, lion, inventory)
|
return cls(bot, shop_data, lion, inventory)
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ class Customer:
|
|||||||
"""
|
"""
|
||||||
Refresh the data for this member.
|
Refresh the data for this member.
|
||||||
"""
|
"""
|
||||||
self.lion = await self.bot.core.lions.fetch(self.guildid, self.userid)
|
self.lion = await self.bot.core.lions.fetch_member(self.guildid, self.userid)
|
||||||
await self.lion.data.refresh()
|
await self.lion.data.refresh()
|
||||||
self.inventory = await self.data.MemberInventoryInfo.fetch_inventory_info(self.guildid, self.userid)
|
self.inventory = await self.data.MemberInventoryInfo.fetch_inventory_info(self.guildid, self.userid)
|
||||||
return self
|
return self
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ class ColourShop(Shop):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Ensure the customer member actually exists
|
# Ensure the customer member actually exists
|
||||||
member = await self.customer.lion.get_member()
|
member = await self.customer.lion.fetch_member()
|
||||||
if member is None:
|
if member is None:
|
||||||
raise SafeCancellation(
|
raise SafeCancellation(
|
||||||
t(_p(
|
t(_p(
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ async def get_goals_card(
|
|||||||
):
|
):
|
||||||
data: StatsData = bot.get_cog('StatsCog').data
|
data: StatsData = bot.get_cog('StatsCog').data
|
||||||
|
|
||||||
lion = await bot.core.lions.fetch(guildid, userid)
|
lion = await bot.core.lions.fetch_member(guildid, userid)
|
||||||
today = lion.today
|
today = lion.today
|
||||||
|
|
||||||
# Calculate periodid and select the correct model
|
# Calculate periodid and select the correct model
|
||||||
@@ -63,7 +63,7 @@ async def get_goals_card(
|
|||||||
sessions_complete = 0.5
|
sessions_complete = 0.5
|
||||||
|
|
||||||
# Get member profile
|
# Get member profile
|
||||||
if member := await lion.get_member():
|
if member := await lion.fetch_member():
|
||||||
username = (member.display_name, member.discriminator)
|
username = (member.display_name, member.discriminator)
|
||||||
avatar = member.avatar.key
|
avatar = member.avatar.key
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from ..lib import apply_month_offset
|
|||||||
async def get_monthly_card(bot: LionBot, userid: int, guildid: int, offset: int, mode: CardMode) -> MonthlyStatsCard:
|
async def get_monthly_card(bot: LionBot, userid: int, guildid: int, offset: int, mode: CardMode) -> MonthlyStatsCard:
|
||||||
data: StatsData = bot.get_cog('StatsCog').data
|
data: StatsData = bot.get_cog('StatsCog').data
|
||||||
|
|
||||||
lion = await bot.core.lions.fetch(guildid, userid)
|
lion = await bot.core.lions.fetch_member(guildid, userid)
|
||||||
today = lion.today
|
today = lion.today
|
||||||
month_start = today.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
month_start = today.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||||
target = apply_month_offset(month_start, offset)
|
target = apply_month_offset(month_start, offset)
|
||||||
@@ -77,7 +77,7 @@ async def get_monthly_card(bot: LionBot, userid: int, guildid: int, offset: int,
|
|||||||
monthly[i][day.day - 1] = stat / 3600
|
monthly[i][day.day - 1] = stat / 3600
|
||||||
|
|
||||||
# Get member profile
|
# Get member profile
|
||||||
if member := await lion.get_member():
|
if member := await lion.fetch_member():
|
||||||
username = (member.display_name, member.discriminator)
|
username = (member.display_name, member.discriminator)
|
||||||
else:
|
else:
|
||||||
username = (lion.data.display_name, '#????')
|
username = (lion.data.display_name, '#????')
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ async def get_stats_card(bot: LionBot, userid: int, guildid: int):
|
|||||||
# TODO: Leaderboard rankings
|
# TODO: Leaderboard rankings
|
||||||
guildid = guildid or 0
|
guildid = guildid or 0
|
||||||
|
|
||||||
lion = await bot.core.lions.fetch(guildid, userid)
|
lion = await bot.core.lions.fetch_member(guildid, userid)
|
||||||
|
|
||||||
# Calculate the period timestamps, i.e. start time for each summary period
|
# Calculate the period timestamps, i.e. start time for each summary period
|
||||||
# TODO: Don't do the alltime one like this, not efficient anymore
|
# TODO: Don't do the alltime one like this, not efficient anymore
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from ..data import StatsData
|
|||||||
async def get_weekly_card(bot: LionBot, userid: int, guildid: int, offset: int, mode: CardMode) -> WeeklyStatsCard:
|
async def get_weekly_card(bot: LionBot, userid: int, guildid: int, offset: int, mode: CardMode) -> WeeklyStatsCard:
|
||||||
data: StatsData = bot.get_cog('StatsCog').data
|
data: StatsData = bot.get_cog('StatsCog').data
|
||||||
|
|
||||||
lion = await bot.core.lions.fetch(guildid, userid)
|
lion = await bot.core.lions.fetch_member(guildid, userid)
|
||||||
today = lion.today
|
today = lion.today
|
||||||
week_start = today - timedelta(days=today.weekday()) - timedelta(weeks=offset)
|
week_start = today - timedelta(days=today.weekday()) - timedelta(weeks=offset)
|
||||||
days = [week_start + timedelta(i) for i in range(-7, 7 if offset else (today.weekday() + 1))]
|
days = [week_start + timedelta(i) for i in range(-7, 7 if offset else (today.weekday() + 1))]
|
||||||
@@ -34,7 +34,7 @@ async def get_weekly_card(bot: LionBot, userid: int, guildid: int, offset: int,
|
|||||||
day_stats.append(0)
|
day_stats.append(0)
|
||||||
|
|
||||||
# Get member profile
|
# Get member profile
|
||||||
if member := await lion.get_member():
|
if member := await lion.fetch_member():
|
||||||
username = (member.display_name, member.discriminator)
|
username = (member.display_name, member.discriminator)
|
||||||
else:
|
else:
|
||||||
username = (lion.data.display_name, '#????')
|
username = (lion.data.display_name, '#????')
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from babel.translator import ctx_translator, LazyStr
|
|||||||
from babel.utils import local_month
|
from babel.utils import local_month
|
||||||
from gui.cards import WeeklyGoalCard, WeeklyStatsCard, MonthlyGoalCard, MonthlyStatsCard
|
from gui.cards import WeeklyGoalCard, WeeklyStatsCard, MonthlyGoalCard, MonthlyStatsCard
|
||||||
from gui.base import CardMode
|
from gui.base import CardMode
|
||||||
from core.lion import Lion
|
from core.lion_member import LionMember
|
||||||
|
|
||||||
from ..graphics.weekly import get_weekly_card
|
from ..graphics.weekly import get_weekly_card
|
||||||
from ..graphics.monthly import get_monthly_card
|
from ..graphics.monthly import get_monthly_card
|
||||||
@@ -338,7 +338,7 @@ class WeeklyMonthlyUI(StatsUI):
|
|||||||
self.data: StatsData = bot.get_cog('StatsCog').data
|
self.data: StatsData = bot.get_cog('StatsCog').data
|
||||||
|
|
||||||
# State
|
# State
|
||||||
self.lion: Optional[Lion] = None
|
self.lion: Optional[LionMember] = None
|
||||||
|
|
||||||
self._stat_page: StatPage = StatPage.WEEKLY_VOICE
|
self._stat_page: StatPage = StatPage.WEEKLY_VOICE
|
||||||
self._week_offset = 0
|
self._week_offset = 0
|
||||||
@@ -859,7 +859,7 @@ class WeeklyMonthlyUI(StatsUI):
|
|||||||
"""
|
"""
|
||||||
self._original = interaction
|
self._original = interaction
|
||||||
self._showing_global = False
|
self._showing_global = False
|
||||||
self.lion = await self.bot.core.lions.fetch(self.guildid, self.userid)
|
self.lion = await self.bot.core.lions.fetch_member(self.guildid, self.userid)
|
||||||
|
|
||||||
# TODO: Switch to using data cache in reload to calculate global/local
|
# TODO: Switch to using data cache in reload to calculate global/local
|
||||||
|
|
||||||
|
|||||||
@@ -130,8 +130,8 @@ class TasklistCog(LionCog):
|
|||||||
|
|
||||||
async def cog_load(self):
|
async def cog_load(self):
|
||||||
await self.data.init()
|
await self.data.init()
|
||||||
self.bot.core.guild_settings.attach(self.settings.task_reward)
|
self.bot.core.guild_config.register_model_setting(self.settings.task_reward)
|
||||||
self.bot.core.guild_settings.attach(self.settings.task_reward_limit)
|
self.bot.core.guild_config.register_model_setting(self.settings.task_reward_limit)
|
||||||
|
|
||||||
# TODO: Better method for getting single load
|
# TODO: Better method for getting single load
|
||||||
# Or better, unloading crossloaded group
|
# Or better, unloading crossloaded group
|
||||||
|
|||||||
@@ -88,6 +88,70 @@ class ModelSetting(ModelData, BaseSetting):
|
|||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class ModelConfig:
|
||||||
|
"""
|
||||||
|
A ModelConfig provides a central point of configuration for any object described by a single Model.
|
||||||
|
|
||||||
|
An instance of a ModelConfig represents configuration for a single object
|
||||||
|
(given by a single row of the corresponding Model).
|
||||||
|
|
||||||
|
The ModelConfig also supports registration of non-model configuration,
|
||||||
|
to support associated settings (e.g. list-settings) for the object.
|
||||||
|
|
||||||
|
This is an ABC, and must be subclassed for each object-type.
|
||||||
|
"""
|
||||||
|
settings: SettingDotDict
|
||||||
|
_model_settings: set
|
||||||
|
model: Type[RowModel]
|
||||||
|
|
||||||
|
def __init__(self, parent_id, row, **kwargs):
|
||||||
|
self.parent_id = parent_id
|
||||||
|
self.row = row
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register_setting(cls, setting_cls):
|
||||||
|
"""
|
||||||
|
Decorator to register a non-model setting as part of the object configuration.
|
||||||
|
|
||||||
|
The setting class may be re-accessed through the `settings` class attr.
|
||||||
|
|
||||||
|
Subclasses may provide alternative access pathways to key non-model settings.
|
||||||
|
"""
|
||||||
|
cls.settings[setting_cls.setting_id] = setting_cls
|
||||||
|
return setting_cls
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register_model_setting(cls, model_setting_cls):
|
||||||
|
"""
|
||||||
|
Decorator to register a model setting as part of the object configuration.
|
||||||
|
|
||||||
|
The setting class may be accessed through the `settings` class attr.
|
||||||
|
|
||||||
|
A fresh setting instance may also be retrieved (using cached data)
|
||||||
|
through the `get` instance method.
|
||||||
|
|
||||||
|
Subclasses are recommended to provide model settings as properties
|
||||||
|
for simplified access and type checking.
|
||||||
|
"""
|
||||||
|
cls._model_settings.add(model_setting_cls.setting_id)
|
||||||
|
return cls.register_setting(model_setting_cls)
|
||||||
|
|
||||||
|
def get(self, setting_id):
|
||||||
|
"""
|
||||||
|
Retrieve a freshly initialised copy of the given model-setting.
|
||||||
|
|
||||||
|
The given `setting_id` must have been previously registered through `register_model_setting`.
|
||||||
|
This uses cached data, and so is not guaranteed to be up-to-date.
|
||||||
|
"""
|
||||||
|
if setting_id not in self._model_settings:
|
||||||
|
# TODO: Log
|
||||||
|
raise ValueError
|
||||||
|
setting_cls = self.settings[setting_id]
|
||||||
|
data = setting_cls._read_from_row(self.parent_id, self.row, **self.kwargs)
|
||||||
|
return setting_cls(self.parent_id, data, **self.kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ModelSettings:
|
class ModelSettings:
|
||||||
"""
|
"""
|
||||||
A ModelSettings instance aggregates multiple `ModelSetting` instances
|
A ModelSettings instance aggregates multiple `ModelSetting` instances
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from typing import NamedTuple, Optional, Sequence, Union, overload, List
|
from typing import NamedTuple, Optional, Sequence, Union, overload, List
|
||||||
import datetime
|
import datetime
|
||||||
import iso8601 # type: ignore
|
import iso8601 # type: ignore
|
||||||
|
import pytz
|
||||||
import re
|
import re
|
||||||
from contextvars import Context
|
from contextvars import Context
|
||||||
|
|
||||||
@@ -706,3 +707,34 @@ def parse_duration(string: str) -> Optional[int]:
|
|||||||
seconds += int(match.group('value')) * multiplier
|
seconds += int(match.group('value')) * multiplier
|
||||||
|
|
||||||
return seconds if found else None
|
return seconds if found else None
|
||||||
|
|
||||||
|
|
||||||
|
class Timezoned:
|
||||||
|
"""
|
||||||
|
ABC mixin for objects with a set timezone.
|
||||||
|
|
||||||
|
Provides several useful localised properties.
|
||||||
|
"""
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def timezone(self) -> pytz.timezone:
|
||||||
|
"""
|
||||||
|
Must be implemented by the deriving class!
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def now(self):
|
||||||
|
"""
|
||||||
|
Return the current time localised to the object's timezone.
|
||||||
|
"""
|
||||||
|
return datetime.datetime.now(tz=self.timezone)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def today(self):
|
||||||
|
"""
|
||||||
|
Return the start of the day localised to the object's timezone.
|
||||||
|
"""
|
||||||
|
now = self.now
|
||||||
|
return now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
|||||||
Reference in New Issue
Block a user