Skip to content

Commit

Permalink
feat: add stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
DonDebonair committed Nov 19, 2024
1 parent ce540ce commit 3a3cc49
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 17 deletions.
17 changes: 11 additions & 6 deletions machine/clients/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import asyncio
from datetime import datetime
from typing import Any, AsyncGenerator, Awaitable, Callable
from typing import Any, AsyncGenerator, Awaitable, Callable, Sequence

from slack_sdk.errors import SlackApiError
from slack_sdk.models.views import View
Expand All @@ -13,15 +13,20 @@
from slack_sdk.web.async_client import AsyncWebClient
from slack_sdk.web.async_slack_response import AsyncSlackResponse
from structlog.stdlib import get_logger
from zoneinfo import ZoneInfo

from machine.models import Channel, User
from machine.utils.datetime import calculate_epoch

from zoneinfo import ZoneInfo

logger = get_logger(__name__)


def is_sequence(obj: Any):
if isinstance(obj, str):
return False
return isinstance(obj, Sequence)


def id_for_user(user: User | str) -> str:
if isinstance(user, User):
return user.id
Expand Down Expand Up @@ -290,9 +295,9 @@ async def react(self, channel: Channel | str, ts: str, emoji: str) -> AsyncSlack
channel_id = id_for_channel(channel)
return await self._client.web_client.reactions_add(name=emoji, channel=channel_id, timestamp=ts)

async def open_im(self, user: User | str) -> str:
user_id = id_for_user(user)
response = await self._client.web_client.conversations_open(users=user_id)
async def open_im(self, users: User | str | Sequence[User | str]) -> str:
user_ids = [id_for_user(user) for user in users] if is_sequence(users) else id_for_user(users)
response = await self._client.web_client.conversations_open(users=user_ids)
return response["channel"]["id"]

async def send_dm(self, user: User | str, text: str | None, **kwargs: Any) -> AsyncSlackResponse:
Expand Down
6 changes: 1 addition & 5 deletions machine/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from slack_sdk.socket_mode.aiohttp import SocketModeClient
from slack_sdk.web.async_client import AsyncWebClient
from structlog.stdlib import get_logger
from zoneinfo import ZoneInfo

from machine.clients.slack import SlackClient
from machine.handlers import (
Expand Down Expand Up @@ -40,11 +41,6 @@
from machine.utils.logging import configure_logging
from machine.utils.module_loading import import_string

if sys.version_info >= (3, 9):
from zoneinfo import ZoneInfo # pragma: no cover
else:
from backports.zoneinfo import ZoneInfo # pragma: no cover

logger = get_logger(__name__)


Expand Down
4 changes: 2 additions & 2 deletions machine/plugins/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ async def send_dm(
:param text: message text
:param attachments: optional attachments (see `attachments`_)
:param blocks: optional blocks (see `blocks`_)
:return: Dictionary deserialized from `chat.postMessage`_ request.
:return: Dictionary deserialized from `chat.postMessage`_ response.
.. _chat.postMessage: https://api.slack.com/methods/chat.postMessage
"""
Expand Down Expand Up @@ -471,7 +471,7 @@ async def update_modal(
"""Update a modal dialog
Update a modal dialog that was previously opened. You can update the view by providing the view_id or the
external_id of the modal. external_id has precedence over vie_id, but at least one needs to be provided.
external_id of the modal. external_id has precedence over view_id, but at least one needs to be provided.
You can also provide a hash of the view that you want to update to prevent race conditions.
:param view: view definition for the modal dialog
Expand Down
53 changes: 51 additions & 2 deletions machine/plugins/block_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from slack_sdk.models.attachments import Attachment
from slack_sdk.models.blocks import Block
from slack_sdk.models.views import View
from slack_sdk.web.async_slack_response import AsyncSlackResponse
from slack_sdk.webhook import WebhookResponse
from slack_sdk.webhook.async_client import AsyncWebhookClient
from structlog.stdlib import get_logger
Expand All @@ -30,7 +32,7 @@ class BlockAction:

def __init__(self, client: SlackClient, payload: BlockActionsPayload, triggered_action: Action):
self._client = client
self.payload = payload #: blablab
self.payload = payload
"""The payload that was received by the bot when the action was triggered that this plugin method listens for"""
self.triggered_action = triggered_action
"""The action that triggered this plugin method"""
Expand Down Expand Up @@ -74,7 +76,7 @@ def response_url(self) -> Optional[str]:
def trigger_id(self) -> str:
"""The trigger id associated with the action
The trigger id can be user ot open a modal
The trigger id can be used to open a modal
:return: the trigger id for the action
"""
Expand Down Expand Up @@ -136,3 +138,50 @@ async def say(
delete_original=delete_original,
**kwargs,
)

async def send_dm(
self,
text: str | None = None,
attachments: Sequence[Attachment] | Sequence[dict[str, Any]] | None = None,
blocks: Sequence[Block] | Sequence[dict[str, Any]] | None = None,
**kwargs: Any,
) -> AsyncSlackResponse:
"""Send a DM to the user that triggered the block action
Send a Direct Message to the user that triggered the block action by opening a DM channel and
sending a message to it. Allows for rich formatting using `blocks`_ and/or `attachments`_.
Allows for rich formatting using [blocks] and/or [attachments] . You can provide blocks
and attachments as Python dicts or you can use the [convenient classes] that the
underlying slack client provides.
Any extra kwargs you provide, will be passed on directly to the `chat.postMessage` request.
[attachments]: https://api.slack.com/docs/message-attachments
[blocks]: https://api.slack.com/reference/block-kit/blocks
[convenient classes]: https://github.com/slackapi/python-slack-sdk/tree/main/slack/web/classes
:param text: message text
:param attachments: optional attachments (see [attachments])
:param blocks: optional blocks (see [blocks])
:return: Dictionary deserialized from [chat.postMessage] response.
[chat.postMessage]: https://api.slack.com/methods/chat.postMessage
"""
return await self._client.send_dm(self.user.id, text, attachments=attachments, blocks=blocks, **kwargs)

async def open_modal(
self,
view: dict | View,
**kwargs: Any,
) -> AsyncSlackResponse:
"""Open a modal in response to the block action
Open a modal in response to the block action, using the trigger_id that was returned when the block action was
triggered.
Any extra kwargs you provide, will be passed on directly to `AsyncWebClient.views_open()`
Note: you have to call this method within 3 seconds of receiving the block action payload.
:param view: the view to open
:return: Dictionary deserialized from `AsyncWebClient.views_open()`
"""
return await self._client.open_modal(self.trigger_id, view, **kwargs)
20 changes: 19 additions & 1 deletion machine/plugins/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from slack_sdk.models.attachments import Attachment
from slack_sdk.models.blocks import Block
from slack_sdk.models.views import View
from slack_sdk.web.async_slack_response import AsyncSlackResponse
from slack_sdk.webhook import WebhookResponse
from slack_sdk.webhook.async_client import AsyncWebhookClient

Expand Down Expand Up @@ -116,10 +118,26 @@ async def say(
:param ephemeral: `True/False` wether to send the message as an ephemeral message, only
visible to the sender of the original message
:return: Dictionary deserialized from `AsyncWebhookClient.send()`
"""
response_type = "ephemeral" if ephemeral else "in_channel"

return await self._webhook_client.send(
text=text, attachments=attachments, blocks=blocks, response_type=response_type, **kwargs
)

async def open_modal(
self,
view: dict | View,
**kwargs: Any,
) -> AsyncSlackResponse:
"""Open a modal in response to the command
Open a modal in response to the command, using the trigger_id that was returned when the command was invoked.
Any extra kwargs you provide, will be passed on directly to `AsyncWebClient.views_open()`
Note: you have to call this method within 3 seconds of receiving the command payload.
:param view: the view to open
:return: Dictionary deserialized from `AsyncWebClient.views_open()`
"""
return await self._client.open_modal(self.trigger_id, view, **kwargs)
120 changes: 119 additions & 1 deletion machine/plugins/modals.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
from __future__ import annotations

from typing import Any, Sequence

from slack_sdk.models.attachments import Attachment
from slack_sdk.models.blocks import Block
from slack_sdk.web.async_slack_response import AsyncSlackResponse

from machine.clients.slack import SlackClient
from machine.models.interactive import ViewClosedPayload, ViewSubmissionPayload
from machine.models import User
from machine.models.interactive import View, ViewClosedPayload, ViewSubmissionPayload


class ModalSubmission:
Expand All @@ -9,6 +18,115 @@ def __init__(self, client: SlackClient, payload: ViewSubmissionPayload):
self._client = client
self.payload = payload

@property
def user(self) -> User:
"""The user that submitted the modal
:return: the user that submitted the modal
"""
return self._client.users[self.payload.user.id]

@property
def view(self) -> View:
"""The view that was submitted including the state of all the elements in the view
:return: the view that was submitted
"""
return self.payload.view

@property
def trigger_id(self) -> str:
"""The trigger id associated with the submitted modal
The trigger id can be user ot open another modal
:return: the trigger id for the modal
"""
return self.payload.trigger_id

async def open_modal(
self,
view: dict | View,
**kwargs: Any,
) -> AsyncSlackResponse:
"""Open another modal in response to the modal submission
Open another modal in response to modal submission, using the trigger_id that was returned when the modal was
submitted.
Any extra kwargs you provide, will be passed on directly to `AsyncWebClient.views_open()`
Note: you have to call this method within 3 seconds of receiving the modal submission payload.
:param view: the view to open
:return: Dictionary deserialized from `AsyncWebClient.views_open()`
"""
return await self._client.open_modal(self.trigger_id, view, **kwargs)

async def push_modal(
self,
view: dict | View,
**kwargs: Any,
) -> AsyncSlackResponse:
"""Push a new modal view in response to the modal submission
Push a new modal view on top of the view stack in response to modal submission, using the trigger_id that was
returned when the modal was submitted.
Any extra kwargs you provide, will be passed on directly to `AsyncWebClient.views_push()`
Note: you have to call this method within 3 seconds of receiving the modal submission payload.
:param view: the view to push
:return: Dictionary deserialized from `AsyncWebClient.views_push()`
"""
return await self._client.push_modal(self.trigger_id, view, **kwargs)

async def update_modal(
self,
view: dict | View,
**kwargs: Any,
) -> AsyncSlackResponse:
"""Update the modal view in response to the modal submission
Update the modal view in response to modal submission, using the trigger_id that was returned when the modal was
submitted.
Any extra kwargs you provide, will be passed on directly to `AsyncWebClient.views_update()`
Note: you have to call this method within 3 seconds of receiving the modal submission payload.
:param view: the view to update
:return: Dictionary deserialized from `AsyncWebClient.views_update()`
"""
return await self._client.update_modal(view, self.payload.view.id, self.payload.view.external_id, **kwargs)

async def send_dm(
self,
text: str | None = None,
attachments: Sequence[Attachment] | Sequence[dict[str, Any]] | None = None,
blocks: Sequence[Block] | Sequence[dict[str, Any]] | None = None,
**kwargs: Any,
) -> AsyncSlackResponse:
"""Send a DM to the user that submitted the modal
Send a Direct Message to the user that submitted the modal by opening a DM channel and
sending a message to it.
Allows for rich formatting using [blocks] and/or [attachments] . You can provide blocks
and attachments as Python dicts or you can use the [convenient classes] that the
underlying slack client provides.
Any extra kwargs you provide, will be passed on directly to the `chat.postMessage` request.
[attachments]: https://api.slack.com/docs/message-attachments
[blocks]: https://api.slack.com/reference/block-kit/blocks
[convenient classes]: https://github.com/slackapi/python-slack-sdk/tree/main/slack/web/classes
:param text: message text
:param attachments: optional attachments (see [attachments])
:param blocks: optional blocks (see [blocks])
:return: Dictionary deserialized from [chat.postMessage] response.
[chat.postMessage]: https://api.slack.com/methods/chat.postMessage
"""
return await self._client.send_dm(self.user.id, text, attachments=attachments, blocks=blocks, **kwargs)


class ModalClosure:
payload: ViewClosedPayload
Expand Down

0 comments on commit 3a3cc49

Please sign in to comment.