はじめに
SLackのアプリをJavaで実装する方法に、Slackが提供するフレームワークboltがあります。
今回はこちらを使用して既存のアプリをSlackに対応させる方法・手順について記載します。
アプリの作成
Slack側の設定
新規アプリの登録
Create New App より新規アプリを登録します。
OAuth & Permissions の設定
Permission を設定します。
設定は一例ですので必要に応じて設定を変更してください。
今回は Slash Commands を利用するので、以下のようにしています。
Redirect URLs の設定はアプリごとにOauth認証の実装が個別に必要になります。
OAuthを使用したインストールに詳細が書かれていますが、こちらの記事では割愛しています。
Slash Commandsの登録
今回実装するSlash Commandsを登録していきます。
Java側の設定
Postの処理を実装するために、、ここではSpring bootを使用します。
githubのサンプルを参考に実装していきます。
Mavenの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を実装する形になります。
@SpringBootApplication
@ServletComponentScan
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
ここら辺から少しサンプルを改造しています。
Tomcatなどから動かす場合は、コンストラクタの書き方でエラーが発生していたので、このような形に変更しています。
@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のスラッシュコマンドのタイムアウトエラー解消法
@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にあったのですが、実際にアプリのリリースをするためには、ワークスペース単位では管理できない作りになっていたので、独自に作成しています。
@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に続けていきます。