Source code for slack.events

import re
import copy
import json
import logging
import itertools
from typing import Any, Dict, Iterator, Optional
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