From 202e33bdf0527fb0cfc77ba507e55826e263d6ad Mon Sep 17 00:00:00 2001
From: Stefano Pigozzi <ste.pigozzi@gmail.com>
Date: Wed, 3 Apr 2019 19:31:20 +0200
Subject: [PATCH] SO STUFF     MUCH DOGE

---
 royalgames.py                              |  7 +++++--
 royalnet/bots/telegram.py                  | 21 ++++++++++++++++++--
 royalnet/commands/debug_author.py          | 16 +++++++++++++++
 royalnet/commands/diario.py                | 18 +++++++++++++++--
 royalnet/commands/sync.py                  |  7 ++++---
 royalnet/database/__init__.py              |  3 ++-
 royalnet/database/relationshiplinkchain.py | 22 +++++++++++++++++++++
 royalnet/database/tables/__init__.py       |  3 ++-
 royalnet/database/tables/aliases.py        | 23 ++++++++++++++++++++++
 royalnet/database/tables/diario.py         |  8 ++++----
 royalnet/database/tables/telegram.py       |  1 -
 royalnet/utils/call.py                     |  5 +++++
 12 files changed, 118 insertions(+), 16 deletions(-)
 create mode 100644 royalnet/commands/debug_author.py
 create mode 100644 royalnet/database/relationshiplinkchain.py
 create mode 100644 royalnet/database/tables/aliases.py

diff --git a/royalgames.py b/royalgames.py
index 3aa357ec..c198b58d 100644
--- a/royalgames.py
+++ b/royalgames.py
@@ -3,14 +3,17 @@ import asyncio
 from royalnet.bots import TelegramBot
 from royalnet.commands import PingCommand, ShipCommand, SmecdsCommand, ColorCommand, CiaoruoziCommand, SyncCommand
 from royalnet.commands.debug_create import DebugCreateCommand
+from royalnet.commands.debug_author import DebugAuthorCommand
 from royalnet.network import RoyalnetServer
+from royalnet.database.tables import Royal, Telegram
 
 loop = asyncio.get_event_loop()
 
-commands = [PingCommand, ShipCommand, SmecdsCommand, ColorCommand, CiaoruoziCommand, DebugCreateCommand, SyncCommand]
+commands = [PingCommand, ShipCommand, SmecdsCommand, ColorCommand, CiaoruoziCommand, DebugCreateCommand, SyncCommand,
+            DebugAuthorCommand]
 
 master = RoyalnetServer("localhost", 1234, "sas")
-tg_bot = TelegramBot(os.environ["TG_AK"], "localhost:1234", "sas", commands, os.environ["DB_PATH"])
+tg_bot = TelegramBot(os.environ["TG_AK"], "localhost:1234", "sas", commands, os.environ["DB_PATH"], Royal, Telegram, "tg_id")
 loop.create_task(master.run())
 loop.create_task(tg_bot.run())
 print("Starting loop...")
diff --git a/royalnet/bots/telegram.py b/royalnet/bots/telegram.py
index e7542094..dc69d438 100644
--- a/royalnet/bots/telegram.py
+++ b/royalnet/bots/telegram.py
@@ -5,8 +5,7 @@ import logging as _logging
 from ..commands import NullCommand
 from ..utils import asyncify, Call, Command
 from ..network import RoyalnetLink, Message
-from ..database import Alchemy
-
+from ..database import Alchemy, relationshiplinkchain
 
 loop = asyncio.get_event_loop()
 log = _logging.getLogger(__name__)
@@ -24,6 +23,9 @@ class TelegramBot:
                  master_server_secret: str,
                  commands: typing.List[typing.Type[Command]],
                  database_uri: str,
+                 master_table,
+                 identity_table,
+                 identity_column_name: str,
                  missing_command: Command = NullCommand,
                  error_command: Command = NullCommand):
         self.bot: telegram.Bot = telegram.Bot(api_key)
@@ -40,6 +42,10 @@ class TelegramBot:
             required_tables = required_tables.union(command.require_alchemy_tables)
         # Generate the Alchemy database
         self.alchemy = Alchemy(database_uri, required_tables)
+        self.master_table = self.alchemy.__getattribute__(master_table.__name__)
+        self.identity_table = self.alchemy.__getattribute__(identity_table.__name__)
+        self.identity_column = self.identity_table.__getattribute__(self.identity_table, identity_column_name)
+        self.identity_chain = relationshiplinkchain(self.master_table, self.identity_table)
 
         # noinspection PyMethodParameters
         class TelegramCall(Call):
@@ -54,6 +60,17 @@ class TelegramBot:
                 response = await self.network.request(message, destination)
                 return response
 
+            async def get_author(call):
+                update: telegram.Update = call.kwargs["update"]
+                user: telegram.User = update.effective_user
+                if user is None:
+                    return None
+                query = call.session.query(self.master_table)
+                for link in self.identity_chain:
+                    query = query.join(link.mapper.class_)
+                query = query.filter(self.identity_column == user.id)
+                return await asyncify(query.one_or_none)
+
         self.Call = TelegramCall
 
     async def run(self):
diff --git a/royalnet/commands/debug_author.py b/royalnet/commands/debug_author.py
new file mode 100644
index 00000000..0e9799ff
--- /dev/null
+++ b/royalnet/commands/debug_author.py
@@ -0,0 +1,16 @@
+from ..utils import Command, CommandArgs, Call
+from ..database.tables import Royal, Telegram
+
+
+class DebugAuthorCommand(Command):
+
+    command_name = "debug_author"
+    command_title = "Ottieni informazioni sull'autore di questa chiamata."
+
+    require_alchemy_tables = {Royal, Telegram}
+
+    async def common(self, call: Call, args: CommandArgs):
+        author = await call.get_author()
+        if author is None:
+            await call.reply(f"โ˜๏ธ L'autore di questa chiamata รจ sconosciuto.")
+        await call.reply(f"๐ŸŒž <code>{str(author)}</code> รจ l'autore di questa chiamata.")
diff --git a/royalnet/commands/diario.py b/royalnet/commands/diario.py
index f6a65867..97f3bb6c 100644
--- a/royalnet/commands/diario.py
+++ b/royalnet/commands/diario.py
@@ -1,5 +1,7 @@
-from ..utils import Command, CommandArgs, Call
 import re
+import datetime
+from ..utils import Command, CommandArgs, Call, InvalidInputError
+from ..database.tables import Royal, Diario, Alias
 
 
 class DiarioCommand(Command):
@@ -7,9 +9,21 @@ class DiarioCommand(Command):
     command_name = "diario"
     command_title = "Aggiungi una citazione al Diario."
 
+    require_alchemy_tables = {Royal, Diario, Alias}
+
     async def common(self, call: Call, args: CommandArgs):
         # Recreate the full sentence
         text = " ".join(args)
         # Pass the sentence through the diario regex
         match = re.match(r'["ยซโ€˜โ€œโ€›โ€Ÿโ›โใ€๏ผ‚`]([^"]+)["ยปโ€™โ€โœโžใ€ž๏ผ‚`] *(?:(?:-{1,2}|โ€”) *(\w+))?(?:,? *([^ ].*))?', text)
-        # TODO
\ No newline at end of file
+        # Find the corresponding matches
+        if match is None:
+            raise InvalidInputError("No match found.")
+        text = match.group(1)
+        quoted = match.group(2)
+        context = match.group(3)
+        timestamp = datetime.datetime.now()
+        # Find if there's a Royalnet account associated with the quoted name
+        quoted_alias = call.session.query(call.alchemy.Alias).filter_by(alias=quoted).one_or_none()
+        quoted_account = quoted_alias.royal if quoted_alias is not None else None
+        # Find the creator of the quote
\ No newline at end of file
diff --git a/royalnet/commands/sync.py b/royalnet/commands/sync.py
index aece8edb..89dcf63a 100644
--- a/royalnet/commands/sync.py
+++ b/royalnet/commands/sync.py
@@ -1,6 +1,6 @@
 import typing
 from telegram import Update, User
-from ..utils import Command, CommandArgs, Call, asyncify
+from ..utils import Command, CommandArgs, Call, asyncify, UnsupportedError
 from ..database.tables import Royal, Telegram
 
 
@@ -9,10 +9,10 @@ class SyncCommand(Command):
     command_name = "sync"
     command_title = "Connect your current account to Royalnet"
 
-    require_alchemy_tables = [Royal, Telegram]
+    require_alchemy_tables = {Royal, Telegram}
 
     async def common(self, call: Call, args: CommandArgs):
-        raise NotImplementedError()
+        raise UnsupportedError()
 
     async def telegram(self, call: Call, args: CommandArgs):
         update: Update = args.kwargs["update"]
@@ -24,6 +24,7 @@ class SyncCommand(Command):
         royal = await asyncify(call.session.query(call.alchemy.Royal).filter_by(username=args[0]).one_or_none)
         if royal is None:
             await call.reply("โš ๏ธ Non esiste alcun account Royalnet con quel nome.")
+            return
         # Find if the user is already synced
         telegram = await asyncify(call.session.query(call.alchemy.Telegram).filter_by(tg_id=user.id).one_or_none)
         if telegram is None:
diff --git a/royalnet/database/__init__.py b/royalnet/database/__init__.py
index 9c7956f4..70a54417 100644
--- a/royalnet/database/__init__.py
+++ b/royalnet/database/__init__.py
@@ -1,3 +1,4 @@
 from .alchemy import Alchemy
+from .relationshiplinkchain import relationshiplinkchain
 
-__all__ = ["Alchemy"]
+__all__ = ["Alchemy", "relationshiplinkchain"]
diff --git a/royalnet/database/relationshiplinkchain.py b/royalnet/database/relationshiplinkchain.py
new file mode 100644
index 00000000..2ab72934
--- /dev/null
+++ b/royalnet/database/relationshiplinkchain.py
@@ -0,0 +1,22 @@
+import typing
+from sqlalchemy.inspection import inspect
+
+
+def relationshiplinkchain(starting_class, ending_class) -> typing.Optional[tuple]:
+    """Find the path to follow to get from the starting table to the ending table."""
+    inspected = set()
+
+    def search(_mapper, chain):
+        inspected.add(_mapper)
+        if _mapper.class_ == ending_class:
+            return chain
+        relationships = _mapper.relationships
+        for _relationship in set(relationships):
+            if _relationship.mapper in inspected:
+                continue
+            result = search(_relationship.mapper, chain + (_relationship,))
+            if len(result) == 0:
+                return result
+        return ()
+
+    return search(inspect(starting_class), tuple())
diff --git a/royalnet/database/tables/__init__.py b/royalnet/database/tables/__init__.py
index 15a9634c..dc48894b 100644
--- a/royalnet/database/tables/__init__.py
+++ b/royalnet/database/tables/__init__.py
@@ -1,5 +1,6 @@
 from .royals import Royal
 from .telegram import Telegram
 from .diario import Diario
+from .aliases import Alias
 
-__all__ = ["Royal", "Telegram", "Diario"]
+__all__ = ["Royal", "Telegram", "Diario", "Alias"]
diff --git a/royalnet/database/tables/aliases.py b/royalnet/database/tables/aliases.py
new file mode 100644
index 00000000..7f86ffa6
--- /dev/null
+++ b/royalnet/database/tables/aliases.py
@@ -0,0 +1,23 @@
+from sqlalchemy import Column, \
+                       Integer, \
+                       String, \
+                       BigInteger, \
+                       LargeBinary, \
+                       ForeignKey
+from sqlalchemy.orm import relationship
+from .royals import Royal
+
+
+class Alias:
+    __tablename__ = "aliases"
+
+    royal_id = Column(Integer, ForeignKey("royals.uid"))
+    alias = Column(String, primary_key=True)
+
+    royal = relationship("Royal", backref="aliases")
+
+    def __repr__(self):
+        return f"<Alias {str(self)}>"
+
+    def __str__(self):
+        return f"{self.alias}->{self.royal_id}"
diff --git a/royalnet/database/tables/diario.py b/royalnet/database/tables/diario.py
index 8335c458..c3418cf9 100644
--- a/royalnet/database/tables/diario.py
+++ b/royalnet/database/tables/diario.py
@@ -14,9 +14,9 @@ class Diario:
 
     diario_id = Column(Integer, primary_key=True)
 
-    creator_id = Column(Integer, ForeignKey("royals.id"))
-    quoted_id = Column(Integer, ForeignKey("royals.id"))
-    quoted_name = Column(String)
+    creator_id = Column(Integer, ForeignKey("royals.uid"))
+    quoted_account_id = Column(Integer, ForeignKey("royals.uid"))
+    quoted = Column(String)
     text = Column(Text, nullable=False)
     context = Column(Text)
     timestamp = Column(DateTime, nullable=False)
@@ -24,7 +24,7 @@ class Diario:
     spoiler = Column(Boolean, default=False)
 
     creator = relationship("Royal", foreign_keys=creator_id, backref="diario_created")
-    quoted = relationship("Royal", foreign_keys=quoted_id, backref="diario_quoted")
+    quoted_account = relationship("Royal", foreign_keys=quoted_account_id, backref="diario_quoted")
 
     def __repr__(self):
         return f"<Diario {self.diario_id}>"
diff --git a/royalnet/database/tables/telegram.py b/royalnet/database/tables/telegram.py
index 19a55c5d..e11c9f5f 100644
--- a/royalnet/database/tables/telegram.py
+++ b/royalnet/database/tables/telegram.py
@@ -17,7 +17,6 @@ class Telegram:
     tg_last_name = Column(String)
     tg_username = Column(String)
     tg_avatar = Column(LargeBinary)
-    # TODO: Add an index? https://www.citusdata.com/blog/2016/10/12/count-performance/
 
     royal = relationship("Royal", backref="telegram")
 
diff --git a/royalnet/utils/call.py b/royalnet/utils/call.py
index c21c06a2..e722049a 100644
--- a/royalnet/utils/call.py
+++ b/royalnet/utils/call.py
@@ -26,6 +26,11 @@ class Call:
         The data must be pickleable."""
         raise NotImplementedError()
 
+    async def get_author(self):
+        """Try to find the universal identifier of the user that sent the message.
+        That probably means, the database row identifying the user."""
+        raise NotImplementedError()
+
     # These parameters / methods should be left alone
     def __init__(self, channel, command: Command, *args, **kwargs):
         self.channel = channel