Source code for slack.events

import re
import copy
import json
import logging
import itertools
from typing import Any, Dict, Iterator, Optional, MutableMapping
from collections import defaultdict
from collections.abc import MutableMapping

from . import exceptions

LOG = logging.getLogger(__name__)


[docs]class Event(MutableMapping): """ MutableMapping representing a slack event coming from the RTM API or the Event API. Attributes: metadata: Metadata dispatched with the event when using the Event API (see `slack event API documentation <https://api.slack.com/events-api#receiving_events>`_) """ def __init__( self, raw_event: MutableMapping, metadata: Optional[MutableMapping] = None ) -> None: self.event = raw_event self.metadata = metadata def __getitem__(self, item): return self.event[item] def __setitem__(self, key, value): self.event[key] = value def __delitem__(self, key): del self.event[key] def __iter__(self): return iter(self.event) def __len__(self): return len(self.event) def __repr__(self): return "Slack Event: " + str(self.event)
[docs] def clone(self) -> "Event": """ Clone the event Returns: :class:`slack.events.Event` """ return self.__class__(copy.deepcopy(self.event), copy.deepcopy(self.metadata))
[docs] @classmethod def from_rtm(cls, raw_event: MutableMapping) -> "Event": """ Create an event with data coming from the RTM API. If the event type is a message a :class:`slack.events.Message` is returned. Args: raw_event: JSON decoded data from the RTM API Returns: :class:`slack.events.Event` or :class:`slack.events.Message` """ if raw_event["type"].startswith("message"): return Message(raw_event) else: return Event(raw_event)
[docs] @classmethod def from_http( cls, raw_body: MutableMapping, verification_token: Optional[str] = None, team_id: Optional[str] = None, ) -> "Event": """ Create an event with data coming from the HTTP Event API. If the event type is a message a :class:`slack.events.Message` is returned. Args: raw_body: Decoded body of the Event API request verification_token: Slack verification token used to verify the request came from slack team_id: Verify the event is for the correct team Returns: :class:`slack.events.Event` or :class:`slack.events.Message` Raises: :class:`slack.exceptions.FailedVerification`: when `verification_token` or `team_id` does not match the incoming event's. """ if verification_token and raw_body["token"] != verification_token: raise exceptions.FailedVerification(raw_body["token"], raw_body["team_id"]) if team_id and raw_body["team_id"] != team_id: raise exceptions.FailedVerification(raw_body["token"], raw_body["team_id"]) if raw_body["event"]["type"].startswith("message"): return Message(raw_body["event"], metadata=raw_body) else: return Event(raw_body["event"], metadata=raw_body)
[docs]class Message(Event): """ Type of :class:`slack.events.Event` corresponding to a message event type """ def __init__( self, msg: Optional[MutableMapping] = None, metadata: Optional[MutableMapping] = None, ) -> None: if not msg: msg = {} super().__init__(msg, metadata) def __repr__(self) -> str: return "Slack Message: " + str(self.event)
[docs] def response(self, in_thread: Optional[bool] = None) -> "Message": """ Create a response message. Depending on the incoming message the response can be in a thread. By default the response follow where the incoming message was posted. Args: in_thread (boolean): Overwrite the `threading` behaviour Returns: a new :class:`slack.event.Message` """ data = {"channel": self["channel"]} if in_thread: if "message" in self: data["thread_ts"] = ( self["message"].get("thread_ts") or self["message"]["ts"] ) else: data["thread_ts"] = self.get("thread_ts") or self["ts"] elif in_thread is None: if "message" in self and "thread_ts" in self["message"]: data["thread_ts"] = self["message"]["thread_ts"] elif "thread_ts" in self: data["thread_ts"] = self["thread_ts"] return Message(data)
[docs] def serialize(self) -> dict: """ Serialize the message for sending to slack API Returns: serialized message """ data = {**self} if "attachments" in self: data["attachments"] = json.dumps(self["attachments"]) return data
def to_json(self) -> str: return json.dumps({**self})
[docs]class EventRouter: """ When receiving an event from the RTM API or the slack API it is useful to have a routing mechanisms for dispatching event to individual function/coroutine. This class provide such mechanisms for any :class:`slack.events.Event`. """ def __init__(self): self._routes: Dict[str, Dict] = defaultdict(dict)
[docs] def register(self, event_type: str, handler: Any, **detail: Any) -> None: """ Register a new handler for a specific :class:`slack.events.Event` `type` (See `slack event types documentation <https://api.slack.com/events>`_ for a list of event types). The arbitrary keyword argument is used as a key/value pair to compare against what is in the incoming :class:`slack.events.Event` Args: event_type: Event type the handler is interested in handler: Callback **detail: Additional key for routing """ LOG.info("Registering %s, %s to %s", event_type, detail, handler) if len(detail) > 1: raise ValueError("Only one detail can be provided for additional routing") elif not detail: detail_key, detail_value = "*", "*" else: detail_key, detail_value = detail.popitem() if detail_key not in self._routes[event_type]: self._routes[event_type][detail_key] = {} if detail_value not in self._routes[event_type][detail_key]: self._routes[event_type][detail_key][detail_value] = [] self._routes[event_type][detail_key][detail_value].append(handler)
[docs] def dispatch(self, event: Event) -> Iterator[Any]: """ Yields handlers matching the routing of the incoming :class:`slack.events.Event`. Args: event: :class:`slack.events.Event` Yields: handler """ LOG.debug('Dispatching event "%s"', event.get("type")) if event["type"] in self._routes: for detail_key, detail_values in self._routes.get( event["type"], {} ).items(): event_value = event.get(detail_key, "*") yield from detail_values.get(event_value, []) else: return
[docs]class MessageRouter: """ When receiving an event of type message from the RTM API or the slack API it is useful to have a routing mechanisms for dispatching the message to individual function/coroutine. This class provide such mechanisms for any :class:`slack.events.Message`. The routing is based on regex pattern matching of the message text and the receiving channel. """ def __init__(self): self._routes: Dict[str, Dict] = defaultdict(dict)
[docs] def register( self, pattern: str, handler: Any, flags: int = 0, channel: str = "*", subtype: Optional[str] = None, ) -> None: """ Register a new handler for a specific :class:`slack.events.Message`. The routing is based on regex pattern matching the message text and the incoming slack channel. Args: pattern: Regex pattern matching the message text. handler: Callback flags: Regex flags. channel: Slack channel ID. Use * for any. subtype: Message subtype """ LOG.debug('Registering message endpoint "%s: %s"', pattern, handler) match = re.compile(pattern, flags) if subtype not in self._routes[channel]: self._routes[channel][subtype] = dict() if match in self._routes[channel][subtype]: self._routes[channel][subtype][match].append(handler) else: self._routes[channel][subtype][match] = [handler]
[docs] def dispatch(self, message: Message) -> Iterator[Any]: """ Yields handlers matching the routing of the incoming :class:`slack.events.Message` Args: message: :class:`slack.events.Message` Yields: handler """ if "text" in message: text = message["text"] or "" elif "message" in message: text = message["message"].get("text", "") else: text = "" msg_subtype = message.get("subtype") for subtype, matchs in itertools.chain( self._routes[message["channel"]].items(), self._routes["*"].items() ): if msg_subtype == subtype or subtype is None: for match, endpoints in matchs.items(): if match.search(text): yield from endpoints