xou816/slack-components

Slack交互消息的声明式库

dev-master 2018-06-12 14:13 UTC

This package is not auto-updated.

Last update: 2024-09-27 16:33:43 UTC


README

Slack交互消息的声明式库。

特性

  • 强大:管理智能、有状态的组件
  • 简单灵活:可以根据需要选择是否使用组件
  • 易于测试:对您的消息进行Slack交互的模拟
  • 与PHP 5.5+兼容,仅依赖Guzzle

安装

通过Composercomposer require xou816/slack-components

入门指南

本指南假设您对Slack交互消息有一定的了解。

创建路由器

SlackRouter负责处理传入的Slack交互以及派发您的消息、对话框等。

// $client is a GuzzleHttp\Client
$router = new SlackRouter($client, $options); 

有效选项

或者,您也可以构建一个启用默认中间件的路由器

// $client is still a GuzzleHttp\Client
$router = SlackRouter::defaults($client, $options); 

请求URL

一旦您设置了请求URL并在您的应用程序中设置了一个匹配的路由,响应来自Slack的请求为HTTP 200,并在您的响应发送后调用一次hookAfterResponse

例如,在Symfony的terminate事件之后添加以下片段,其中$router是一个SlackRouter实例,$request是一个Symfony请求

$payload = json_decode($request->request->get('payload'), true);
$router->hookAfterResponse($payload);

此钩子负责向Slack交互发送响应。

中间件

中间件是可以在处理之前更改传入的Slack操作负载的函数

$router->push(function($payload, $next) {
    $modified = modify($payload);
    return $next($modified);
});

或者修改发送的响应

$router->push(function($payload, $next) {
    $response = $next($payload);
    return modify($response);    
});

默认中间件

  • $router->checkToken():验证负载令牌,您必须有一个路由器实例才能使用该中间件,并且它应该是堆栈中的第一个
  • Middleware::parseCallbacks():用CallbackId组件替换原始回调ID
  • Middleware::parseInteractions():用相应的SlackInteraction对象替换交互
  • Middleware::parseUser():用SlackUser对象替换负载用户
  • Middleware::wrapResponse():将普通对象包装在适当的SlackResponse对象中(默认使用response_url)。

其他中间件可以编写来处理授权、日志记录等。

发送消息

发送示例(静态)消息

$msg = new SlackPayload(SlackPayload::WEBHOOK, '#channel', 'Hello world!'); // uses the webhooks option
$router->send($msg);

幸运的是,交互式消息通过智能组件让您无需自己构建SlackPayload,这简化了操作。

交互

使用when方法注册由特定回调ID触发的处理程序。回调ID通常附加到您发送的消息中的操作。

$router->when('callback', function($payload) {
    // if $payload matches a button press...
});

组件

组件用于简化构建Slack消息的过程。例如,如果您打算发布一条带有点击计数器的消息,而不是发送这个(您当然可以!)

[
    'text' => $count,
    'attachments' => [
        [
            'callback_id' => 'mycallback',
            'actions' => [
                [
                    'type' => 'button',
                    'style' => 'primary',
                    'value' => 'increment',
                    'name' => 'increment',
                    'text' => 'Increment'
                ]
            ]
        ]
    ]
]

您可能会写成这样

[
    'text' => $count,
    'attachments' => [
        [
            'callback_id' => CallbackId::wrap(['count' => 0]),
            'actions' => [
                Button::create('btn_name')->withLabel('Increment')
            ]
        ]
    ]
]

这有什么好处?除了您的IDE中的代码补全问题之外,使用组件将大大简化对操作的响应和管理状态的过程。实际上,在这个例子中,您需要跟踪点击次数。

管理状态

此库通过回调ID提供状态管理。由于它在每条消息和交互中都会发送,因此它似乎是解决我们的状态管理问题的合适候选者。

它可以用于存储“完整”状态,当它足够小的时候...

['count' => 0]

...或者它还可以帮助在数据库中跟踪该状态的更大表示。

['state_id' => 123]

CallbackId 组件是一个相当简单的组件,当 构建 后会产生一个字符串——这是一个键和某些数据的 base64 编码。

$callbackId
    ->withKey('mykey')
    ->withData(['count' => 0])

识别消息的来源(以及能够处理未来交互的内容),而 数据 可以用来存储状态。

如果您想以这种方式处理传入的回调 ID,则 必须 使用 parseCallbacks 中间件。

构建组件

组件是复杂的对象,我们需要向 Slack 发送纯对象(JSON)。因此,组件必须通过一个 构建步骤:首先构建完整的组件树,然后将其渲染为一个纯对象。

组件可能看起来与流行的前端框架中可以找到的内容相似,但有几个重要区别,其中之一是 组件树中的状态由所有组件共享

根组件通常可以通过组件的 getContext 方法访问。

为什么状态很重要

给定一个组件树的先前渲染(例如 Slack 有时提供的 original_message)和先前的状态(存储在我们的回调 ID 中),我们能够高效地重建树(即我们的消息)...但关于这一点稍后详述。

交互

组件提供了一种方便的方法在路由器中注册交互处理程序。例如,启用 parseInteractions 中间件后

$router->when('callback', $button->clicked(function(ButtonAction $action) {
    // ...
}));

当按钮实例 $button 被点击时,此处理程序将为指定的回调触发。

反射

反射在处理程序中与提供给 clicked 的处理程序一样被使用。因此,您可以在这样的闭包中注入以下元素

  • 交互对象,通过请求 SlackInteraction 的子类,例如 ButtonActionDialogSubmission
  • 负责交互的用户,通过请求 SlackUser 类型的参数
  • 完整的 Slack 有效负载,通过请求单个参数或名为 payload 的参数
  • 状态键,通过请求具有相同名称的参数

而不是闭包,您还可以提供可调用对象。

响应用户交互

在响应用户交互时,您可以返回以下之一

  • 一条消息(构建):例如,使用 InteractiveMessage::patchState,但它也可以是完全新的消息
  • 使用 Dialog::openDialog::doOpen 打开对话框(见对话框)的请求
  • 使用 SlackUser::sendMessage 向触发动作的用户发送消息。

交互式消息

交互式消息提供了一个方便的方法来管理组件、交互、回调键和状态,所有这些都在一个地方。

class MyMessage extends InteractiveMessage {

    public function __construct(SlackRouter $router) {
        parent::__construct($router);
        $this->increment = new Button('increment');
        $this->when($this->increment->clicked(function($count) {
            return $this->patchState(['count' => $count + 1]);
        }));
    }

    protected function buildMessage($count) {
        return [
            'text' => $count,
            'attachments' => [
                [
                    'callback_id' => $this->callback([
                        'count' => $count
                    ]),
                    'actions' => [
                        $this->increment
                            ->withLabel('Increment')
                    ]
                ]
            ]
        ];
    }
}

注意事项

  • 我们使用一个类来表示我们的消息
  • 其内容由 buildMessage 描述
  • 我们使用 when 监听我们对 increment 按钮的交互
  • 我们使用 callback 存储消息的状态
  • 在交互时,我们会修补我们的消息,即再次使用更新后的状态渲染它。

您还可以使用 InteractiveMessage::create($router, $buildMesssageClosure) 构建所谓的匿名消息。

根据消息类自动为您选择回调键。这就是为什么您不需要向消息的 when 方法提供它。

构建和发送

假设您有一个消息类的实例

$built = $myMessage->build('#channel', ['count' => 0]);
$myMessage->send($built);
// or...
$myMessage->buildAndSend('#channel', ['count' => 0]);

消息将通过您的 webhooks 发送到指定的频道。

send 方法接受任何 SlackPayload 对象。

修补消息

打补丁消息意味着使用补丁(新状态的一个子集)更新状态。《patchState》方法返回一个普通对象的消息。

《patchState》方法在使用《LazyComponent》时特别有用。后者仅在需要时渲染--如果我们有它的前一个渲染版本,并且它所依赖的状态相关部分没有变化,我们就保留前一个渲染。

如果你需要对特定组件执行昂贵的计算,这非常有用--将其包装在《LazyComponent》中!

可以使用简单的闭包构建此类组件,其中参数表示我们依赖的状态。

function($count) {
    return /**/;
}

这是一个有效的组件,它仅在状态中的《count》发生变化时渲染。下面是一个完整的示例。

class MyMessage extends InteractiveMessage {

    public function __construct(SlackRouter $router) {
        parent::__construct($router);
        $this->increment = new Button('increment');
        $this->when($this->increment->clicked(function($count, $reverse) {
            $patch = ['count' => $count + ($reverse ? -1 : 1)];
            if (abs($patch['count']) === 10) {
                $patch['reverse'] = !$reverse;
            }
            return $this->patchState($patch);
        }));   
    }

    protected function defaultState() {
        return ['reverse' => false, 'count' => 0];
    }

    protected function buildMessage($count, $reverse) {
        return [
            'text' => $count,
            'attachments' => [
                [
                    'callback_id' => $this->callback([
                        'count' => $count,
                        'reverse' => $reverse
                    ]),
                    'actions' => function($reverse) {
                        return [
                            $this->increment
                                ->withLabel($reverse ? 'Decrement' : 'Increment')
                        ];
                    }
                ]
            ]
        ];
    }
}

在那个例子中,消息的《actions》部分仅在十次点击中计算一次--直到标签需要更改!你不信?在这个按钮中显示日期!

计算属性

计算属性是直接依赖于状态且你希望尽量避免频繁计算的性质。

例如,如果你在数据库中存储一个非常复杂的状态,你可能会通过在消息状态中保存其数据库ID来访问它。

然而,如果你在某个时刻从数据库中获取了这个状态,你希望避免在消息内部再次获取它。

创建计算属性

// assuming $state = ['id' => ..., ...]
$this->fullState = function($id) {
    return $this->database->fetch($id);
};

访问它

$fullState = $this->fullState;

分配现有的计算

$this->fullState = $theFullState;

故障排除

重要提示。你的消息实例必须存在,以便在路由器中注册交互处理程序。

常见组件

在消息中:按钮《Button》,选择《Select》。你可以使用《clicked》和《selected》分别将这些组件附加(反射就绪)处理程序。

在对话框中:《TextInput》,《Textarea》,《Select》。

对话框

对话框不是常见组件,因为它们不是直接附加到消息上的。然而,就像按钮或选择一样,它们提供了一个轻松响应用户交互的方法。

$myDialog = Dialog::create('Test dialog')
    ->withElements([
        function($default) {
            return TextInput::create('name')
                ->withValue($default)
                ->withLabel('Please enter your name below');
        },
        Select::create('select')
            ->withOption('opt1', 'Option 1')
            ->withOption('opt2', 'Option 2')
    ]);

对话框本身没有状态,当你第一次创建它时,它不像按钮那样附加到消息主体上。然而,当一个交互导致对话框打开时,那个交互携带一个回调ID,因此有一个状态。这个状态被传达给对话框,并且组件可以查询它(这就是上述《LazyComponent》/闭包所做的那样)。

class MyMessageWithDialog extends InteractiveMessage {

    private $dialog;
    private $button;

    public function __construct(SlackRouter $router) {
        global $myDialog;
        parent::__construct($router);
        $this->dialog = $myDialog;
        $this->button = new Button('btn');
        $this->when($this->button->clicked($this->dialog->doOpen()));
        $this->when($this->dialog->submitted(function(DialogSubmission $sub, $greet) {
            return $greet.', '.$sub->name;
        }));
    }

    protected function buildMessage($greet) {
        return [
            'text' => 'Dialog demo',
            'attachments' => [
                [
                    'callback_id' => $this->callback([
                        'greet' => $greet,
                        'default' => 'Robert' // this part is communcated to the dialog
                    ]),
                    'actions' => [
                        $this->button
                            ->withLabel('Open dialog')
                    ]
                ]
            ]
        ];
    }
}

为了打开对话框,必须先发生一个交互。然后你可以调用以下方法之一

  • 《doOpen》,它返回一个适当的闭包
  • 或《open》,你必须《必须》给出完整的交互《$payload》。

一旦打开,对话框就可以像按钮或选择一样使用,你可以使用《submitted》来响应用户提交。