Skip to content

fix: enable reopen awaiting module #845

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
35 changes: 21 additions & 14 deletions src/main/kotlin/io/github/mojira/arisa/apiclient/JiraClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,48 @@

package io.github.mojira.arisa.apiclient

import io.github.mojira.arisa.apiclient.exceptions.JiraClientException
import io.github.mojira.arisa.apiclient.exceptions.ClientErrorException
import io.github.mojira.arisa.apiclient.exceptions.JiraClientException
import io.github.mojira.arisa.apiclient.interceptors.BasicAuthInterceptor
import io.github.mojira.arisa.apiclient.interceptors.LoggingInterceptor
import io.github.mojira.arisa.apiclient.models.IssueBean
import io.github.mojira.arisa.apiclient.models.Project
import io.github.mojira.arisa.apiclient.models.SearchResults
import io.github.mojira.arisa.apiclient.models.AttachmentBean
import io.github.mojira.arisa.apiclient.models.BodyType
import io.github.mojira.arisa.apiclient.models.Comment
import io.github.mojira.arisa.apiclient.models.GroupName
import io.github.mojira.arisa.apiclient.models.IssueBean
import io.github.mojira.arisa.apiclient.models.IssueLink
import io.github.mojira.arisa.apiclient.models.Project
import io.github.mojira.arisa.apiclient.models.SearchResults
import io.github.mojira.arisa.apiclient.models.Transitions
import io.github.mojira.arisa.apiclient.models.User
import io.github.mojira.arisa.apiclient.models.Visibility
import io.github.mojira.arisa.apiclient.requestModels.*
import io.github.mojira.arisa.apiclient.requestModels.AddCommentBody
import io.github.mojira.arisa.apiclient.requestModels.CreateIssueLinkBody
import io.github.mojira.arisa.apiclient.requestModels.EditIssueBody
import io.github.mojira.arisa.apiclient.requestModels.JiraSearchRequest
import io.github.mojira.arisa.apiclient.requestModels.TransitionIssueBody
import io.github.mojira.arisa.apiclient.requestModels.UpdateCommentBody
import io.github.mojira.arisa.apiclient.requestModels.UpdateCommentQueryParams
import io.github.mojira.arisa.log
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.ResponseBody
import okhttp3.MultipartBody
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.kotlinx.serialization.asConverterFactory
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query
import retrofit2.http.Part
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Part
import retrofit2.http.Path
import retrofit2.http.Query
import retrofit2.http.QueryMap
import java.io.File
import java.io.InputStream
Expand Down Expand Up @@ -134,7 +141,7 @@ interface JiraApi {
@GET("issue/{issueIdOrKey}/transitions")
fun getTransition(
@Path("issueIdOrKey") issueIdOrKey: String,
): Call<Unit>
): Call<Transitions>

@POST("issue/{issueIdOrKey}/transitions")
fun performTransition(
Expand Down Expand Up @@ -317,8 +324,8 @@ class JiraClient(
jiraApi.deleteIssueLink(linkId).executeOrThrow()
}

fun getTransition(issueIdOrKey: String) {
jiraApi.deleteIssueLink(issueIdOrKey).executeOrThrow()
fun getTransitions(issueIdOrKey: String): Transitions {
return jiraApi.getTransition(issueIdOrKey).executeOrThrow()
}

fun performTransition(issueIdOrKey: String, body: TransitionIssueBody) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ data class IssueFields(
val versions: List<JsonElement> = emptyList(),
val timeoriginalestimate: JsonElement? = null,
val timespent: Long? = null,
val updated: String? = null,
@Serializable(with = OffsetDateTimeSerializer::class)
val updated: OffsetDateTime? = null,
val workratio: Int? = null,
@Transient
private val additionalProperties: Map<String, JsonElement> = emptyMap()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.github.mojira.arisa.apiclient.models

import kotlinx.serialization.Serializable

/**
* List of issue transitions.
*
* @param expand Expand options that include additional transitions details in the response.
* @param transitions List of issue transitions.
*/
@Serializable
data class Transitions(
val expand: String? = null,
val transitions: List<IssueTransition>? = null
)
1 change: 0 additions & 1 deletion src/main/kotlin/io/github/mojira/arisa/domain/User.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,5 @@ data class User(
val name: String?,
val displayName: String?,
val getGroups: () -> List<String>?,
val isNewUser: () -> Boolean,
val isBotUser: () -> Boolean
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.github.mojira.arisa.domain.ChangeLogItem
import io.github.mojira.arisa.domain.Comment
import io.github.mojira.arisa.domain.CommentOptions
import io.github.mojira.arisa.domain.Project
import io.github.mojira.arisa.domain.User
import java.io.File
import java.time.Instant

Expand All @@ -15,10 +16,10 @@ data class CloudIssue(
val description: String?,
val environment: String?,
val securityLevel: String?,
// val reporter: User?,
val reporter: User?,
val resolution: String?,
val created: Instant,
// val updated: Instant,
val updated: Instant,
// val resolved: Instant?,
// val chk: String?,
// val confirmationStatus: String?,
Expand All @@ -35,7 +36,7 @@ data class CloudIssue(
val comments: List<Comment>,
val links: List<CloudLink>,
val changeLog: List<ChangeLogItem>,
// val reopen: () -> Unit,
val reopen: () -> Unit,
// val resolveAsAwaitingResponse: () -> Unit,
// val resolveAsInvalid: () -> Unit,
// val resolveAsDuplicate: () -> Unit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import java.time.temporal.ChronoField
import io.github.mojira.arisa.apiclient.JiraClient
import io.github.mojira.arisa.apiclient.builders.FluentObjectBuilder
import io.github.mojira.arisa.apiclient.builders.string
import io.github.mojira.arisa.apiclient.exceptions.JiraClientException
import io.github.mojira.arisa.apiclient.models.IssueTransition
import io.github.mojira.arisa.apiclient.models.Visibility
import io.github.mojira.arisa.apiclient.requestModels.EditIssueBody
import io.github.mojira.arisa.apiclient.requestModels.TransitionIssueBody
Expand Down Expand Up @@ -232,13 +234,30 @@ private fun applyFluentUpdate(issueKey: String, edit: FluentObjectBuilder) = run
}
}

private fun getIssueTransition(issueKey: String, transitionName: String): IssueTransition = runBlocking {
val allTransitions = jiraClient.getTransitions(issueKey)
val transition = allTransitions.transitions?.firstOrNull { it.name == transitionName }

if (transition == null) {
val availableTransitions = allTransitions.transitions?.joinToString(",\n") { "${it.name} (${it.id})" }
throw JiraClientException("Transition $transitionName not found. Available transitions:\n$availableTransitions")
Comment on lines +242 to +243
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the absence of given transition be just logged with warning/error level instead of throwing?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No transitions being found sound like an immediate issue we should handle, not sure we would look at warnings/error logs as closely

}

return@runBlocking transition
}

private fun applyFluentTransition(issueKey: String, update: FluentObjectBuilder, transitionName: String) = runBlocking {
Either.catch {
val transition = getIssueTransition(issueKey, transitionName)

val fieldsJson = update.toJson()
jiraClient.performTransition(
issueKey,
TransitionIssueBody(
fields = fieldsJson["fields"]
fields = fieldsJson["fields"],
transition = IssueTransition(
id = transition.id
)
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import arrow.core.right
import arrow.syntax.function.partially1
import com.uchuhimo.konf.Config
import io.github.mojira.arisa.domain.Attachment
// import io.github.mojira.arisa.domain.Attachment
import io.github.mojira.arisa.domain.ChangeLogItem
import io.github.mojira.arisa.domain.Comment
import io.github.mojira.arisa.domain.IssueUpdateContext
Expand All @@ -24,10 +23,7 @@ import io.github.mojira.arisa.infrastructure.ProjectCache
import io.github.mojira.arisa.apiclient.builders.FluentObjectBuilder
import io.github.mojira.arisa.apiclient.models.Changelog
import io.github.mojira.arisa.infrastructure.config.Arisa
import io.github.mojira.arisa.infrastructure.escapeIssueFunction
import net.rcarz.jiraclient.JiraClient
import io.github.mojira.arisa.apiclient.JiraClient as MojiraClient
import net.rcarz.jiraclient.JiraException
import java.text.SimpleDateFormat
import java.time.Instant
import java.time.LocalDate
Expand All @@ -39,6 +35,7 @@ import io.github.mojira.arisa.apiclient.models.Comment as MojiraComment
import io.github.mojira.arisa.apiclient.models.IssueBean as MojiraIssue
import io.github.mojira.arisa.apiclient.models.IssueLink as MojiraIssueLink
import io.github.mojira.arisa.apiclient.models.Project as MojiraProject
import io.github.mojira.arisa.apiclient.models.User as MojiraUser
import io.github.mojira.arisa.apiclient.models.UserDetails as MojiraUserDetails
import io.github.mojira.arisa.apiclient.models.Version as MojiraVersion

Expand Down Expand Up @@ -100,10 +97,10 @@ fun MojiraIssue.toDomain(
description = fields.description,
environment = fields.environment,
securityLevel = fields.security?.id,
// reporter?.toDomain(jiraClient, config),
reporter = fields.reporter?.toDomain(jiraClient, config),
resolution = fields.resolution?.name,
created = fields.created?.toInstant() ?: Instant.now(),
// updatedDate.toInstant(),
updated = fields.updated?.toInstant() ?: Instant.now(),
// resolutionDate?.toInstant(),
// getCHK(config),
// getConfirmation(config),
Expand All @@ -120,7 +117,7 @@ fun MojiraIssue.toDomain(
comments = mapComments(jiraClient, config),
links = mapLinks(jiraClient, config),
changeLog = getChangeLogEntries(jiraClient, config),
// ::reopen.partially1(context),
reopen = ::reopen.partially1(context),
// ::resolveAs.partially1(context).partially1("Awaiting Response"),
// ::resolveAs.partially1(context).partially1("Invalid"),
// ::resolveAs.partially1(context).partially1("Duplicate"),
Expand Down Expand Up @@ -244,8 +241,16 @@ fun MojiraUserDetails.toDomain(jiraClient: MojiraClient, config: Config) = User(
accountId = accountId,
name = displayName,
displayName = displayName,
getGroups = ::getUserGroups.partially1(jiraClient).partially1(accountId),
isNewUser = { false }
getGroups = ::getUserGroups.partially1(jiraClient).partially1(accountId)
) {
accountId.equals(config[Arisa.Credentials.accountId], ignoreCase = true)
}

fun MojiraUser.toDomain(jiraClient: MojiraClient, config: Config) = User(
accountId = accountId,
name = displayName,
displayName = displayName,
getGroups = ::getUserGroups.partially1(jiraClient).partially1(accountId)
) {
accountId.equals(config[Arisa.Credentials.accountId], ignoreCase = true)
}
Expand All @@ -255,28 +260,6 @@ private fun getUserGroups(jiraClient: MojiraClient, accountId: String) = getGrou
accountId
).fold({ null }, { it })

private fun isNewUser(jiraClient: JiraClient, username: String): Boolean {
val commentJql = "issueFunction IN commented(${escapeIssueFunction(username) { "by $it before -24h" }})"

val oldCommentsExist = try {
jiraClient.countIssues(commentJql) > 0
} catch (_: JiraException) {
false
}

if (oldCommentsExist) return false

val reportJql = """project != TRASH AND reporter = '${username.replace("'", "\\'")}' AND created < -24h"""

val oldReportsExist = try {
jiraClient.countIssues(reportJql) > 0
} catch (_: JiraException) {
true
}

return !oldReportsExist
}

@Suppress("LongParameterList")
fun MojiraIssue.toLinkedIssue(
jiraClient: MojiraClient,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import arrow.core.left
import arrow.core.right
import io.github.mojira.arisa.domain.ChangeLogItem
import io.github.mojira.arisa.domain.Comment
import io.github.mojira.arisa.domain.Issue
import io.github.mojira.arisa.domain.User
import io.github.mojira.arisa.domain.cloud.CloudIssue
import java.time.Instant
import java.time.temporal.ChronoUnit

Expand All @@ -20,14 +20,14 @@ class ReopenAwaitingModule(
private val keepARTag: String,
private val onlyOPTag: String,
private val message: String
) : Module {
override fun invoke(issue: Issue, lastRun: Instant): Either<ModuleError, ModuleResponse> = with(issue) {
) : CloudModule {
override fun invoke(issue: CloudIssue, lastRun: Instant): Either<ModuleError, ModuleResponse> = with(issue) {
Either.fx {
assertEquals(resolution, "Awaiting Response").bind()
assertCreationIsNotRecent(updated.toEpochMilli(), created.toEpochMilli()).bind()

val resolveTime = changeLog.last(::isAwaitingResolve).created
val validComments = getValidComments(comments, reporter, resolveTime, lastRun)
val validComments = getValidComments(comments, resolveTime, lastRun)
val validChangeLog = getValidChangeLog(changeLog, reporter, resolveTime)

assertAny(
Expand All @@ -39,7 +39,7 @@ class ReopenAwaitingModule(
if (shouldReopen) {
reopen()
} else {
assertNotEquals(changeLog.maxByOrNull { it.created }?.author?.name, "arisabot")
assertNotEquals(changeLog.maxByOrNull { it.created }?.author?.isBotUser?.invoke(), true)
if (comments.none { isKeepARMessage(it) }) {
addRawBotComment(message)
}
Expand All @@ -66,7 +66,7 @@ class ReopenAwaitingModule(
// regular users can reopen and have commented OR
(!onlyOp && isSoftAR) ||
// reporter has commented
validComments.any { it.author?.name == reporter?.name }
validComments.any { it.author?.accountId == reporter?.accountId }
}

private fun isOPTag(comment: Comment) = comment.visibilityType == "group" &&
Expand All @@ -78,16 +78,15 @@ class ReopenAwaitingModule(
(comment.body?.contains(keepARTag) ?: false)

private fun isKeepARMessage(comment: Comment) =
comment.author?.name == "arisabot" && comment.body?.contains(message) ?: false
comment.author?.isBotUser?.invoke() == true && comment.body?.contains(message) ?: false

private fun getValidComments(
comments: List<Comment>,
reporter: User?,
resolveTime: Instant,
lastRun: Instant
): List<Comment> = comments
.filter { it.created.isAfter(resolveTime) && it.created.isAfter(lastRun) }
.filter { it.author != null && (!it.author.isNewUser() || it.author.name == reporter?.name) }
.filter { it.author != null }
.filter {
val roles = it.getAuthorGroups()
roles == null || roles.intersect(blacklistedRoles).isEmpty()
Expand All @@ -100,7 +99,7 @@ class ReopenAwaitingModule(
resolveTime: Instant
): List<ChangeLogItem> = changeLog
.filter { it.created.isAfter(resolveTime) }
.filter { it.author.name == reporter?.name }
.filter { it.author.accountId == reporter?.accountId }
.filter { it.field != "Comment" }

private fun isAwaitingResolve(change: ChangeLogItem) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package io.github.mojira.arisa.registry
import com.uchuhimo.konf.Config
import io.github.mojira.arisa.ExecutionTimeframe
import io.github.mojira.arisa.domain.cloud.CloudIssue
import io.github.mojira.arisa.infrastructure.HelperMessageService
import io.github.mojira.arisa.infrastructure.config.Arisa
import io.github.mojira.arisa.modules.PrivateDuplicateModule
import io.github.mojira.arisa.modules.ReopenAwaitingModule
import io.github.mojira.arisa.modules.privacy.AccessTokenRedactor
import io.github.mojira.arisa.modules.privacy.PrivacyModule

Expand Down Expand Up @@ -36,6 +38,18 @@ class InstantModuleRegistry(config: Config) : ModuleRegistry<CloudIssue>(config)
config[Arisa.Modules.Privacy.sensitiveFileNameRegexes].map(String::toRegex)
)
)

register(
Arisa.Modules.ReopenAwaiting,
ReopenAwaitingModule(
config[Arisa.Modules.ReopenAwaiting.blacklistedRoles].toSetNoDuplicates(),
config[Arisa.Modules.ReopenAwaiting.blacklistedVisibilities].toSetNoDuplicates(),
config[Arisa.Modules.ReopenAwaiting.softARDays],
config[Arisa.Modules.ReopenAwaiting.keepARTag],
config[Arisa.Modules.ReopenAwaiting.onlyOPTag],
HelperMessageService.getMessage("MC", keys = listOf(config[Arisa.Modules.ReopenAwaiting.message]))
)
)
}

private fun <T> List<T>.toSetNoDuplicates(): Set<T> {
Expand Down
Loading