How to interact
Slack Machine provides several convenient ways to interact with channels and users in your Slack workspace. To this end, two very similar sets of functions are exposed through two classes:
MachineBasePlugin
- The
MachineBasePlugin
class every plugin extends, provides methods to send messages to channels (public, private and DM), using the WebAPI, with support for rich messages/blocks/attachment. It also supports adding reactions to messages, pinning and unpinning messages, replying in-thread, sending ephemeral messages to a channel (only visible to 1 user), updating and deleting messages and much more.
Message
- An instance of the
Message
class is automatically supplied to your plugin functions when using the@respond_to
or@respond_to
decorators. It has a similar set of methods as theMachineBasePlugin
class, but without the need to manually specify the channel you want to talk to. It lets you send messages and reply to messages in the same channel the original message was received in.
It is recommended to use the passed-in Message
object to interact with channels
and users, whenever you use the @respond_to
or
@respond_to
decorators, as this takes away the pain of having to manually
target the right channels.
For a detailed description of all the methods available to you, please read the api documentation. What follows are some examples of how you would respond in common scenarios.
Responding to a message
- If your plugin receives a message through the
@respond_to
- or
@listen_to
decorators, - the simplest way to reply is using
msg.reply()
. It takes 2 parameters: -
text: the message you want to send
-
in_thread: if Slack Machine should reply to the original message in-thread
msg.reply()
will start the reply with a mention of the sender of the
original message.
Example:
@respond_to(r"^I love you")
async def spread_love(self, msg):
await msg.reply("I love you too!")
If this function is triggered by a message @superbot I love you, sent by @john, the response will be: @john: I love you too!
msg.reply()
will use the Slack WebAPI to send
messages, which means you can send richly formatted messages
using blocks and/or
attachments.
The underlying Python slack-sdk that Slack Machine uses, provides some
convenience classes
that can help with creating blocks or attachments. All Slack Machine methods that can be used to send messages, accept
lists of Block
objects and/or Attachment
objects from the aforementioned convience classes.
- This method has 2 extra parameters that unlock 2 extra features:
-
ephemeral: if
True
, the message will be visible only to the sender of the original message. -
in_thread: this will send the message in a thread instead of to the main channel
There is 1 more method to respond to a message in the same channel: msg.say()
is very similar to its reply
counterpart, with the exception that it won't mention the sender of the original message.
If you want to reply to the sender of the original message in a DM instead of in the original channel, you can use the
msg.reply_dm()
methods. This will open a DM convo between the sender of
the original message and the bot (if it doesn't exist already) and send a message there. If the original message was
already received in a DM channel, this is no different from using reply()
.
Message properties
- The
Message
object your plugin function receives, has some convenient properties - about the message that triggered the function:
-
sender: a
User
object with information about the sender, such as theirid
andname
-
channel: a Channel object with information about the channel the message was received in
-
text: the contents of the original message
Plugin properties
- The
MachineBasePlugin
class every plugin extends, exposes some properties - about your Slack workspace. These properties are not filled when your plugin is instantiated, but reflect the current
- status of the Slack client:
-
users: a dict of user ids and the associated
User
objects for all users that Slack Machine knows about. This is usually all the active users in your Slack workspace. This data structure is filled when Slack Machine starts and is automatically updated whenever a new user joins or the properties of a user change. -
channels: a dict of channel ids and the associated
User
objects for channels that Slack Machine knows about. This contains all the public channels in your Slack workspace, plus all private channels that your Slack Machine instance was invited to.
Sending messages without a msg object
There are situations in which you want to send messages to users/channels, but there is no original message to respond
to. For example when implementing your own event listener using the @process
decorator. In this case you can call functions similar as those described before, but from your plugin itself:
self.say()
and
self.send_dm()
.
These behave similar to their Message
counterparts, except that they require a
channel id or object, or user id or object (in case of DM) to be passed in. You can use
find_channel_by_name()
to find the channel you want
to send a message to.
Scheduling messages
Sometimes you want to reply to a message, send a message to some channel, send a DM etc. but you don't want to do it
now. You want to do it in the future. Slack Machine provides scheduled versions of many methods, both in the
MachineBasePlugin
all plugins extend from and in the
Message
object @respond_to
and
@respond_to
functions receive. These methods can be recognized by their
_scheduled prefix. They work almost the same as their regular counterparts, except that they receive 1 extra
argument: a datetime
object that tells Slack Machine when to send the message.
Example:
@respond_to(r"greet me in the future")
async def future(self, msg):
await msg.say("command received!")
in_10_sec = datetime.now() + timedelta(seconds=10)
await msg.reply_dm_scheduled(in_10_sec, "A Delayed Hello!")
This function will send a greeting 10 seconds after it has received a message: @superbot greet me in the future.
Caveat
You cannot schedule a reaction to a message. It doesn't make sense to react to a message in the future.
For more information about scheduling message, have a look at the api documentation.
Protecting commands
Sometimes you may want to restrict certain commands in your bot, so they can only be invoked by certain users.
To use these restrictions you must appoint one user to be the root user. For security reasons there can be only one
root user, and it must be configured through local_settings.py
or environment variables. That way you will never
lose control over your bot.
To enable all the role based features, your local_settings.py
would look something like this:
SLACK_APP_TOKEN = "xapp-my-app-token"
SLACK_BOT_TOKEN = "xoxb-my-bot-token"
ROOT_USER = "0000007"
PLUGINS = [
'machine.plugins.builtin.admin.RBACPlugin',
]
You can get the member ID from a user by going to their Slack profile, clicking more and selecting Copy member ID.
If you wish to share the powers of root you can enable the RBAC admin
plugin machine.plugins.builtin.admin.RBACPlugin
and grant the admin role to users you trust.
The RBAC plugin provides you with three new commands that lets you lookup, grant and revoke roles to users:
- @superbot who has role admin
- @superbot grant role admin to @trusted-user
- @superbot revoke role admin from @trusted-user.
Now you can decorate certain functions in your plugin with the
@require_any_role
or
@require_all_roles
decorators to make them only usable by users with
certain roles.
Here is an example of a command that requires either the admin or channel role:
@respond_to(
r"^say in"
r"\s+<#\w+\|(?P<channel_name>[^>]+)>"
r"\s+(?P<message>.+)"
)
@require_any_role(["admin", "channel"])
async def say_in_channel(self, msg, channel_name, message):
logging.info(channel_name)
await self.say(channel_name, message)
You can define as many roles as you want, any string without spaces is acceptable.
Emitting events
Your plugin can emit arbitrary events that other plugins (or your own) can listen for. Events are a convenient
mechanism for exchanging data between plugins and/or for a plugin to expose an api that other plugins
can hook into. Emitting an event is done with self.emit()
.
You have to provide a name for the event you want to emit, so others can listen for an event by that name. You can
optionally provide extra data as keyword arguments.
Example:
@respond_to(r"I have used the bathroom")
async def broadcast_bathroom_usage(self, msg):
self.emit('bathroom_used', toilet_flushed=True)
You can read the events section to see how your plugin can listen for events.
Using the Slack Web API in other ways
Sometimes you want to use Slack Web API in ways that are not directly exposed by
MachineBaserPlugin
. In these cases you can use
self.web_client
. self.web_client
references the
AsyncWebClient
object of the underlying Slack Python SDK. You should be able to call any
Web API method with that client.