0
0

More than 1 year has passed since last update.

JavaでSlack botをリリースするまで その1

Last updated at Posted at 2022-06-29

はじめに

SLackのアプリをJavaで実装する方法に、Slackが提供するフレームワークboltがあります。
今回はこちらを使用して既存のアプリをSlackに対応させる方法・手順について記載します。

アプリの作成

Slack側の設定

新規アプリの登録

Create New App より新規アプリを登録します。
image.png

OAuth & Permissions の設定

Permission を設定します。

設定は一例ですので必要に応じて設定を変更してください。
今回は Slash Commands を利用するので、以下のようにしています。
image.png

Workspace にアプリをインストールしてください。
image.png

Tokenが発行されます。
FireShot Capture 093 - Slack API_ Applications - livlog Slack - api.slack.com.png

Redirect URLs の設定はアプリごとにOauth認証の実装が個別に必要になります。
OAuthを使用したインストールに詳細が書かれていますが、こちらの記事では割愛しています。
image.png

Slash Commandsの登録

今回実装するSlash Commandsを登録していきます。
image.png

Postで呼び出すサーバ側のURLを登録します。
68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f32303436342f32356462393163332d386565302d636638622d613862612d3661383534313635393734342e706e67.png

Java側の設定

Postの処理を実装するために、、ここではSpring bootを使用します。
githubのサンプルを参考に実装していきます。

Mavenのpom.xmlに以下を追加します。

pom.xml
<dependency>
    <groupId>com.slack.api</groupId>
    <artifactId>bolt</artifactId>
    <version>1.22.1</version>
</dependency>
<dependency>
    <groupId>com.slack.api</groupId>
    <artifactId>bolt-servlet</artifactId>
    <version>1.22.1</version>
</dependency>

@ServletComponentScan のアノテーションをつけているので、Spring bootにServletを実装する形になります。

Application.java
@SpringBootApplication
@ServletComponentScan
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

ここら辺から少しサンプルを改造しています。
Tomcatなどから動かす場合は、コンストラクタの書き方でエラーが発生していたので、このような形に変更しています。

SlackEventsServlet.java
@WebServlet ("/slack/events")
public class SlackEventsServlet extends AbsSlackAppServlet {

    private final AppService appService = this.injector.getInstance(AppServiceImpl.class);

    public SlackEventsServlet() {

        super(new SlackApp().initSlackApp(new SlackApp().loadAppConfig()));
    }


    public SlackEventsServlet(App app) {

        super(app);
    }


    @Override
    protected void performTask(HttpServletRequest req, HttpServletResponse res) throws Exception {

        final Request <?> slackReq = this.getAdapter().buildSlackRequest(req);
        if (slackReq != null) {
            final var queryString = slackReq.getRequestBodyAsString();
            final var queryPairs = this.splitQuery(queryString);
            // System.out.println(queryPairs);
            final var appList = this.appService.getTeam(AppType.SLACK_NEW.cd, queryPairs.get("team_id"));
            if (!appList.isEmpty()) {
                final var appDto = appList.get(0);
                this.getApp().config().setSingleTeamBotToken(appDto.getToken());
                final var slackResp = this.getApp().run(slackReq);
                this.getAdapter().writeResponse(res, slackResp);
            }
        }
    }
}

SlackApp では、Slash Commands の処理を書くのですが、3秒ルールがあるようで、3秒以内でレスポンスを返す必要があります。
そのため、Thread を利用しており、Slackからのレスポンスの中に response_url にPostを返信します。

※参考:Slackのスラッシュコマンドのタイムアウトエラー解消法

SlackApp.java
@Slf4j
public class SlackApp {

    @Bean
    public AppConfig loadAppConfig() {

        final var config = new AppConfig();
        final var classLoader = SlackApp.class.getClassLoader();
        // src/test/resources/appConfig.json
        try (var is = classLoader.getResourceAsStream("slack.json");
                var isr = new InputStreamReader(is)) {
            final var json = new BufferedReader(isr).lines().collect(Collectors.joining());
            // JsonObject j = new Gson().fromJson(json, JsonElement.class).getAsJsonObject();
            final var j = (Map <?, ?>) JSON.decode(json);
            config.setSigningSecret(j.get("signingSecret").toString());
            // config.setSingleTeamBotToken(j.get("singleTeamBotToken").toString());
        } catch (final IOException e) {
            SlackApp.log.error(e.getMessage(), e);
        }
        return config;
    }


    @Bean
    public App initSlackApp(AppConfig config) {

        final var app = new App(config);

        app.command("/talk", (req, ctx) -> {
            final var userId = ctx.getRequestUserId();
            final var text = req.getPayload().getText();
            final var url = req.getPayload().getResponseUrl();

            SlackQueue r = new SlackQueue();
            r.setUserId(userId);
            r.setText(text);
            r.setUrl(url);

            Thread t = new Thread(r);
            t.start();

            return ctx.ack();
        });

        return app;
    }

}

@Data
@Slf4j
class SlackQueue implements Runnable {

    /** TransactionManager. */
    protected final TransactionManager tm         = jp.livlog.cotogoto.AppConfig.singleton().getTransactionManager();

    /** Injector. */
    private final Injector             injector   = Guice.createInjector();

    /** BotService. */
    private final BotService           botService = this.injector.getInstance(BotServiceImpl.class);

    private String                     userId;

    private String                     text;

    private String                     url;

    @Override
    public void run() {

        this.tm.required(() -> {
            try {
                final var repry = this.botService.nobySlackMessenger(userId, text);
                // System.out.println(repry);
                final Map <String, String> params = new LinkedHashMap <>();
                params.put("text", repry);
                final Map <String, String> httpHeaders = new LinkedHashMap <>();
                doPost(url, Symbol.UTF_8, httpHeaders, JSON.encode(params));

            } catch (final Exception e) {
                try {
                    throw new CotogotoException(e);
                } catch (final CotogotoException e1) {
                    log.error("Failed to handle a request - {}", e.getMessage(), e);
                }
            }
        });
    }

    public String doPost(String url, String encoding, Map <String, String> headers, String jsonString) throws IOException {

        final okhttp3.MediaType mediaTypeJson = okhttp3.MediaType.parse("application/json; charset=" + encoding);

        final RequestBody requestBody = RequestBody.create(mediaTypeJson, jsonString);

        final Request request = new Request.Builder()
                .url(url)
                .headers(Headers.of(headers))
                .post(requestBody)
                .build();

        final OkHttpClient client = new OkHttpClient.Builder()
                .build();
        final Response response = client.newCall(request).execute();
        final String resultStr = response.body().string();
        return resultStr;
    }
}

AbsSlackAppServlet は SlackAppServletというクラスがMavenのbolt-servletにあったのですが、実際にアプリのリリースをするためには、ワークスペース単位では管理できない作りになっていたので、独自に作成しています。

AbsSlackAppServlet.java
@Slf4j
@Data
@EqualsAndHashCode (callSuper = false)
public abstract class AbsSlackAppServlet extends HttpServlet {

    /** App. */
    private final App                    app;

    /** SlackAppServletAdapter. */
    private final SlackAppServletAdapter adapter;

    /** TransactionManager. */
    protected final TransactionManager   tm       = AppConfig.singleton().getTransactionManager();

    /** Injector. */
    protected final Injector             injector = Guice.createInjector();

    public AbsSlackAppServlet(App app) {

        this.app = app;
        this.adapter = new SlackAppServletAdapter(app.config());
    }


    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException {

        try {
            this.tm.required(() -> {
                try {
                    this.performTask(req, res);
                } catch (final Exception e) {
                    throw new RuntimeException(e);
                }
            });
        } catch (final Exception e) {
            try {
                throw new CotogotoException(e);
            } catch (final CotogotoException e1) {
                AbsSlackAppServlet.log.error("Failed to handle a request - {}", e.getMessage(), e);
                res.setStatus(500);
                res.setContentType("application/json");
                res.getWriter().write("{\"error\":\"Something is wrong\"}");
            }
        }
    }


    protected Map <String, String> splitQuery(String query) throws UnsupportedEncodingException {

        final Map <String, String> queryPairs = new LinkedHashMap <>();
        final var pairs = query.split("&");
        for (final String pair : pairs) {
            final var idx = pair.indexOf("=");
            queryPairs.put(URLDecoder.decode(pair.substring(0, idx), "UTF-8"), URLDecoder.decode(pair.substring(idx + 1), "UTF-8"));
        }
        return queryPairs;
    }

    protected abstract void performTask(HttpServletRequest req, HttpServletResponse res) throws Exception;
}

続く

今回はこのように Slash Commands の送受信部分を作成しましたが、次回ではアプリの提出以降の手順はその2に続けていきます。

第1回
第2回
第3回

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0