forked from platypush/platypush
Added Slack integration
This commit is contained in:
parent
286bab7489
commit
90ec108580
12 changed files with 409 additions and 3 deletions
|
@ -3,12 +3,14 @@
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
Given the high speed of development in the first phase, changes are being reported only starting from v0.20.2.
|
Given the high speed of development in the first phase, changes are being reported only starting from v0.20.2.
|
||||||
|
|
||||||
## [Unreleased]
|
## [0.21.3] - 2021-07-28
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added `sun` plugin for sunrise/sunset events.
|
- Added `sun` plugin for sunrise/sunset events.
|
||||||
|
|
||||||
|
- Added `slack` integration.
|
||||||
|
|
||||||
## [0.21.2] - 2021-07-20
|
## [0.21.2] - 2021-07-20
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -13,6 +13,7 @@ Events
|
||||||
platypush/events/bluetooth.rst
|
platypush/events/bluetooth.rst
|
||||||
platypush/events/button.flic.rst
|
platypush/events/button.flic.rst
|
||||||
platypush/events/camera.rst
|
platypush/events/camera.rst
|
||||||
|
platypush/events/chat.slack.rst
|
||||||
platypush/events/chat.telegram.rst
|
platypush/events/chat.telegram.rst
|
||||||
platypush/events/clipboard.rst
|
platypush/events/clipboard.rst
|
||||||
platypush/events/covid19.rst
|
platypush/events/covid19.rst
|
||||||
|
|
5
docs/source/platypush/events/chat.slack.rst
Normal file
5
docs/source/platypush/events/chat.slack.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``platypush.message.event.chat.slack``
|
||||||
|
======================================
|
||||||
|
|
||||||
|
.. automodule:: platypush.message.event.chat.slack
|
||||||
|
:members:
|
5
docs/source/platypush/plugins/chat.rst
Normal file
5
docs/source/platypush/plugins/chat.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``chat``
|
||||||
|
========
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.chat
|
||||||
|
:members:
|
5
docs/source/platypush/plugins/slack.rst
Normal file
5
docs/source/platypush/plugins/slack.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``slack``
|
||||||
|
=========
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.slack
|
||||||
|
:members:
|
|
@ -25,6 +25,7 @@ Plugins
|
||||||
platypush/plugins/camera.gstreamer.rst
|
platypush/plugins/camera.gstreamer.rst
|
||||||
platypush/plugins/camera.ir.mlx90640.rst
|
platypush/plugins/camera.ir.mlx90640.rst
|
||||||
platypush/plugins/camera.pi.rst
|
platypush/plugins/camera.pi.rst
|
||||||
|
platypush/plugins/chat.rst
|
||||||
platypush/plugins/chat.telegram.rst
|
platypush/plugins/chat.telegram.rst
|
||||||
platypush/plugins/clipboard.rst
|
platypush/plugins/clipboard.rst
|
||||||
platypush/plugins/config.rst
|
platypush/plugins/config.rst
|
||||||
|
@ -112,6 +113,7 @@ Plugins
|
||||||
platypush/plugins/sensor.rst
|
platypush/plugins/sensor.rst
|
||||||
platypush/plugins/serial.rst
|
platypush/plugins/serial.rst
|
||||||
platypush/plugins/shell.rst
|
platypush/plugins/shell.rst
|
||||||
|
platypush/plugins/slack.rst
|
||||||
platypush/plugins/smartthings.rst
|
platypush/plugins/smartthings.rst
|
||||||
platypush/plugins/sound.rst
|
platypush/plugins/sound.rst
|
||||||
platypush/plugins/ssh.rst
|
platypush/plugins/ssh.rst
|
||||||
|
|
70
platypush/message/event/chat/slack.py
Normal file
70
platypush/message/event/chat/slack.py
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
from abc import ABC
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Union, Optional, Iterable
|
||||||
|
|
||||||
|
from dateutil.tz import gettz
|
||||||
|
|
||||||
|
from platypush.message.event import Event
|
||||||
|
|
||||||
|
|
||||||
|
class SlackEvent(Event, ABC):
|
||||||
|
"""
|
||||||
|
Base class for Slack events.
|
||||||
|
"""
|
||||||
|
def __init__(self, *args, timestamp: Optional[Union[int, float, datetime]] = None, **kwargs):
|
||||||
|
"""
|
||||||
|
:param timestamp: Event timestamp.
|
||||||
|
"""
|
||||||
|
kwargs['event_time'] = self._convert_timestamp(timestamp)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _convert_timestamp(timestamp: Optional[Union[int, float, datetime]] = None) -> Optional[datetime]:
|
||||||
|
if not (isinstance(timestamp, int) or isinstance(timestamp, float)):
|
||||||
|
return timestamp
|
||||||
|
|
||||||
|
return datetime.fromtimestamp(timestamp, tz=gettz())
|
||||||
|
|
||||||
|
|
||||||
|
class SlackMessageEvent(SlackEvent, ABC):
|
||||||
|
"""
|
||||||
|
Base class for message-related events.
|
||||||
|
"""
|
||||||
|
def __init__(self, *args, text: str, user: str, channel: Optional[str] = None, team: Optional[str] = None,
|
||||||
|
icons: dict = None, blocks: Iterable[dict] = None, **kwargs):
|
||||||
|
"""
|
||||||
|
:param text: Message text.
|
||||||
|
:param user: ID of the sender.
|
||||||
|
:param channel: ID of the channel.
|
||||||
|
:param team: ID of the team.
|
||||||
|
:param icons: Mapping of the icons for this message.
|
||||||
|
:param blocks: Extra blocks in the message.
|
||||||
|
"""
|
||||||
|
super().__init__(*args, text=text, user=user, channel=channel, team=team, icons=icons, blocks=blocks, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class SlackMessageReceivedEvent(SlackMessageEvent):
|
||||||
|
"""
|
||||||
|
Event triggered when a message is received on a monitored resource.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class SlackMessageEditedEvent(SlackMessageEvent):
|
||||||
|
"""
|
||||||
|
Event triggered when a message is edited on a monitored resource.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class SlackMessageDeletedEvent(SlackMessageEvent):
|
||||||
|
"""
|
||||||
|
Event triggered when a message is deleted from a monitored resource.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class SlackAppMentionReceivedEvent(SlackMessageEvent):
|
||||||
|
"""
|
||||||
|
Event triggered when a message that mentions the app is received on a monitored resource.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
|
@ -0,0 +1,10 @@
|
||||||
|
from platypush.plugins import Plugin, action
|
||||||
|
|
||||||
|
|
||||||
|
class ChatPlugin(Plugin):
|
||||||
|
"""
|
||||||
|
Base class for chat plugins.
|
||||||
|
"""
|
||||||
|
@action
|
||||||
|
def send_message(self, *args, **kwargs):
|
||||||
|
raise NotImplementedError()
|
|
@ -13,7 +13,8 @@ from telegram.user import User as TelegramUser
|
||||||
|
|
||||||
from platypush.message.response.chat.telegram import TelegramMessageResponse, TelegramFileResponse, \
|
from platypush.message.response.chat.telegram import TelegramMessageResponse, TelegramFileResponse, \
|
||||||
TelegramChatResponse, TelegramUserResponse, TelegramUsersResponse
|
TelegramChatResponse, TelegramUserResponse, TelegramUsersResponse
|
||||||
from platypush.plugins import Plugin, action
|
from platypush.plugins import action
|
||||||
|
from platypush.plugins.chat import ChatPlugin
|
||||||
|
|
||||||
|
|
||||||
class Resource:
|
class Resource:
|
||||||
|
@ -36,7 +37,7 @@ class Resource:
|
||||||
self._file.close()
|
self._file.close()
|
||||||
|
|
||||||
|
|
||||||
class ChatTelegramPlugin(Plugin):
|
class ChatTelegramPlugin(ChatPlugin):
|
||||||
"""
|
"""
|
||||||
Plugin to programmatically send Telegram messages through a Telegram bot. In order to send messages to contacts,
|
Plugin to programmatically send Telegram messages through a Telegram bot. In order to send messages to contacts,
|
||||||
groups or channels you'll first need to register a bot. To do so:
|
groups or channels you'll first need to register a bot. To do so:
|
||||||
|
|
258
platypush/plugins/slack.py
Normal file
258
platypush/plugins/slack.py
Normal file
|
@ -0,0 +1,258 @@
|
||||||
|
import json
|
||||||
|
from typing import Optional, Iterable
|
||||||
|
|
||||||
|
import multiprocessing
|
||||||
|
import requests
|
||||||
|
import websocket
|
||||||
|
from websocket import WebSocketApp
|
||||||
|
|
||||||
|
from platypush.context import get_bus
|
||||||
|
from platypush.message.event.chat.slack import SlackMessageReceivedEvent, SlackMessageDeletedEvent, \
|
||||||
|
SlackMessageEditedEvent, SlackAppMentionReceivedEvent
|
||||||
|
from platypush.plugins import RunnablePlugin, action
|
||||||
|
from platypush.plugins.chat import ChatPlugin
|
||||||
|
|
||||||
|
|
||||||
|
class SlackPlugin(ChatPlugin, RunnablePlugin):
|
||||||
|
"""
|
||||||
|
Plugin used to interact with Slack instances.
|
||||||
|
|
||||||
|
You'll have to generate your own Slack app and tokens in order to use this plugin. Steps:
|
||||||
|
|
||||||
|
- Create a new Slack app `here <https://api.slack.com/apps>`_ and associate a Slack workspace to it.
|
||||||
|
- In the configuration panel of your app scroll to _Socket Mode_ and select _Enable Socket Mode_.
|
||||||
|
- Scroll to _Event Subscriptions_ and select _Enable Events_.
|
||||||
|
- Choose the type of events that you want to subscribe to. You can select bot events (i.e. when somebody in
|
||||||
|
the channel mentions the name of your app) or any of the workspace events (e.g. creation of messages, user
|
||||||
|
events etc.).
|
||||||
|
- Scroll to _App-Level Tokens_ and generate a new token with ``connections:write`` scope. This token will be
|
||||||
|
used to receive Slack events over websocket.
|
||||||
|
- Scroll to _OAuth & Permissions_ and select the scopes that you want to enable. You may usually want to enable
|
||||||
|
_Bot Token Scopes_ -> ``app_mentions:read``, so the script can react when somebody mentions its name. You may
|
||||||
|
also want to select the user scopes relevant to your application - e.g. read/write messages, manage users etc.
|
||||||
|
- If you changed scopes and settings, you may have to reinstall the app in the workspace through the
|
||||||
|
_Install App_ menu.
|
||||||
|
- Navigate to the _Install App_ menu. If the app has been correctly installed in your workspace then you should
|
||||||
|
see a _Bot User OAuth Token_, used to authenticate API calls performed as the app/bot. If you also granted
|
||||||
|
user permissions to the app then you should also see a _User OAuth Token_ on the page.
|
||||||
|
|
||||||
|
Triggers:
|
||||||
|
|
||||||
|
- :class:`platypush.message.event.chat.slack.SlackMessageReceivedEvent` when a message is received on a
|
||||||
|
monitored channel.
|
||||||
|
- :class:`platypush.message.event.chat.slack.SlackMessageEditedEvent` when a message is edited on a
|
||||||
|
monitored channel.
|
||||||
|
- :class:`platypush.message.event.chat.slack.SlackMessageDeletedEvent` when a message is deleted from a
|
||||||
|
monitored channel.
|
||||||
|
- :class:`platypush.message.event.chat.slack.SlackAppMentionReceivedEvent` when a message that mentions
|
||||||
|
the app is received on a monitored channel.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
_api_base_url = 'https://slack.com/api'
|
||||||
|
|
||||||
|
def __init__(self, app_token: str, bot_token: str, user_token: Optional[str] = None, **kwargs):
|
||||||
|
"""
|
||||||
|
:param app_token: Your Slack app token.
|
||||||
|
:param bot_token: Bot OAuth token reported on the _Install App_ menu.
|
||||||
|
:param user_token: User OAuth token reported on the _Install App_ menu.
|
||||||
|
"""
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self._app_token = app_token
|
||||||
|
self._bot_token = bot_token
|
||||||
|
self._user_token = user_token
|
||||||
|
self._ws_url: Optional[str] = None
|
||||||
|
self._ws_app: Optional[websocket.WebSocketApp] = None
|
||||||
|
self._ws_listener: Optional[multiprocessing.Process] = None
|
||||||
|
self._connected_event = multiprocessing.Event()
|
||||||
|
self._disconnected_event = multiprocessing.Event()
|
||||||
|
self._state_lock = multiprocessing.RLock()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _url_for(cls, method: str) -> str:
|
||||||
|
return f'{cls._api_base_url}/{method}'
|
||||||
|
|
||||||
|
@action
|
||||||
|
def send_message(self, channel: str, as_user: bool = False, text: Optional[str] = None,
|
||||||
|
blocks: Optional[Iterable[str]] = None, **kwargs):
|
||||||
|
"""
|
||||||
|
Send a message to a channel.
|
||||||
|
It requires a token with ``chat:write`` bot/user scope.
|
||||||
|
|
||||||
|
:param channel: Channel ID or name.
|
||||||
|
:param as_user: If true then the message will be sent as the authorized user, otherwise as the application bot
|
||||||
|
(default: false).
|
||||||
|
:param text: Text to be sent.
|
||||||
|
:param blocks: Extra blocks to be added to the message (e.g. images, links, markdown). See
|
||||||
|
`Slack documentation for blocks <https://api.slack.com/reference/block-kit/blocks>`_.
|
||||||
|
"""
|
||||||
|
rs = requests.post(
|
||||||
|
self._url_for('chat.postMessage'),
|
||||||
|
headers={
|
||||||
|
'Authorization': f'Bearer {self._user_token if as_user else self._bot_token}'
|
||||||
|
},
|
||||||
|
json={
|
||||||
|
'channel': channel,
|
||||||
|
'text': text,
|
||||||
|
'blocks': blocks or [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
rs.raise_for_status()
|
||||||
|
rs = rs.json()
|
||||||
|
assert rs.get('ok'), rs.get('error', rs.get('warning'))
|
||||||
|
except Exception as e:
|
||||||
|
raise AssertionError(e)
|
||||||
|
|
||||||
|
def main(self):
|
||||||
|
self._connect()
|
||||||
|
stop_events = []
|
||||||
|
|
||||||
|
while not any(stop_events):
|
||||||
|
stop_events = self._should_stop.wait(timeout=1), self._disconnected_event.wait(timeout=1)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if self._ws_app:
|
||||||
|
self._ws_app.close()
|
||||||
|
self._ws_app = None
|
||||||
|
|
||||||
|
if self._ws_listener and self._ws_listener.is_alive():
|
||||||
|
self.logger.info('Terminating websocket process')
|
||||||
|
self._ws_listener.terminate()
|
||||||
|
self._ws_listener.join(5)
|
||||||
|
|
||||||
|
if self._ws_listener and self._ws_listener.is_alive():
|
||||||
|
self.logger.warning('Terminating the websocket process failed, killing the process')
|
||||||
|
self._ws_listener.kill()
|
||||||
|
|
||||||
|
if self._ws_listener:
|
||||||
|
self._ws_listener.join()
|
||||||
|
self._ws_listener = None
|
||||||
|
|
||||||
|
super().stop()
|
||||||
|
|
||||||
|
def _connect(self):
|
||||||
|
with self._state_lock:
|
||||||
|
if self.should_stop() or self._connected_event.is_set():
|
||||||
|
return
|
||||||
|
|
||||||
|
self._ws_url = None
|
||||||
|
rs = requests.post('https://slack.com/api/apps.connections.open', headers={
|
||||||
|
'Authorization': f'Bearer {self._app_token}',
|
||||||
|
})
|
||||||
|
|
||||||
|
try:
|
||||||
|
rs.raise_for_status()
|
||||||
|
except:
|
||||||
|
if rs.status_code == 401 or rs.status_code == 403:
|
||||||
|
self.logger.error('Unauthorized/Forbidden Slack API request, stopping the service')
|
||||||
|
self.stop()
|
||||||
|
return
|
||||||
|
|
||||||
|
raise
|
||||||
|
|
||||||
|
rs = rs.json()
|
||||||
|
assert rs.get('ok')
|
||||||
|
self._ws_url = rs.get('url')
|
||||||
|
self._ws_app = websocket.WebSocketApp(self._ws_url,
|
||||||
|
on_open=self._on_open(),
|
||||||
|
on_message=self._on_msg(),
|
||||||
|
on_error=self._on_error(),
|
||||||
|
on_close=self._on_close())
|
||||||
|
|
||||||
|
def server():
|
||||||
|
self._ws_app.run_forever()
|
||||||
|
|
||||||
|
self._ws_listener = multiprocessing.Process(target=server)
|
||||||
|
self._ws_listener.start()
|
||||||
|
|
||||||
|
def _on_open(self):
|
||||||
|
def hndl(*_):
|
||||||
|
with self._state_lock:
|
||||||
|
self._disconnected_event.clear()
|
||||||
|
self._connected_event.set()
|
||||||
|
self.logger.info('Connected to the Slack websocket')
|
||||||
|
|
||||||
|
return hndl
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _send_ack(ws: WebSocketApp, msg):
|
||||||
|
envelope_id = msg.get('envelope_id')
|
||||||
|
if envelope_id:
|
||||||
|
# Send ACK
|
||||||
|
ws.send(json.dumps({
|
||||||
|
'envelope_id': envelope_id,
|
||||||
|
}))
|
||||||
|
|
||||||
|
def _on_msg(self):
|
||||||
|
def hndl(*args):
|
||||||
|
ws = args[0] if len(args) > 1 else self._ws_app
|
||||||
|
data = json.loads(args[1] if len(args) > 1 else args[0])
|
||||||
|
output_event = None
|
||||||
|
self._send_ack(ws, data)
|
||||||
|
|
||||||
|
if data['type'] == 'events_api':
|
||||||
|
event = data.get('payload', {}).get('event', {})
|
||||||
|
event_args = {}
|
||||||
|
|
||||||
|
if event['type'] == 'app_mention':
|
||||||
|
output_event = SlackAppMentionReceivedEvent(
|
||||||
|
text=event['text'],
|
||||||
|
user=event['user'],
|
||||||
|
channel=event['channel'],
|
||||||
|
team=event['team'],
|
||||||
|
timestamp=event['event_ts'],
|
||||||
|
icons=event.get('icons'),
|
||||||
|
blocks=event.get('blocks')
|
||||||
|
)
|
||||||
|
elif event['type'] == 'message':
|
||||||
|
msg = event.copy()
|
||||||
|
prev_msg = event.get('previous_message')
|
||||||
|
event_type = SlackMessageReceivedEvent
|
||||||
|
|
||||||
|
if event.get('subtype') == 'message_deleted':
|
||||||
|
msg = prev_msg
|
||||||
|
event_type = SlackMessageDeletedEvent
|
||||||
|
event_args['timestamp'] = event['deleted_ts']
|
||||||
|
else:
|
||||||
|
event_args['timestamp'] = msg.get('ts')
|
||||||
|
if event.get('subtype') == 'message_changed':
|
||||||
|
msg = msg.get('message', msg)
|
||||||
|
event_args['previous_message'] = prev_msg
|
||||||
|
event_type = SlackMessageEditedEvent
|
||||||
|
|
||||||
|
event_args.update({
|
||||||
|
'text': msg.get('text'),
|
||||||
|
'user': msg.get('user'),
|
||||||
|
'channel': msg.get('channel', event.get('channel')),
|
||||||
|
'team': msg.get('team'),
|
||||||
|
'icons': msg.get('icons'),
|
||||||
|
'blocks': msg.get('blocks'),
|
||||||
|
})
|
||||||
|
|
||||||
|
output_event = event_type(**event_args)
|
||||||
|
|
||||||
|
if output_event:
|
||||||
|
get_bus().post(output_event)
|
||||||
|
|
||||||
|
return hndl
|
||||||
|
|
||||||
|
def _on_error(self):
|
||||||
|
def hndl(*args):
|
||||||
|
error = args[1] if len(args) > 1 else args[0]
|
||||||
|
ws = args[0] if len(args) > 1 else None
|
||||||
|
self.logger.warning('Slack websocket error: {}'.format(error))
|
||||||
|
if ws:
|
||||||
|
ws.close()
|
||||||
|
|
||||||
|
return hndl
|
||||||
|
|
||||||
|
def _on_close(self):
|
||||||
|
def hndl(*_):
|
||||||
|
with self._state_lock:
|
||||||
|
self._disconnected_event.set()
|
||||||
|
self._connected_event.clear()
|
||||||
|
self.logger.warning('Slack websocket connection closed')
|
||||||
|
|
||||||
|
return hndl
|
|
@ -1,6 +1,19 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from marshmallow import fields
|
||||||
|
|
||||||
|
|
||||||
|
class StrippedString(fields.Function):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs['serialize'] = self._strip
|
||||||
|
kwargs['deserialize'] = self._strip
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _strip(value: str):
|
||||||
|
return value.strip()
|
||||||
|
|
||||||
|
|
||||||
def normalize_datetime(dt: str) -> Optional[datetime]:
|
def normalize_datetime(dt: str) -> Optional[datetime]:
|
||||||
if not dt:
|
if not dt:
|
||||||
|
|
34
platypush/schemas/slack.py
Normal file
34
platypush/schemas/slack.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
from marshmallow import fields, INCLUDE
|
||||||
|
from marshmallow.schema import Schema
|
||||||
|
|
||||||
|
from platypush.schemas import StrippedString
|
||||||
|
|
||||||
|
|
||||||
|
class SlackMessageBlockSchema(Schema):
|
||||||
|
class Meta:
|
||||||
|
unknown = INCLUDE
|
||||||
|
|
||||||
|
type = fields.String(required=True, metadata=dict(description='Message block type'))
|
||||||
|
block_id = fields.String(required=True, metadata=dict(description='Block ID'))
|
||||||
|
|
||||||
|
|
||||||
|
class SlackMessageIconSchema(Schema):
|
||||||
|
image_36 = fields.URL()
|
||||||
|
image_48 = fields.URL()
|
||||||
|
image_72 = fields.URL()
|
||||||
|
|
||||||
|
|
||||||
|
class SlackMessageSchema(Schema):
|
||||||
|
text = StrippedString(required=True, metadata=dict(description='Message text'))
|
||||||
|
user = fields.String(required=True, metadata=dict(description='User ID of the sender'))
|
||||||
|
channel = fields.String(metadata=dict(description='Channel ID associated with the message'))
|
||||||
|
team = fields.String(metadata=dict(description='Team ID associated with the message'))
|
||||||
|
timestamp = fields.DateTime(metadata=dict(description='Date and time of the event'))
|
||||||
|
icons = fields.Nested(SlackMessageIconSchema)
|
||||||
|
blocks = fields.Nested(SlackMessageBlockSchema, many=True)
|
||||||
|
previous_message = fields.Nested(
|
||||||
|
'SlackMessageSchema', metadata=dict(
|
||||||
|
description='For received replies, it includes the parent message in the reply chain. '
|
||||||
|
'For edited messages, it contains the previous version.'
|
||||||
|
)
|
||||||
|
)
|
Loading…
Reference in a new issue