add webhook APIs (#2551)

This commit is contained in:
onukura
2020-09-20 01:42:14 +09:00
committed by GitHub
parent 3555519392
commit ad147e8dd5
11 changed files with 309 additions and 20 deletions

View 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>

View File

@@ -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")),
)

View 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}")
)
}

View File

@@ -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"
}
}

View File

@@ -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)

View File

@@ -22,6 +22,7 @@ class ApiController
with ApiRepositoryContentsControllerBase
with ApiRepositoryControllerBase
with ApiRepositoryStatusControllerBase
with ApiRepositoryWebhookControllerBase
with ApiUserControllerBase
with RepositoryService
with AccountService

View File

@@ -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 =

View File

@@ -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
*/
}

View File

@@ -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]

View File

@@ -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])] =

View File

@@ -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"))
)
)
}