Module kibicara.platformapi

API classes for implementing bots for platforms.

Expand source code
# Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
#
# SPDX-License-Identifier: 0BSD

""" API classes for implementing bots for platforms. """

from asyncio import create_task, Queue
from enum import auto, Enum
from kibicara.model import BadWord, Trigger
from logging import getLogger
from re import search, IGNORECASE


logger = getLogger(__name__)


class Message:
    """The Message object that is send through the censor.

    Examples:
        ```
        message = Message('Message sent 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.
    """

    def __init__(self, text, **kwargs):
        self.text = text
        self.__dict__.update(kwargs)


class BotStatus(Enum):
    INSTANTIATED = auto()
    RUNNING = auto()
    STOPPED = auto()


class Censor:
    """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 = {}

    def __init__(self, hood):
        self.hood = hood
        self.enabled = True
        self._inbox = Queue()
        self.__task = None
        self.__hood_censors = self.__instances.setdefault(hood.id, [])
        self.__hood_censors.append(self)
        self.status = BotStatus.INSTANTIATED

    def start(self):
        """ Start the bot. """
        if self.__task is None:
            self.__task = create_task(self.__run())

    def stop(self):
        """ Stop the bot. """
        if self.__task is not None:
            self.__task.cancel()

    async def __run(self):
        await self.hood.load()
        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

    async def run(self):
        """Entry point for a bot.

        Note: Override this in the derived bot class.
        """
        pass

    @classmethod
    async def destroy_hood(cls, hood):
        """Removes all its database entries.

        Note: Override this in the derived bot class.
        """
        pass

    async def publish(self, message):
        """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.
        """
        if not await self.__is_appropriate(message):
            return False
        for censor in self.__hood_censors:
            await censor._inbox.put(message)
        return True

    async def receive(self):
        """Receive a message.

        Returns (Message): Received message
        """
        return await self._inbox.get()

    async def __is_appropriate(self, message):
        for badword in await BadWord.objects.filter(hood=self.hood).all():
            if search(badword.pattern, message.text, IGNORECASE):
                logger.debug('Matched bad word - dropped message: %s' % message.text)
                return False
        for trigger in await Trigger.objects.filter(hood=self.hood).all():
            if search(trigger.pattern, message.text, IGNORECASE):
                logger.debug('Matched trigger - passed message: %s' % message.text)
                return True
        logger.debug('Did not match any trigger - dropped message: %s' % message.text)
        return False


class Spawner:
    """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 = []

    def __init__(self, ORMClass, BotClass):
        self.ORMClass = ORMClass
        self.BotClass = BotClass
        self.__bots = {}
        self.__instances.append(self)

    @classmethod
    async def init_all(cls):
        """Instantiate and start a bot for every row in the corresponding ORM model."""
        for spawner in cls.__instances:
            await spawner._init()

    @classmethod
    async def destroy_hood(cls, hood):
        for spawner in cls.__instances:
            for pk in list(spawner.__bots):
                bot = spawner.__bots[pk]
                if bot.hood.id == hood.id:
                    del spawner.__bots[pk]
                    bot.stop()
            await spawner.BotClass.destroy_hood(hood)

    async def _init(self):
        for item in await self.ORMClass.objects.all():
            self.start(item)

    def start(self, item):
        """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()

    def stop(self, item):
        """Stop and delete a bot.

        Args:
            item (ORM Model object): ORM object corresponding to bot.
        """
        bot = self.__bots.pop(item.pk, None)
        if bot is not None:
            bot.stop()

    def get(self, item):
        """Get a running bot.

        Args:
            item (ORM Model object): ORM object corresponding to bot.
        """
        return self.__bots.get(item.pk)

Classes

class BotStatus (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code
class BotStatus(Enum):
    INSTANTIATED = auto()
    RUNNING = auto()
    STOPPED = auto()

Ancestors

  • enum.Enum

Class variables

var INSTANTIATED
var RUNNING
var STOPPED
class Censor (hood)

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
Expand source code
class Censor:
    """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 = {}

    def __init__(self, hood):
        self.hood = hood
        self.enabled = True
        self._inbox = Queue()
        self.__task = None
        self.__hood_censors = self.__instances.setdefault(hood.id, [])
        self.__hood_censors.append(self)
        self.status = BotStatus.INSTANTIATED

    def start(self):
        """ Start the bot. """
        if self.__task is None:
            self.__task = create_task(self.__run())

    def stop(self):
        """ Stop the bot. """
        if self.__task is not None:
            self.__task.cancel()

    async def __run(self):
        await self.hood.load()
        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

    async def run(self):
        """Entry point for a bot.

        Note: Override this in the derived bot class.
        """
        pass

    @classmethod
    async def destroy_hood(cls, hood):
        """Removes all its database entries.

        Note: Override this in the derived bot class.
        """
        pass

    async def publish(self, message):
        """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.
        """
        if not await self.__is_appropriate(message):
            return False
        for censor in self.__hood_censors:
            await censor._inbox.put(message)
        return True

    async def receive(self):
        """Receive a message.

        Returns (Message): Received message
        """
        return await self._inbox.get()

    async def __is_appropriate(self, message):
        for badword in await BadWord.objects.filter(hood=self.hood).all():
            if search(badword.pattern, message.text, IGNORECASE):
                logger.debug('Matched bad word - dropped message: %s' % message.text)
                return False
        for trigger in await Trigger.objects.filter(hood=self.hood).all():
            if search(trigger.pattern, message.text, IGNORECASE):
                logger.debug('Matched trigger - passed message: %s' % message.text)
                return True
        logger.debug('Did not match any trigger - dropped message: %s' % message.text)
        return False

Subclasses

Static methods

async def destroy_hood(hood)

Removes all its database entries.

Note: Override this in the derived bot class.

Expand source code
@classmethod
async def destroy_hood(cls, hood):
    """Removes all its database entries.

    Note: Override this in the derived bot class.
    """
    pass

Methods

async def publish(self, message)

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.

Expand source code
async def publish(self, message):
    """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.
    """
    if not await self.__is_appropriate(message):
        return False
    for censor in self.__hood_censors:
        await censor._inbox.put(message)
    return True
async def receive(self)

Receive a message.

Returns (Message): Received message

Expand source code
async def receive(self):
    """Receive a message.

    Returns (Message): Received message
    """
    return await self._inbox.get()
async def run(self)

Entry point for a bot.

Note: Override this in the derived bot class.

Expand source code
async def run(self):
    """Entry point for a bot.

    Note: Override this in the derived bot class.
    """
    pass
def start(self)

Start the bot.

Expand source code
def start(self):
    """ Start the bot. """
    if self.__task is None:
        self.__task = create_task(self.__run())
def stop(self)

Stop the bot.

Expand source code
def stop(self):
    """ Stop the bot. """
    if self.__task is not None:
        self.__task.cancel()
class Message (text, **kwargs)

The Message object that is send through the censor.

Examples

message = Message('Message sent 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.
Expand source code
class Message:
    """The Message object that is send through the censor.

    Examples:
        ```
        message = Message('Message sent 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.
    """

    def __init__(self, text, **kwargs):
        self.text = text
        self.__dict__.update(kwargs)
class Spawner (ORMClass, BotClass)

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
Expand source code
class Spawner:
    """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 = []

    def __init__(self, ORMClass, BotClass):
        self.ORMClass = ORMClass
        self.BotClass = BotClass
        self.__bots = {}
        self.__instances.append(self)

    @classmethod
    async def init_all(cls):
        """Instantiate and start a bot for every row in the corresponding ORM model."""
        for spawner in cls.__instances:
            await spawner._init()

    @classmethod
    async def destroy_hood(cls, hood):
        for spawner in cls.__instances:
            for pk in list(spawner.__bots):
                bot = spawner.__bots[pk]
                if bot.hood.id == hood.id:
                    del spawner.__bots[pk]
                    bot.stop()
            await spawner.BotClass.destroy_hood(hood)

    async def _init(self):
        for item in await self.ORMClass.objects.all():
            self.start(item)

    def start(self, item):
        """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()

    def stop(self, item):
        """Stop and delete a bot.

        Args:
            item (ORM Model object): ORM object corresponding to bot.
        """
        bot = self.__bots.pop(item.pk, None)
        if bot is not None:
            bot.stop()

    def get(self, item):
        """Get a running bot.

        Args:
            item (ORM Model object): ORM object corresponding to bot.
        """
        return self.__bots.get(item.pk)

Static methods

async def destroy_hood(hood)
Expand source code
@classmethod
async def destroy_hood(cls, hood):
    for spawner in cls.__instances:
        for pk in list(spawner.__bots):
            bot = spawner.__bots[pk]
            if bot.hood.id == hood.id:
                del spawner.__bots[pk]
                bot.stop()
        await spawner.BotClass.destroy_hood(hood)
async def init_all()

Instantiate and start a bot for every row in the corresponding ORM model.

Expand source code
@classmethod
async def init_all(cls):
    """Instantiate and start a bot for every row in the corresponding ORM model."""
    for spawner in cls.__instances:
        await spawner._init()

Methods

def get(self, item)

Get a running bot.

Args

item : ORM Model object
ORM object corresponding to bot.
Expand source code
def get(self, item):
    """Get a running bot.

    Args:
        item (ORM Model object): ORM object corresponding to bot.
    """
    return self.__bots.get(item.pk)
def start(self, item)

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
Expand source code
def start(self, item):
    """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()
def stop(self, item)

Stop and delete a bot.

Args

item : ORM Model object
ORM object corresponding to bot.
Expand source code
def stop(self, item):
    """Stop and delete a bot.

    Args:
        item (ORM Model object): ORM object corresponding to bot.
    """
    bot = self.__bots.pop(item.pk, None)
    if bot is not None:
        bot.stop()