【Java, Slack】Spring で Slack メッセージ送信

# 背景
- Springプロジェクトで Slack にメッセージを送りたい（Apache Velocity テンプレートで）

# 環境
- Spring Framework 4
- slack-api-client 1.XX
- Apache Velocity 1.7
- Java 8

# 参考リンク
https://slack.com/intl/ja-jp/help/articles/215770388-API-%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%AE%E7%94%9F%E6%88%90%E3%81%A8%E5%86%8D%E7%94%9F%E6%88%90

https://slack.dev/java-slack-sdk/guides/composing-messages

# 基底クラス
- AbstractSlackService：使いたいサービスで extend して使うイメージ
```java:AbstractSlackService.java
public abstract class AbstractSlackService {

    // ダミー用TS
	public static final String SLACK_DUMMY_TS = "0.0";
	protected Logger logger = Logger.getLogger(this.getClass());

    // 環境取得用：本番用、テスト用なのか判別するためのもの（環境に依存するのでここでは割愛）
	private EnvironmentService environmentService;
    // メッセージテンプレートに Velocity を使うため（利用しない場合は不要）
	protected VelocityEngine velocityEngine;
    // 本番用環境じゃなくても強制的に送信するフラグ (application.propertiesに記載)
	protected boolean sendForce = false;
	
    // Slackメッセージ用文字列エスケープ
	public static String escape(String string) {
		
		String result = "";
		if (StringUtils.isNotEmpty(string)) {
			result = string.replaceAll("&", "&amp;");
			result = result.replaceAll("<", "&lt;");
			result = result.replaceAll(">", "&gt;");
		}
		return result;
	}
	// SlackID をメンション用文字列にする
	public static String id2Mention(String id) {
		return String.format("<@%s>", id);
	}
	
    // レスポンスがOKかどうか
	public static boolean responseIsOk(Object slackResponse) {
		
		if (slackResponse == null) {
			return false;
		}
		if (slackResponse instanceof SlackApiTextResponse) {
			SlackApiTextResponse response = (SlackApiTextResponse)slackResponse;
			return response.isOk();
		}
		return false;
	}

	// メッセージ送信（チャンネルID,Slackトークン,メッセージ内容）
	protected ChatPostMessageResponse chatPostMessageInternal(String channel, String token, String text) {

		if (StringUtils.isBlank(channel) ||
				StringUtils.isBlank(token) || StringUtils.isBlank(text)) {
			return null;
		}
		// 初期化
		Slack slack = Slack.getInstance();

		// API メソッドのクライアントを上のトークンと共に初期化
		MethodsClient methods = slack.methods(token);

		// API リクエスト内容を構成
		ChatPostMessageRequest slackRequest = ChatPostMessageRequest.builder()
				.channel(channel)
				.text(text)
				.blocks(
					asBlocks(
						section(section -> section.text(markdownText(text)))
					)
				)
				.build();

		// API リクエストを送信
		return slackChatPostMessage(methods, slackRequest);
	}

	// メッセージ送信
	protected ChatPostMessageResponse slackChatPostMessage(MethodsClient methods, ChatPostMessageRequest slackRequest) {
		
		ChatPostMessageResponse slackResponse = null;
		if (environmentService.isProduction() || sendForce) {
			// API リクエストを送信
			try {
				slackResponse = methods.chatPostMessage(slackRequest);
				if (!slackResponse.isOk()) {
					logger.error(String.format("Slack送信に失敗:%s", ReflectionToStringBuilder.toString(slackResponse)));
					return slackResponse;
				}
				
				logger.info(String.format("Slack送信完了。:%s\n%s",
						slackRequest.getChannel(), slackRequest.getText()));
			} catch (Exception e) {
				StringWriter sw = new StringWriter();
				e.printStackTrace(new PrintWriter(sw));
				logger.error(sw.toString());
				return slackResponse;
			}
		} 
		else {
			slackResponse = new ChatPostMessageResponse();
			slackResponse.setChannel(slackRequest.getChannel());
			slackResponse.setTs(SLACK_DUMMY_TS);
			slackResponse.setOk(true);
			
			logger.info(String.format("本番環境でないため、送信はしない。:%s\n%s",
					slackRequest.getChannel(), slackRequest.getText()));
		}
		return slackResponse;
	}
	
	// メッセージリンク取得
	protected ChatGetPermalinkResponse getChatPermalinkInternal(String token, String channel, String messageTs) {
		
		if (StringUtils.isBlank(channel) ||
				StringUtils.isBlank(token) || StringUtils.isBlank(messageTs)) {
			return null;
		}
		// 初期化
		Slack slack = Slack.getInstance();

		// API メソッドのクライアントを上のトークンと共に初期化
		MethodsClient methods = slack.methods(token);
		
		ChatGetPermalinkRequest permalinkRequest = ChatGetPermalinkRequest.builder()
				.token(token)
				.channel(channel)
				.messageTs(messageTs)
				.build();
		
		return slackChatPermalink(methods, permalinkRequest);
	}
	
	// メッセージリンク取得
	protected ChatGetPermalinkResponse slackChatPermalink(MethodsClient methods, ChatGetPermalinkRequest permalinkRequest) {
		
		ChatGetPermalinkResponse slackResponse = null;
		if (environmentService.isProduction() || sendForce) {
			try {
				slackResponse = methods.chatGetPermalink(permalinkRequest);
				if (!slackResponse.isOk()) {
					logger.error(String.format("Slackメッセージリンク取得に失敗:%s", ReflectionToStringBuilder.toString(slackResponse)));
					return slackResponse;
				}
				
				logger.info(String.format("Slackメッセージリンク取得完了。:%s\n%s:%s",
						permalinkRequest.getChannel(), permalinkRequest.getMessageTs(), slackResponse.getPermalink()));
			} catch (Exception e) {
				StringWriter sw = new StringWriter();
				e.printStackTrace(new PrintWriter(sw));
				logger.info(sw.toString());
				return slackResponse;
			}
		} else {
			slackResponse = new ChatGetPermalinkResponse();
			slackResponse.setChannel(permalinkRequest.getChannel());
			slackResponse.setOk(true);
			logger.info(String.format("本番環境でないため、送信はしない。:%s\n%s",
					permalinkRequest.getChannel(), permalinkRequest.getMessageTs()));
		}
		return slackResponse;
	}

	public EnvironmentService getEnvironmentService() {
		return environmentService;
	}
	
	@Autowired
	public final void setEnvironmentService(EnvironmentService environmentService) {
		this.environmentService = environmentService;
	}

	public VelocityEngine getVelocityEngine() {
		return velocityEngine;
	}

	@Autowired
	public final void setVelocityEngine(VelocityEngine velocityEngine) {
		this.velocityEngine = velocityEngine;
	}

	public boolean getSendForce() {
		return sendForce;
	}

	@Value("${slack.sendForce}")
	public final void setSendForce(boolean sendForce) {
		this.sendForce = sendForce;
	}
}
```

# サービス用クラス
- ExtendedSlackService
```java:ExtendedSlackService.java
@Component
@Configuration
@PropertySource(value= {"classpath:application.properties"})
public class ExtendedSlackService extends AbstractSlackService {

	@Autowired
	private Environment environment;

	/*
	 * メッセージ送信（テンプレート利用）
	 */
	public ChatPostMessageResponse chatPostMessage(String channel, String templateName, Map<String, Object> replaceModel) {

		String token = environment.getProperty("slack.token");
		String text = VelocityEngineUtils.mergeTemplateIntoString(velocityEngine,
				"path/to/templateDir/" + templateName + ".slack.vm", "utf-8", replaceModel);
		return chatPostMessageInternal(channel, token, text);
	}

	/*
	 * メッセージ送信（直接）
	 */
	public ChatPostMessageResponse chatPostMessageDirect(String channel, String message, Map<String, String> replaceModel, String mentions) {

		if (StringUtils.isBlank(message)) return null;
		String token = environment.getProperty("slack.token");
		for (Map.Entry<String, String> entry: replaceModel.entrySet()) {
			message = message.replace("${"+entry.getKey()+"}", entry.getValue());
		}
		if (!StringUtils.isBlank(mentions)) {
			message = mentions + "\n" + message;
		}
		return chatPostMessageInternal(channel, token, message);
	}

	/*
	 * メッセージリンク取得
	 */
	public ChatGetPermalinkResponse getChatPermalink(String channel, String messageTs) {

		String token = environment.getProperty("slack.token");
		return getChatPermalinkInternal(token, channel, messageTs);
	}

	public Environment getEnvironment() {
		return environment;
	}

	public void setEnvironment(Environment environment) {
		this.environment = environment;
	}

}
```

# application.properties
```properties:application.properties
# for slack
slack.sendForce=false
slack.token=XXXX-XXXXXXXXXXXX
```

以上、お疲れさまでした！