class _Schema: config = immp.Schema({"token": str, immp.Optional("bot", True): bool, immp.Optional("webhooks", dict): {str: str}, immp.Optional("playing"): immp.Nullable(str)}) webhook = immp.Schema(immp.Any({"code": int, "message": str}, {"id": str}))
def _api(nested={}): return immp.Schema( immp.Any( { "ok": True, immp.Optional("response_metadata", dict): { immp.Optional("next_cursor", ""): str }, **nested }, { "ok": False, "error": str }))
class AsyncShellHook(immp.ResourceHook): """ Hook to launch an asynchonous console alongside a :class:`.Host` instance. Attributes: buffer (collections.deque): Queue of recent messages, the length defined by the ``buffer`` config entry. last ((.SentMessage, .Message) tuple): Most recent message received from a connected plug. """ schema = immp.Schema({ "bind": immp.Any(str, int), immp.Optional("buffer"): immp.Nullable(int) }) def __init__(self, name, config, host): super().__init__(name, config, host) if not aioconsole: raise immp.PlugError("'aioconsole' module not installed") self.buffer = None self._server = None @property def last(self): return self.buffer[-1] if self.buffer else None async def start(self): await super().start() if self.config["buffer"] is not None: self.buffer = deque(maxlen=self.config["buffer"] or None) if isinstance(self.config["bind"], str): log.debug("Launching console on socket %s", self.config["bind"]) bind = {"path": self.config["bind"]} else: log.debug("Launching console on port %d", self.config["bind"]) bind = {"port": self.config["bind"]} self._server = await aioconsole.start_interactive_server( factory=self._factory, **bind) async def stop(self): await super().stop() self.buffer = None if self._server: log.debug("Stopping console server") self._server.close() self._server = None @staticmethod def _pprint(console, obj): console.print(pformat(obj)) def _factory(self, streams=None): context = {"host": self.host, "shell": self, "immp": immp} console = aioconsole.AsynchronousConsole(locals=context, streams=streams) context["pprint"] = partial(self._pprint, console) return console async def on_receive(self, sent, source, primary): await super().on_receive(sent, source, primary) if self.buffer is not None: self.buffer.append((sent, source))
class _Schema: image_sizes = ("original", "512", "192", "72", "48", "32", "24") _images = { immp.Optional("image_{}".format(size)): immp.Nullable(str) for size in image_sizes } config = immp.Schema({ "token": str, immp.Optional("fallback-name", "IMMP"): str, immp.Optional("fallback-image"): immp.Nullable(str), immp.Optional("thread-broadcast", False): bool }) team = immp.Schema({ "id": str, "name": str, "domain": str, "prefs": { immp.Optional("display_real_names", False): bool, str: immp.Any() } }) user = immp.Schema({ "id": str, "name": str, "profile": { immp.Optional("real_name"): immp.Nullable(str), immp.Optional("bot_id"): immp.Nullable(str), **_images } }) bot = immp.Schema({ "id": str, "app_id": str, "name": str, "icons": _images }) channel = immp.Schema({"id": str, "name": str}) direct = immp.Schema({"id": str, "user": str}) _shares = {str: [{"ts": str}]} file = immp.Schema( immp.Any( { "id": str, "name": immp.Nullable(str), "pretty_type": str, "url_private": str, immp.Optional("mode"): immp.Nullable(str), immp.Optional("shares", dict): { immp.Optional("public", dict): _shares, immp.Optional("private", dict): _shares } }, { "id": str, "mode": "tombstone" })) attachment = immp.Schema({ immp.Optional("fallback"): immp.Nullable(str), immp.Optional("title"): immp.Nullable(str), immp.Optional("image_url"): immp.Nullable(str), immp.Optional("is_msg_unfurl", False): bool }) msg_unfurl = immp.Schema({"channel_id": str, "ts": str}, attachment) _base_msg = immp.Schema({ "ts": str, "type": "message", immp.Optional("hidden", False): bool, immp.Optional("channel"): immp.Nullable(str), immp.Optional("edited", dict): { immp.Optional("user"): immp.Nullable(str) }, immp.Optional("thread_ts"): immp.Nullable(str), immp.Optional("replies", list): [{ "ts": str }], immp.Optional("files", list): [file], immp.Optional("attachments", list): [attachment], immp.Optional("is_ephemeral", False): bool }) _plain_msg = immp.Schema( { immp.Optional("user"): immp.Nullable(str), immp.Optional("bot_id"): immp.Nullable(str), immp.Optional("username"): immp.Nullable(str), immp.Optional("icons", dict): dict, "text": str }, _base_msg) message = immp.Schema( immp.Any( immp.Schema({"subtype": "file_comment"}, _base_msg), immp.Schema({"subtype": "message_changed"}, _base_msg), immp.Schema({ "subtype": "message_deleted", "deleted_ts": str }, _base_msg), immp.Schema( { "subtype": immp.Any("channel_name", "group_name"), "name": str }, _plain_msg), immp.Schema({immp.Optional("subtype"): immp.Nullable(str)}, _plain_msg))) # Circular references to embedded messages. message.raw.choices[1].raw.update({ "message": message, "previous_message": message }) event = immp.Schema( immp.Any( message, { "type": "team_pref_change", "name": "str", "value": immp.Any() }, { "type": immp.Any("team_join", "user_change"), "user": user }, { "type": immp.Any("channel_created", "channel_joined", "channel_rename", "group_created", "group_joined", "group_rename"), "channel": { "id": str, "name": str } }, { "type": "im_created", "channel": { "id": str } }, { "type": immp.Any("member_joined_channel", "member_left_channel"), "user": str, "channel": str }, { "type": "message", immp.Optional("subtype"): immp.Nullable(str) }, {"type": str})) def _api(nested={}): return immp.Schema( immp.Any( { "ok": True, immp.Optional("response_metadata", dict): { immp.Optional("next_cursor", ""): str }, **nested }, { "ok": False, "error": str })) rtm = _api({ "url": str, "self": { "id": str }, "team": { "id": str, "name": str, "domain": str }, "users": [user], "channels": [channel], "groups": [channel], "ims": [direct], "bots": [{ "id": str, "deleted": bool }] }) im_open = _api({"channel": direct}) members = _api({"members": [str]}) post = _api({"ts": str}) upload = _api({"file": file}) history = _api({"messages": [message]}) api = _api()
class WebHook(immp.ResourceHook): """ Hook that provides a generic webserver, which other hooks can bind routes to. Attributes: app (aiohttp.web.Application): Web application instance, used to add new routes. """ schema = immp.Schema( immp.Any({ immp.Optional("host"): immp.Nullable(str), "port": int }, {"path": str})) def __init__(self, name, config, host): super().__init__(name, config, host) self.app = web.Application() if aiohttp_jinja2: # Empty mapping by default, other hooks can add to this via add_loader(). self._loader = PrefixLoader({}) self._jinja = aiohttp_jinja2.setup(self.app, loader=self._loader) self._jinja.filters["json"] = json.dumps self._jinja.globals["immp"] = immp self._jinja.globals["host"] = self.host self._runner = web.AppRunner(self.app) self._site = None self._contexts = {} def context(self, prefix, module, path=None, env=None): """ Retrieve a context for the current module. Args: prefix (str): URL prefix acting as the base path. module (str): Dotted module name of the Python module using this context. Callers should use :data:`__name__` from the root of their module. path (str): Base path of the module, needed for static routes. Callers should use ``os.path.dirname(__file__)`` from the root of their module. env (dict): Additional variables to make available in the Jinja context. See :attr:`.WebContext.env` for details. Returns: .WebContext: Linked context instance for that module. """ self._contexts[module] = WebContext(self, prefix, module, path, env) return self._contexts[module] def add_loader(self, module): """ Register a Jinja2 package loader for the given module. Args: module (str): Module name to register. """ if not aiohttp_jinja2: raise immp.HookError("Loaders require Jinja2 and aiohttp_jinja2") self._loader.mapping[module] = PackageLoader(module) def add_route(self, *args, **kwargs): """ Equivalent to :meth:`aiohttp.web.UrlDispatcher.add_route`. """ return self.app.router.add_route(*args, **kwargs) def add_static(self, *args, **kwargs): """ Equivalent to :meth:`aiohttp.web.UrlDispatcher.add_static`. """ return self.app.router.add_static(*args, **kwargs) async def start(self): await super().start() await self._runner.setup() if "path" in self.config: log.debug("Starting server on socket %s", self.config["path"]) self._site = web.UnixSite(self._runner, self.config["path"]) else: log.debug("Starting server on host %s:%d", self.config["host"], self.config["port"]) self._site = web.TCPSite(self._runner, self.config["host"], self.config["port"]) await self._site.start() async def stop(self): await super().stop() if self._site: log.debug("Stopping server") await self._runner.cleanup() self._site = None