Laravel로 만드는 Slack Bot (2) - 슬랙 봇 구현

 

Laravel로 만드는 Slack Bot (2) - 슬랙 봇 구현

 

Laravel로 만드는 Slack Bot (1) - slack app 만들기 및 초기설정

Laravel로 만드는 Slack Bot (1) - slack app 만들기 및 초기설정 요즘 현업에서 많이 사용하는 slack. slack 봇을 라라벨을 이용하여 구현합니다. Slack App toekn DB화를 위한 모델 및 마이그레이션..

min-nine.tistory.com

 

Component Object Model이란?

  • 마이크로소프트에서 개발한, 다양한 언어로 만들어진 소프트웨어 컴포넌트들이 자신의 기능을 다른 소프트웨어와 공유하고 통합될 수 있도록 하는  이진 코드 레벨에서의 표준과 서비스를 총칭합니다.
  • 쉽게 말하자면 컴포넌트 '자동차' 1개의 컴포넌트로 구성되어 있는 것이 아닌 엔진,타이어,변속기 등이 각각 컴포넌트로 분리되어 있는 것을 의미합니다.
  •  프로그래밍 언어 독립성과 하위 호환성을 가능하게 하는 기술로써 객체 내부는 노출하지 않되, 응용프로그램이 호출 할 수 있는 메서드 단위로 구성된 객체지향 모델링 방법중 하나입니다.
  • Model이기 때문에 본인은 App\Models\ 에 만들어 사용하였습니다.

 

SettingComponentObjectModel 생성하기

<?php

namespace App\Models;

class SettingComponentObjectModel
{
    // Settings 테이블 조회
    public static function get($key, $default = null)
    {
        if (!$row = Setting::where('key', $key)->first()) {
            return $default;
        }
        return json_decode($row->value, true);
    }

    // Settings 테이블 삽입
    public static function set($key, $value = null)
    {
        if (is_array($key) && $value === null) {
            $rows = $key;
            foreach ($rows as $key => $value) {
                self::set($key, $value);
            }
        } else {
            $row = Setting::where('key', $key)->first() ?: new Setting;
            $row->key = $key;
            $row->value = json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
            $row->save();
        }
    }
}

 

SlackCompoentObjectModel 생성하기

<?php

namespace App\Models;

use GuzzleHttp\Client;
use Illuminate\Support\Arr;
use App\Models\SettingComponentObjectModel;

class SlackComponentObjectModel
{
    const CHANNEL1 = 'channel1';
    const CHANNEL2 = 'channel2';
    const CHANNEL3 = 'channel3';

    private static $last_channel_category = null;

    public static function setLastSlackCategory($slack_category)
    {
        self::$last_channel_category = $slack_category;
    }

    public static function getLastSlackCategory($default = null)
    {
        return self::$last_channel_category ?: $default;
    }

    public static function getSlackCategoryWithChannelToken($channel_token)
    {
        $slack = SettingComponentObjectModel::get('slack', null);

        foreach (Arr::get($slack, "channels") as $news_category => $channel) {
            if ($channel['token'] === $channel_token) {
                return $news_category;
            }
        }

        return null;
    }

    public static function getTokensWithSlackCategory($slack_category)
    {
        $tokens = [];

        $slack = SettingComponentObjectModel::get('slack', null);

        
        $channel_key = Arr::get($slack, "channels.$slack_category", [$slack_category]);    
        $channel_bot = Arr::get($channel_key, 'bot');
        $channel_token = Arr::get($channel_key, 'token');
        if ($bot =Arr::get($slack, "bots.$channel_bot")) {
            $bot_token = Arr::get($bot, 'token');
            $tokens[] = ['bot_token' => $bot_token, 'channel_token' => $channel_token];
        }

        return $tokens;
    }


    public static function async($slack_category, $msg, $data = [])
    {

        $slack_call_idx = SettingComponentObjectModel::get('slack_call_idx', 0);
        $slack_call_idx++;
        SettingComponentObjectModel::set('slack_call_idx', $slack_call_idx);
        $data = array_merge($data, ['slack_call_idx' => $slack_call_idx]);

        $tokens = self::getTokensWithSlackCategory($slack_category);
        foreach ($tokens as $token) {
            \App\Jobs\SendSlackJob::dispatch(
                Arr::get($token, 'bot_token'),
                Arr::get($token, 'channel_token'),
                $msg,
                $data
            );
        }
    }

    public static function send($bot_token, $channel_token, $msg, $data = [])
    {

        $json = [
            "channel" => $channel_token,
            "text" => $msg,
        ];

        if ($data) {
            $json = array_merge($json, $data);
        }

        $client = new Client(['base_uri' => 'https://slack.com/api/']);
        $client->request('POST', 'chat.postMessage', [
            'headers' => ['Authorization' => 'Bearer ' . $bot_token],
            'json' => $json,
        ]);

        self::setLastSlackCategory(null);
    }

    
}

 

SendSlackJob 생성하기

  • make:job 명령어로 위 SlackCom에서 사용할 SendSlackJob 생성
$ php artisan make:job SendSlackJob
<?php

namespace App\Jobs;

use App\Models\SlackComponentObjectModel;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class SendSlackJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $bot_token;
    protected $channel_token;
    protected $msg;
    protected $data;

    public $tries = 2;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct($bot_token, $channel_token, $msg, $data)
    {
        $this->bot_token = $bot_token;
        $this->channel_token = $channel_token;
        $this->msg = $msg;
        $this->data = $data;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        // 슬랙은 전송 실패시 30초 뒤에 한번더 시도한다
        // 이떄도 실패했을때 failed queue 에 등록됨
        try {
            SlackComponentObjectModel::send($this->bot_token, $this->channel_token, $this->msg, $this->data);
        } catch (\Throwable $exception) {
            if ($this->attempts() > 1) {
                throw $exception;
            }
            $this->release(30);
            return;
        }
    }
}

 

artisan tinker로 테스트하기

  • COM 방식으로 만들었기 때문에, 호출에 의해 작동하여 테스트또한 tinker로 호출합니다.
$ php artisan tinker
Psy Shell v0.11.2 (PHP 8.0.16 — cli) by Justin Hileman
>>> App\Models\SlackComponentObjectModel::async(SlackComponentObjectModel::CHANNEL3, 'MyNameIsMingyu!!!!! ');
=> null
>>> App\Models\SlackComponentObjectModel::async(SlackComponentObjectModel::CHANNEL3, 'this is channel 3@@@!@#!@#!#');
=> null

 

마무리

  • 챗봇은 채널 가입이 안 되어있기 때문에 에러가 났었습니다.
  • 때문에 스코프에서 chat:write.public을 추가하여 채널 가입이 안된 멤버에게도 메세지 전송을 허락해 줬습니다.