mirror of
https://github.com/gitbucket/gitbucket.git
synced 2026-05-07 00:16:10 +02:00
add webhook APIs (#2551)
This commit is contained in:
14
src/main/resources/update/gitbucket-core_4.35.xml
Normal file
14
src/main/resources/update/gitbucket-core_4.35.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<changeSet>
|
||||
<dropForeignKeyConstraint constraintName="IDX_WEB_HOOK_EVENT_FK0" baseTableName="WEB_HOOK_EVENT"/>
|
||||
<dropForeignKeyConstraint constraintName="IDX_WEB_HOOK_FK0" baseTableName="WEB_HOOK"/>
|
||||
<dropPrimaryKey tableName="WEB_HOOK" constraintName="IDX_WEB_HOOK_PK"/>
|
||||
<addColumn tableName="WEB_HOOK">
|
||||
<column name="HOOK_ID" type="int" nullable="false" unique="true"/>
|
||||
</addColumn>
|
||||
<addPrimaryKey constraintName="IDX_WEB_HOOK_PK" tableName="WEB_HOOK" columnNames="USER_NAME, REPOSITORY_NAME, URL, HOOK_ID"/>
|
||||
<addUniqueConstraint constraintName="IDX_WEB_HOOK_1" tableName="WEB_HOOK" columnNames="USER_NAME, REPOSITORY_NAME, URL"/>
|
||||
<addForeignKeyConstraint constraintName="IDX_WEB_HOOK_FK0" baseTableName="WEB_HOOK" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
|
||||
<addForeignKeyConstraint constraintName="IDX_WEB_HOOK_EVENT_FK0" baseTableName="WEB_HOOK_EVENT" baseColumnNames="USER_NAME, REPOSITORY_NAME, URL" referencedTableName="WEB_HOOK" referencedColumnNames="USER_NAME, REPOSITORY_NAME, URL" onDelete="CASCADE" onUpdate="CASCADE"/>
|
||||
<addAutoIncrement columnName="HOOK_ID" columnDataType="int" tableName="WEB_HOOK"/>
|
||||
</changeSet>
|
||||
@@ -113,5 +113,6 @@ object GitBucketCoreModule
|
||||
}
|
||||
},
|
||||
new LiquibaseMigration("update/gitbucket-core_4.34.xml")
|
||||
)
|
||||
),
|
||||
new Version("4.35.0", new LiquibaseMigration("update/gitbucket-core_4.35.xml")),
|
||||
)
|
||||
|
||||
46
src/main/scala/gitbucket/core/api/ApiWebhook.scala
Normal file
46
src/main/scala/gitbucket/core/api/ApiWebhook.scala
Normal file
@@ -0,0 +1,46 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.model.Profile.{RepositoryWebHookEvents, RepositoryWebHooks}
|
||||
import gitbucket.core.model.{RepositoryWebHook, WebHook}
|
||||
import gitbucket.core.util.RepositoryName
|
||||
|
||||
/**
|
||||
* https://docs.github.com/en/rest/reference/repos#webhooks
|
||||
*/
|
||||
case class ApiWebhookConfig(
|
||||
content_type: String,
|
||||
// insecure_ssl: String,
|
||||
url: String
|
||||
)
|
||||
|
||||
case class ApiWebhook(
|
||||
`type`: String,
|
||||
id: Int,
|
||||
name: String,
|
||||
active: Boolean,
|
||||
events: List[String],
|
||||
config: ApiWebhookConfig,
|
||||
// updated_at: Option[Date],
|
||||
// created_at: Option[Date],
|
||||
url: ApiPath,
|
||||
// test_url: ApiPath,
|
||||
// ping_url: ApiPath,
|
||||
// last_response: ...
|
||||
)
|
||||
|
||||
object ApiWebhook {
|
||||
def apply(
|
||||
_type: String,
|
||||
hook: RepositoryWebHook,
|
||||
hookEvents: Set[WebHook.Event]
|
||||
): ApiWebhook =
|
||||
ApiWebhook(
|
||||
`type` = _type,
|
||||
id = hook.hookId,
|
||||
name = "web", // dummy
|
||||
active = true, // dummy
|
||||
events = hookEvents.toList.map(_.name),
|
||||
config = ApiWebhookConfig(hook.ctype.code, hook.url),
|
||||
url = ApiPath(s"/api/v3/${hook.userName}/${hook.repositoryName}/hooks/${hook.hookId}")
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
case class CreateARepositoryWebhookConfig(
|
||||
url: String,
|
||||
content_type: String = "form",
|
||||
insecure_ssl: String = "0",
|
||||
secret: Option[String]
|
||||
)
|
||||
|
||||
/**
|
||||
* https://docs.github.com/en/rest/reference/repos#create-a-repository-webhook
|
||||
*/
|
||||
case class CreateARepositoryWebhook(
|
||||
name: String = "web",
|
||||
config: CreateARepositoryWebhookConfig,
|
||||
events: List[String] = List("push"),
|
||||
active: Boolean = true
|
||||
) {
|
||||
def isValid: Boolean = {
|
||||
config.content_type == "form" || config.content_type == "json"
|
||||
}
|
||||
}
|
||||
|
||||
case class UpdateARepositoryWebhook(
|
||||
name: String = "web",
|
||||
config: CreateARepositoryWebhookConfig,
|
||||
events: List[String] = List("push"),
|
||||
add_events: List[String] = List(),
|
||||
remove_events: List[String] = List(),
|
||||
active: Boolean = true
|
||||
) {
|
||||
def isValid: Boolean = {
|
||||
config.content_type == "form" || config.content_type == "json"
|
||||
}
|
||||
}
|
||||
@@ -522,7 +522,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
val url = params("url")
|
||||
val token = Some(params("token"))
|
||||
val ctype = WebHookContentType.valueOf(params("ctype"))
|
||||
val dummyWebHookInfo = RepositoryWebHook(userName, "dummy", url, ctype, token)
|
||||
val dummyWebHookInfo =
|
||||
RepositoryWebHook(userName = userName, repositoryName = "dummy", url = url, ctype = ctype, token = token)
|
||||
val dummyPayload = {
|
||||
val ownerAccount = getAccountByUserName(userName).get
|
||||
WebHookPushPayload.createDummyPayload(ownerAccount)
|
||||
|
||||
@@ -22,6 +22,7 @@ class ApiController
|
||||
with ApiRepositoryContentsControllerBase
|
||||
with ApiRepositoryControllerBase
|
||||
with ApiRepositoryStatusControllerBase
|
||||
with ApiRepositoryWebhookControllerBase
|
||||
with ApiUserControllerBase
|
||||
with RepositoryService
|
||||
with AccountService
|
||||
|
||||
@@ -228,7 +228,13 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
* Display the web hook edit page.
|
||||
*/
|
||||
get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository =>
|
||||
val webhook = RepositoryWebHook(repository.owner, repository.name, "", WebHookContentType.FORM, None)
|
||||
val webhook = RepositoryWebHook(
|
||||
userName = repository.owner,
|
||||
repositoryName = repository.name,
|
||||
url = "",
|
||||
ctype = WebHookContentType.FORM,
|
||||
token = None
|
||||
)
|
||||
html.edithook(webhook, Set(WebHook.Push), repository, true)
|
||||
})
|
||||
|
||||
@@ -271,7 +277,13 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
val url = params("url")
|
||||
val token = Some(params("token"))
|
||||
val ctype = WebHookContentType.valueOf(params("ctype"))
|
||||
val dummyWebHookInfo = RepositoryWebHook(repository.owner, repository.name, url, ctype, token)
|
||||
val dummyWebHookInfo = RepositoryWebHook(
|
||||
userName = repository.owner,
|
||||
repositoryName = repository.name,
|
||||
url = url,
|
||||
ctype = ctype,
|
||||
token = token
|
||||
)
|
||||
val dummyPayload = {
|
||||
val ownerAccount = getAccountByUserName(repository.owner).get
|
||||
val commits =
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package gitbucket.core.controller.api
|
||||
import gitbucket.core.api._
|
||||
import gitbucket.core.controller.ControllerBase
|
||||
import gitbucket.core.model.{WebHook, WebHookContentType}
|
||||
import gitbucket.core.service.{RepositoryService, WebHookService}
|
||||
import gitbucket.core.util._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import org.scalatra.NoContent
|
||||
|
||||
trait ApiRepositoryWebhookControllerBase extends ControllerBase {
|
||||
self: RepositoryService with WebHookService with ReferrerAuthenticator with WritableUsersAuthenticator =>
|
||||
|
||||
/*
|
||||
* i. List repository webhooks
|
||||
* https://docs.github.com/en/rest/reference/repos#list-repository-webhooks
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/hooks")(referrersOnly { repository =>
|
||||
val apiWebhooks = for {
|
||||
(hook, events) <- getWebHooks(repository.owner, repository.name)
|
||||
} yield {
|
||||
ApiWebhook("Repository", hook, events)
|
||||
}
|
||||
JsonFormat(apiWebhooks)
|
||||
})
|
||||
|
||||
/*
|
||||
* ii. Create a repository webhook
|
||||
* https://docs.github.com/en/rest/reference/repos#create-a-repository-webhook
|
||||
*/
|
||||
post("/api/v3/repos/:owner/:repository/hooks")(writableUsersOnly { repository =>
|
||||
(for {
|
||||
data <- extractFromJsonBody[CreateARepositoryWebhook] if data.isValid
|
||||
ctype = if (data.config.content_type == "form") WebHookContentType.FORM else WebHookContentType.JSON
|
||||
events = data.events.map(p => WebHook.Event.valueOf(p)).toSet
|
||||
} yield {
|
||||
addWebHook(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
data.config.url,
|
||||
events,
|
||||
ctype,
|
||||
data.config.secret
|
||||
)
|
||||
getWebHook(repository.owner, repository.name, data.config.url) match {
|
||||
case Some(createdHook) => JsonFormat(ApiWebhook("Repository", createdHook._1, createdHook._2))
|
||||
case _ =>
|
||||
}
|
||||
}) getOrElse NotFound()
|
||||
})
|
||||
|
||||
/*
|
||||
* iii. Get a repository webhook
|
||||
* https://docs.github.com/en/rest/reference/repos#get-a-repository-webhook
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/hooks/:id")(referrersOnly { repository =>
|
||||
val hookId = params("id").toInt
|
||||
getWebHookById(hookId) match {
|
||||
case Some(hook) => JsonFormat(ApiWebhook("Repository", hook._1, hook._2))
|
||||
case _ => NotFound()
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
* iv. Update a repository webhook
|
||||
* https://docs.github.com/en/rest/reference/repos#update-a-repository-webhook
|
||||
*/
|
||||
patch("/api/v3/repos/:owner/:repository/hooks/:id")(writableUsersOnly { repository =>
|
||||
val hookId = params("id").toInt
|
||||
(for {
|
||||
data <- extractFromJsonBody[UpdateARepositoryWebhook] if data.isValid
|
||||
ctype = data.config.content_type match {
|
||||
case "json" => WebHookContentType.JSON
|
||||
case _ => WebHookContentType.FORM
|
||||
}
|
||||
} yield {
|
||||
val events = (data.events ++ data.add_events)
|
||||
.filterNot(p => data.remove_events.contains(p))
|
||||
.map(p => WebHook.Event.valueOf(p))
|
||||
.toSet
|
||||
updateWebHookByApi(
|
||||
hookId,
|
||||
repository.owner,
|
||||
repository.name,
|
||||
data.config.url,
|
||||
events,
|
||||
ctype,
|
||||
data.config.secret
|
||||
)
|
||||
getWebHookById(hookId) match {
|
||||
case Some(updatedHook) => JsonFormat(ApiWebhook("Repository", updatedHook._1, updatedHook._2))
|
||||
case _ =>
|
||||
}
|
||||
}) getOrElse NotFound()
|
||||
})
|
||||
|
||||
/*
|
||||
* v. Delete a repository webhook
|
||||
* https://docs.github.com/en/rest/reference/repos#delete-a-repository-webhook
|
||||
*/
|
||||
delete("/api/v3/repos/:owner/:repository/hooks/:id")(writableUsersOnly { repository =>
|
||||
val hookId = params("id").toInt
|
||||
getWebHookById(hookId) match {
|
||||
case Some(_) =>
|
||||
deleteWebHookById(params("id").toInt)
|
||||
NoContent()
|
||||
case _ => NotFound()
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
* vi. Ping a repository webhook
|
||||
* https://docs.github.com/en/rest/reference/repos#ping-a-repository-webhook
|
||||
*/
|
||||
|
||||
/*
|
||||
* vi. Test the push repository webhook
|
||||
* https://docs.github.com/en/rest/reference/repos#test-the-push-repository-webhook
|
||||
*/
|
||||
|
||||
}
|
||||
@@ -9,20 +9,25 @@ trait RepositoryWebHookComponent extends TemplateComponent { self: Profile =>
|
||||
lazy val RepositoryWebHooks = TableQuery[RepositoryWebHooks]
|
||||
|
||||
class RepositoryWebHooks(tag: Tag) extends Table[RepositoryWebHook](tag, "WEB_HOOK") with BasicTemplate {
|
||||
val hookId = column[Int]("HOOK_ID", O AutoInc)
|
||||
val url = column[String]("URL")
|
||||
val token = column[Option[String]]("TOKEN")
|
||||
val ctype = column[WebHookContentType]("CTYPE")
|
||||
def * =
|
||||
(userName, repositoryName, url, ctype, token) <> ((RepositoryWebHook.apply _).tupled, RepositoryWebHook.unapply)
|
||||
(userName, repositoryName, hookId, url, ctype, token) <> ((RepositoryWebHook.apply _).tupled, RepositoryWebHook.unapply)
|
||||
|
||||
def byPrimaryKey(owner: String, repository: String, url: String) =
|
||||
def byRepositoryUrl(owner: String, repository: String, url: String) =
|
||||
byRepository(owner, repository) && (this.url === url.bind)
|
||||
|
||||
def byId(id: Int) =
|
||||
(this.hookId === id.bind)
|
||||
}
|
||||
}
|
||||
|
||||
case class RepositoryWebHook(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
hookId: Int = 0,
|
||||
url: String,
|
||||
ctype: WebHookContentType,
|
||||
token: Option[String]
|
||||
|
||||
@@ -83,7 +83,7 @@ trait WebHookService {
|
||||
implicit s: Session
|
||||
): Option[(RepositoryWebHook, Set[WebHook.Event])] =
|
||||
RepositoryWebHooks
|
||||
.filter(_.byPrimaryKey(owner, repository, url))
|
||||
.filter(_.byRepositoryUrl(owner, repository, url))
|
||||
.join(RepositoryWebHookEvents)
|
||||
.on { (w, t) =>
|
||||
t.byRepositoryWebHook(w)
|
||||
@@ -95,6 +95,24 @@ trait WebHookService {
|
||||
.mapValues(_.map(_._2).toSet)
|
||||
.headOption
|
||||
|
||||
/** get All WebHook informations of repository */
|
||||
def getWebHookById(id: Int)(
|
||||
implicit s: Session
|
||||
): Option[(RepositoryWebHook, Set[WebHook.Event])] =
|
||||
RepositoryWebHooks
|
||||
.filter(_.byId(id))
|
||||
.join(RepositoryWebHookEvents)
|
||||
.on { (w, t) =>
|
||||
t.byRepositoryWebHook(w)
|
||||
}
|
||||
.map { case (w, t) => w -> t.event }
|
||||
.list
|
||||
.groupBy(_._1)
|
||||
.view
|
||||
.mapValues(_.map(_._2).toSet)
|
||||
.toList
|
||||
.headOption
|
||||
|
||||
def addWebHook(
|
||||
owner: String,
|
||||
repository: String,
|
||||
@@ -103,7 +121,13 @@ trait WebHookService {
|
||||
ctype: WebHookContentType,
|
||||
token: Option[String]
|
||||
)(implicit s: Session): Unit = {
|
||||
RepositoryWebHooks insert RepositoryWebHook(owner, repository, url, ctype, token)
|
||||
RepositoryWebHooks insert RepositoryWebHook(
|
||||
userName = owner,
|
||||
repositoryName = repository,
|
||||
url = url,
|
||||
ctype = ctype,
|
||||
token = token
|
||||
)
|
||||
events.map { event: WebHook.Event =>
|
||||
RepositoryWebHookEvents insert RepositoryWebHookEvent(owner, repository, url, event)
|
||||
}
|
||||
@@ -118,7 +142,7 @@ trait WebHookService {
|
||||
token: Option[String]
|
||||
)(implicit s: Session): Unit = {
|
||||
RepositoryWebHooks
|
||||
.filter(_.byPrimaryKey(owner, repository, url))
|
||||
.filter(_.byRepositoryUrl(owner, repository, url))
|
||||
.map(w => (w.ctype, w.token))
|
||||
.update((ctype, token))
|
||||
RepositoryWebHookEvents.filter(_.byRepositoryWebHook(owner, repository, url)).delete
|
||||
@@ -127,8 +151,30 @@ trait WebHookService {
|
||||
}
|
||||
}
|
||||
|
||||
def updateWebHookByApi(
|
||||
id: Int,
|
||||
owner: String,
|
||||
repository: String,
|
||||
url: String,
|
||||
events: Set[WebHook.Event],
|
||||
ctype: WebHookContentType,
|
||||
token: Option[String]
|
||||
)(implicit s: Session): Unit = {
|
||||
RepositoryWebHooks
|
||||
.filter(_.byId(id))
|
||||
.map(w => (w.url, w.ctype, w.token))
|
||||
.update((url, ctype, token))
|
||||
RepositoryWebHookEvents.filter(_.byRepositoryWebHook(owner, repository, url)).delete
|
||||
events.map { event: WebHook.Event =>
|
||||
RepositoryWebHookEvents insert RepositoryWebHookEvent(owner, repository, url, event)
|
||||
}
|
||||
}
|
||||
|
||||
def deleteWebHook(owner: String, repository: String, url: String)(implicit s: Session): Unit =
|
||||
RepositoryWebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete
|
||||
RepositoryWebHooks.filter(_.byRepositoryUrl(owner, repository, url)).delete
|
||||
|
||||
def deleteWebHookById(id: Int)(implicit s: Session): Unit =
|
||||
RepositoryWebHooks.filter(_.byId(id)).delete
|
||||
|
||||
/** get All AccountWebHook informations of user */
|
||||
def getAccountWebHooks(owner: String)(implicit s: Session): List[(AccountWebHook, Set[WebHook.Event])] =
|
||||
|
||||
@@ -55,17 +55,23 @@ class WebHookServiceSpec extends AnyFunSuite with ServiceSpecBase {
|
||||
service.addWebHook("user1", "repo1", "http://example.com", Set(WebHook.PullRequest), formType, Some("key"))
|
||||
assert(
|
||||
service.getWebHooks("user1", "repo1") == List(
|
||||
(RepositoryWebHook("user1", "repo1", "http://example.com", formType, Some("key")), Set(WebHook.PullRequest))
|
||||
(
|
||||
RepositoryWebHook("user1", "repo1", 1, "http://example.com", formType, Some("key")),
|
||||
Set(WebHook.PullRequest)
|
||||
)
|
||||
)
|
||||
)
|
||||
assert(
|
||||
service.getWebHook("user1", "repo1", "http://example.com") == Some(
|
||||
(RepositoryWebHook("user1", "repo1", "http://example.com", formType, Some("key")), Set(WebHook.PullRequest))
|
||||
(
|
||||
RepositoryWebHook("user1", "repo1", 1, "http://example.com", formType, Some("key")),
|
||||
Set(WebHook.PullRequest)
|
||||
)
|
||||
)
|
||||
)
|
||||
assert(
|
||||
service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == List(
|
||||
(RepositoryWebHook("user1", "repo1", "http://example.com", formType, Some("key")))
|
||||
(RepositoryWebHook("user1", "repo1", 1, "http://example.com", formType, Some("key")))
|
||||
)
|
||||
)
|
||||
assert(service.getWebHooksByEvent("user1", "repo1", WebHook.Push) == Nil)
|
||||
@@ -83,7 +89,7 @@ class WebHookServiceSpec extends AnyFunSuite with ServiceSpecBase {
|
||||
assert(
|
||||
service.getWebHook("user1", "repo1", "http://example.com") == Some(
|
||||
(
|
||||
RepositoryWebHook("user1", "repo1", "http://example.com", jsonType, Some("key")),
|
||||
RepositoryWebHook("user1", "repo1", 1, "http://example.com", jsonType, Some("key")),
|
||||
Set(WebHook.Push, WebHook.Issues)
|
||||
)
|
||||
)
|
||||
@@ -91,7 +97,7 @@ class WebHookServiceSpec extends AnyFunSuite with ServiceSpecBase {
|
||||
assert(service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == Nil)
|
||||
assert(
|
||||
service.getWebHooksByEvent("user1", "repo1", WebHook.Push) == List(
|
||||
(RepositoryWebHook("user1", "repo1", "http://example.com", jsonType, Some("key")))
|
||||
(RepositoryWebHook("user1", "repo1", 1, "http://example.com", jsonType, Some("key")))
|
||||
)
|
||||
)
|
||||
service.deleteWebHook("user1", "repo1", "http://example.com")
|
||||
@@ -115,9 +121,11 @@ class WebHookServiceSpec extends AnyFunSuite with ServiceSpecBase {
|
||||
)
|
||||
assert(
|
||||
service.getWebHooks("user1", "repo1") == List(
|
||||
RepositoryWebHook("user1", "repo1", "http://example.com/1", ctype, Some("key")) -> Set(WebHook.PullRequest),
|
||||
RepositoryWebHook("user1", "repo1", "http://example.com/2", ctype, Some("key")) -> Set(WebHook.Push),
|
||||
RepositoryWebHook("user1", "repo1", "http://example.com/3", ctype, Some("key")) -> Set(
|
||||
RepositoryWebHook("user1", "repo1", 1, "http://example.com/1", ctype, Some("key")) -> Set(
|
||||
WebHook.PullRequest
|
||||
),
|
||||
RepositoryWebHook("user1", "repo1", 2, "http://example.com/2", ctype, Some("key")) -> Set(WebHook.Push),
|
||||
RepositoryWebHook("user1", "repo1", 3, "http://example.com/3", ctype, Some("key")) -> Set(
|
||||
WebHook.PullRequest,
|
||||
WebHook.Push
|
||||
)
|
||||
@@ -125,8 +133,8 @@ class WebHookServiceSpec extends AnyFunSuite with ServiceSpecBase {
|
||||
)
|
||||
assert(
|
||||
service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == List(
|
||||
RepositoryWebHook("user1", "repo1", "http://example.com/1", ctype, Some("key")),
|
||||
RepositoryWebHook("user1", "repo1", "http://example.com/3", ctype, Some("key"))
|
||||
RepositoryWebHook("user1", "repo1", 1, "http://example.com/1", ctype, Some("key")),
|
||||
RepositoryWebHook("user1", "repo1", 3, "http://example.com/3", ctype, Some("key"))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user