From 5dbae70ef3cbae9f6f75cc69c8e0f75d17646ad1 Mon Sep 17 00:00:00 2001 From: Chris Harding Date: Tue, 24 Oct 2017 23:34:41 +0000 Subject: [PATCH] halibot is studid --- .gitignore | 57 ---- .travis.yml | 10 - LICENSE | 27 -- README.md | 147 --------- doc/ConfigureOptions.md | 116 ------- doc/HelpInterface.md | 89 ------ doc/MessageFormat.md | 57 ---- doc/ModuleInteractivity.md | 103 ------- halibot/__init__.py | 7 - halibot/commandmodule.py | 57 ---- halibot/halagent.py | 15 - halibot/halauth.py | 77 ----- halibot/halconfigurer.py | 87 ------ halibot/halibot.py | 151 --------- halibot/halmodule.py | 22 -- halibot/halobject.py | 123 -------- halibot/jsdict.py | 7 - halibot/message.py | 21 -- halibot/packages/__init__.py | 1 - install.sh | 101 ------ main.py | 425 -------------------------- packages/container/__init__.py | 2 - packages/container/agentcontainer.py | 9 - packages/container/modulecontainer.py | 9 - packages/core/__init__.py | 2 - packages/core/help.py | 71 ----- packages/hello/__init__.py | 2 - packages/hello/config.json | 3 - packages/hello/hello.py | 42 --- run-scripts/halibot | 6 - test.sh | 12 - tests/test_auth.py | 152 --------- tests/test_cli.py | 81 ----- tests/test_configurer.py | 140 --------- tests/test_core.py | 397 ------------------------ tests/test_jsdict.py | 19 -- tests/util.py | 26 -- 37 files changed, 2673 deletions(-) delete mode 100644 .gitignore delete mode 100644 .travis.yml delete mode 100644 LICENSE delete mode 100644 README.md delete mode 100644 doc/ConfigureOptions.md delete mode 100644 doc/HelpInterface.md delete mode 100644 doc/MessageFormat.md delete mode 100644 doc/ModuleInteractivity.md delete mode 100644 halibot/__init__.py delete mode 100644 halibot/commandmodule.py delete mode 100644 halibot/halagent.py delete mode 100644 halibot/halauth.py delete mode 100644 halibot/halconfigurer.py delete mode 100644 halibot/halibot.py delete mode 100644 halibot/halmodule.py delete mode 100644 halibot/halobject.py delete mode 100644 halibot/jsdict.py delete mode 100644 halibot/message.py delete mode 100644 halibot/packages/__init__.py delete mode 100755 install.sh delete mode 100644 main.py delete mode 100644 packages/container/__init__.py delete mode 100644 packages/container/agentcontainer.py delete mode 100644 packages/container/modulecontainer.py delete mode 100644 packages/core/__init__.py delete mode 100644 packages/core/help.py delete mode 100644 packages/hello/__init__.py delete mode 100644 packages/hello/config.json delete mode 100644 packages/hello/hello.py delete mode 100755 run-scripts/halibot delete mode 100755 test.sh delete mode 100644 tests/test_auth.py delete mode 100644 tests/test_cli.py delete mode 100644 tests/test_configurer.py delete mode 100644 tests/test_core.py delete mode 100644 tests/test_jsdict.py delete mode 100644 tests/util.py diff --git a/.gitignore b/.gitignore deleted file mode 100644 index ba74660..0000000 --- a/.gitignore +++ /dev/null @@ -1,57 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*,cover - -# Translations -*.mo -*.pot - -# Django stuff: -*.log - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f6cda76..0000000 --- a/.travis.yml +++ /dev/null @@ -1,10 +0,0 @@ -language: python -python: - - "3.5" - - "3.6" -install: - - pip install coveralls -script: ./test.sh -after_success: coveralls -notifications: - email: false diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 694acf6..0000000 --- a/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2015, Halibot -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of halibot nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md deleted file mode 100644 index 8d1147c..0000000 --- a/README.md +++ /dev/null @@ -1,147 +0,0 @@ -# Halibot [![Build Status](https://travis-ci.org/Halibot/halibot.svg)](https://travis-ci.org/Halibot/halibot) [![Coverage Status](https://coveralls.io/repos/github/Halibot/halibot/badge.svg?branch=master)](https://coveralls.io/github/Halibot/halibot?branch=master) -The world's greatest saltwater multi-protocol chat bot framework! - -For the module and agent repositories, see [halibot-extra](https://github.com/halibot-extra). - -This is the repository for the new rewrite (codename halv3). For the old XMPP-specific version which this is mostly based on, please see [halibot/halibot-legacy](http://github.com/halibot/halibot-legacy). - -You can contact us in ##halibot on freenode. If you wait around for long enough you will get a response! - -Check out our [sweet site](https://halibot.github.io)! - -## Installation - -Currently, Halibot is only available from this repo. - -``` -git clone https://github.com/halibot/halibot -cd halibot -./install.sh -``` - -Extra packages are available at `https://github.com/halibot-extra`. - -## Basic Usage - -In order for Halibot to run, you need a config.json (see the next section(s) for an example config and explanation). - -To create a default config, you can execute: - -``` -halibot init -``` - -To actually run the bot, you can execute: -``` -halibot run - -# If you want an interactive python REPL to play with the bot: -halibot run -i -``` - -Now the fun stuff. - -You can fetch packages from a remote repository via the `fetch` command. To search for additional packages to install, the `search` command is available. - -``` -# Fetch the 'dice' and 'cli' modules -halibot fetch dice cli -``` - -These packages now exist in your local package directory (see `packages/`). You can add instances of these packages to your local configuration via the `add` command. - -``` -halibot add cli -Enter instance name: my_cli -halibot add dice -Enter instance name: my_dice -``` - -Then you can run halibot and roll dice! - -``` -halibot run ->> !roll 1d20 -18/20 = 18 ->> -``` - -## Configuration - -Halibot needs a valid configuration to run named `config.json`. -The following subsections will provide an example config, and a line-by-line explanation of each option. -Halibot uses valid python JSON, and will throw errors if the `config.json` does not conform to the strict JSON specification. - -### Example Config - -```json -{ - "package-path": [ - "packages" - ], - - "agent-instances": { - "irc0": { - "of": "irc:IrcAgent", - "channel": "##example", - "nickname": "ExampleName" - } - }, - - "module-instances": { - "hello0": { - "of": "hello:Hello" - } - }, - - "repos": [ - "http://halibot.fish:4842" - ] -} -``` - -### Packages - -A halibot *package* is, simply put, a collection of code. -Each package can contain a collection of module and agent classes, which can be instatied inside the `module-instances` and `agent-instances` section of the config. - -The `package-path` key is a list of path strings informing halibot where to look for packages. - -In the above example, the single directory `packages` is used. -This path is relative to where Halibot is being run from. -Alternatively, you could specify the absolute path `/usr/lib/halibot/modules`. - -Halibot ships with an example IRC agent, and an example "Hello world!" module, so if you run Halibot from the installation (repo) directory, the above config should be valid. - -Jump to the section titled **Modules and Agents** below for more information on how these function in the larger Halibot system. - -### `agent-instances` and `module-instances` - -Agents and modules need to be instantiated before they can be used. -Each instance has its own name and configuration -- this means there can be multiple agents and modules of the same type. -Specifying this deviates a bit from what might be considered "normal" JSON. -The `agent-instances` and `module-instances` keys each are mapped to an object, whose keys are the **names** of the relevant Halibot objects. -So, in the above example in `agent-instances`, we mapped the following: - -```json -"irc0": { - "of": "irc:IrcAgent", - "channel": "##example", - "nickname": "ExampleName" -} -``` - -Here, `irc0` is the name, and the object is its configuration. -Agents and modules may specify what configuration is required, but **ALL** objects need and `of` key, which refers to *what* it actually is. -The component of the `of` field before the colon (`:`) specifies the name of the package in which the class the instance is of is located in, -while the component after the colon is the python name of the class within that package. - -Modules operate in the same sense. - -#### Per-agent/module configuration - -Some modules/agents may require certain key-value pairs to be defined to operate correctly. -For example, an `irc:IrcAgent` agent needs a `nickname` string, to use for connection. -This is only for this particular agent instance, so in theory, another instance could be spun up with a different name, and specify a different nickname. - -These keys are module/agent specific, so see the individual documentation for a package on what fields are allowed. diff --git a/doc/ConfigureOptions.md b/doc/ConfigureOptions.md deleted file mode 100644 index b841890..0000000 --- a/doc/ConfigureOptions.md +++ /dev/null @@ -1,116 +0,0 @@ - -It is common for modules and agents to have various options that they want their -users to set in the config.json, but users, being inheritly lazy, do not want to -actually edit a file, so it is nice if they have friendly prompts they can -answer when adding a module or agent to a local Halibot instance. - -This interface enables the object implementer to configure his configuration. - -The Configure class -=================== - -The `add` command will automatically create a class named `Configurer` that exists -on the module or agent class and call the `configure` function on that class. As -long as that class sets up the `config` field of the `Configurer` instance by -the end of the `configure` function, everything will work hunky dory, but in -order to make things easier, Halibot provides a class called `HalConfigurer`, -which you should definitely subclass. - -The `HalConfigurer` class provides a method called `option` which prompts the -user to configure an option. For syntax is as follows: - -```python -HalConfigurer.option(option_type, key, prompt=, default=) -``` - -* `option_type` is an instance of a subclass of `Halibot.Option` that determines - how to validate and prompt for the option. -* `key` is the name of the key in the config to set. If this is not set, the - config object is returned from the function and it is the user's - responsibility to set the appropriate field in `self.config`. Not setting this - is useful for configuring things such as entries of an array within the config. -* `prompt` is the string to prompt the user with. If this is left blank, the - `key` is used. If the `key` is also not set, an error is thrown. -* `default` a default value to use if the option is left blank. If no default - value is given the key in the config is left unset. - -Halibot further provides built-in subclasses of `Halibot.Option` for ease of -use. They are as follows: - -* `Halibot.Option.String` for specifying an string option. -* `Halibot.Option.Int` for specifying an integer option. -* `Halibot.Option.Number` for specifying an real-valued number option. -* `Halibot.Option.Bool` for specifying an boolean option. - -These option types are further made easily available via wrapper functions that -simply wrap to the underlying `HalConfigurer.option`, and are respectively as -follows: - -* `HalConfigurer.optionString(key, prompt=, default=)` -* `HalConfigurer.optionInt(key, prompt=, default=)` -* `HalConfigurer.optionNumber(key, prompt=, default=)` -* `HalConfigurer.optionBool(key, prompt=, default=)` - -The `HalConfigurer` class also has a `valid` function, which consumes the -user-defined configure function automatically and returns True or False -depending on whether or not the currently given value is valid. - -Example module with Configurer ------------------------------- - -```python -import time -from halibot import HalModule, HalConfigurer - -class Responder(HalModule): - - class Configurer(HalConfigurer): - def configure(self): - self.optionString('accept', prompt='Accept string') - self.optionString('return', prompt='Return string') - self.optionInt('delay', prompt='Wait seconds before responding', default=0) - - def message(self, msg): - if 'accept' in self.config and msg.body == self.config['accept']: - time.sleep(self.config.get('delay', 0)) - self.reply(msg, body=self.config['return']) -``` - -The Option class -================ - -The `Option` class allows you to write your own prompting or validation -mechanisms for a specific object type. - -There are methods one can override are: - -* `ask(self)` which prompts the user for a configuration value and returns it -* `configure(self)` which by default wraps to `ask` and checks that the - response given is valid. -* `valid(self, value)` which returns true if the option's value is valid. Always - returns true by default. - -Example custom Option class ---------------------------- - -```python -import halibot - -class OptAlphanumeric(halibot.Option.String): - def validate(self): - return self.value.isalpha() - -class OptEvenInt(halibot.Option.Int): - def validate(self): - return self.value % 2 == 0 - -import os -from tkinter.filedialog import askopenfilename - -class OptFile(halibot.Option.String): - def ask(self): - self.value = askopenfilename() - - def validate(self): - os.path.exists(self.value) -``` diff --git a/doc/HelpInterface.md b/doc/HelpInterface.md deleted file mode 100644 index ff8f035..0000000 --- a/doc/HelpInterface.md +++ /dev/null @@ -1,89 +0,0 @@ -# Halibot Help Text Interface - -Help text is crucial for new users to learn how to operate modules and Halibot itself. -While Halibot core does not require module developers to supply help text, it is greatly encouraged to do so. -This document will define the help text ecosystem that is strongly recommended to adhere to. - -# Sample Usage - -## How to get help text? - -### Via an agent (or "By chatting with it") - -Halibot will provide a "Help" module as a default that needs to be enabled. -This handles the "!help" command, which has the following usage: - -``` -someuser> !help -halibot1> Your friendly bot! Use !help to get specific module help -halibot1> Loaded modules: admin, help, quote - -someuser> !help quote -halibot1> Display quotes from a file. Available commands are !quote, !quoteadd, !quotedel - -someuser> !help quote !quoteadd -halibot1> Add a new quote to the db -halibot1> Call like so: !quoteadd text of new quote here - -``` - -### Via command line - -*do we actually want to support this?* - -## How to implement help text? - -```python -class Quote(HalModule): - ... - def help(args=None): - if args == "!quoteadd": - return "Add a new quote to the db\nCall like so: !quoteadd text of new quote here" - return "Display quotes from a file. Available commands are !quote, !quoteadd, !quotedel" -``` - -That's it! -Nothing else needs to be done to support text through the !help interface. - - -# Specification - -## Help Module - -The Help module is fairly simple at its core. -It handles commands of the form `!help [module] [module topic]` - -If nothing is supplied, it responds with a generic greeting, instructing the user to call again with `!help [module]` to learn more about specific modules. -It also should return a list of the currently loaded modules (which should also be available via the `admin` module's `!modules` command). - -When only `module` is supplied (e.g. `!help quote`), the help module attempts to call the `help()` method on the selected module. -So in the example of the `quote` module, `!help quote` will find the module named `quote`, and call `.help()` on it via the `.invoke()` feature (see ModuleInteractivity.md for more on `.invoke()`). -If the specified module does not implement a `.help()` method, an apologetic error message is return instead. - -If there are more than two words (separated by spaces) supplied to `!help`, the first word is considered to be module to inquire about, and everything after that space is considered supplementary text to query the module. -This supplementary text is passed into the module's `.help()` method, where it may or may not decide to respond differently. -Again, using the above example, `!help quote !quoteadd` invokes `quote`'s `.help()` method with the argument `"!quoteadd"`. -See the next section for recommendations on handling help topics. - -## The `.help()` method - -Help text on a module at its core, is just a function named `help` that returns a string. -All arguments to `.help()` should be treated as a string, and `.help()` should ONLY return a string. -Arguments may be omitted (as in, nonexistant, not just passing `None` or `""`), and thus the method should be able to handle either case. -Arguments do not need to be handled; the help method may choose to ignore any arguments. -Returned help text does not need to be deterministic, help methods may choose to modify the text based on the current running state. - -## Tips for implementing a `.help()`. - -### Output Formatting - -As a general rule, try to avoid help text longer than three lines. -Most users will likely be coming from some kind of chat protocol like IRC or XMPP, therefore long messages could trigger a server rate limit. - -### Command Modules - -Modules that utilize a `!command` should probably list the available commands. -If using a dictionary to map command strings to functions, the keys of that dictionary can be conveniently used as a list. - -Command modules should probably support both prefixed (`!command`) and non-prefixed (`command`) topics, for ease of use. - diff --git a/doc/MessageFormat.md b/doc/MessageFormat.md deleted file mode 100644 index 1da0d75..0000000 --- a/doc/MessageFormat.md +++ /dev/null @@ -1,57 +0,0 @@ -# Halibot Message Format - -Internally, Halibot functions by translating messages from various protocols into a generic format. -These translated messages are then handed to the modules. -Thanks to the protocol-agnostic format, modules do not need to know anything about how these modules were received. -Therefore, a module that works in an IRC room should also work in XMPP. - -# The Specification - -Messages are received as an object that works like a javascript-like dictionary, either `msg.body` or `msg["body"]` is acceptable for accessing. -The following are top-level attributes: - - - `body`: The data being sent. In most cases, this is the text of the message. - - `type`: Type of message, determines what may be in `body`. - - `author`: A short name for who sent the message. - - `identity`: A string that uniquely identifies a user. - - `origin`: Where this message originated from. - - `target`: Where this message is intended to go. - - `misc`: Unspecified, miscellaneous data. Typically agent-specific. - -## Body (*string*) - -The body of the message is simply the data that is being passed along. -See the **type** section below for what can be expected in this section. -This will always be a `str` object, and can be empty (`""`). - -## Type (*string*) - -The type of message should be set to reflect the kind of message being passed along. -The valid types of messages are: - -| Type String | Description | What is in `body` | -| ----------- | ----------------------------------- | ----------------- | -| `simple` | Plain text message sent from a user | Plain text | - -## Author (*string*) - -The `author` field is a short, sensible name to address the sender of a message by. -This is typically the nickname of a user. -This should *not* be a fully qualified user name if possible. -Using XMPP as an example, the `author` for the JID `foo@bar.baz.server.com` should just be something like `foo`. -This string should never be empty. -Cases where an author is not specified (like server messages) are to be documented in the agent protocol specifications. - -## Identity (*string*) - -Unlike `author`, this is a field that is *not* designed to fit into conversation. -Instead, this field carries uniquely identifying information about the `author`. -This should be used by modules for permissions verification, per-user behavior, and so on **instead** of `author`. -As `author` may be a changeable nickname, this provides a handle to a user that is static. -Using XMPP as an example again, the nickname in a MUC may be `foobar`, but the `msg.identity` is still `foo@bar.baz.server.com`. -Unlike `author`, however, this may be an empty string (`""`), as some protocols support anonymous users (e.g. IRC). -The permissions system uses `msg.identity` under the hood *by design*, to prevent spoofing via nickname change. - -## Origin (*string*) -## Target (*string*) -## Misc (*dict*) diff --git a/doc/ModuleInteractivity.md b/doc/ModuleInteractivity.md deleted file mode 100644 index c3d619e..0000000 --- a/doc/ModuleInteractivity.md +++ /dev/null @@ -1,103 +0,0 @@ -# Module Interactivity - -Modules are intentionally not closed off from one another so that they may share resources or provide services. -In many cases, it doesn't make sense to duplicate effort across various modules. -Thus, Halibot core provides an `invoke` method on all HalObjects that can be used to simplify the process. -This document will use calling a method on a module as an example, but the exact same interface can be used for calling a method from/on an agent. - -# HalModule Invoke API Usage - -## Overview - -Module interactivity is dependent on two things: - - Instance name of target module - - Name of method to call - -The instance name needs to be provided to ensure that the right module is used. -It is recommend to include the module instance name as a configuration option, so that it can be changed easily. -If the instance name is not found, then an `KeyError` exception will be thrown. - -The method name must also be exactly as it appears in the module's source. -It is recommended to document all methods that are intended to be exposed for interactive use in the respective module's README. -If the method is not found on the target module's instantiation (object), then a `NameError` exception will be thrown. - -## Examples - -Consider the following example module. -This module will be used as the basis for the following examples, instantiated under the name "hello". - -```python -class Hello(HalModule): - - # Regular method that only returns a string - def sayHello(self): - return "Hello!" - - # Method with one mandatory argument - def sayHelloToUser(self, user): - return "Hello {}!".format(user) - - # Method with an optional argument - def sayHelloOrNot(self, user=""): - return self.sayHelloToUser(user) if user else self.sayHello() - -``` - -### Example \#1: No arguments - -```python -class Respond(HalModule): - - def receive(self, msg): - if msg.body == "!hello": - resp = self.invoke("hello","sayHello") - self.reply(msg, body=resp) -``` - -This calls the `sayHello()` method on the `Hello` module instantiated as `hello`. -Note there are no protections here, so if the `hello` module is either renamed or removed, an exception will be uncaught. -While Halibot can handle the uncaught exceptions, it is generally not a good idea to let those go. - -This is a better version of the same implementation: - -```python -class Respond(HalModule): - - def receive(self, msg): - if msg.body == "!hello": - try: - resp = self.invoke("hello","sayHello") - except NameError: - resp = "Could not find .sayHello()" - except KeyError: - resp = "Cound not find 'hello' module" - self.reply(msg, body=resp) -``` -This is a bit more unwieldy, but this provides some useful feedback to debug the problem, and does not clutter the log with unhandled exceptions. - -### Example \#2: Positional Arguments - -The `.invoke()` method will relay positional arguments, so there are two forms that this can take: - -```python -resp = self.invoke("hello","sayHelloToUser", msg.author) -# resp -> "Hello {author}!" - - -resp = self.invoke("hello","sayHelloToUser", *(msg.author)) -# resp -> "Hello {author}!" -``` - -Arguments can be passed either as additional arguments to the `.invoke()` function, or as a tuple using the * operator. -Either are valid, and can be used to suit the use case. - -### Example \#3: Optional Arguments - -```python -resp = self.invoke("hello","sayHelloOrNot") -# resp -> "Hello!" - - -resp = self.invoke("hello","sayHelloOrNot", user=msg.author) -# resp -> "Hello {author}!" -``` diff --git a/halibot/__init__.py b/halibot/__init__.py deleted file mode 100644 index abdf79d..0000000 --- a/halibot/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .halibot import Halibot, Version -from .halagent import HalAgent -from .halmodule import HalModule -from .halobject import HalObject, SyncSendSelfException -from .halconfigurer import HalConfigurer -from .message import Message -from .commandmodule import CommandModule, AsArgs diff --git a/halibot/commandmodule.py b/halibot/commandmodule.py deleted file mode 100644 index 3f58590..0000000 --- a/halibot/commandmodule.py +++ /dev/null @@ -1,57 +0,0 @@ -# -# CommandModule -# Subclass of HalModule that does the heavy lifting of commands for you -# -from . import HalModule - -# Decorator for converting the string stripped of the command to an argument list -def AsArgs(func): - def wrapper(self, string, msg=None): - args = string.split(" ") - func(self, args, msg=msg) - return wrapper - -class CommandModule(HalModule): - - # Subclasses should implement a commands table like the following. - # Note, the "values" should be methods on the CommandModule-derived class - # Forgoing this is inadvisable (e.g. loss of _hal reference) - # "command" -> func(self, string, msg=msg) - # string: by default, will be the message not split into arguments - # use the annotation AsArgs above to override this - # msg: not needed in all commands, the message object as received - - def __init__(self, hal, conf={}): - super().__init__(hal, conf=conf) - - self.commands = {} - # Get the command prefix if defined, otherwise use "!" as default - self.prefix = self._hal.config.get("command_prefix", "!") - - # Override only if you know what you are doing! - def receive(self, msg): - self._cmd_receive(msg) - - # Actually does the command handling logic. Separate callable so multi-inheritance - # can work nicely (maybe) - def _cmd_receive(self, msg): - body = msg.body.split(" ", 1) - if body and not body[0].startswith(self.prefix): - self.default(msg) - return - - # Ugly fix for commands without args - if len(body) == 1: - body.append("") - - # This maybe should prevent a empty string as a key... - func = self.commands.get(body[0][1:]) - - if func: - func(body[1], msg=msg) - else: - self.default(msg) - - # Override this to provide some functionality if there is no match in the table - def default(self, msg): - pass diff --git a/halibot/halagent.py b/halibot/halagent.py deleted file mode 100644 index 7c6ddd9..0000000 --- a/halibot/halagent.py +++ /dev/null @@ -1,15 +0,0 @@ -from .halobject import HalObject - -class HalAgent(HalObject): - - def dispatch(self, msg): - out = self.config.get('out', self._hal.objects.modules.keys()) - self.send_to(msg, out) - - def connect(self, to): - # FIXME Don't modify the config like this? - if 'out' in self.config: - self.config['out'].append(to.name) - else: - self.config['out'] = [ to.name ] - diff --git a/halibot/halauth.py b/halibot/halauth.py deleted file mode 100644 index 00cbf8f..0000000 --- a/halibot/halauth.py +++ /dev/null @@ -1,77 +0,0 @@ -import json -import logging - -def hasPermission(perm, reply=False): - def real_dec(func): - def wrapper(self, msg, *args, **kwargs): - if self._hal.auth.hasPermission(msg.origin, msg.identity, perm): - func(self, msg, *args, **kwargs) - elif reply: - self.reply(msg, body="Permission Denied") - return wrapper - return real_dec - -class HalAuth(): - - def __init__(self): - self.perms = [] - self.enabled = False - self.log = logging.getLogger("Auth") - - # Load permission file, and set to enabled - def load_perms(self, path): - self.path = path - - try: - with open(self.path, "r") as f: - temp = json.loads(f.read()) - - # Roll back into triple, also technically validates format - self.perms = [(a,b,c) for a,b,c in temp] - - except Exception as e: - self.log.error("Error loading permissions: {}".format(e)) - # Return if can't find auth? - - self.enabled = True - - # Write permissions back to the file that was originally loaded - def write_perms(self): - try: - temp = [list(l) for l in self.perms] - - with open(self.path, "w") as f: - f.write(json.dumps(temp, indent=4)) - except Exception as e: # pragma: no cover - self.log.error("Error storing permissions: {}".format(e)) - - def grantPermission(self, ri, identity, perm): - if not self.enabled: - return - - t = (ri, identity, perm) - if t not in self.perms: - self.perms.append(t) - - def revokePermission(self, ri, identity, perm): - if not self.enabled: - return - - try: - self.perms.remove((ri,identity, perm)) - except Exception as e: - self.log.error("Revocation failed: {}".format(e)) - - def hasPermission(self, ri, identity, perm): - if not self.enabled: - return True - - def tester(x): - a,b,c = x - return a in (ri, "*") and b in (identity, "*") and c in (perm, "*") - - # Return True on the first successful perm match - for l in self.perms: - if tester(l): - return True - return False diff --git a/halibot/halconfigurer.py b/halibot/halconfigurer.py deleted file mode 100644 index 60a00e5..0000000 --- a/halibot/halconfigurer.py +++ /dev/null @@ -1,87 +0,0 @@ -get_input = input - -class Option(): - def __init__(self, key, prompt=None, default=None): - self.key = key - self.prompt = prompt if prompt != None else key - self.default = default - - def ask(self): - prompt = self.prompt - if self.default != None: - prompt += ' [' + str(self.default) + ']' - prompt += ': ' - - v = get_input(prompt) - if v == '': return self.default - return v - - def configure(self): - while True: - try: - v = self.ask() - except ValueError: - continue - if self.valid(): - break - return v - - def valid(self): - return True - -# Builtin option classes -class StringOption(Option): - pass - -class IntOption(Option): - def ask(self): - return int(super().ask()) - -class NumberOption(Option): - def ask(self): - return float(super().ask()) - -class BooleanOption(Option): - def ask(self): - v = super().ask() - if isinstance(v, str): - if v.lower() == 'true': return True - if v.lower() == 'false': return False - raise ValueError() - return v - -Option.String = StringOption -Option.Int = IntOption -Option.Number = NumberOption -Option.Boolean = BooleanOption - -class HalConfigurer(): - - def __init__(self, options={}): - self.options = options - - def option(self, option_type, key, **kwargs): - opt = option_type(key, **kwargs) - - # Override the default if the option is already set - if key in self.options: - opt.default = self.options[key] - - val = opt.configure() - if val != None: - self.options[key] = val - - def optionString(self, key, **kwargs): - self.option(Option.String, key, **kwargs) - - def optionInt(self, key, **kwargs): - self.option(Option.Int, key, **kwargs) - - def optionNumber(self, key, **kwargs): - self.option(Option.Number, key, **kwargs) - - def optionBoolean(self, key, **kwargs): - self.option(Option.Boolean, key, **kwargs) - - def configure(self): - pass # pragma: no cover diff --git a/halibot/halibot.py b/halibot/halibot.py deleted file mode 100644 index e0830be..0000000 --- a/halibot/halibot.py +++ /dev/null @@ -1,151 +0,0 @@ -# -# Main bot class -# Handles routing, config, agent/module loading -# -import json -import threading -import os, sys -import importlib -import halibot.packages -from distutils.version import StrictVersion as Version -from queue import Queue,Empty -from .halmodule import HalModule -from .halagent import HalAgent -from .halauth import HalAuth - -# Avoid appending "." if it i -if "." not in sys.path: - sys.path.append(".") -import logging - -class ObjectDict(dict): - - @property - def modules(self): - return dict(filter(lambda x: isinstance(x[1], HalModule), self.items())) - - @property - def agents(self): - return dict(filter(lambda x: isinstance(x[1], HalAgent), self.items())) - - -class Halibot(): - - VERSION = "0.1.0" - - config = {} - - running = False - log = None - - def __init__(self, **kwargs): - self.log = logging.getLogger(self.__class__.__name__) - - self.use_config = kwargs.get("use_config", True) - self.use_auth = kwargs.get("use_auth", True) - - self.auth = HalAuth() - self.objects = ObjectDict() - - # Start the Hal instance - def start(self, block=True): - self.running = True - - if self.use_config: - self._load_config() - self._instantiate_objects("agent") - self._instantiate_objects("module") - if self.use_auth: - self.auth.load_perms(self.config.get("auth-path","permissions.json")) - - def shutdown(self): - self.log.info("Shutting down halibot..."); - - for o in self.objects.values(): - o._shutdown() - - self.log.info("Halibot shutdown. Threads left: " + str(threading.active_count())) - - def _check_version(self, obj): - v = Version(self.VERSION) - if not hasattr(obj, "HAL_MINIMUM"): - self.log.warn("Module class '{}' does not define a minimum version, trying to load anyway...".format(obj.__class__.__name__)) - return True - - if v < Version(obj.HAL_MINIMUM): - self.log.error("Rejecting load of '{}', requires minimum Halibot version '{}'. (Currently running '{}')".format(obj.__class__.__name__, obj.HAL_MINIMUM, self.VERSION)) - return False - - if hasattr(obj, "HAL_MAXIMUM"): - if v >= Version(obj.HAL_MAXIMUM): - self.log.error("Rejecting load of '{}', requires maximum Halibot version '{}'. (Currently running '{}')".format(obj.__class__.__name__, obj.HAL_MAXIMUM, self.VERSION)) - return False - return True - - def add_instance(self, name, inst): - self.objects[name] = inst - inst.name = name - inst.init() - self.log.info("Instantiated object '" + name + "'") - - def _load_config(self): - with open("config.json","r") as f: - self.config = json.loads(f.read()) - halibot.packages.__path__ = self.config.get("package-path", []) - - def _get_class_from_package(self, pkgname, clsname): - pkg = self.get_package(pkgname) - if pkg == None: - self.log.error("Cannot find package {}!".format(pkgname)) - return None - - obj = getattr(pkg, clsname, None) - if obj == None: - self.log.error("Cannot find class {} in package {}!".format(clsname, pkgname)) - return None - - return obj - - # Load a halobject from a package descriptor (e.g. hello:Hello), but does NOT - # start or add it to the current instance. - def load_object(self, pkg, conf={}): - split = pkg.split(":") - - if len(split) == 2: - obj = self._get_class_from_package(*split) - if obj and self._check_version(obj): - return obj(self, conf=conf) - else: - self.log.error("Invalid class identifier {}, must contain only 1 ':'".format(conf["of"])) - return None - - def _instantiate_objects(self, key): - inst = self.config[key + "-instances"] - - for k in inst.keys(): - conf = inst[k] - obj = self.load_object(conf["of"], conf=conf) - - if obj: - self.add_instance(k, obj) - - def get_package(self, name): - return importlib.import_module('halibot.packages.' + name) - - def reload(self, name): - parent = 'halibot.packages.' + name - for k,o in self.objects.items(): - if o.__module__.startswith(parent + '.') or o.__module__ == parent: - o._shutdown() - mod = importlib.reload(importlib.import_module(o.__module__)) - cls = getattr(mod, o.__class__.__name__) - self.add_instance(k, cls(self, o.config)) - - # Restart a module instance by name - def restart(self, name): - o = self.objects.get(name) - if o: - o.shutdown() - o.init() - else: - self.log.warning("Failed to restart instance '{}'".format(name)) diff --git a/halibot/halmodule.py b/halibot/halmodule.py deleted file mode 100644 index 56facbe..0000000 --- a/halibot/halmodule.py +++ /dev/null @@ -1,22 +0,0 @@ -from .halobject import HalObject -from .message import Message - -class HalModule(HalObject): - - def reply(self, msg0=None, **kwargs): - # Create the reply message - body = kwargs.get('body', msg0.body) - mtype = kwargs.get('type', msg0.type) - author = kwargs.get('author', msg0.author) - origin = kwargs.get('origin', self.name) - - msg = Message(body=body, type=mtype, author=author, origin=origin) - - # Synchronous reply? - if msg0.sync: - self.sync_replies[msg0.uuid].append(msg) - else: - self.send_to(msg, [ msg0.origin ]) - - def hasPermission(self, msg, perm): - return self._hal.auth.hasPermission(msg.origin, msg.identity, perm) diff --git a/halibot/halobject.py b/halibot/halobject.py deleted file mode 100644 index b0531cb..0000000 --- a/halibot/halobject.py +++ /dev/null @@ -1,123 +0,0 @@ -import logging -import asyncio -import inspect -import copy -from threading import Thread -from collections import defaultdict -from .halconfigurer import HalConfigurer - -class SyncSendSelfException(Exception): 'Cannot sync_send_to oneself.' - -class HalObject(): - - def __init__(self, hal, conf={}): - self._hal = hal - self.config = conf - self.log = logging.getLogger(self.__class__.__name__) # TODO: Put instantiated name in here too - - # Only used in HalModule.reply right now, but accessed on potentially any - # HalObject, so it exists on every HalObject to avoid attribute errors - self.sync_replies = defaultdict(lambda: []) # UUID -> [Message, ...] - - self.eventloop = asyncio.SelectorEventLoop() - self._thread = Thread(target=self._run_eventloop) - self._thread.start() - - def _run_eventloop(self): - self.eventloop.run_forever() - self.eventloop.close() - - def _shutdown(self): - self.eventloop.call_soon_threadsafe(self.eventloop.stop) - self._thread.join() - self.shutdown() - - def _queue_msg(self, msg): - fut = asyncio.run_coroutine_threadsafe(self._receive(msg), self.eventloop) - return fut - - def init(self): - pass - - def shutdown(self): - pass - - def send_to(self, msg, dests): - # Set origin for those who have not set it manually - if msg.origin == None: - msg.origin = self.name - - ret = {} - for ri in dests: - name = ri.split('/')[0] - to = self._hal.objects.get(name) - if to: - nmsg = copy.copy(msg) - nmsg.target = ri - ret[ri] = to._queue_msg(nmsg) - else: - self.log.warning('Unknown module/agent: ' + str(name)) - return ret - - def sync_send_to(self, msg, dests): - # Check for potential deadlocks - for ri in dests: - if ri.split('/')[0] == self.name: - raise SyncSendSelfException - - msg.sync = True - - futs = self.send_to(msg, dests) - - r = {} - for name, fut in futs.items(): - fut.result() - to = self._hal.objects.get(name) - if to and msg.uuid in to.sync_replies: - # Assure that the module was not removed in the interim - r[name] = to.sync_replies.pop(msg.uuid) - return r - - async def _receive(self, msg): - try: - fname = 'receive_' + msg.type - if hasattr(self, fname) and callable(getattr(self, fname)): - # Type specific receive function - getattr(self, fname)(msg) - else: - # Generic receive function - self.receive(msg) - except Exception as e: - self.log.error("Exception in message receive", exc_info=True) - - def receive(self, msg): - pass - - def receive_help(self, msg): - if hasattr(self, 'topics'): - if msg.body == []: - # Retrieve available topics - self.reply(msg, body=list(self.topics.keys())) - else: - # Retrieve help text for topic - key = '/'.join(msg.body) - if key in self.topics: - t = self.topics[key] - self.reply(msg, body=t() if callable(t) else t) - - class Configurer(HalConfigurer): - pass - - # Configures an instance of this class based on the 'options' attribute - @classmethod - def configure(cls, conf, name=None): - if name == None: - name = input('Enter instance name: ') - - configurer = cls.Configurer(options=conf) - configurer.configure() - - return name, configurer.options - - def invoke(self, inst, method, *args, **kwargs): - return getattr(self._hal.objects[inst], method)(*args, **kwargs) diff --git a/halibot/jsdict.py b/halibot/jsdict.py deleted file mode 100644 index b4a7f79..0000000 --- a/halibot/jsdict.py +++ /dev/null @@ -1,7 +0,0 @@ - -class jsdict(dict): - def __getattr__(self, name): - return self[name] - - def __setattr__(self, name, value): - self[name] = value diff --git a/halibot/message.py b/halibot/message.py deleted file mode 100644 index a87befd..0000000 --- a/halibot/message.py +++ /dev/null @@ -1,21 +0,0 @@ -import logging -import uuid -from .jsdict import jsdict - -class Message(): - - def __init__(self, **kwargs): - self.log = logging.getLogger(self.__class__.__name__) - self.uuid = uuid.uuid4() - self.sync = False - - self.body = kwargs.get('body', None) - self.type = kwargs.get('type', 'simple') - self.author = kwargs.get('author', None) - self.identity = kwargs.get('identity', None) - self.origin = kwargs.get('origin', None) - self.misc = kwargs.get('misc', jsdict()) - self.target = kwargs.get('target', '') - - def whom(self): - return '/'.join(self.target.split('/')[1:]) diff --git a/halibot/packages/__init__.py b/halibot/packages/__init__.py deleted file mode 100644 index 2c87dd7..0000000 --- a/halibot/packages/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__path__ = [] diff --git a/install.sh b/install.sh deleted file mode 100755 index c4384e4..0000000 --- a/install.sh +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env bash -# -# *nix halibot installer - -BINLOC=/usr/bin/halibot -SRCLOC=/usr/local/share/halibot -VERBOSE= - -# Detect existing installation and override default (BIN|SRC)LOC if already installed -if command -v halibot 2>&1 >/dev/null; then - BINLOC=$(command -v halibot) - SRCLOC=$(grep SRCLOC= $BINLOC) # THere should be a SRCLOC=... line in the run script - SRCLOC=${SRCLOC/SRCLOC=/} # Remove the SRCLOC= -fi - -# Define functions -function uninstall { - rm -f $VERBOSE $BINLOC && - rm -rf $VERBOSE $SRCLOC -} - -function install { - # Sed was being fussy on FreeBSD, used ed, the one true text editor - printf "1a\nSRCLOC=$SRCLOC\n.\n,p\n" | ed -s run-scripts/halibot > $BINLOC && - chmod +x $BINLOC && - - mkdir -p $SRCLOC && - cp $VERBOSE main.py $SRCLOC && - cp -r $VERBOSE halibot $SRCLOC/halibot && - cp -r $VERBOSE packages $SRCLOC/packages -} - -# Handle arguments -while getopts huvb:s: opt; do - case "${opt}" in - h) cat < 2: - print("Invalid class path '{}', expected no more than 1 colon (:).".format(clspath)) - continue - - pkg = bot.get_package(split[0]) - if pkg == None: - print("Cannot find package '{}'.".format(split[0])) - continue - - if len(split) == 1: - if not hasattr(pkg, "Default"): - print("Package '{}' has no default class, must specify the class to add explicitly.".format(split[0])) - continue - cls = pkg.Default - clspath += ":Default" - else: - cls = getattr(pkg, split[1], None) - if cls == None: - print("Class '{}' does not exist on package '{}'.".format(split[1], split[0])) - continue - - if args.destkey: - destkey = args.destkey - else: - if issubclass(cls, halibot.HalModule): - destkey = "module-instances" - elif issubclass(cls, halibot.HalAgent): - destkey = "agent-instances" - else: - print("Cannot determine if '{}' is a module or agent, use '-m' or '-a'.") - continue - - (name, conf) = cls.configure({ 'of': clspath }) - - if name in bot.config["agent-instances"] or name in bot.config["module-instances"]: - print("Instance name '{}' is already in configuration, please choose a different instance name".format(name)) - return - - bot.config[destkey][name] = conf - - with open("config.json","w") as f: - f.write(json.dumps(bot.config, sort_keys=True, indent=4)) - -def h_rm(args): - # In order to access the config easily - bot = halibot.Halibot() - bot._load_config() - - for name in args.names: - if name in bot.config["agent-instances"]: - bot.config["agent-instances"].pop(name) - elif name in bot.config["module-instances"]: - bot.config["module-instances"].pop(name) - else: - print("No such object '{}'".format(name)) - continue - print("Removed '{}'.".format(name)) - - with open("config.json", "w") as f: - f.write(json.dumps(bot.config, sort_keys=True, indent=4)) - -def h_config(args): - # In order to access the config easily - bot = halibot.Halibot() - bot._load_config() - - if args.name in bot.config["agent-instances"]: - destkey = "agent-instances" - elif args.name in bot.config["module-instances"]: - destkey = "module-instances" - else: - print('No such module or agent exists.') - return - pkgconf = bot.config[destkey][args.name] - - # Show or edit the config? - if args.show: - # Only a single key? - if args.key != None: - print(pkgconf[args.key]) - else: - for k in pkgconf: - print(k, "=", pkgconf[k]) - else: - # Reconfigure the package - if args.key != None: - if args.value == None: - print("You must specify a value with -v.") - return - - # Detect the type to set - if args.type: - ty = args.type - else: - if args.key in pkgconf: - typecls = type(pkgconf[args.key]) - if typecls == int: ty = "number" - elif typecls == float: ty = "number" - elif typecls == str: ty = "string" - elif typecls == bool: ty = "boolean" - else: - print("The type of the key '" + args.key + "' is not a settable type.") - return - else: - print("That key does not exist, you must specify the type with -t.") - - # Get the value - if ty == "string": value = args.value - if ty == "number": value = float(args.value) - if ty == "boolean": - if args.value.lower() == 'true': value = True - elif args.value.lower() == 'false': value = False - else: - print("Invalid boolean value, specify 'true' or 'false'.") - return - - pkgconf[args.key] = value - else: - if not "of" in pkgconf: - print("Corrupt config, package has no 'of' key.") - return - - split = pkgconf["of"].split(":") - if len(split) != 2: - print("Corrupt config, malformed 'of' value."); - return - - pkg = bot.get_package(split[0]) - if pkg == None: - print("The package '" + split[0] + "' does not exist so it cannot be configured.") - return - - cls = getattr(pkg, split[1], None) - if not cls: - print("The package '" + split[0] + "' has no module or agent named '" + split[1] + "'.") - return - - - (name, conf) = cls.configure(pkgconf, name=args.name) - bot.config[destkey][name] = conf - - with open("config.json", "w") as f: - f.write(json.dumps(bot.config, sort_keys=True, indent=4)) - -if __name__ == "__main__": - subcmds = { - "init": h_init, - "run": h_run, - "fetch": h_fetch, - "unfetch": h_unfetch, - "add": h_add, - "rm": h_rm, - "packages": h_list_packages, - "search": h_search, - "config": h_config, - } - - # Setup argument parsing - parser = argparse.ArgumentParser(description="The world's greatest saltwater chat bot!") - - sub = parser.add_subparsers(title="commands", dest="cmd", metavar="COMMAND") - - init = sub.add_parser("init", help="initialize a new local halibot instance") - init.add_argument("path", help="directory path to initialize the halibot instance in", nargs="?", default=".") - - run = sub.add_parser("run", help="run the local halibot instance") - run.add_argument("-i", "--interactive", help="enter a python shell after starting halibot", action="store_true", required=False) - run.add_argument("-f", "--log-file", help="file to output logs to, none by default") - run.add_argument("-l", "--log-level", help="level of logs, DEBUG by default") - - fetch = sub.add_parser("fetch", help="fetch remote packages") - fetch.add_argument("packages", help="name of package to fetch", nargs="+", metavar="package") - - unfetch = sub.add_parser("unfetch", help="as if you never fetched them at all") - unfetch.add_argument("packages", help="name of package to unfetch", nargs="+", metavar="package") - - add = sub.add_parser("add", help="add agents or modules to the local halibot instance") - add.add_argument("things", help="path to class to add", nargs="+", metavar="class") - addtype = add.add_mutually_exclusive_group() - addtype.add_argument("-a", "--agent", dest="destkey", action="store_const", const="agent-instances", help="add instances as agents") - addtype.add_argument("-m", "--module", dest="destkey", action="store_const", const="module-instances", help="add instances as modules") - - rm = sub.add_parser("rm", help="remove agents or modules from the local halibot instance") - rm.add_argument("names", help="names of agents or modules to remove", nargs="+", metavar="name") - - list_packages = sub.add_parser("packages", help="list all installed packages") - - search = sub.add_parser("search", help="search for packages") - search.add_argument("term", help="what to search for", nargs="?", metavar="term") - - config_cmd = sub.add_parser("config", help="configure or reconfigure a module or agent") - config_cmd.add_argument("name", help="name of the module or agent to show or reconfigure") - config_cmd.add_argument("-s", "--show", action="store_true", help="show the configuration rather than set it", required=False) - config_cmd.add_argument("-k", "--key", help="key to set or key to display with -s", required=False) - config_cmd.add_argument("-v", "--value", help="value to set key to", required=False) - config_cmd.add_argument("-t", "--type", choices=["string", "number", "boolean"], help="the type used while setting a config value with -k. If not given, it uses the type of the existing value") - - args = parser.parse_args() - - # Try to run a subcommand - if args.cmd != None: - subcmds[args.cmd](args) - else: - parser.print_help() diff --git a/packages/container/__init__.py b/packages/container/__init__.py deleted file mode 100644 index cd7099f..0000000 --- a/packages/container/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .modulecontainer import ModuleContainer as Modules -from .modulecontainer import ModuleContainer as Agents diff --git a/packages/container/agentcontainer.py b/packages/container/agentcontainer.py deleted file mode 100644 index 6553dfa..0000000 --- a/packages/container/agentcontainer.py +++ /dev/null @@ -1,9 +0,0 @@ -from halibot import HalAgent - -class AgentContainer(HalAgent): - - def receive(self, msg): - suff = '/' + msg.whom() if msg.whom() != '' else '' - cont = self.config.get('contents', []) - self.send_to(msg, [ name + suff for name in cont ]) - diff --git a/packages/container/modulecontainer.py b/packages/container/modulecontainer.py deleted file mode 100644 index 55e1f97..0000000 --- a/packages/container/modulecontainer.py +++ /dev/null @@ -1,9 +0,0 @@ -from halibot import HalModule - -class ModuleContainer(HalModule): - - def receive(self, msg): - suff = '/' + msg.whom() if msg.whom() != '' else '' - cont = self.config.get('contents', []) - self.send_to(msg, [ name + suff for name in cont ]) - diff --git a/packages/core/__init__.py b/packages/core/__init__.py deleted file mode 100644 index 9b350ed..0000000 --- a/packages/core/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ - -from .help import Help diff --git a/packages/core/help.py b/packages/core/help.py deleted file mode 100644 index 002641f..0000000 --- a/packages/core/help.py +++ /dev/null @@ -1,71 +0,0 @@ -# -# HelpModule -# -from halibot import CommandModule, Message - -class Help(CommandModule): - - def init(self): - self.commands = { - 'help' : self.command - } - - def general_help(self, target): - hmsg = Message(body=[], type='help', origin=self.name) - replies = self.sync_send_to(hmsg, target) - - text = '''The help command gives useful help messages. - -Syntax: - !help [ ...] - -Available topics: -''' - # Collect the available topics - topics = [] - for r in replies.values(): - topics += [t for t in r[0].body if not t in topics] - - # Append the available topics to the list - c = 2 - line = ' ' - max_column = 80 - first = True - for t in topics: - if c + len(t) + 2 > max_column: - text += line + '\n' - c = 2 - line = ' ' - first = True - - if first: - first = False - else: - line += ', ' - c += 2 - - line += t - c += len(t) - - # Append final line - if line.strip() != '': - text += line + '\n' - - return text - - def command(self, args, msg=None): - # TODO When containers/routes become more integrated, just look at the - # container the message was sent to, which shoudl work for the - # default container as well. - target = self.config.get('target', self._hal.objects.modules.keys()) - target = [t for t in target if t.split('/')[0] != self.name] # Can't sync send to this module - - if args == '': - self.reply(msg, body=self.general_help(target)) - else: - hmsg = Message(body=args.split(' '), type='help', origin=self.name) - replies = self.sync_send_to(hmsg, target) - - if len(replies) > 0: - # TODO check for discrepancies among replies - self.reply(msg, body=list(replies.values())[0][0].body) diff --git a/packages/hello/__init__.py b/packages/hello/__init__.py deleted file mode 100644 index a69f0ad..0000000 --- a/packages/hello/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .hello import Hello -Default = Hello diff --git a/packages/hello/config.json b/packages/hello/config.json deleted file mode 100644 index 26c653c..0000000 --- a/packages/hello/config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "main": "hello.py:Hello" -} diff --git a/packages/hello/hello.py b/packages/hello/hello.py deleted file mode 100644 index 49a04d4..0000000 --- a/packages/hello/hello.py +++ /dev/null @@ -1,42 +0,0 @@ -# -# Hello World reference Halibot Module -# Replies to any message starting with "!hello" -# -from halibot import HalModule, Message - -# Hello Module -# Upon receiving a message from any source, checks if the body of that message -# starts with the literal string "!quote" -# If so, responds with "Hello World!" via the same way the message was received -class Hello(HalModule): - - # Module version strings - # These should be defined so that Halibot can safety-check modules prior to - # loading that may use unimplemented or deprecated features - # VERSION - what is the version of this module (for reference), using semver. - # HAL_MINIMUM - minimum version of Halibot core that this module will work with - # HAL_MAXIMUM - (optional) reject Halibot core versions newer than this. - - VERSION = "1.0.0" - HAL_MINIMUM = "0.1" - - # Called when the module is initialized - # Put any initialization logic here, instead of __init__() - # In this case, no initialization is needed, thus it is a no-op - # This is already defined in HalModule, so it can be excluded if not needed - def init(self): - pass - - # Called when a new message from the Halibot base is received - # The `msg` argument is in the standard Halibot message format, regardless of the actual source - # Thus, even it was from IRC, XMPP, etc, it should have keys like "body", for the actual text. - # To where it comes from, check the "context" field. - def receive(self, msg): - # The "body" field should always be populated, thus this is a safe assumption.(otherwise, an agent isn't working properly! - if msg.body.startswith("!hello"): - # Send a message back to the sender, using the same method that was used to receive it - self.reply(msg, body="Hello world!") - - # NOTE: The HalModule also includes a send() method, which reply() calls out to - # reply() is the same as send()'ing a copy of the received message, with a new body - # Do NOT override send() or reply(), they are already implemented for you. diff --git a/run-scripts/halibot b/run-scripts/halibot deleted file mode 100755 index 85b50d7..0000000 --- a/run-scripts/halibot +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -command -v python 2>&1 >/dev/null && { PYTHON=python; } -command -v python3 2>&1 >/dev/null && { PYTHON=python3; } -command -v python3.5 2>&1 >/dev/null && { PYTHON=python3.5; } -command -v python3.6 2>&1 >/dev/null && { PYTHON=python3.6; } -$PYTHON $SRCLOC/main.py $@ diff --git a/test.sh b/test.sh deleted file mode 100755 index c3e4e98..0000000 --- a/test.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh - -set -e - -command -v python 2>&1 >/dev/null && { PYTHON=python; } -command -v python3 2>&1 >/dev/null && { PYTHON=python3; } -command -v coverage 2>&1 >/dev/null && { coverage erase; PYTHON="coverage run -a --source=. --omit=main.py"; } - -for TEST in tests/test_*.py -do - PYTHONPATH=. $PYTHON $TEST -done diff --git a/tests/test_auth.py b/tests/test_auth.py deleted file mode 100644 index 4d194b6..0000000 --- a/tests/test_auth.py +++ /dev/null @@ -1,152 +0,0 @@ -# Test core halibot functionality - -import time -import util -import halibot -import unittest -import os - -class StubModuleFunc(halibot.HalModule): - - def init(self): - self.called = False - - def function(self, msg): - self.called = True - - def receive(self, msg): - if self.hasPermission(msg, "Foo"): - self.function(msg) - -class StubModuleDec(halibot.HalModule): - - def init(self): - self.called = False - - @halibot.halauth.hasPermission("Foo", reply=True) - def function(self, msg): - self.called = True - - def receive(self, msg): - self.function(msg) - -class TestAuth(util.HalibotTestCase): - - def test_grantperm(self): - self.bot.auth.perms = [] - self.bot.auth.enabled = False - self.bot.auth.grantPermission("foo", "bar", "baz") - - self.assertEqual(len(self.bot.auth.perms), 0) - - self.bot.auth.enabled = True - - self.bot.auth.grantPermission("foo", "bar", "baz") - self.assertEqual(len(self.bot.auth.perms), 1) - self.assertEqual(self.bot.auth.perms[0][0], "foo") - self.assertEqual(self.bot.auth.perms[0][1], "bar") - self.assertEqual(self.bot.auth.perms[0][2], "baz") - - def test_revokeperm(self): - self.bot.auth.perms = [("foo", "bar", "baz")] - self.bot.auth.enabled = False - - self.bot.auth.revokePermission("foo", "bar", "baz") - # Permissions aren't enabled, so we should ignore revocations - self.assertEqual(len(self.bot.auth.perms), 1) - - self.bot.auth.enabled = True - - self.bot.auth.revokePermission("foo", "bar", "baz") - self.assertEqual(len(self.bot.auth.perms), 0) - - # This should remain empty, and fail to find the perm to revoke - self.bot.auth.revokePermission("foo", "bar", "baz") - self.assertEqual(len(self.bot.auth.perms), 0) - - def test_hasperm_func(self): - self.bot.auth.enabled = True - stub = StubModuleFunc(self.bot) - self.bot.add_instance('stub_mod', stub) - - ri = "test/foobar" - user = "tester" - perm = "Foo" - msg = halibot.Message(body="", origin=ri, identity=user) - - stub.receive(msg) - self.assertFalse(stub.called) - - self.bot.auth.grantPermission(ri, user, perm) - stub.receive(msg) - self.assertTrue(stub.called) - - stub.called = False - self.bot.auth.revokePermission(ri, user, perm) - stub.receive(msg) - self.assertFalse(stub.called) - - # NOTE: This assumes that enabled = allow all perms. - # Is this really expected behavior? - stub.called = False - self.bot.auth.enabled = False - self.bot.auth.revokePermission(ri, user, perm) - stub.receive(msg) - self.assertTrue(stub.called) - - def test_hasperm_dec(self): - self.bot.auth.enabled = True - stub = StubModuleDec(self.bot) - self.bot.add_instance('stub_mod', stub) - - ri = "test/foobar" - user = "tester" - perm = "Foo" - msg = halibot.Message(body="", origin=ri, identity=user) - - stub.receive(msg) - self.assertFalse(stub.called) - - self.bot.auth.grantPermission(ri, user, perm) - stub.receive(msg) - self.assertTrue(stub.called) - - stub.called = False - self.bot.auth.revokePermission(ri, user, perm) - stub.receive(msg) - self.assertFalse(stub.called) - - def test_load_perms(self): - with open("testperms.json", "w") as f: - f.write("[]") - - self.bot.auth.load_perms("testperms.json") - self.assertEqual(len(self.bot.auth.perms), 0) - - os.remove("testperms.json") - self.bot.auth.enabled = False - self.bot.auth.load_perms("testperms.json") - self.assertTrue(self.bot.auth.enabled) - - def test_write_perms(self): - self.bot.auth.enabled = True - self.bot.auth.path = "testperms.json" - self.bot.auth.perms = [("foo", "bar", "baz")] - self.bot.auth.write_perms() - - self.bot.auth.enabled = False - self.bot.auth.perms = [] - - self.bot.auth.load_perms("testperms.json") - self.assertTrue(self.bot.auth.enabled) - p = self.bot.auth.perms[0] - self.assertEqual(p[0], "foo") - self.assertEqual(p[1], "bar") - self.assertEqual(p[2], "baz") - - os.remove("testperms.json") - - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_cli.py b/tests/test_cli.py deleted file mode 100644 index e194865..0000000 --- a/tests/test_cli.py +++ /dev/null @@ -1,81 +0,0 @@ -import unittest -import os, sys, shutil -import subprocess, tempfile -import json - -class CliTestCase(unittest.TestCase): - def setUp(self): - self.path = tempfile.mkdtemp(prefix='halibot-test') - self.mainPy = os.path.join(os.getcwd(), 'main.py') - os.chdir(self.path) - - def tearDown(self): - os.chdir(os.path.dirname(self.mainPy)) - shutil.rmtree(self.path) - - # Run a halibot CLI command - def cmd(self, args, input=None): - args = [sys.executable, self.mainPy] + args - proc = subprocess.run(args, input=input, universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - self.assertEqual(proc.returncode, 0) - return proc - -class TestInit(CliTestCase): - - def test_init(self): - # Create the config.json with 'halibot init' - self.assertFalse(os.path.exists('config.json')) - self.assertFalse(os.path.exists('packages')) - self.cmd(['init']) - self.assertTrue(os.path.exists('config.json')) - self.assertTrue(os.path.exists('packages')) - - with open('config.json') as f: - # Verify that the config looks vaguely right - conf = json.load(f) - self.assertTrue('agent-instances' in conf) - self.assertTrue('module-instances' in conf) - self.assertTrue('package-path' in conf) - self.assertTrue(len(conf['package-path']) > 0) - -class TestFetch(CliTestCase): - - def test_fetch(self): - ircPath = os.path.join('packages', 'irc') - - self.cmd(['init']) - self.assertFalse(os.path.exists(ircPath)) - self.cmd(['fetch', 'irc']) - self.assertTrue(os.path.exists(ircPath)) - self.cmd(['unfetch', 'irc']) - self.assertFalse(os.path.exists(ircPath)) - -class TestInstall(CliTestCase): - - def test_install(self): - ircPath = os.path.join('packages', 'irc') - - self.cmd(['init']) - - with open('config.json') as f: - conf = json.load(f) - self.assertFalse('hello0' in conf['module-instances']) - - self.cmd(['add', 'hello'], input='hello0\n') - - with open('config.json') as f: - conf = json.load(f) - self.assertTrue('hello0' in conf['module-instances']) - self.assertEqual('hello:Default', conf['module-instances']['hello0']['of']) - - self.cmd(['rm', 'hello0']) - - with open('config.json') as f: - conf = json.load(f) - self.assertFalse('hello0' in conf['module-instances']) - - self.assertFalse(os.path.exists(ircPath)) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_configurer.py b/tests/test_configurer.py deleted file mode 100644 index 46267a4..0000000 --- a/tests/test_configurer.py +++ /dev/null @@ -1,140 +0,0 @@ - -import util -import halibot -import unittest -from unittest.mock import patch - - -class TestConfigurer(unittest.TestCase): - - @patch('halibot.halconfigurer.get_input', return_value='bar') - def test_optionString_str(self, input): - class Configurer(halibot.HalConfigurer): - def configure(self): - self.optionString('foo', default="foo") - c = Configurer() - c.configure() - - self.assertTrue('foo' in c.options) - self.assertEqual(c.options['foo'], "bar") - - @patch('halibot.halconfigurer.get_input', return_value=2) - def test_optionInt_int(self, input): - class Configurer(halibot.HalConfigurer): - def configure(self): - self.optionInt('foo', default=1) - c = Configurer() - c.configure() - - self.assertTrue('foo' in c.options) - self.assertEqual(c.options['foo'], 2) - - @patch('halibot.halconfigurer.get_input', return_value="2") - def test_optionInt_str(self, input): - class Configurer(halibot.HalConfigurer): - def configure(self): - self.optionInt('foo', default=1) - c = Configurer() - c.configure() - - self.assertTrue('foo' in c.options) - self.assertEqual(c.options['foo'], 2) - - @patch('halibot.halconfigurer.get_input', return_value=None) - def test_optionInt_none(self, input): - class Configurer(halibot.HalConfigurer): - def configure(self): - self.optionInt('foo', default=1) - c = Configurer() - try: - c.configure() - except TypeError as e: - # Yay, we type errored as expected - return - - self.assertTrue(False) # pragma: no cover - - @patch('halibot.halconfigurer.get_input', return_value=2.1) - def test_optionNumber_int(self, input): - class Configurer(halibot.HalConfigurer): - def configure(self): - self.optionNumber('foo', default=1.0) - c = Configurer() - c.configure() - - self.assertTrue('foo' in c.options) - self.assertEqual(c.options['foo'], 2.1) - - @patch('halibot.halconfigurer.get_input', return_value="2") - def test_optionNumber_str(self, input): - class Configurer(halibot.HalConfigurer): - def configure(self): - self.optionNumber('foo', default=1.0) - c = Configurer() - c.configure() - - self.assertTrue('foo' in c.options) - self.assertEqual(c.options['foo'], 2) - - @patch('halibot.halconfigurer.get_input', return_value=None) - def test_optionNumber_none(self, input): - class Configurer(halibot.HalConfigurer): - def configure(self): - self.optionNumber('foo', default=1.0) - c = Configurer() - try: - c.configure() - except TypeError as e: - # Yay, we type errored as expected - return - - self.assertTrue(False) # pragma: no cover - - @patch('halibot.halconfigurer.get_input', return_value=True) - def test_optionBoolean_true(self, input): - class Configurer(halibot.HalConfigurer): - def configure(self): - self.optionBoolean('foo', default=False) - c = Configurer() - c.configure() - - self.assertTrue('foo' in c.options) - self.assertEqual(c.options['foo'], True) - - @patch('halibot.halconfigurer.get_input', return_value=False) - def test_optionBoolean_false(self, input): - class Configurer(halibot.HalConfigurer): - def configure(self): - self.optionBoolean('foo', default=True) - c = Configurer() - c.configure() - - self.assertTrue('foo' in c.options) - self.assertEqual(c.options['foo'], False) - - @patch('halibot.halconfigurer.get_input', return_value="TrUe") - def test_optionBoolean_strTrue(self, input): - class Configurer(halibot.HalConfigurer): - def configure(self): - self.optionBoolean('foo', default=False) - c = Configurer() - c.configure() - - self.assertTrue('foo' in c.options) - self.assertEqual(c.options['foo'], True) - - @patch('halibot.halconfigurer.get_input', return_value="FaLsE") - def test_optionBoolean_strFalse(self, input): - class Configurer(halibot.HalConfigurer): - def configure(self): - self.optionBoolean('foo', default=True) - c = Configurer() - c.configure() - - self.assertTrue('foo' in c.options) - self.assertEqual(c.options['foo'], False) - - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_core.py b/tests/test_core.py deleted file mode 100644 index 035eb1c..0000000 --- a/tests/test_core.py +++ /dev/null @@ -1,397 +0,0 @@ -# Test core halibot functionality - -import time -import util -import halibot -import unittest - -topic1_text = 'Help text one' -topic2_text = 'Help text two' - -class StubModule(halibot.HalModule): - inited = False - - topics = { - 'topic1': lambda: topic1_text, - 'topic2': topic2_text - } - - def init(self): - self.inited = True - self.received = [] - self.received_mytype = [] - - def receive(self, msg): - self.received.append(msg) - - def receive_mytype(self, msg): - self.received_mytype.append(msg) - -class StubReplier(halibot.HalModule): - inited = False - - def init(self): - self.inited = True - self.received = [] - - def receive(self, msg): - self.received.append(msg) - self.reply(msg, body=msg.body + "bar") - -class StubCommand(halibot.CommandModule): - def init(self): - self.commands = { - "foo": self.foo, - "bar": self.bar - } - self.foo_args = [] - self.bar_args = [] - - def foo(self, args, msg=None): - self.foo_args.append(args) - self.reply(msg, body="fooed " + args) - - @halibot.AsArgs - def bar(self, args, msg=None): - self.bar_args.append(args) - self.reply(msg, body="barred " + str(len(args))) - -class StubAgent(halibot.HalAgent): - inited = False - - def init(self): - self.inited = True - self.received = [] - - def receive(self, msg): - self.received.append(msg) - -class TestCore(util.HalibotTestCase): - - def test_add_module(self): - stub = StubModule(self.bot) - self.bot.add_instance('stub_mod', stub) - - self.assertTrue(stub.inited) - self.assertEqual(stub, self.bot.objects.get('stub_mod')) - self.assertEqual(stub, self.bot.objects.modules.get('stub_mod')) - self.assertEqual(len(self.bot.objects.agents.keys()), 0) - - - def test_add_agent(self): - stub = StubAgent(self.bot) - self.bot.add_instance('stub_agent', stub) - - self.assertTrue(stub.inited) - self.assertEqual(stub, self.bot.objects.get('stub_agent')) - self.assertEqual(stub, self.bot.objects.agents.get('stub_agent')) - self.assertEqual(len(self.bot.objects.modules.keys()), 0) - - def test_send_recv(self): - agent = StubAgent(self.bot) - mod = StubModule(self.bot) - mod2 = StubModule(self.bot) - self.bot.add_instance('stub_agent', agent) - self.bot.add_instance('stub_mod', mod) - self.bot.add_instance('stub_mod2', mod2) - - # mod should receive: foo, bar, baz, qua - # mod2 should receive: foo, bar, qua - foo = halibot.Message(body='foo') - bar = halibot.Message(body='bar') - baz = halibot.Message(body='baz', origin='glub_agent') - qua = halibot.Message(body='qua') - qua2 = halibot.Message(body='qua', type='mytype') - - agent.dispatch(foo) # 0 - agent.send_to(bar, [ 'stub_mod/able', 'stub_mod2/baker' ] ) # 1 - agent.connect(mod) - agent.dispatch(baz) # 2 - agent.connect(mod2) - agent.dispatch(qua) # 3 - - agent.dispatch(qua2) # 3 - - util.waitOrTimeout(100, lambda: len(mod.received) == 4 and len(mod2.received) == 3 and len(mod.received_mytype) == 1 and len(mod2.received_mytype) == 1) - - # Check mod received mesages - self.assertEqual(4, len(mod.received)) - self.assertEqual(foo.body, mod.received[0].body) - self.assertEqual(bar.body, mod.received[1].body) - self.assertEqual(baz.body, mod.received[2].body) - self.assertEqual(qua.body, mod.received[3].body) - self.assertEqual('', mod.received[0].whom()) - self.assertEqual('able', mod.received[1].whom()) - self.assertEqual('', mod.received[2].whom()) - self.assertEqual('', mod.received[3].whom()) - self.assertEqual('stub_mod', mod.received[0].target) - self.assertEqual('stub_mod/able', mod.received[1].target) - self.assertEqual('stub_mod', mod.received[2].target) - self.assertEqual('stub_mod', mod.received[3].target) - self.assertEqual('stub_agent', mod.received[0].origin) - self.assertEqual('stub_agent', mod.received[1].origin) - self.assertEqual('glub_agent', mod.received[2].origin) - self.assertEqual('stub_agent', mod.received[3].origin) - - # Check mod2 received mesages - self.assertEqual(3, len(mod2.received)) - self.assertEqual(foo.body, mod2.received[0].body) - self.assertEqual(bar.body, mod2.received[1].body) - self.assertEqual(qua.body, mod2.received[2].body) - self.assertEqual('', mod.received[0].whom()) - self.assertEqual('able', mod.received[1].whom()) - self.assertEqual('', mod.received[2].whom()) - self.assertEqual('stub_mod2', mod2.received[0].target) - self.assertEqual('stub_mod2/baker', mod2.received[1].target) - self.assertEqual('stub_mod2', mod2.received[2].target) - self.assertEqual('stub_agent', mod2.received[0].origin) - self.assertEqual('stub_agent', mod2.received[1].origin) - self.assertEqual('stub_agent', mod2.received[2].origin) - - # Check mytype messages - self.assertEqual(1, len(mod.received_mytype)) - self.assertEqual(1, len(mod2.received_mytype)) - self.assertEqual(qua2.body, mod.received_mytype[0].body) - self.assertEqual(qua2.body, mod2.received_mytype[0].body) - - - def test_send_reply(self): - agent = StubAgent(self.bot) - mod = StubReplier(self.bot) - self.bot.add_instance('stub_agent', agent) - self.bot.add_instance('stub_module', mod) - - self.assertTrue(agent.inited) - self.assertTrue(mod.inited) - - self.assertTrue(agent.eventloop.is_running()) - - foo = halibot.Message(body='foo') - agent.send_to(foo, ['stub_module']) - - util.waitOrTimeout(100, lambda: len(agent.received) != 0) - - self.assertEqual(len(agent.received), 1) - self.assertEqual(agent.received[0].body, "foobar") - - def test_sync_send(self): - agent = StubAgent(self.bot) - mod = StubReplier(self.bot) - self.bot.add_instance('stub_agent', agent) - self.bot.add_instance('stub_module', mod) - - foo = halibot.Message(body='foo') - rep = agent.sync_send_to(foo, ['stub_module']) - - self.assertEqual(rep["stub_module"][0].body, "foobar") - - with self.assertRaises(halibot.SyncSendSelfException): - agent.sync_send_to(foo, ['stub_agent']) - - def test_help(self): - agent = StubAgent(self.bot) - mod = StubModule(self.bot) - self.bot.add_instance('stub_agent', agent) - self.bot.add_instance('stub_module', mod) - - msgt0 = halibot.Message(type='help', body=[]) - msgt1 = halibot.Message(type='help', body=['topic1']) - msgt2 = halibot.Message(type='help', body=['topic2']) - - self.assertEqual(set(agent.sync_send_to(msgt0, ['stub_module'])['stub_module'][0].body), set(['topic1', 'topic2'])) - self.assertEqual(agent.sync_send_to(msgt1, ['stub_module'])['stub_module'][0].body, topic1_text) - self.assertEqual(agent.sync_send_to(msgt2, ['stub_module'])['stub_module'][0].body, topic2_text) - - def test_version_class(self): - # Test major comparison - self.assertTrue(halibot.Version("1.0.0") >= halibot.Version("0.1.0")) - self.assertTrue(halibot.Version("0.1.0") <= halibot.Version("1.0.0")) - self.assertTrue(halibot.Version("1.0.0") > halibot.Version("0.1.0")) - self.assertTrue(halibot.Version("0.1.0") < halibot.Version("1.0.0")) - - # Test minor comparison - self.assertTrue(halibot.Version("1.1.0") >= halibot.Version("1.0.0")) - self.assertTrue(halibot.Version("1.0.0") <= halibot.Version("1.1.0")) - self.assertTrue(halibot.Version("1.1.0") > halibot.Version("1.0.0")) - self.assertTrue(halibot.Version("1.0.0") < halibot.Version("1.1.0")) - - # Test negatives of above - self.assertFalse(halibot.Version("1.0.0") <= halibot.Version("0.1.0")) - self.assertFalse(halibot.Version("0.1.0") >= halibot.Version("1.0.0")) - self.assertFalse(halibot.Version("1.0.0") < halibot.Version("0.1.0")) - self.assertFalse(halibot.Version("0.1.0") > halibot.Version("1.0.0")) - - self.assertFalse(halibot.Version("1.1.0") <= halibot.Version("1.0.0")) - self.assertFalse(halibot.Version("1.0.0") >= halibot.Version("1.1.0")) - self.assertFalse(halibot.Version("1.1.0") < halibot.Version("1.0.0")) - self.assertFalse(halibot.Version("1.0.0") > halibot.Version("1.1.0")) - - # Test equalities - self.assertEqual(halibot.Version("1.0.0"), halibot.Version("1.0.0")) - self.assertNotEqual(halibot.Version("1.0.0"), halibot.Version("1.0.1")) - self.assertNotEqual(halibot.Version("1.0.0"), halibot.Version("1.1.1")) - self.assertNotEqual(halibot.Version("1.0.0"), halibot.Version("0.0.0")) - - def test_version_check(self): - class VersionOkModule(halibot.HalModule): - HAL_MINIMUM = "0.0.1" - HAL_MAXIMUM = "1.0.0" - - class VersionFailMinModule(halibot.HalModule): - HAL_MINIMUM = "1.0.0" - HAL_MAXIMUM = "1.0.0" - - class VersionFailMaxModule(halibot.HalModule): - HAL_MINIMUM = "0.0.1" - HAL_MAXIMUM = "0.0.1" - - mod0 = VersionOkModule(self.bot) # Load ok - mod1 = VersionFailMinModule(self.bot) # Fail minimum - mod2 = VersionFailMaxModule(self.bot) # Fail maximum - - self.bot.VERSION = "0.1.0" - - self.assertTrue(self.bot._check_version(mod0)) - self.assertFalse(self.bot._check_version(mod1)) - self.assertFalse(self.bot._check_version(mod2)) - - # Add the instances so they get cleaned up properly... - self.bot.add_instance("mod0", mod0) - self.bot.add_instance("mod1", mod1) - self.bot.add_instance("mod2", mod2) - - def test_command(self): - mod = StubCommand(self.bot) - agent = StubAgent(self.bot) - self.bot.add_instance('stub_cagent', agent) - self.bot.add_instance('stub_cmodule', mod) - - # Test regular bare string args - foo = halibot.Message(body='foo') - cmdfoo = halibot.Message(body='!foo') - cmdfoo2 = halibot.Message(body='!foo moo') - - # Test AsArgs arg splitting - bar = halibot.Message(body='bar') - cmdbar = halibot.Message(body='!bar') - cmdbar2 = halibot.Message(body='!bar moo') - cmdbar3 = halibot.Message(body='!bar moo foo') - - # Test unhandled command - baz = halibot.Message(body="!baz") - - agent.dispatch(foo) - agent.dispatch(cmdfoo) - agent.dispatch(cmdfoo2) - - util.waitOrTimeout(100, lambda: len(agent.received) == 2) - - self.assertEqual(2, len(agent.received)) - self.assertEqual("fooed ", agent.received[0].body) - self.assertEqual("", mod.foo_args[0]) - self.assertEqual("fooed moo", agent.received[1].body) - self.assertEqual("moo", mod.foo_args[1]) - - agent.received = [] - - agent.dispatch(bar) - agent.dispatch(cmdbar) - agent.dispatch(cmdbar2) - agent.dispatch(cmdbar3) - - util.waitOrTimeout(100, lambda: len(agent.received) == 3) - - self.assertEqual(3, len(agent.received)) - self.assertEqual("barred 1", agent.received[0].body) - self.assertEqual([""], mod.bar_args[0]) - self.assertEqual("barred 1", agent.received[1].body) - self.assertEqual(["moo"], mod.bar_args[1]) - self.assertEqual("barred 2", agent.received[2].body) - self.assertEqual(["moo", "foo"], mod.bar_args[2]) - - agent.received = [] - - agent.sync_send_to(baz, [ 'stub_cmodule' ]) - - self.assertEqual(0, len(agent.received)) - - def test_restart(self): - mod = StubModule(self.bot) - mod2 = StubModule(self.bot) - - self.bot.add_instance("stub_module", mod) - self.bot.add_instance("stub_module2", mod2) - - # .inited should be reset to True if module was restarted - mod.inited = False - mod2.inited = False #...but this module should be unaffected - - self.bot.restart("stub_module") - self.bot.restart("nope") - - self.assertTrue(mod.inited) - self.assertFalse(mod2.inited) - - def test_invoke(self): - class InvokeModule(halibot.HalModule): - def receive(self, msg): - ret = self.invoke("stub_target","target", msg.body, foo=msg.body) - self.reply(msg, body="target said: " + msg.body) - class TargetModule(halibot.HalModule): - def init(self): - self.invoked = False - def target(self, *args, **kwargs): - self.invoked = True - return " ".join(args) + " " + " ".join(kwargs.keys()) - - inv = InvokeModule(self.bot) - tar = TargetModule(self.bot) - agent = StubAgent(self.bot) - - self.bot.add_instance("stub_invoker", inv) - self.bot.add_instance("stub_target", tar) - self.bot.add_instance("stub_agent", agent) - - agent.dispatch(halibot.Message(body="bar")) - - util.waitOrTimeout(100, lambda: len(agent.received) != 0) - - self.assertTrue(tar.invoked) - self.assertEqual(1, len(agent.received)) - self.assertTrue("target said: bar foo") - - def test_module_fail_recover(self): - class ExceptionModule(halibot.HalModule): - def init(self): - self.received = [] - def receive(self, msg): - self.received.append(msg) - if msg.body == "explode": - raise NotImplementedError() - - agent = StubAgent(self.bot) - mod = ExceptionModule(self.bot) - - self.bot.add_instance("stub_agent", agent) - self.bot.add_instance("stub_mod", mod) - - self.assertEqual(0, len(agent.received)) - - agent.dispatch(halibot.Message(body="foo")) - util.waitOrTimeout(100, lambda: len(mod.received) == 1) - self.assertEqual(1, len(mod.received)) - self.assertEqual("foo", mod.received[0].body) - - agent.dispatch(halibot.Message(body="explode")) - util.waitOrTimeout(100, lambda: len(mod.received) == 2) - self.assertEqual(2, len(mod.received)) - self.assertEqual("explode", mod.received[1].body) - - agent.dispatch(halibot.Message(body="foo")) - util.waitOrTimeout(100, lambda: len(mod.received) == 3) - self.assertEqual(3, len(mod.received)) - self.assertEqual("foo", mod.received[2].body) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_jsdict.py b/tests/test_jsdict.py deleted file mode 100644 index c044a9e..0000000 --- a/tests/test_jsdict.py +++ /dev/null @@ -1,19 +0,0 @@ - -import unittest -from halibot.jsdict import jsdict - -class TestJsdict(unittest.TestCase): - - def test_jsdict(self): - d = jsdict() - - d.foo = 'iamblichus' - self.assertEqual(d['foo'], 'iamblichus') - self.assertEqual(d.foo, 'iamblichus') - - d['bar'] = 'porphyry' - self.assertEqual(d['bar'], 'porphyry') - self.assertEqual(d.bar, 'porphyry') - -if __name__ == '__main__': - unittest.main() diff --git a/tests/util.py b/tests/util.py deleted file mode 100644 index 2659338..0000000 --- a/tests/util.py +++ /dev/null @@ -1,26 +0,0 @@ -# Utilities to assist in writing tests - -import halibot -import unittest -import logging -import time - -def waitOrTimeout(timeout, condition): - for i in range(timeout): - if condition(): - break - time.sleep(0.1) - else: - print("warning: timeout reached") # pragma: no cover - - -# Provides a unique bot in self.bot for every test case -class HalibotTestCase(unittest.TestCase): - - def setUp(self): - # Silence expected error messages - logging.basicConfig(level=logging.CRITICAL) - self.bot = halibot.Halibot(use_config=False) - - def tearDown(self): - self.bot.shutdown()