はじめに
弊社ではチャットにSlack、タスク管理にZenHubを使っています。
タスク管理ツールを使ってると、タスクの状態を更新し忘れることが多いですよね。
弊社でも、イシューをIn Progressにするのを忘れて、今何やってるんでしたっけ…? と聞かれるという事案が発生していました。
「〜することを忘れないようにする」というTryはエンジニアとしての敗北なので、ボットにお知らせしてもらうようにしましょう。
Kotlin製ボットフレームワークBotlin
Kotlinが大好きなので、ノリでボットフレームワークをつくりました。
- mizoguche/botlin: Bot framework built with Kotlin
- Ktorが面白かったのでインスパイアしてボットフレームワークをつくった話 // Speaker Deck
これをしれっと業務で使っていきます。
Featureをつくる
Speaker Deckのスライドにもある通り、Ktorに影響を受けていますので、なにかやりたいことが生まれたらFeatureをつくっていきます。
GitHubクライアントにはkohsuke/github-api: Java API for GitHub、ZenHubのAPIはkittinunf/Fuel: The easiest HTTP networking library for Kotlin/Androidで直接叩きます。
今回はZenHubの特定のパイプライン(今回はIn Progress)にイシューがない人を探して通知するというFeatureを作っていきます。
Botlinには組み込みでCronというFeatureがあり定期的にコマンドを実行できるので、このFeatureが平日の定時内に30分に1回呼ばれるようにします。
実装の主要なところをダイジェストでご紹介します。
// ...
class ZenHub(internal val configuration: Configuration) : BotFeature {
// ...
override fun install(context: BotFeatureContext) {
FuelManager.instance.baseHeaders = mapOf("X-Authentication-Token" to configuration.authenticationToken)
context.pipelineOf<BotMessageCommand>().intercept {
if (it.command != "zenhub") {
return@intercept
}
if (it.args.startsWith("in-progress")) {
executeCheckIssuesInProgress(context, it, it.args.contains("--silent"))
return@intercept
}
postMessage(context, it, """|使い方
|```
|zenhub in-progress
| パイプラインIn Progressにイシューがない人を怒る
|```
""".trimMargin())
}
}
internal fun getBoardPipelines(context: BotFeatureContext, command: BotMessageCommand, callback: (Pipelines) -> Unit) {
"https://api.zenhub.io/p1/repositories/${configuration.repositoryId}/board"
.httpGet()
.responseObject<Pipelines> { _, _, result ->
val (pipelines, error) = result
// エラーハンドリング...
callback.invoke(pipelines)
}
}
internal fun postMessage(context: BotFeatureContext, command: BotMessageCommand, message: String) {
val message = BotMessageRequest(BotEngineId("Slack"), command.channelId, message)
context.pipelineOf<BotMessageRequest>().execute(message)
}
class Configuration {
lateinit var authenticationToken: String
// ...
}
companion object ZenHubFactory : BotFeatureFactory<Configuration> {
override fun create(configure: Configuration.() -> Unit): BotFeature {
val conf = Configuration().apply(configure)
return ZenHub(conf)
}
}
}
// ...
fun ZenHub.executeCheckIssuesInProgress(context: BotFeatureContext, command: BotMessageCommand, shouldBeSilent: Boolean) {
getBoardPipelines(context, command) { pipelines ->
val issueNumbers = pipelines.issueIdsInPipeline(configuration.pipelineName)
val repository = getRepository()
val issues = issueNumbers.mapToGitHubIssues(repository)
val assignedUsers = issues.distinctBy { it.number }.mapNotNull {
if (it.assignee == null) {
val message = "In Progressにアサインされてないイシュー(#${it.number})があるよ (#^ω^)ビキビキ\nhttps://github.com/${configuration.gitHubOrganization}/${configuration.gitHubRepositoryName}/issues/${it.number}"
postMessage(context, command, message)
null
} else {
it.assignees
}
}.flatten().map { it.login }
val usersNotAssigned = configuration.usernamePairs.usersNotIncluded(assignedUsers)
if (usersNotAssigned.count() > 0) {
usersNotAssigned.forEach {
val message = "@${configuration.usernamePairs.slackUsername(it)} In Progressにイシューがないよ〜〜 :innocent: :innocent: :innocent:"
postMessage(context, command, message)
}
} else {
if (!shouldBeSilent) {
postMessage(context, command, "【朗報】全員In Progressのイシューがある【:tada::tada::tada:】")
}
}
}
}
fun main(args: Array<String>) {
botlin {
install(SlackEngine) {
token = System.getenv("SLACK_TOKEN")
}
install(RedisStorage) {
uri = URI(System.getenv("REDIS_URL"))
}
install(MessageCommand)
install(Cron)
install(Echo)
install(ZenHub) {
authenticationToken = System.getenv("ZENHUB_TOKEN")
repositoryId = System.getenv("ZENHUB_REPOSITORY_ID")
pipelineName = System.getenv("ZENHUB_PIPELINE_NAME")
gitHubUsername = System.getenv("GITHUB_USERNAME")
gitHubToken = System.getenv("GITHUB_TOKEN")
gitHubOrganization = System.getenv("GITHUB_ORGANIZATION")
gitHubRepositoryName = System.getenv("GITHUB_REPOSITORY_NAME")
// GitHubとSlackのユーザー名が違う人のためのマッピング
usernamePairs = setOf(
UsernamePair(github = "mizoguche", slack = "mizoguche"),
// ...
)
}
}.start()
}
結果
怒られが発生しました😇
まとめ
- ちゃんとイシューのステータスを更新していきましょう🎉
- Tryは気持ちではなく仕組みにしていきましょう🤖
- Kotlinかわいい🐣