diff --git a/royalpack/__main__.py b/royalpack/__main__.py index d65c67db..7a7a334a 100644 --- a/royalpack/__main__.py +++ b/royalpack/__main__.py @@ -48,16 +48,21 @@ register_telegram(commands.ping, ["ping"]) register_telegram(commands.ship, ["ship"], r"(?P[A-Za-z]+)[\s+&]+(?P[A-Za-z]+)") register_telegram(commands.emojify, ["emojify"], r"(?P.+)") register_telegram(commands.dog_any, ["dog", "doggo", "cane", "woof", "bau"]) -register_telegram(commands.dog_breedlist, ["dog", "doggo", "cane", "woof", "bau"], "(?:list|help|aiuto)") -register_telegram(commands.dog_breed, ["dog", "doggo", "cane", "woof", "bau"], "(?P[A-Za-z/]+)") +register_telegram(commands.dog_breedlist, ["dog", "doggo", "cane", "woof", "bau"], r"(?:list|help|aiuto)") +register_telegram(commands.dog_breed, ["dog", "doggo", "cane", "woof", "bau"], r"(?P[A-Za-z/]+)") register_telegram(commands.fortune, ["fortune"]) register_telegram(commands.pmots, ["pmots"]) -register_telegram(commands.spell, ["spell", "cast"], "(?P.+)") +register_telegram(commands.spell, ["spell", "cast"], r"(?P.+)") register_telegram(commands.smecds, ["smecds"]) -register_telegram(commands.man, ["man", "help"], "(?P[A-Za-z]+)") +register_telegram(commands.man, ["man", "help"], r"(?P[A-Za-z]+)") register_telegram(commands.login, ["login"]) register_telegram(commands.whoami, ["whoami"]) -register_telegram(commands.balance, ["balance", "fiorygi"]) +register_telegram(commands.fiorygi_balance_self, ["balance"]) +register_telegram(commands.fiorygi_balance_other, ["balance"], r"(?P\S+)") +register_telegram(commands.fiorygi_give, ["give"], r"(?P\S+)\s+(?P[0-9]+)\s+(?P.+)") +register_telegram(commands.fiorygi_magick, ["magick"], r"(?P\S+)\s+(?P[0-9]+)\s+(?P.+)") +register_telegram(commands.fiorygi_transactions_self, ["transactions"]) +register_telegram(commands.fiorygi_transactions_other, ["transactions"], r"(?P\S+)") pda.implementations["telethon.1"].register_conversation(r) diff --git a/royalpack/bolts/__init__.py b/royalpack/bolts/__init__.py index b7ed19b9..59e69117 100644 --- a/royalpack/bolts/__init__.py +++ b/royalpack/bolts/__init__.py @@ -1 +1,2 @@ from .login import * +from .target import * diff --git a/royalpack/bolts/target.py b/royalpack/bolts/target.py new file mode 100644 index 00000000..9b53c6a6 --- /dev/null +++ b/royalpack/bolts/target.py @@ -0,0 +1,40 @@ +""" + +""" + +from __future__ import annotations + +import functools +import logging + +import royalnet.engineer as engi +import sqlalchemy.orm as so + +import royalpack.database as db + +log = logging.getLogger(__name__) + + +def with_target(): + """ + .. todo:: Document this. + """ + + def decorator(f): + @functools.wraps(f) + async def decorated(_msg: engi.Message, _session: so.Session, target: str, **f_kwargs): + user = db.UserAlias.find(session=_session, string=target) + if user is None: + await _msg.reply(text=f"⚠️ L'utente specificato non esiste.") + return + + return await f(_msg=_msg, _session=_session, **f_kwargs, _target=user) + + return decorated + + return decorator + + +__all__ = ( + "with_target", +) diff --git a/royalpack/commands/__init__.py b/royalpack/commands/__init__.py index e8f9f07e..fa465e3b 100644 --- a/royalpack/commands/__init__.py +++ b/royalpack/commands/__init__.py @@ -14,4 +14,4 @@ from .smecds import * from .man import * from .login import * from .whoami import * -from .balance import * +from .fiorygi import * diff --git a/royalpack/commands/balance.py b/royalpack/commands/balance.py deleted file mode 100644 index 3e40bcde..00000000 --- a/royalpack/commands/balance.py +++ /dev/null @@ -1,16 +0,0 @@ -import royalnet.engineer as engi -import royalpack.database as rd -import royalpack.bolts as rb - - -@engi.use_database(rd.lazy_session_class) -@rb.use_ryglogin(allow_anonymous=False) -@engi.TeleportingConversation -async def balance(*, _sentry: engi.Sentry, _msg: engi.Message, _user: rd.User, **__): - """ - Visualizza il tuo portafoglio di fiorygi. - """ - await _msg.reply(text=f"💰 Al momento, possiedi \uE01Bƒ {_user.fiorygi}\uE00B.") - - -__all__ = ("balance",) diff --git a/royalpack/commands/fiorygi.py b/royalpack/commands/fiorygi.py new file mode 100644 index 00000000..82f51bc9 --- /dev/null +++ b/royalpack/commands/fiorygi.py @@ -0,0 +1,207 @@ +import royalnet.engineer as engi +import royalpack.database as db +import royalpack.bolts as rb +import sqlalchemy.sql as ss +import functools + + +@engi.use_database(db.lazy_session_class) +@rb.use_ryglogin(allow_anonymous=False) +@engi.TeleportingConversation +async def fiorygi_balance_self(*, _user: db.User, _msg: engi.Message, **__): + """ + Visualizza il tuo portafoglio attuale di fiorygi. + """ + + await _msg.reply(text=f"💰 Attualmente, possiedi \uE01Bƒ {_user.fiorygi}\uE00B.") + + +@engi.use_database(db.lazy_session_class) +@rb.with_target() +@engi.TeleportingConversation +async def fiorygi_balance_other(*, _target: db.User, _session: db.SessionType, _msg: engi.Message, **__): + """ + Visualizza il portafoglio di fiorygi di un altro membro. + """ + + await _msg.reply(text=f"💰 {_target} possiede \uE01Bƒ {_target.fiorygi}\uE00B.") + + +def render(transaction: db.Transaction, user: db.User): + row = [] + + if transaction.plus == user: + is_plus = True + other = transaction.minus + else: + is_plus = False + other = transaction.plus + + if transaction.amount != 0: + row.append(f"{'➕' if is_plus else '➖'} \uE01Bƒ {transaction.amount}\uE00B") + + if other is not None: + row.append(f"\uE011{'da' if is_plus else 'a'} {other}\uE001") + + if transaction.reason: + row.append(f"{transaction.reason}") + + return " - ".join(row) + + +@engi.use_database(db.lazy_session_class) +@rb.use_ryglogin(allow_anonymous=False) +@engi.TeleportingConversation +async def fiorygi_transactions_self(*, _session: db.SessionType, _user: db.User, _msg: engi.Message, **__): + """ + Visualizza le ultime 10 transazioni del tuo portafoglio. + """ + + transactions = _session.execute( + ss.select( + db.Transaction + ).where( + ss.or_( + db.Transaction.minus == _user, + db.Transaction.plus == _user, + ) + ).order_by( + db.Transaction.timestamp.desc() + ).limit( + 10 + ) + ).scalars() + + msg = map(functools.partial(render, user=_user), transactions) + + await _msg.reply(text="\n\n".join(msg)) + + +@engi.use_database(db.lazy_session_class) +@rb.with_target() +@engi.TeleportingConversation +async def fiorygi_transactions_other(*, _session: db.SessionType, _target: db.User, _msg: engi.Message, **__): + """ + Visualizza le ultime 10 transazioni del portafoglio di un membro. + """ + + transactions = _session.execute( + ss.select( + db.Transaction + ).where( + ss.or_( + db.Transaction.minus == _target, + db.Transaction.plus == _target, + ) + ).order_by( + db.Transaction.timestamp.desc() + ).limit( + 10 + ) + ).scalars() + + msg = map(functools.partial(render, user=_target), transactions) + + await _msg.reply(text="\n\n".join(msg)) + + +@engi.use_database(db.lazy_session_class) +@rb.use_ryglogin(allow_anonymous=False) +@rb.with_target() +@engi.TeleportingConversation +async def fiorygi_give( + *, + _user: db.User, + _target: db.User, + _msg: engi.Message, + _session: db.SessionType, + amount: int, + reason: str, + **__ +): + """ + Dai dei fiorygi a un altro membro. + """ + + if amount <= 0: + await _msg.reply(text=f"⚠️ Puoi trasferire solo numeri interi positivi di fiorygi.") + return + + if _user.fiorygi < amount: + await _msg.reply(text=f"⚠️ Non hai sufficienti fiorygi per effettuare il trasferimento.") + return + + if _user == _target: + await _msg.reply(text=f"⚠️ Non puoi dare fiorygi a te stesso!") + return + + trans = db.Transaction( + minus=_user, + plus=_target, + amount=amount, + reason=reason, + ) + _session.add(trans) + + _user.fiorygi -= amount + _target.fiorygi += amount + + _session.commit() + + await _msg.reply(text=f"💸 Hai trasferito \uE01Bƒ {amount}\uE00B a {_target}.") + + +@engi.use_database(db.lazy_session_class) +@rb.use_ryglogin(allow_anonymous=False) +@rb.with_target() +@engi.TeleportingConversation +async def fiorygi_magick( + *, + _user: db.User, + _target: db.User, + _msg: engi.Message, + _session: db.SessionType, + amount: int, + reason: str, + **__ +): + """ + Modifica il portafoglio di fiorygi di un membro. + """ + + if _user.sub != "auth0|5ed2debf7308300c1ea230c3": + await _msg.reply(text=f"⚠️ Non sei autorizzato ad eseguire questo comando.") + return + + if amount >= 0: + trans = db.Transaction( + minus=None, + plus=_target, + amount=amount, + reason=reason, + ) + else: + trans = db.Transaction( + minus=_target, + plus=None, + amount=amount, + reason=reason, + ) + + _session.add(trans) + + _target.fiorygi += amount + + _session.commit() + + await _msg.reply(text=f"🏦 Hai modificato il portafoglio di {_target} di \uE01Bƒ {amount}\uE00B.") + + +__all__ = ( + "fiorygi_balance_self", + "fiorygi_balance_other", + "fiorygi_give", + "fiorygi_magick", + "fiorygi_transactions_self", + "fiorygi_transactions_other", +) diff --git a/royalpack/commands/login.py b/royalpack/commands/login.py index c5fc1bd6..1711ff32 100644 --- a/royalpack/commands/login.py +++ b/royalpack/commands/login.py @@ -66,6 +66,7 @@ async def login(*, _msg: engi.Message, _session: so.Session, _imp, **__): ) user = await register_user_generic(session=_session, user_info=ui) + uas = await register_user_alias(session=_session, user_info=ui) log.debug(f"Committing session...") _session.commit() @@ -246,9 +247,45 @@ async def register_user_generic( email=user_info['email'], ) session.merge(user) + return user +async def register_user_alias( + session: so.Session, + user_info: dict[str, t.Any], +): + """ + .. todo:: Document this. + """ + + log.debug("Syncing user aliases...") + + uas = [ + db.UserAlias( + user_fk=user_info["sub"], + name=user_info["name"] + ), + db.UserAlias( + user_fk=user_info["sub"], + name=user_info["nickname"] + ), + db.UserAlias( + user_fk=user_info["sub"], + name=user_info["email"] + ), + db.UserAlias( + user_fk=user_info["sub"], + name=user_info["sub"] + ), + ] + + for ua in uas: + session.merge(ua) + + return uas + + async def register_user_telethon( session: so.Session, user_info: dict[str, t.Any], diff --git a/royalpack/database/alembic/versions/8b28a4a94552_second_revision.py b/royalpack/database/alembic/versions/c6aacbd5d796_first_revision.py similarity index 97% rename from royalpack/database/alembic/versions/8b28a4a94552_second_revision.py rename to royalpack/database/alembic/versions/c6aacbd5d796_first_revision.py index aa0fe5be..3ec6dfe2 100644 --- a/royalpack/database/alembic/versions/8b28a4a94552_second_revision.py +++ b/royalpack/database/alembic/versions/c6aacbd5d796_first_revision.py @@ -1,8 +1,8 @@ -"""Second revision +"""First revision -Revision ID: 8b28a4a94552 +Revision ID: c6aacbd5d796 Revises: -Create Date: 2021-04-18 00:38:17.976303 +Create Date: 2021-04-25 01:00:49.131019 """ from alembic import op @@ -11,7 +11,7 @@ import sqlalchemy_utils # revision identifiers, used by Alembic. -revision = '8b28a4a94552' +revision = 'c6aacbd5d796' down_revision = None branch_labels = None depends_on = None @@ -116,7 +116,7 @@ def upgrade(): sa.Column('user_fk', sa.String(), nullable=False), sa.Column('name', sa.String(), nullable=False), sa.ForeignKeyConstraint(['user_fk'], ['users.sub'], ), - sa.PrimaryKeyConstraint('user_fk') + sa.PrimaryKeyConstraint('user_fk', 'name') ) op.create_table('user_title_association', sa.Column('user_fk', sa.String(), nullable=True), diff --git a/royalpack/database/alembic/versions/e6b0d97063a1_add_timestamp_to_transactions.py b/royalpack/database/alembic/versions/e6b0d97063a1_add_timestamp_to_transactions.py new file mode 100644 index 00000000..437962fd --- /dev/null +++ b/royalpack/database/alembic/versions/e6b0d97063a1_add_timestamp_to_transactions.py @@ -0,0 +1,33 @@ +"""Add timestamp to transactions + +Revision ID: e6b0d97063a1 +Revises: c6aacbd5d796 +Create Date: 2021-04-25 02:18:44.248837 + +""" +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utils + + +# revision identifiers, used by Alembic. +revision = 'e6b0d97063a1' +down_revision = 'c6aacbd5d796' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('fiorygi_transactions', sa.Column('timestamp', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True)) + op.add_column('fiorygi_treasures', sa.Column('creation_time', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False)) + op.add_column('fiorygi_treasures', sa.Column('find_time', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('fiorygi_treasures', 'find_time') + op.drop_column('fiorygi_treasures', 'creation_time') + op.drop_column('fiorygi_transactions', 'timestamp') + # ### end Alembic commands ### diff --git a/royalpack/database/base.py b/royalpack/database/base.py index c5f420ff..b95b8947 100644 --- a/royalpack/database/base.py +++ b/royalpack/database/base.py @@ -1,8 +1,9 @@ from __future__ import annotations +import typing as t import sqlalchemy as s +import sqlalchemy.sql as ss import sqlalchemy.orm as so import sqlalchemy.ext.declarative as sed -import sqlalchemy.ext.associationproxy as seap import sqlalchemy_utils as su import royalnet.alchemist as ra import colour @@ -38,6 +39,9 @@ class User(Base, ra.ColRepr, ra.Updatable): title_fk = s.Column(su.UUIDType, s.ForeignKey("titles.uuid")) fiorygi = s.Column(s.Integer, nullable=False, default=0) + def __str__(self): + return self.name + class UserAlias(Base, ra.ColRepr, ra.Updatable): """ @@ -48,7 +52,21 @@ class UserAlias(Base, ra.ColRepr, ra.Updatable): user_fk = s.Column(s.String, s.ForeignKey("users.sub"), primary_key=True) user = so.relationship("User", backref="aliases") - name = s.Column(s.String, nullable=False) + name = s.Column(s.String, nullable=False, primary_key=True) + + @so.validates("name") + def convert_lower(self, key, value: str) -> str: + return value.lower() + + @classmethod + def find(cls, session: so.Session, string: str) -> t.Optional[User]: + ua = session.execute( + ss.select(cls).where(cls.name == string) + ).scalar() + return ua.user if ua else None + + def __str__(self): + return self.name class TelegramAccount(Base, ra.ColRepr, ra.Updatable): @@ -80,6 +98,9 @@ class TelegramAccount(Base, ra.ColRepr, ra.Updatable): else: return f"[{self.name}](tg://user?id={self.tg_id})" + def __str__(self): + return self.name() + class DiscordAccount(Base, ra.ColRepr, ra.Updatable): """ @@ -98,6 +119,9 @@ class DiscordAccount(Base, ra.ColRepr, ra.Updatable): def name(self) -> str: return f"{self.username}#{self.discriminator}" + def __str__(self): + return self.name() + class SteamAccount(Base, ra.ColRepr, ra.Updatable): """ @@ -114,6 +138,9 @@ class SteamAccount(Base, ra.ColRepr, ra.Updatable): # TODO: make steamid return steam.steamid.SteamID objects + def __str__(self): + return self.persona_name + class OsuAccount(Base, ra.ColRepr, ra.Updatable): """ @@ -128,6 +155,9 @@ class OsuAccount(Base, ra.ColRepr, ra.Updatable): username = s.Column(s.String) avatar_url = s.Column(su.URLType) + def __str__(self): + return self.username + class LeagueAccount(Base, ra.ColRepr, ra.Updatable): """ @@ -143,6 +173,9 @@ class LeagueAccount(Base, ra.ColRepr, ra.Updatable): summoner_name = s.Column(s.String, nullable=False) avatar_id = s.Column(s.Integer, nullable=False) + def __str__(self): + return self.summoner_name + class Title(Base, ra.ColRepr, ra.Updatable): """ @@ -159,6 +192,9 @@ class Title(Base, ra.ColRepr, ra.Updatable): unlocked_by = so.relationship("User", secondary=user_title_association, backref="unlocked_titles") + def __str__(self): + return self.name + class DiarioGroup(Base, ra.ColRepr, ra.Updatable): """ @@ -203,13 +239,14 @@ class Transaction(Base, ra.ColRepr, ra.Updatable): id = s.Column(s.Integer, primary_key=True) minus_fk = s.Column(s.String, s.ForeignKey("users.sub")) - minus = so.relationship("User", foreign_keys=(minus_fk,)) + minus = so.relationship("User", foreign_keys=(minus_fk,), backref="transactions_minus") plus_fk = s.Column(s.String, s.ForeignKey("users.sub")) - plus = so.relationship("User", foreign_keys=(plus_fk,)) + plus = so.relationship("User", foreign_keys=(plus_fk,), backref="transactions_plus") amount = s.Column(s.Integer, nullable=False) reason = s.Column(s.Text) + timestamp = s.Column(su.ArrowType) class Treasure(Base, ra.ColRepr, ra.Updatable): @@ -222,9 +259,11 @@ class Treasure(Base, ra.ColRepr, ra.Updatable): creator_fk = s.Column(s.String, s.ForeignKey("users.sub")) creator = so.relationship("User", foreign_keys=(creator_fk,)) + creation_time = s.Column(su.ArrowType, nullable=False) finder_fk = s.Column(s.String, s.ForeignKey("users.sub")) finder = so.relationship("User", foreign_keys=(finder_fk,)) + find_time = s.Column(su.ArrowType, nullable=False) value = s.Column(s.Integer, nullable=False) message = s.Column(s.Text) diff --git a/royalpack/database/engine.py b/royalpack/database/engine.py index b233aa2c..8b6d667b 100644 --- a/royalpack/database/engine.py +++ b/royalpack/database/engine.py @@ -14,8 +14,10 @@ lazy_session_class = royalnet.lazy.Lazy(lambda e: sqlalchemy.orm.sessionmaker(bi The uninitialized sqlalchemy session class. """ +SessionType = sqlalchemy.orm.Session __all__ = ( "lazy_engine", "lazy_session_class", + "SessionType", )