Skip to content
This repository was archived by the owner on Aug 31, 2021. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions example/plugins/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Example file for signals."""

from pyiab.plugins import plugin_class
from pyaib.components import observe, awaits_signal
from pyaib.signals import emit_signal, await_signal
import re

@plugin_class('names')
class Names:
"""This plugin provides a command ('names') that outputs a list of all
nicks currently in the channel."""
def __init__(self, irc_c, config):
print("Names plugin loaded")

@keyword('names')
def get_list_of_names(self, irc_c, message, trigger, args, kwargs):
# Sends a NAMES request to the server, to get a list of nicks for the
# current channel.
# Issue the NAMES request:
irc_c.RAW("NAMES %s" % message.channel)
# The request has been sent.
# pyaib is asynchronous, so another function will recieve the response
# from this request.
# That function must send the data here via a signal.
try:
# Wait for the signal (up to 10 seconds).
response = await_signal(irc_c, 'NAMES_RESPONSE', timeout=10.0)
# await_signal returns whatever data we choose to send, or True.
except TimeoutError:
message.reply("The request timed out.")
return
# The NAMES response is now saved.
channel = response[0]
names = response[1]
assert channel == message.channel
message.reply("List of channel members: %s" % ", ".join(names))
# Warning, this will annoy everyone in the channel.

@observe('IRC_MSG_353') # 353 indicates a NAMES response.
def recieve_names(self, irc_c, message):
# The response is in message.args as a single string.
# "MYNICK = #channel :nick1 nick2 nick3"
# Split that up into individual names:
response = re.split(r"\s:?", message.args.strip())[2:]
channel = response[0]
names = response[1:]
# Great, we've caught the NAMES response.
# Now send it back to the function that wanted it.
emit_signal(irc_c, 'NAMES_RESPONSE', data=(channel, names))
# The signal name can be anything, so long as emit_signal and
# await_signal use the same one.
12 changes: 12 additions & 0 deletions pyaib/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,15 @@ def wrapper(func):
handles = watches


def awaits_signal(*signals):
"""Define a series of signals to later be subscribed to"""
def wrapper(func):
splugs = _get_plugs(func, 'signals')
splugs.extend([signal for signal in signals if signal not in splugs])
return func
return wrapper


class _Ignore(EasyDecorator):
"""Only pass if triggers is from user not ignored"""
def wrapper(dec, irc_c, msg, *args):
Expand Down Expand Up @@ -335,6 +344,9 @@ def _install_hooks(self, context, hooked_methods):
elif kind == 'parsers':
for name, chain in args:
self._add_parsers(method, name, chain)
elif kind == 'signals':
for signal in args:
context.signals(signal).observe(method)

def _add_parsers(self, method, name, chain):
""" Handle Message parser adding and chaining """
Expand Down
2 changes: 2 additions & 0 deletions pyaib/ircbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from .config import Config
from .events import Events
from .timers import Timers
from .signals import Signals
from .components import ComponentManager
from . import irc

Expand All @@ -54,6 +55,7 @@ def __init__(self, *args, **kargs):
#Install most basic fundamental functionality
install('events', self._loadComponent(Events, False))
install('timers', self._loadComponent(Timers, False))
install('signals', self._loadComponent(Signals, False))

#Load the ComponentManager and load components
autoload = ['triggers', 'channels', 'plugins'] # Force these to load
Expand Down
105 changes: 105 additions & 0 deletions pyaib/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env python

import collections
import gevent.event
import gevent.queue
import gevent

from . import irc

def emit_signal(irc_c, name, *, data=None):
"""Emits the signal of the given name."""
if not isinstance(irc_c, irc.Context):
raise TypeError("First argument must be IRC context")
if data is False:
raise ValueError("Signalled data cannot be False")
# create signal if it doesn't already exist
signal = irc_c.signals(name)
signal.fire(irc_c, data)

def await_signal(irc_c, name, *, timeout=None):
"""Blocks until the signal of the given name is recieved, returning any
data that was passed to it."""
if not isinstance(irc_c, irc.Context):
raise TypeError("First argument must be IRC context")
# create signal if it doesn't already exist
signal = irc_c.signals(name)
return signal.wait(timeout)

class Signal:
def __init__(self, name):
self.__event = gevent.event.Event()
self.__observers = [] # decorated observers
self.__waiters = [] # waiting greenlets
self.name = name

def observe(self, observer):
if isinstance(observer, collections.Callable):
self.__observers.append(observer)
else:
raise TypeError("%s not callable" % repr(observer))
return self

def unobserve(self, observer):
self.__observers.remove(observer)
return self

def fire(self, irc_c, data):
assert isinstance(irc_c, irc.Context)
# resume waiting greenlets
waiters = list(self.__waiters)
self.__waiters.clear()
gevent.spawn(self._notify, waiters, data)
# manually initiate decorated observers
for observer in self.__observers:
if isinstance(observer, collections.Callable):
irc_c.bot_greenlets.spawn(observer, irc_c, copy(data))
else:
raise TypeError("%s not callable" % repr(observer))

@staticmethod
def _notify(waiters, data):
for queue in waiters:
queue.put_nowait(data)

def wait(self, timeout):
queue = gevent.queue.Channel()
self.__waiters.append(queue)
data = queue.get(timeout)
if data is False:
raise TimeoutError("The request timed out.")
return data

class Signals:
# Stores all the different signals.
# There are no pre-defined signals - they will be created by the end user.
def __init__(self, irc_c):
self.__signals = {}
self.__nullSignal = NullSignal() # is this necessary?

def list(self):
return self.__signals.keys()

def isSignal(self, name):
return name.lower() in self.__signals

def getOrMake(self, name):
if not self.isSignal(name):
#Make Event if it does not exist
self.__signals[name.lower()] = Signal(name)
return self.get(name)

#Return the null signal on non existent signal
def get(self, name):
signal = self.__signals.get(name.lower())
if signal is None: # Only on undefined events
return self.__nullSignal
return signal

__contains__ = isSignal
__call__ = getOrMake
__getitem__ = get

class NullSignal:
# not sure this is even needed
pass