Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.tolgee.hateoas.ee.webhooks

import io.swagger.v3.oas.annotations.media.Schema
import io.tolgee.component.automations.processors.WebhookEventType
import org.springframework.hateoas.RepresentationModel
import org.springframework.hateoas.server.core.Relation
import java.io.Serializable
Expand Down Expand Up @@ -29,5 +30,7 @@ class WebhookConfigModel(
description = """Date of the last webhook request.""",
)
var lastExecuted: Long?,
@Schema(description = "Event types this webhook is subscribed to.")
val eventTypes: Set<WebhookEventType>,
) : RepresentationModel<WebhookConfigModel>(),
Serializable
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.tolgee.model.webhook

import io.tolgee.AbstractSpringTest
import io.tolgee.component.automations.processors.WebhookEventType
import io.tolgee.development.testDataBuilder.data.WebhooksTestData
import io.tolgee.repository.WebhookConfigRepository
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
class WebhookConfigEventTypesTest : AbstractSpringTest() {
@Autowired
lateinit var webhookConfigRepository: WebhookConfigRepository

@Test
fun `persists and reads back event types`() {
val testData = WebhooksTestData()
testData.webhookConfig.self.eventTypes =
mutableSetOf(WebhookEventType.CONTENT_DELIVERY_PUBLISH)
testDataService.saveTestData(testData.root)

val reloaded = webhookConfigRepository.findById(testData.webhookConfig.self.id).get()
assertThat(reloaded.eventTypes).containsExactly(WebhookEventType.CONTENT_DELIVERY_PUBLISH)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import io.tolgee.batch.processors.TagKeysChunkProcessor
import io.tolgee.batch.processors.TrialExpirationNoticeProcessor
import io.tolgee.batch.processors.UnassignTranslationLabelChunkProcessor
import io.tolgee.batch.processors.UntagKeysChunkProcessor
import io.tolgee.batch.processors.WebhookDispatchChunkProcessor
import kotlin.reflect.KClass

enum class BatchJobType(
Expand Down Expand Up @@ -136,4 +137,9 @@ enum class BatchJobType(
processor = NoOpChunkProcessor::class,
defaultExclusive = false,
),
WEBHOOK_DISPATCH(
maxRetries = 3,
processor = WebhookDispatchChunkProcessor::class,
defaultExclusive = false,
),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.tolgee.batch.processors

import com.fasterxml.jackson.databind.ObjectMapper
import io.tolgee.batch.AbstractChunkProcessor
import io.tolgee.batch.data.BatchJobDto
import io.tolgee.batch.request.WebhookDispatchBjRequest
import io.tolgee.component.automations.processors.WebhookDeliveryManager
import io.tolgee.component.automations.processors.WebhookEventType
import io.tolgee.component.automations.processors.WebhookRequest
import io.tolgee.model.batch.params.WebhookDispatchJobParams
import io.tolgee.repository.WebhookConfigRepository
import org.springframework.stereotype.Component
import kotlin.coroutines.CoroutineContext

@Component
class WebhookDispatchChunkProcessor(
private val webhookConfigRepository: WebhookConfigRepository,
private val webhookDeliveryManager: WebhookDeliveryManager,
objectMapper: ObjectMapper,
) : AbstractChunkProcessor<WebhookDispatchBjRequest, WebhookDispatchJobParams, Long>(objectMapper) {
override fun process(
job: BatchJobDto,
chunk: List<Long>,
coroutineContext: CoroutineContext,
) {
val params = getParams(job)
chunk.forEach { webhookConfigId ->
val config = webhookConfigRepository.findById(webhookConfigId).orElse(null) ?: return@forEach
val data =
WebhookRequest(
webhookConfigId = config.id,
projectId = params.projectId,
eventType = WebhookEventType.CONTENT_DELIVERY_PUBLISH,
activityData = null,
contentDeliveryConfig = params.data,
)
webhookDeliveryManager.signExecuteAndHandle(config, data)
}
}

override fun getTarget(data: WebhookDispatchBjRequest): List<Long> = listOf(data.webhookConfigId)

override fun getParamsType(): Class<WebhookDispatchJobParams> = WebhookDispatchJobParams::class.java

override fun getTargetItemType(): Class<Long> = Long::class.java

override fun getParams(data: WebhookDispatchBjRequest): WebhookDispatchJobParams =
WebhookDispatchJobParams().apply {
projectId = data.projectId
this.data = data.data
}

override fun getChunkSize(
request: WebhookDispatchBjRequest,
projectId: Long?,
): Int = 1
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.tolgee.batch.request

import io.tolgee.component.automations.processors.ContentDeliveryPublishWebhookData

data class WebhookDispatchBjRequest(
var webhookConfigId: Long = 0,
var projectId: Long = 0,
var data: ContentDeliveryPublishWebhookData = ContentDeliveryPublishWebhookData(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.tolgee.component.automations.processors

import io.swagger.v3.oas.annotations.media.Schema

data class ContentDeliveryPublishWebhookData(
@get:Schema(description = "ID of the project the content delivery config belongs to")
val projectId: Long = 0,
@get:Schema(description = "ID of the content delivery config that was published")
val id: Long = 0,
@get:Schema(description = "Name of the content delivery config")
val name: String = "",
@get:Schema(description = "Slug (storage path prefix) of the content delivery config")
val slug: String = "",
@get:Schema(description = "Epoch millis when the publish completed")
val lastPublished: Long? = null,
@get:Schema(description = "Relative paths of the files that were published")
val files: List<String> = listOf(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package io.tolgee.component.automations.processors

import io.tolgee.batch.RequeueWithDelayException
import io.tolgee.component.CurrentDateProvider
import io.tolgee.constants.Message
import io.tolgee.model.webhook.WebhookConfig
import jakarta.persistence.EntityManager
import org.springframework.stereotype.Component

@Component
class WebhookDeliveryManager(
private val webhookExecutor: WebhookExecutor,
private val currentDateProvider: CurrentDateProvider,
private val webhookAutoDisableChecker: WebhookAutoDisableChecker,
private val entityManager: EntityManager,
) {
/**
* Delivers a single webhook request and applies the shared failure handling:
* success/failure streak tracking, auto-disable, and requeue-on-failure.
* Throws [RequeueWithDelayException] when the batch job should retry.
*/
fun signExecuteAndHandle(
config: WebhookConfig,
data: WebhookRequest,
) {
if (!config.enabled) return

try {
webhookExecutor.signAndExecute(config, data)
updateEntity(webhookConfig = config, failing = false)
} catch (e: Exception) {
updateEntity(config, true)
if (webhookAutoDisableChecker.checkAfterFailure(config)) return
when (e) {
is WebhookRespondedWithNon200Status -> throw RequeueWithDelayException(
Message.WEBHOOK_RESPONDED_WITH_NON_200_STATUS,
cause = e,
delayInMs = 5000,
)

else -> throw RequeueWithDelayException(
Message.UNEXPECTED_ERROR_WHILE_EXECUTING_WEBHOOK,
cause = e,
delayInMs = 5000,
)
}
}
}

private fun updateEntity(
webhookConfig: WebhookConfig,
failing: Boolean,
) {
webhookConfig.lastExecuted = currentDateProvider.date
if (!failing) {
webhookConfig.firstFailed = null
webhookConfig.autoDisableNotified = false
entityManager.persist(webhookConfig)
return
}
if (webhookConfig.firstFailed == null) {
webhookConfig.firstFailed = currentDateProvider.date
}
entityManager.persist(webhookConfig)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ package io.tolgee.component.automations.processors
enum class WebhookEventType {
TEST,
PROJECT_ACTIVITY,
CONTENT_DELIVERY_PUBLISH,
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,26 @@ package io.tolgee.component.automations.processors

import io.tolgee.activity.ActivityService
import io.tolgee.api.IProjectActivityModelAssembler
import io.tolgee.batch.RequeueWithDelayException
import io.tolgee.component.CurrentDateProvider
import io.tolgee.component.automations.AutomationProcessor
import io.tolgee.constants.Message
import io.tolgee.model.automations.AutomationAction
import io.tolgee.model.webhook.WebhookConfig
import io.tolgee.security.ProjectHolder
import jakarta.persistence.EntityManager
import org.springframework.stereotype.Component

@Component
class WebhookProcessor(
val projectHolder: ProjectHolder,
val activityModelAssembler: IProjectActivityModelAssembler,
val activityService: ActivityService,
val currentDateProvider: CurrentDateProvider,
val webhookExecutor: WebhookExecutor,
val entityManager: EntityManager,
val webhookAutoDisableChecker: WebhookAutoDisableChecker,
private val activityModelAssembler: IProjectActivityModelAssembler,
private val activityService: ActivityService,
private val webhookDeliveryManager: WebhookDeliveryManager,
) : AutomationProcessor {
override fun process(
action: AutomationAction,
activityRevisionId: Long?,
) {
activityRevisionId ?: return
val config = action.webhookConfig ?: return
if (!config.eventTypes.contains(WebhookEventType.PROJECT_ACTIVITY)) return

val view = activityService.findProjectActivity(activityRevisionId) ?: return
val activityModel = activityModelAssembler.toModel(view)
val config = action.webhookConfig ?: return
if (!config.enabled) return

val data =
WebhookRequest(
Expand All @@ -40,42 +31,6 @@ class WebhookProcessor(
activityData = activityModel,
)

try {
webhookExecutor.signAndExecute(config, data)
updateEntity(webhookConfig = config, failing = false)
} catch (e: Exception) {
updateEntity(config, true)
if (webhookAutoDisableChecker.checkAfterFailure(config)) return
when (e) {
is WebhookRespondedWithNon200Status -> throw RequeueWithDelayException(
Message.WEBHOOK_RESPONDED_WITH_NON_200_STATUS,
cause = e,
delayInMs = 5000,
)

else -> throw RequeueWithDelayException(
Message.UNEXPECTED_ERROR_WHILE_EXECUTING_WEBHOOK,
cause = e,
delayInMs = 5000,
)
}
}
}

fun updateEntity(
webhookConfig: WebhookConfig,
failing: Boolean,
) {
webhookConfig.lastExecuted = currentDateProvider.date
if (!failing) {
webhookConfig.firstFailed = null
webhookConfig.autoDisableNotified = false
entityManager.persist(webhookConfig)
return
}
if (webhookConfig.firstFailed == null) {
webhookConfig.firstFailed = currentDateProvider.date
}
entityManager.persist(webhookConfig)
webhookDeliveryManager.signExecuteAndHandle(config, data)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ data class WebhookRequest(
val projectId: Long?,
val eventType: WebhookEventType,
val activityData: IProjectActivityModel?,
val contentDeliveryConfig: ContentDeliveryPublishWebhookData? = null,
)
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package io.tolgee.component.contentDelivery

import io.tolgee.component.CurrentDateProvider
import io.tolgee.component.automations.processors.ContentDeliveryPublishWebhookData
import io.tolgee.component.contentDelivery.cachePurging.ContentDeliveryCachePurgingProvider
import io.tolgee.component.fileStorage.FileStorage
import io.tolgee.constants.Message
import io.tolgee.events.OnContentDeliveryPublished
import io.tolgee.exceptions.BadRequestException
import io.tolgee.model.contentDelivery.ContentDeliveryConfig
import io.tolgee.service.contentDelivery.ContentDeliveryConfigService
import io.tolgee.service.export.ExportService
import io.tolgee.util.Logging
import io.tolgee.util.logger
import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Component
import java.io.ByteArrayOutputStream
import java.io.InputStream
Expand All @@ -23,6 +26,7 @@ class ContentDeliveryUploader(
private val contentDeliveryConfigService: ContentDeliveryConfigService,
private val contentDeliveryCachePurgingProvider: ContentDeliveryCachePurgingProvider,
private val currentDateProvider: CurrentDateProvider,
private val applicationEventPublisher: ApplicationEventPublisher,
) : Logging {
fun upload(contentDeliveryConfigId: Long) {
val config = contentDeliveryConfigService.get(contentDeliveryConfigId)
Expand All @@ -39,9 +43,25 @@ class ContentDeliveryUploader(
storeToStorage(withFullPaths, storage)
purgeCacheIfConfigured(config, files.keys)

config.lastPublished = currentDateProvider.date
config.lastPublishedFiles = files.map { it.key }.toList()
val publishedDate = currentDateProvider.date
val publishedFiles = files.map { it.key }.toList()
config.lastPublished = publishedDate
config.lastPublishedFiles = publishedFiles
contentDeliveryConfigService.save(config)

applicationEventPublisher.publishEvent(
OnContentDeliveryPublished(
this,
ContentDeliveryPublishWebhookData(
projectId = config.project.id,
id = config.id,
name = config.name,
slug = config.slug,
lastPublished = publishedDate.time,
files = publishedFiles,
),
),
)
}

private fun createZipArchive(files: Map<String, InputStream>): Map<String, InputStream> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package io.tolgee.dtos.request

import io.tolgee.component.automations.processors.WebhookEventType
import jakarta.validation.constraints.Size

data class WebhookConfigRequest(
@field:Size(max = 255)
var url: String = "",
var enabled: Boolean? = null,
@field:Size(min = 1, message = "At least one event type must be selected")
var eventTypes: Set<WebhookEventType>? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.tolgee.events

import io.tolgee.component.automations.processors.ContentDeliveryPublishWebhookData
import org.springframework.context.ApplicationEvent

class OnContentDeliveryPublished(
source: Any,
val data: ContentDeliveryPublishWebhookData,
) : ApplicationEvent(source)
Loading
Loading