Skip to content

Commit

Permalink
변경사항이 많은 경우 배치 동기화 하지 않고 슬랙 채널에 알림
Browse files Browse the repository at this point in the history
슬랙 커맨드로 푸시 보내기

lint

Update SlackConfig.kt

귀차니즘 죄송

Update SlackConfig.kt
  • Loading branch information
Jhvictor4 committed Jan 14, 2024
1 parent f9f1b26 commit 100214d
Show file tree
Hide file tree
Showing 10 changed files with 427 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ class SugangSnuLectureCompareResult(
val createdLectureList: List<Lecture>,
val deletedLectureList: List<Lecture>,
val updatedLectureList: List<UpdatedLecture>,
)
) {
fun needsConfirmOnProduction(): Boolean {
return createdLectureList.size > 50 || deletedLectureList.size > 10 || updatedLectureList.size > 50
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ package com.wafflestudio.snu4t.sugangsnu.job.sync.service

import com.wafflestudio.snu4t.bookmark.repository.BookmarkRepository
import com.wafflestudio.snu4t.common.cache.Cache
import com.wafflestudio.snu4t.common.cache.CacheKey
import com.wafflestudio.snu4t.common.cache.get
import com.wafflestudio.snu4t.common.slack.CONFIRM_DONE_EMOJI
import com.wafflestudio.snu4t.common.slack.CONFIRM_ONGOING_EMOJI
import com.wafflestudio.snu4t.common.slack.SlackMessageBlock
import com.wafflestudio.snu4t.common.slack.SlackMessageRequest
import com.wafflestudio.snu4t.common.slack.SlackMessageService
import com.wafflestudio.snu4t.config.Phase
import com.wafflestudio.snu4t.config.SNUTT_MENTION
import com.wafflestudio.snu4t.coursebook.data.Coursebook
import com.wafflestudio.snu4t.coursebook.repository.CoursebookRepository
import com.wafflestudio.snu4t.lectures.data.Lecture
Expand Down Expand Up @@ -49,7 +58,10 @@ class SugangSnuSyncServiceImpl(
private val bookmarkRepository: BookmarkRepository,
private val tagListRepository: TagListRepository,
private val cache: Cache,
private val phase: Phase,
private val slackMessageService: SlackMessageService
) : SugangSnuSyncService {

override suspend fun updateCoursebook(coursebook: Coursebook): List<UserLectureSyncResult> {
val newLectures = sugangSnuFetchService.getSugangSnuLectures(coursebook.year, coursebook.semester)
val oldLectures =
Expand Down Expand Up @@ -140,9 +152,66 @@ class SugangSnuSyncServiceImpl(
diff.newData.apply { id = diff.oldData.id }
}

lectureService.upsertLectures(compareResult.createdLectureList)
lectureService.upsertLectures(updatedLectures)
lectureService.deleteLectures(compareResult.deletedLectureList)
val ongoingConfirmThread = cache.get<String>(CacheKey.LOCK_LIVE_SUGANG_SNU_SYNC_UNTIL_CONFIRMED.build())
val hasOngoingConfirmProcessOnProd = phase.isProd && ongoingConfirmThread != null
val currentResultRequiresConfirm = phase.isProd && compareResult.needsConfirmOnProduction()

when {
currentResultRequiresConfirm -> {
// 이전 검토 요청 존재 여부와 별개로 새로운 검토 요청
val newThreadTs = slackMessageService.postMessage(
message =
SlackMessageRequest(
SlackMessageBlock.Section("*$SNUTT_MENTION 수강스누 업데이트 검토가 필요합니다.*"),
SlackMessageBlock.Section("추가된 강좌 수: ${compareResult.createdLectureList.size}"),
SlackMessageBlock.Section("업데이트된 강좌 수: ${compareResult.updatedLectureList.size}"),
SlackMessageBlock.Section("삭제된 강좌 수: ${compareResult.deletedLectureList.size}"),
SlackMessageBlock.Section("개발환경에서 검토 후 아래 버튼 통해 라이브 배포해주세요."),
SlackMessageBlock.Action.SUGANG_SNU_CONFIRM
)
).threadTs

// 기존 결과가 검토 대기 중이었으면 내림
if (hasOngoingConfirmProcessOnProd) {
slackMessageService.deleteEmoji(threadTs = ongoingConfirmThread!!, emoji = CONFIRM_ONGOING_EMOJI)
slackMessageService.addEmoji(threadTs = ongoingConfirmThread, emoji = CONFIRM_DONE_EMOJI)
slackMessageService.postMessageToThread(
threadTs = ongoingConfirmThread,
message = SlackMessageRequest(
SlackMessageBlock.Header("새로운 동기화 결과가 업데이트 되어 검토 요청을 자동 종료합니다."),
SlackMessageBlock.Section("새로운 동기화 결과가 자동 배포 기준을 충족하지 않아 검토가 필요합니다."),
)
)
}

slackMessageService.addEmoji(threadTs = newThreadTs, emoji = CONFIRM_ONGOING_EMOJI)
cache.set(CacheKey.LOCK_LIVE_SUGANG_SNU_SYNC_UNTIL_CONFIRMED.build(), newThreadTs)
}

// 이전 검토 요청이 있었고, 새로운 결과가 검토 대기 중이 아니면 검토 종료, 새로운 결과 반영
!currentResultRequiresConfirm && hasOngoingConfirmProcessOnProd -> {
slackMessageService.deleteEmoji(threadTs = ongoingConfirmThread!!, emoji = CONFIRM_ONGOING_EMOJI)
slackMessageService.addEmoji(threadTs = ongoingConfirmThread, emoji = CONFIRM_DONE_EMOJI)
slackMessageService.postMessageToThread(
threadTs = ongoingConfirmThread,
message = SlackMessageRequest(
SlackMessageBlock.Header("동기화 결과가 업데이트 되어 검토 요청을 자동 종료합니다."),
SlackMessageBlock.Section("새로운 동기화 결과가 자동 배포 기준을 충족하여 검토가 필요하지 않습니다."),
SlackMessageBlock.Section("검토 요청을 종료합니다."),
)
)

lectureService.upsertLectures(compareResult.createdLectureList)
lectureService.upsertLectures(updatedLectures)
lectureService.deleteLectures(compareResult.deletedLectureList)
}

else -> {
lectureService.upsertLectures(compareResult.createdLectureList)
lectureService.upsertLectures(updatedLectures)
lectureService.deleteLectures(compareResult.deletedLectureList)
}
}
}

private suspend fun syncSavedUserLectures(compareResult: SugangSnuLectureCompareResult): List<UserLectureSyncResult> =
Expand Down Expand Up @@ -278,7 +347,10 @@ class SugangSnuSyncServiceImpl(
updatedLecture.updatedField.contains(Lecture::classPlaceAndTimes) &&
timetable.lectures.any {
it.lectureId != updatedLecture.oldData.id &&
ClassTimeUtils.timesOverlap(it.classPlaceAndTimes, updatedLecture.newData.classPlaceAndTimes)
ClassTimeUtils.timesOverlap(
it.classPlaceAndTimes,
updatedLecture.newData.classPlaceAndTimes
)
}

private fun deleteBookmarkLectures(deletedLecture: Lecture): Flow<BookmarkLectureDeleteResult> =
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ subprojects {
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8")
implementation("org.springframework.boot:spring-boot-starter-data-mongodb-reactive")

implementation("org.springframework.boot:spring-boot-starter-data-redis")
Expand Down
3 changes: 3 additions & 0 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ dependencies {
implementation("software.amazon.awssdk:secretsmanager:2.17.276")
implementation("software.amazon.awssdk:sts:2.17.276")
implementation("com.google.firebase:firebase-admin:9.1.1")
implementation("com.slack.api:bolt-jetty:1.37.0")
implementation("com.slack.api:slack-api-model-kotlin-extension:1.21.0")
implementation("com.slack.api:slack-api-client-kotlin-extension:1.21.0")

testFixturesImplementation("org.testcontainers:mongodb:1.19.0")
testFixturesImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
Expand Down
1 change: 1 addition & 0 deletions core/src/main/kotlin/common/cache/CacheKey.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ enum class CacheKey(
CLIENT_CONFIGS("client_configs:%s_%s", Duration.ofMinutes(5)), // osType, appVersion
LOCK_REGISTER_LOCAL("lock:register_local:%s", Duration.ofMinutes(1)), // localId
LOCK_ADD_FCM_REGISTRATION_ID("lock:add_registration_id:%s_%s", Duration.ofMinutes(1)), // userId, registrationId
LOCK_LIVE_SUGANG_SNU_SYNC_UNTIL_CONFIRMED("lock:live_sugang_snu_sync_until_confirmed", Duration.ofDays(14)),
;

fun build(vararg args: Any?): BuiltCacheKey {
Expand Down
30 changes: 30 additions & 0 deletions core/src/main/kotlin/common/slack/SlackMessageRequest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.wafflestudio.snu4t.common.slack

data class SlackMessageRequest(
val text: String = "",
val blocks: List<SlackMessageBlock>,
) {
constructor(vararg blocks: SlackMessageBlock, text: String = "") : this(text, blocks.toList())
}

interface SlackMessageBlock {

data class Header(val text: String) : SlackMessageBlock

data class Section(val text: String) : SlackMessageBlock

data class Button(val text: String, val url: String? = null) : SlackMessageBlock

enum class Action(
val actionId: String,
val value: String,
val text: String,
) : SlackMessageBlock {
SUGANG_SNU_CONFIRM("sugang_snu_confirm", "ok", "확인 완료"),
}
}

data class SlackMessageResponse(
val channel: String,
val threadTs: String
)
127 changes: 127 additions & 0 deletions core/src/main/kotlin/common/slack/SlackMessageService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.wafflestudio.snu4t.common.slack

import com.slack.api.Slack
import com.slack.api.methods.kotlin_extension.request.chat.blocks
import com.slack.api.model.block.composition.PlainTextObject
import com.slack.api.model.block.element.BlockElements.button
import com.slack.api.model.kotlin_extension.block.dsl.LayoutBlockDsl
import kotlinx.coroutines.future.await
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component

private const val SNUTT_CHANNEL_ID = "C0PAVPS5T"
const val CONFIRM_ONGOING_EMOJI = "acongablob"
const val CONFIRM_DONE_EMOJI = "done"

interface SlackMessageService {

suspend fun postMessage(channel: String = SNUTT_CHANNEL_ID, message: SlackMessageRequest): SlackMessageResponse

suspend fun postMessageToThread(channel: String = SNUTT_CHANNEL_ID, threadTs: String, message: SlackMessageRequest): SlackMessageResponse

suspend fun addEmoji(channel: String = SNUTT_CHANNEL_ID, threadTs: String, emoji: String)

suspend fun deleteEmoji(channel: String = SNUTT_CHANNEL_ID, threadTs: String, emoji: String)
}

@Component
internal class Impl(
@Value("\${slack.bot.token}") secretKey: String,
) : SlackMessageService {

private val client = Slack.getInstance()
.methodsAsync(secretKey)

override suspend fun postMessage(channel: String, message: SlackMessageRequest): SlackMessageResponse {
return client.chatPostMessage { builder ->
builder.channel(channel)
.text(message.text)
.blocks { apply(message.blocks) }
}
.await()
.let {
if (!it.isOk) {
log.error("Failed to post message to slack: ${it.error}")
}

SlackMessageResponse(it.channel, it.ts)
}
}

override suspend fun postMessageToThread(channel: String, threadTs: String, message: SlackMessageRequest): SlackMessageResponse {
return client.chatPostMessage { builder ->
builder
.channel(channel)
.threadTs(threadTs)
.text(message.text)
.blocks { apply(message.blocks) }
}
.await()
.let {
if (!it.isOk) {
log.error("Failed to post message to slack: ${it.error}")
}

SlackMessageResponse(it.channel, it.ts)
}
}

override suspend fun addEmoji(channel: String, threadTs: String, emoji: String) {
client.reactionsAdd { builder ->
builder
.channel(channel)
.timestamp(threadTs)
.name(emoji)
}
.await()
.let {
if (!it.isOk) {
log.error("Failed to add emoji to slack: ${it.error}")
}
}
}

override suspend fun deleteEmoji(channel: String, threadTs: String, emoji: String) {
client.reactionsRemove { builder ->
builder
.channel(channel)
.timestamp(threadTs)
.name(emoji)
}
.await()
.let {
if (!it.isOk) {
log.error("Failed to delete emoji to slack: ${it.error}")
}
}
}

private fun LayoutBlockDsl.apply(blocks: List<SlackMessageBlock>) {
blocks.forEach { block ->
when (block) {
is SlackMessageBlock.Header -> header {
text(block.text, emoji = false)
}
is SlackMessageBlock.Section -> section {
markdownText(block.text, verbatim = false)
}
is SlackMessageBlock.Button -> button {
it.text(PlainTextObject(block.text, false))
block.url?.let { url -> it.url(url) }
}
is SlackMessageBlock.Action -> actions {
elements {
button {
actionId(block.actionId)
value(block.value)
text(block.text)
}
}
}
}
}
}

private val log = LoggerFactory.getLogger(javaClass)
}
Loading

0 comments on commit 100214d

Please sign in to comment.