rewrite: Initial rewrite skeleton.

Remove modules that will no longer be required.
Move pending modules to pending-rewrite folders.
This commit is contained in:
2022-09-17 17:06:13 +10:00
parent a7f7dd6e7b
commit a5147323b5
162 changed files with 1 additions and 866 deletions

View File

@@ -1,347 +0,0 @@
import pytz
import discord
from functools import reduce
from datetime import datetime, timedelta
from meta import client
from data import tables as tb
from settings import UserSettings, GuildSettings
from LionContext import LionContext
class Lion:
"""
Class representing a guild Member.
Mostly acts as a transparent interface to the corresponding Row,
but also adds some transaction caching logic to `coins` and `tracked_time`.
"""
__slots__ = ('guildid', 'userid', '_pending_coins', '_member')
# Members with pending transactions
_pending = {} # userid -> User
# Lion cache. Currently lions don't expire
_lions = {} # (guildid, userid) -> Lion
# Extra methods supplying an economy bonus
_economy_bonuses = []
def __init__(self, guildid, userid):
self.guildid = guildid
self.userid = userid
self._pending_coins = 0
self._member = None
self._lions[self.key] = self
@classmethod
def fetch(cls, guildid, userid):
"""
Fetch a Lion with the given member.
If they don't exist, creates them.
If possible, retrieves the user from the user cache.
"""
key = (guildid, userid)
if key in cls._lions:
return cls._lions[key]
else:
# TODO: Debug log
lion = tb.lions.fetch(key)
if not lion:
tb.user_config.fetch_or_create(userid)
tb.guild_config.fetch_or_create(guildid)
tb.lions.create_row(
guildid=guildid,
userid=userid,
coins=GuildSettings(guildid).starting_funds.value
)
return cls(guildid, userid)
@property
def key(self):
return (self.guildid, self.userid)
@property
def guild(self) -> discord.Guild:
return client.get_guild(self.guildid)
@property
def member(self) -> discord.Member:
"""
The discord `Member` corresponding to this user.
May be `None` if the member is no longer in the guild or the caches aren't populated.
Not guaranteed to be `None` if the member is not in the guild.
"""
if self._member is None:
guild = client.get_guild(self.guildid)
if guild:
self._member = guild.get_member(self.userid)
return self._member
@property
def data(self):
"""
The Row corresponding to this member.
"""
return tb.lions.fetch(self.key)
@property
def user_data(self):
"""
The Row corresponding to this user.
"""
return tb.user_config.fetch_or_create(self.userid)
@property
def guild_data(self):
"""
The Row corresponding to this guild.
"""
return tb.guild_config.fetch_or_create(self.guildid)
@property
def settings(self):
"""
The UserSettings interface for this member.
"""
return UserSettings(self.userid)
@property
def guild_settings(self):
"""
The GuildSettings interface for this member.
"""
return GuildSettings(self.guildid)
@property
def ctx(self) -> LionContext:
"""
Manufacture a `LionContext` with the lion member as an author.
Useful for accessing member context utilities.
Be aware that `author` may be `None` if the member was not cached.
"""
return LionContext(client, guild=self.guild, author=self.member)
@property
def time(self):
"""
Amount of time the user has spent studying, accounting for a current session.
"""
# Base time from cached member data
time = self.data.tracked_time
# Add current session time if it exists
if session := self.session:
time += session.duration
return int(time)
@property
def coins(self):
"""
Number of coins the user has, accounting for the pending value and current session.
"""
# Base coin amount from cached member data
coins = self.data.coins
# Add pending coin amount
coins += self._pending_coins
# Add current session coins if applicable
if session := self.session:
coins += session.coins_earned
return int(coins)
@property
def economy_bonus(self):
"""
Economy multiplier
"""
return reduce(
lambda x, y: x * y,
[func(self) for func in self._economy_bonuses]
)
@classmethod
def register_economy_bonus(cls, func):
cls._economy_bonuses.append(func)
@classmethod
def unregister_economy_bonus(cls, func):
cls._economy_bonuses.remove(func)
@property
def session(self):
"""
The current study session the user is in, if any.
"""
if 'sessions' not in client.objects:
raise ValueError("Cannot retrieve session before Study module is initialised!")
return client.objects['sessions'][self.guildid].get(self.userid, None)
@property
def timezone(self):
"""
The user's configured timezone.
Shortcut to `Lion.settings.timezone.value`.
"""
return self.settings.timezone.value
@property
def day_start(self):
"""
A timezone aware datetime representing the start of the user's day (in their configured timezone).
NOTE: This might not be accurate over DST boundaries.
"""
now = datetime.now(tz=self.timezone)
return now.replace(hour=0, minute=0, second=0, microsecond=0)
@property
def day_timestamp(self):
"""
EPOCH timestamp representing the current day for the user.
NOTE: This is the timestamp of the start of the current UTC day with the same date as the user's day.
This is *not* the start of the current user's day, either in UTC or their own timezone.
This may also not be the start of the current day in UTC (consider 23:00 for a user in UTC-2).
"""
now = datetime.now(tz=self.timezone)
day_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
return int(day_start.replace(tzinfo=pytz.utc).timestamp())
@property
def week_timestamp(self):
"""
EPOCH timestamp representing the current week for the user.
"""
now = datetime.now(tz=self.timezone)
day_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
week_start = day_start - timedelta(days=day_start.weekday())
return int(week_start.replace(tzinfo=pytz.utc).timestamp())
@property
def month_timestamp(self):
"""
EPOCH timestamp representing the current month for the user.
"""
now = datetime.now(tz=self.timezone)
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
return int(month_start.replace(tzinfo=pytz.utc).timestamp())
@property
def remaining_in_day(self):
return ((self.day_start + timedelta(days=1)) - datetime.now(self.timezone)).total_seconds()
@property
def studied_today(self):
"""
The amount of time, in seconds, that the member has studied today.
Extracted from the session history.
"""
return tb.session_history.queries.study_time_since(self.guildid, self.userid, self.day_start)
@property
def remaining_study_today(self):
"""
Maximum remaining time (in seconds) this member can study today.
May not account for DST boundaries and leap seconds.
"""
studied_today = self.studied_today
study_cap = self.guild_settings.daily_study_cap.value
remaining_in_day = self.remaining_in_day
if remaining_in_day >= (study_cap - studied_today):
remaining = study_cap - studied_today
else:
remaining = remaining_in_day + study_cap
return remaining
@property
def profile_tags(self):
"""
Returns a list of profile tags, or the default tags.
"""
tags = tb.profile_tags.queries.get_tags_for(self.guildid, self.userid)
prefix = self.ctx.best_prefix
return tags or [
f"Use {prefix}setprofile",
"and add your tags",
"to this section",
f"See {prefix}help setprofile for more"
]
@property
def name(self):
"""
Returns the best local name possible.
"""
if self.member:
name = self.member.display_name
elif self.data.display_name:
name = self.data.display_name
else:
name = str(self.userid)
return name
def update_saved_data(self, member: discord.Member):
"""
Update the stored discord data from the givem member.
Intended to be used when we get member data from events that may not be available in cache.
"""
if self.guild_data.name != member.guild.name:
self.guild_data.name = member.guild.name
if self.user_data.avatar_hash != member.avatar:
self.user_data.avatar_hash = member.avatar
if self.data.display_name != member.display_name:
self.data.display_name = member.display_name
def localize(self, naive_utc_dt):
"""
Localise the provided naive UTC datetime into the user's timezone.
"""
timezone = self.settings.timezone.value
return naive_utc_dt.replace(tzinfo=pytz.UTC).astimezone(timezone)
def addCoins(self, amount, flush=True, bonus=False):
"""
Add coins to the user, optionally store the transaction in pending.
"""
self._pending_coins += amount * (self.economy_bonus if bonus else 1)
self._pending[self.key] = self
if flush:
self.flush()
def flush(self):
"""
Flush any pending transactions to the database.
"""
self.sync(self)
@classmethod
def sync(cls, *lions):
"""
Flush pending transactions to the database.
Also refreshes the Row cache for updated lions.
"""
lions = lions or list(cls._pending.values())
if lions:
# Build userid to pending coin map
pending = [
(lion.guildid, lion.userid, int(lion._pending_coins))
for lion in lions
]
# Write to database
tb.lions.queries.add_pending(pending)
# Cleanup pending users
for lion in lions:
lion._pending_coins -= int(lion._pending_coins)
cls._pending.pop(lion.key, None)