Skip to content

Commit

Permalink
Merge pull request #65 from nasfarley88/dev
Browse files Browse the repository at this point in the history
Updating documentation
  • Loading branch information
LanceMaverick committed Jan 16, 2017
2 parents 9f05f33 + a46c761 commit 26f660e
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 52 deletions.
110 changes: 83 additions & 27 deletions skybeard/beards.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@


def regex_predicate(pattern):
"""Returns a predicate function which returns True if pattern is matched."""
def retfunc(chat_handler, msg):
try:
logging.debug("Matching regex: '{}' in '{}'".format(
Expand All @@ -28,7 +29,9 @@ def retfunc(chat_handler, msg):
return retfunc


# TODO make command_predicate in terms of regex_predicate
def command_predicate(cmd):
"""Returns a predicate coroutine which returns True if command is sent."""
async def retcoro(beard_chat_handler, msg):
bot_username = await beard_chat_handler.get_username()
pattern = r"^/{}(?:@{}|[^@]|$)".format(
Expand All @@ -50,13 +53,15 @@ async def retcoro(beard_chat_handler, msg):
# TODO rename coro to coro_name or something better than that

class Command(object):
"""Holds information to determine whether a function should be triggered."""
def __init__(self, pred, coro, hlp=None):
self.pred = pred
self.coro = coro
self.hlp = hlp


class SlashCommand(object):
"""Holds information to determine whether a telegram command was sent."""
def __init__(self, cmd, coro, hlp=None):
self.cmd = cmd
self.pred = command_predicate(cmd)
Expand All @@ -65,6 +70,9 @@ def __init__(self, cmd, coro, hlp=None):


def create_command(cmd_or_pred, coro, hlp=None):
"""Creates a Command or SlashCommand object as appropriate.
Used to make __commands__ tuples into Command objects."""
if isinstance(cmd_or_pred, str):
return SlashCommand(cmd_or_pred, coro, hlp)
elif callable(cmd_or_pred):
Expand All @@ -87,6 +95,8 @@ def emit(self, record):


class Beard(type):
"""Metaclass for creating beards."""

beards = list()

def __new__(mcs, name, bases, dct):
Expand All @@ -113,13 +123,17 @@ def __init__(cls, name, bases, attrs):
super().__init__(name, bases, attrs)

def register(cls, beard):
"""Add beard to internal list of beards."""
cls.beards.append(beard)


class Filters:
"""Filters used to call plugin methods when particular types of
messages are received.
For usage, see description of the BeardChatHandler.__commands__ variable. """
"""Filters used to call plugin methods when particular types of
messages are received.
For usage, see description of the BeardChatHandler.__commands__ variable.
"""
@classmethod
def text(cls, chat_handler, msg):
"""Filters for text messages"""
Expand All @@ -137,35 +151,40 @@ def location(cls, chat_handler, msg):


class ThatsNotMineException(Exception):
"""Used to check if serialized callback data belongs
to the plugin. See BeardChatHandler.serialize()"""
"""Raised if data does not match beard.
Used to check if serialized callback data belongs to the plugin. See
BeardChatHandler.serialize()"""
pass


class BeardChatHandler(telepot.aio.helper.ChatHandler, metaclass=Beard):
"""Chat handler for beards. This is the primary interface between
skybeard and any plug-in. The plug-in must define a class that inherets
from BeardChatHandler.
"""Chat handler for beards.
This is the primary interface between skybeard and any plug-in. The plug-in
must define a class that inherets from BeardChatHandler.
This class should overwrite __commands__ with a list of tuples that route
messages containing commands, or if they pass certain "Filters"
messages containing commands, or if they pass certain "Filters"
(see skybeard.beards.Filters).
E.g:
```Python
__commands__ = [
('mycommand', 'my_func', 'this is a help message'),
(Filters.location, 'my_other_func', 'another help message')]
In this case, when the bot receives the command "/mycommand", it
will call self.my_func(msg) where msg is a dict containing all the
message information.
The filter (from skybeard.beards) will call self.my_other_func(msg)
whenever "msg" contains a location.
The help messages are collected by the help functions and automatically
formatted and sent when a user sends /help to the bot.
```
In this case, when the bot receives the command "/mycommand", it will call
self.my_func(msg) where msg is a dict containing all the message
information. The filter (from skybeard.beards) will call
self.my_other_func(msg) whenever "msg" contains a location. The help
messages are collected by the help functions and automatically formatted
and sent when a user sends /help to the bot.
Instances of the plug-in classes are created when required (such as when
a filter is passed, a command or a regex pattern for the bot is matched
etc.) and they are destructed after a set timeout. The default is 10
etc.) and they are destructed after a set timeout. The default is 10
seconds, but this can be overwritten with, for example
_timeout = 90
Expand Down Expand Up @@ -200,10 +219,18 @@ def __init__(self, *args, **kwargs):
self.logger.addHandler(self._handler)

def on_close(self, e):
"""Removes per beard logger handler and calls telepot default on_close."""
self.logger.removeHandler(self._handler)
super().on_close(e)

async def __onerror__(self, e):
"""Runs when functions decorated with @onerror except.
Useful for emitting debug crash logs. Can be overridden to use custom
error tracking (e.g. telegramming the author of the beard when a crash
happens.)
"""
self.logger.debug(
"More details on crash of {}:\n\n{}".format(
self,
Expand All @@ -215,11 +242,14 @@ def _make_uid(self):
return type(self).__name__+str(self.chat_id)

def serialize(self, data):
"""Serialize callback data (such as with inline keyboard
buttons). The id of the plug-in is encoded into the
callback data so ownership of callbacks can be easily
checked when it is deserialized. Also avoids the same
plug-in receiving callback data from another chat"""
"""Serialises data to be specific for each beard instance.
Serialize callback data (such as with inline keyboard buttons). The id
of the plug-in is encoded into the callback data so ownership of
callbacks can be easily checked when it is deserialized. Also avoids
the same plug-in receiving callback data from another chat
"""
return json.dumps((self._make_uid(), data))

def deserialize(self, data):
Expand All @@ -233,20 +263,46 @@ def deserialize(self, data):

@classmethod
def setup_beards(cls, key):
"""Perform setup necessary for all beards."""
cls.key = key

def register_command(self, pred_or_cmd, coro, hlp=None):
"""Registers an instance level command.
This can be used to create instance specific commands e.g. if a user
needs to type /cmdSOMEAPIKEY:
```
self.register_commmand('cmd{}'.format(SOMEAPIKEY), 'name_of_coro')
```
"""

logging.debug("Registering instance command: {}".format(pred_or_cmd))
self._instance_commands.append(create_command(pred_or_cmd, coro, hlp))

@classmethod
def get_name(cls):
"""Get the name of the beard (e.g. cls.__name__)."""
return cls.__name__

async def on_chat_message(self, msg):
"""Can be overwritten in order to define the behaviour of the plug-in
whenever any message is received. super() MUST be called in the overwrite
to preserve default behaviour"""
"""Default on_chat_message for beards.
Can be overwritten in order to define the behaviour of the plug-in
whenever any message is received.
NOTE: super().on_chat_message(msg) must be called in the overwrite to
preserve default behaviour. This is usually done after custom
behaviour, e.g.
```Python
async def on_chat_message(self, msg):
await self.sender.sendMessage("I got your message!")
super().on_chat_message(msg)
```
"""
for cmd in self._instance_commands + type(self).__commands__:
if asyncio.iscoroutinefunction(cmd.pred):
pred_value = await cmd.pred(self, msg)
Expand Down
29 changes: 26 additions & 3 deletions skybeard/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,34 @@ async def g(beard, *fargs, **fkwargs):
return g


def debugonly(f):
@wraps(f)
def debugonly(f_or_text=None, **kwargs):
"""A decorator to prevent commands being run outside of debug mode.
If the function is awaited when skybeard is not in debug mode, it sends a
message to the user. If skybeard is run in debug mode, then it executes the
body of the function.
If passed a string as the first argument, it sends that message instead of
the default message when not in debug mode.
e.g.
```Python
@debugonly("Skybeard is not in debug mode.")
async def foo(self, msg):
# This message will only be sent if skybeard is run in debug mode
await self.sender.sendMessage("You are in debug mode!")
```
"""

if isinstance(f_or_text, str):
return partial(onerror, text=f_or_text, **kwargs)
elif f_or_text is None:
return partial(onerror, **kwargs)

@wraps(f_or_text)
async def g(beard, *fargs, **fkwargs):
if logger.getEffectiveLevel() <= logging.DEBUG:
return await f(beard, *fargs, **fkwargs)
return await f_or_text(beard, *fargs, **fkwargs)
else:
return await beard.sender.sendMessage(
"This command can only be run in debug mode.")
Expand Down
49 changes: 27 additions & 22 deletions skybeard/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@
logger = logging.getLogger(__name__)


def is_module(filename):
fname, ext = os.path.splitext(filename)
def is_module(path):
"""Checks if path is a module."""

fname, ext = os.path.splitext(path)
if ext == ".py":
return True
elif os.path.exists(os.path.join(filename, "__init__.py")):
elif os.path.exists(os.path.join(path, "__init__.py")):
return True
else:
return False


def get_literal_path(path_or_autoloader):
"""Gets literal path from AutoLoader or returns input."""

try:
return path_or_autoloader.path
except AttributeError:
Expand All @@ -24,6 +28,8 @@ def get_literal_path(path_or_autoloader):


def get_literal_beard_paths(beard_paths):
"""Returns list of literal beard paths."""

return [get_literal_path(x) for x in beard_paths]


Expand Down Expand Up @@ -68,37 +74,36 @@ def get_args(msg_or_text, return_string=False, **kwargs):
else:
return shlex.split(text)[1:]


def partition_text(text):
"""Generator for splitting long texts into ones below the
"""Generator for splitting long texts into ones below the
character limit. Messages are split at the nearest line break
and each successive chunk is yielded. Relatively untested"""
if len(text) < 3500:
yield text
else:
text_list = text.split('\n')
l = 0 #length iterator of current block
i= 0 #start position of block
j = 0 #end position of block

#j scans through list of lines from start position i
#l tracks length of all characters in the current scan
#If length of everything from i to j+1 > the limit,
#yield current block, joined into single string, and
#shift the scanning position up to the start of the new
#block.
l = 0 # length iterator of current block
i = 0 # start position of block
j = 0 # end position of block

# j scans through list of lines from start position i l tracks length
# of all characters in the current scan If length of everything from i
# to j+1 > the limit, yield current block, joined into single string,
# and shift the scanning position up to the start of the new block.
for m in text_list:
l+=len(m)
l += len(m)
try:
#if adding another line will breach the limit,
#yield current block
if l+len(text_list[j+1])> 3500:
# if adding another line will breach the limit,
# yield current block
if l+len(text_list[j+1]) > 3500:
indices = [i, j]
yield '\n'.join(
[msg for k, msg in enumerate(text_list) if k in indices])
#shift start position for the next block
[msg for k, msg in enumerate(text_list)
if k in indices])
# shift start position for the next block
i = j+1
l = 0
j+=1
j += 1
except IndexError:
yield text_list[i]

0 comments on commit 26f660e

Please sign in to comment.