ticketfrei3/kibicara/platformapi.py

227 lines
6.3 KiB
Python
Raw Normal View History

2020-07-01 19:28:00 +00:00
# Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
#
# SPDX-License-Identifier: 0BSD
2020-07-11 10:54:07 +00:00
""" API classes for implementing bots for platforms. """
2020-07-01 19:28:00 +00:00
from asyncio import create_task, Queue
from enum import auto, Enum
2020-07-01 19:28:00 +00:00
from kibicara.model import BadWord, Trigger
from logging import getLogger
from re import match
logger = getLogger(__name__)
class Message:
2020-07-11 10:54:07 +00:00
"""The Message object that is send through the censor.
Examples:
```
message = Message('Message sent by a user from platform xyz', xyz_message_id=123)
```
Args:
text (str): The message text
**kwargs (object, optional): Other platform-specific data.
Attributes:
text (str): The message text
**kwargs (object, optional): Other platform-specific data.
"""
2020-07-01 19:28:00 +00:00
def __init__(self, text, **kwargs):
self.text = text
self.__dict__.update(kwargs)
class BotStatus(Enum):
INSTANTIATED = auto()
RUNNING = auto()
STOPPED = auto()
2020-07-01 19:28:00 +00:00
class Censor:
2020-07-11 10:54:07 +00:00
""" The superclass for a platform bot.
The censor is the superclass for every platform bot. It distributes a message to all
other bots from the same hood if it passes the message filter. It provides methods
to start and stop the bot and an overwritable stub for a starting routine.
Examples:
```
class XYZPlatform(Censor):
def __init__(self, xyz_model):
super().__init__(xyz_model.hood)
...
async def run(self):
await gather(self.poll(), self.push())
...
async def poll(self):
while True:
# XXX get text message from platform xyz
await self.publish(Message(text))
...
async def push(self):
while True:
message = await self.receive()
# XXX send message.text to platform xyz
```
Args:
hood (Hood): A Hood Model object
Attributes:
hood (Hood): A Hood Model object
"""
__instances = {}
2020-07-01 19:28:00 +00:00
def __init__(self, hood):
self.hood = hood
self.enabled = True
2020-07-11 10:54:07 +00:00
self._inbox = Queue()
self.__task = None
self.__hood_censors = self.__instances.setdefault(hood.id, [])
self.__hood_censors.append(self)
self.status = BotStatus.INSTANTIATED
2020-07-01 19:28:00 +00:00
def start(self):
""" Start the bot. """
2020-07-11 10:54:07 +00:00
if self.__task is None:
self.__task = create_task(self.__run())
2020-07-01 19:28:00 +00:00
def stop(self):
""" Stop the bot. """
2020-07-11 10:54:07 +00:00
if self.__task is not None:
self.__task.cancel()
2020-07-01 19:28:00 +00:00
async def __run(self):
await self.hood.load()
2020-07-11 10:54:07 +00:00
self.__task.set_name('%s %s' % (self.__class__.__name__, self.hood.name))
try:
self.status = BotStatus.RUNNING
await self.run()
except Exception as e:
logger.exception(e)
finally:
self.__task = None
self.status = BotStatus.STOPPED
2020-07-01 19:28:00 +00:00
async def run(self):
2020-07-11 10:54:07 +00:00
""" Entry point for a bot.
Note: Override this in the derived bot class.
"""
2020-07-01 19:28:00 +00:00
pass
async def publish(self, message):
2020-07-11 10:54:07 +00:00
""" Distribute a message to the bots in a hood.
Args:
message (Message): Message to distribute
Returns (Boolean): returns True if message is accepted by Censor.
2020-07-11 10:54:07 +00:00
"""
2020-07-01 19:28:00 +00:00
if not await self.__is_appropriate(message):
return False
for censor in self.hood_censors:
await censor.inbox.put(message)
return True
2020-07-01 19:28:00 +00:00
async def receive(self):
2020-07-11 10:54:07 +00:00
""" Receive a message.
Returns (Message): Received message
"""
return await self._inbox.get()
2020-07-01 19:28:00 +00:00
async def __is_appropriate(self, message):
for badword in await BadWord.objects.filter(hood=self.hood).all():
if match(badword.pattern, message.text):
logger.debug('Matched bad word - dropped message: %s' % message.text)
2020-07-01 19:28:00 +00:00
return False
for trigger in await Trigger.objects.filter(hood=self.hood).all():
if match(trigger.pattern, message.text):
logger.debug('Matched trigger - passed message: %s' % message.text)
2020-07-01 19:28:00 +00:00
return True
logger.debug('Did not match any trigger - dropped message: %s' % message.text)
2020-07-01 19:28:00 +00:00
return False
class Spawner:
2020-07-11 10:54:07 +00:00
""" Spawns a bot with a specific bot model.
Examples:
```
class XYZPlatform(Censor):
# bot class
class XYZ(Model):
# bot model
spawner = Spawner(XYZ, XYZPlatform)
```
Args:
ORMClass (ORM Model subclass): A Bot Model object
BotClass (Censor subclass): A Bot Class object
Attributes:
ORMClass (ORM Model subclass): A Hood Model object
BotClass (Censor subclass): A Bot Class object
"""
__instances = []
2020-07-01 19:28:00 +00:00
def __init__(self, ORMClass, BotClass):
self.ORMClass = ORMClass
self.BotClass = BotClass
2020-07-11 10:54:07 +00:00
self.__bots = {}
self.__instances.append(self)
2020-07-01 19:28:00 +00:00
@classmethod
async def init_all(cls):
2020-07-11 10:54:07 +00:00
""" Instantiate and start a bot for every row in the corresponding ORM model. """
for spawner in cls.__instances:
2020-07-01 19:28:00 +00:00
await spawner._init()
async def _init(self):
for item in await self.ORMClass.objects.all():
self.start(item)
def start(self, item):
2020-07-11 10:54:07 +00:00
""" Instantiate and start a bot with the provided ORM object.
Example:
```
xyz = await XYZ.objects.create(hood=hood, **values.__dict__)
spawner.start(xyz)
```
Args:
item (ORM Model object): Argument to the bot constructor
"""
bot = self.__bots.setdefault(item.pk, self.BotClass(item))
if bot.enabled:
bot.start()
2020-07-01 19:28:00 +00:00
def stop(self, item):
2020-07-11 10:54:07 +00:00
""" Stop and delete a bot.
Args:
item (ORM Model object): ORM object corresponding to bot.
"""
bot = self.__bots.pop(item.pk, None)
2020-07-01 19:28:00 +00:00
if bot is not None:
bot.stop()
def get(self, item):
2020-07-11 10:54:07 +00:00
""" Get a running bot.
Args:
item (ORM Model object): ORM object corresponding to bot.
"""
return self.__bots.get(item.pk)