commit 06e79562f8ecd2cb125558fc05bca748ecb5ffdf Author: Stefano Pigozzi Date: Fri Mar 15 01:05:30 2019 +0100 First commit! diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..506efd03 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +python-telegram-bot>=11.1.0 diff --git a/royalnet/__init__.py b/royalnet/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/royalnet/bots/__init__.py b/royalnet/bots/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/royalnet/bots/telegram.py b/royalnet/bots/telegram.py new file mode 100644 index 00000000..c7748592 --- /dev/null +++ b/royalnet/bots/telegram.py @@ -0,0 +1,61 @@ +import telegram +import asyncio +import typing +from ..commands import PingCommand +from ..utils import asyncify, Call + + +class TelegramBot: + def __init__(self, api_key: str): + self.bot = telegram.Bot(api_key) + self.should_run = False + self.offset = -100 + self.commands = { + "/ping": PingCommand + } + + class TelegramCall(Call): + interface_name = "telegram" + interface_obj = self + + async def reply(call, text: str): + await asyncify(call.channel.send_message, text, parse_mode="HTML") + self.Call = TelegramCall + + async def run(self): + self.should_run = True + while self.should_run: + # Get the latest 100 updates + last_updates: typing.List[telegram.Update] = await asyncify(self.bot.get_updates, offset=self.offset) + # Handle updates + for update in last_updates: + # noinspection PyAsyncCall + asyncio.create_task(self.handle_update(update)) + # Recalculate offset + try: + self.offset = last_updates[-1].update_id + 1 + except IndexError: + pass + # Wait for a while TODO: use long polling + await asyncio.sleep(1) + + async def handle_update(self, update: telegram.Update): + # Skip non-message updates + if update.message is None: + return + message: telegram.Message = update.message + # Skip no-text messages + if message.text is None: + return + text: str = message.text + # Find and clean parameters + command_text, *parameters = text.split(" ") + command_text.replace(f"@{self.bot.username}", "") + # Find the function + try: + command = self.commands[command_text] + except KeyError: + # Skip inexistent commands + return + # Call the command + return await self.Call(message.chat, command).run() diff --git a/royalnet/commands/__init__.py b/royalnet/commands/__init__.py new file mode 100644 index 00000000..55d2e0d7 --- /dev/null +++ b/royalnet/commands/__init__.py @@ -0,0 +1,3 @@ +from .ping import PingCommand + +__all__ = ["PingCommand"] diff --git a/royalnet/commands/ping.py b/royalnet/commands/ping.py new file mode 100644 index 00000000..432598dd --- /dev/null +++ b/royalnet/commands/ping.py @@ -0,0 +1,6 @@ +from ..utils import Command, Call + + +class PingCommand(Command): + async def common(self, call: Call, *args, **kwargs): + await call.reply("Pong!") diff --git a/royalnet/utils/__init__.py b/royalnet/utils/__init__.py new file mode 100644 index 00000000..d55b5717 --- /dev/null +++ b/royalnet/utils/__init__.py @@ -0,0 +1,5 @@ +from .asyncify import asyncify +from .call import Call +from .command import Command + +__all__ = ["asyncify", "Call", "Command"] diff --git a/royalnet/utils/asyncify.py b/royalnet/utils/asyncify.py new file mode 100644 index 00000000..4d4a364e --- /dev/null +++ b/royalnet/utils/asyncify.py @@ -0,0 +1,8 @@ +import asyncio +import functools +import typing + + +async def asyncify(function: typing.Callable, *args, **kwargs): + loop = asyncio.get_running_loop() + return await loop.run_in_executor(None, functools.partial(function, *args, **kwargs)) diff --git a/royalnet/utils/call.py b/royalnet/utils/call.py new file mode 100644 index 00000000..2cbd624f --- /dev/null +++ b/royalnet/utils/call.py @@ -0,0 +1,25 @@ +from .command import Command + + +class Call: + """A command call. Still an abstract class, subbots should create a new call from this.""" + + # These parameters / methods should be overridden + interface_name = NotImplemented + interface_obj = NotImplemented + + async def reply(cls, text: str): + """Send a text message to the channel the call was made.""" + raise NotImplementedError() + + # These parameters / methods should be left alone + def __init__(self, channel, command: Command): + self.channel = channel + self.command = command + + async def run(self, *args, **kwargs): + try: + coroutine = getattr(self.command, self.interface_name) + except AttributeError: + coroutine = getattr(self.command, "common") + return await coroutine(self.command, self, *args, **kwargs) diff --git a/royalnet/utils/command.py b/royalnet/utils/command.py new file mode 100644 index 00000000..bc1c1ff0 --- /dev/null +++ b/royalnet/utils/command.py @@ -0,0 +1,10 @@ +import typing +if typing.TYPE_CHECKING: + from .call import Call + + +class Command: + """A generic command, called from any source.""" + + async def common(self, call: "Call", *args, **kwargs): + raise NotImplementedError() diff --git a/test.py b/test.py new file mode 100644 index 00000000..dfbc98ec --- /dev/null +++ b/test.py @@ -0,0 +1,6 @@ +import asyncio +from royalnet.bots.telegram import TelegramBot + +bot = TelegramBot("375232708:AAExsD_prmxJOXzmJwYZyNUt5zc_EbXxR38") + +asyncio.get_event_loop().run_until_complete(bot.run())