From c5d48530db615655cd039285e074ccfaf6382a6f Mon Sep 17 00:00:00 2001 From: LanceMaverick Date: Mon, 16 Jan 2017 12:02:28 +0000 Subject: [PATCH 1/5] example config and README added --- beards/gaming/README.txt | 2 ++ beards/gaming/config.py.example | 12 ++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 beards/gaming/README.txt create mode 100644 beards/gaming/config.py.example diff --git a/beards/gaming/README.txt b/beards/gaming/README.txt new file mode 100644 index 0000000..834adcf --- /dev/null +++ b/beards/gaming/README.txt @@ -0,0 +1,2 @@ +This beard requires a steam API key which can be obtained here: +http://steamcommunity.com/dev/apikey diff --git a/beards/gaming/config.py.example b/beards/gaming/config.py.example new file mode 100644 index 0000000..18f28ec --- /dev/null +++ b/beards/gaming/config.py.example @@ -0,0 +1,12 @@ +#add steam games to access news and updates +game_ids = { + 'dota': 570, + 'overwatch': 0 + } + +#games to check automatically for updates +check_ud = ['dota'] + +#dev key for steam API. Request one here: +#http://steamcommunity.com/dev/apikey +steam_api_key = '' From 0ba59a8a86610374791154c6b5d1e31c42dc1251 Mon Sep 17 00:00:00 2001 From: Nathanael Farley Date: Mon, 16 Jan 2017 12:24:49 +0000 Subject: [PATCH 2/5] Documented @debugonly decorator. --- skybeard/decorators.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/skybeard/decorators.py b/skybeard/decorators.py index eab7e99..e466893 100644 --- a/skybeard/decorators.py +++ b/skybeard/decorators.py @@ -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.") From 2a39532f7a58fb216c82831107cac2fe3ddbbaa0 Mon Sep 17 00:00:00 2001 From: Nathanael Farley Date: Mon, 16 Jan 2017 12:44:25 +0000 Subject: [PATCH 3/5] Added documentation to beards.py. --- skybeard/beards.py | 76 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 59 insertions(+), 17 deletions(-) diff --git a/skybeard/beards.py b/skybeard/beards.py index 6a8ca25..58d5143 100644 --- a/skybeard/beards.py +++ b/skybeard/beards.py @@ -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( @@ -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( @@ -50,6 +53,7 @@ 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 @@ -57,6 +61,7 @@ def __init__(self, pred, coro, hlp=None): 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) @@ -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): @@ -117,8 +125,8 @@ def register(cls, beard): class Filters: - """Filters used to call plugin methods when particular types of - messages are received. + """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): @@ -145,27 +153,27 @@ class ThatsNotMineException(Exception): 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. + 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: - + __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 + + 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. + 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 @@ -200,10 +208,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, @@ -216,10 +232,10 @@ def _make_uid(self): 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""" + 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): @@ -233,20 +249,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) From f068c2f3043f0d4848acb985cc6e4176666ba15c Mon Sep 17 00:00:00 2001 From: Nathanael Farley Date: Mon, 16 Jan 2017 12:51:10 +0000 Subject: [PATCH 4/5] Updated docs in beards.py. --- skybeard/beards.py | 47 ++++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/skybeard/beards.py b/skybeard/beards.py index 58d5143..fddd7cd 100644 --- a/skybeard/beards.py +++ b/skybeard/beards.py @@ -127,7 +127,10 @@ def register(cls, 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. """ + + For usage, see description of the BeardChatHandler.__commands__ variable. + + """ @classmethod def text(cls, chat_handler, msg): """Filters for text messages""" @@ -145,31 +148,36 @@ 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" (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 @@ -231,11 +239,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): From fa4ad67a4d4a4e6b0a0135a42897e63f94e049bd Mon Sep 17 00:00:00 2001 From: Nathanael Farley Date: Mon, 16 Jan 2017 16:13:31 +0000 Subject: [PATCH 5/5] More docs in utils.py. --- skybeard/beards.py | 3 +++ skybeard/utils.py | 49 +++++++++++++++++++++++++--------------------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/skybeard/beards.py b/skybeard/beards.py index fddd7cd..e0bd099 100644 --- a/skybeard/beards.py +++ b/skybeard/beards.py @@ -95,6 +95,8 @@ def emit(self, record): class Beard(type): + """Metaclass for creating beards.""" + beards = list() def __new__(mcs, name, bases, dct): @@ -121,6 +123,7 @@ 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) diff --git a/skybeard/utils.py b/skybeard/utils.py index 817e9ee..67a7afa 100644 --- a/skybeard/utils.py +++ b/skybeard/utils.py @@ -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: @@ -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] @@ -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] -