diff --git a/doc/notification.md b/doc/notification.md
index f9fb1e0..db0e48d 100644
--- a/doc/notification.md
+++ b/doc/notification.md
@@ -17,6 +17,7 @@
Notified users are as follows:
* individual repository's owner
+* group members of group repository
* collaborators
* participants
diff --git a/src/main/scala/gitbucket/core/controller/AccountController.scala b/src/main/scala/gitbucket/core/controller/AccountController.scala
index 4786bea..c2235f7 100644
--- a/src/main/scala/gitbucket/core/controller/AccountController.scala
+++ b/src/main/scala/gitbucket/core/controller/AccountController.scala
@@ -206,9 +206,13 @@
// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
// }
-// // Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
+ // Remove from GROUP_MEMBER and COLLABORATOR
removeUserRelatedData(userName)
updateAccount(account.copy(isRemoved = true))
+
+ // call hooks
+ PluginRegistry().getAccountHooks.foreach(_.deleted(userName))
+
session.invalidate
redirect("/")
}
diff --git a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala
index 45b7627..4029ff0 100644
--- a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala
+++ b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala
@@ -1,6 +1,7 @@
package gitbucket.core.controller
import gitbucket.core.model.WebHook
+import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.pulls.html
import gitbucket.core.service.CommitStatusService
import gitbucket.core.service.MergeService
@@ -277,10 +278,8 @@
// call web hook
callPullRequestWebHook("closed", repository, issueId, context.baseUrl, context.loginAccount.get)
- // notifications
- Notifier().toNotify(repository, issue, "merge"){
- Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}")
- }
+ // call hooks
+ PluginRegistry().getPullRequestHooks.foreach(_.merged(issue, repository))
redirect(s"/${owner}/${name}/pull/${issueId}")
}
@@ -484,10 +483,8 @@
// extract references and create refer comment
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get)
- // notifications
- Notifier().toNotify(repository, issue, form.content.getOrElse("")) {
- Notifier.msgPullRequest(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}")
- }
+ // call hooks
+ PluginRegistry().getPullRequestHooks.foreach(_.created(issue, repository))
}
redirect(s"/${owner}/${name}/pull/${issueId}")
diff --git a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala
index caebd04..9220853 100644
--- a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala
+++ b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala
@@ -225,7 +225,7 @@
// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
// }
- // Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
+ // Remove from GROUP_MEMBER and COLLABORATOR
removeUserRelatedData(userName)
}
@@ -239,6 +239,10 @@
isRemoved = form.isRemoved))
updateImage(userName, form.fileId, form.clearImage)
+
+ // call hooks
+ if(form.isRemoved) PluginRegistry().getAccountHooks.foreach(_.deleted(userName))
+
redirect("/admin/users")
}
} getOrElse NotFound()
diff --git a/src/main/scala/gitbucket/core/plugin/AccountHook.scala b/src/main/scala/gitbucket/core/plugin/AccountHook.scala
new file mode 100644
index 0000000..b6db885
--- /dev/null
+++ b/src/main/scala/gitbucket/core/plugin/AccountHook.scala
@@ -0,0 +1,10 @@
+package gitbucket.core.plugin
+
+import gitbucket.core.model.Profile._
+import profile.api._
+
+trait AccountHook {
+
+ def deleted(userName: String)(implicit session: Session): Unit = ()
+
+}
diff --git a/src/main/scala/gitbucket/core/plugin/IssueHook.scala b/src/main/scala/gitbucket/core/plugin/IssueHook.scala
new file mode 100644
index 0000000..8bed047
--- /dev/null
+++ b/src/main/scala/gitbucket/core/plugin/IssueHook.scala
@@ -0,0 +1,20 @@
+package gitbucket.core.plugin
+
+import gitbucket.core.controller.Context
+import gitbucket.core.model.Issue
+import gitbucket.core.service.RepositoryService.RepositoryInfo
+
+trait IssueHook {
+
+ def created(issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
+ def addedComment(commentId: Int, content: String, issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
+ def closed(issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
+ def reopened(issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
+
+}
+
+trait PullRequestHook extends IssueHook {
+
+ def merged(issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
+
+}
diff --git a/src/main/scala/gitbucket/core/plugin/Plugin.scala b/src/main/scala/gitbucket/core/plugin/Plugin.scala
index 4367c68..3d2b51b 100644
--- a/src/main/scala/gitbucket/core/plugin/Plugin.scala
+++ b/src/main/scala/gitbucket/core/plugin/Plugin.scala
@@ -1,12 +1,14 @@
package gitbucket.core.plugin
import javax.servlet.ServletContext
+
import gitbucket.core.controller.{Context, ControllerBase}
-import gitbucket.core.model.Account
+import gitbucket.core.model.{Account, Issue}
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.SystemSettingsService.SystemSettings
import gitbucket.core.util.SyntaxSugars._
import io.github.gitbucket.solidbase.model.Version
+import play.twirl.api.Html
/**
* Trait for define plugin interface.
@@ -70,6 +72,16 @@
def repositoryRoutings(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[GitRepositoryRouting] = Nil
/**
+ * Override to add account hooks.
+ */
+ val accountHooks: Seq[AccountHook] = Nil
+
+ /**
+ * Override to add account hooks.
+ */
+ def accountHooks(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[AccountHook] = Nil
+
+ /**
* Override to add receive hooks.
*/
val receiveHooks: Seq[ReceiveHook] = Nil
@@ -90,6 +102,26 @@
def repositoryHooks(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[RepositoryHook] = Nil
/**
+ * Override to add issue hooks.
+ */
+ val issueHooks: Seq[IssueHook] = Nil
+
+ /**
+ * Override to add issue hooks.
+ */
+ def issueHooks(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[IssueHook] = Nil
+
+ /**
+ * Override to add pull request hooks.
+ */
+ val pullRequestHooks: Seq[PullRequestHook] = Nil
+
+ /**
+ * Override to add pull request hooks.
+ */
+ def pullRequestHooks(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[PullRequestHook] = Nil
+
+ /**
* Override to add global menus.
*/
val globalMenus: Seq[(Context) => Option[Link]] = Nil
@@ -160,6 +192,16 @@
def dashboardTabs(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(Context) => Option[Link]] = Nil
/**
+ * Override to add issue sidebars.
+ */
+ val issueSidebars: Seq[(Issue, RepositoryInfo, Context) => Option[Html]] = Nil
+
+ /**
+ * Override to add issue sidebars.
+ */
+ def issueSidebars(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(Issue, RepositoryInfo, Context) => Option[Html]] = Nil
+
+ /**
* Override to add assets mappings.
*/
val assetsMappings: Seq[(String, String)] = Nil
@@ -209,12 +251,21 @@
(repositoryRoutings ++ repositoryRoutings(registry, context, settings)).foreach { routing =>
registry.addRepositoryRouting(routing)
}
+ (accountHooks ++ accountHooks(registry, context, settings)).foreach { accountHook =>
+ registry.addAccountHook(accountHook)
+ }
(receiveHooks ++ receiveHooks(registry, context, settings)).foreach { receiveHook =>
registry.addReceiveHook(receiveHook)
}
(repositoryHooks ++ repositoryHooks(registry, context, settings)).foreach { repositoryHook =>
registry.addRepositoryHook(repositoryHook)
}
+ (issueHooks ++ issueHooks(registry, context, settings)).foreach { issueHook =>
+ registry.addIssueHook(issueHook)
+ }
+ (pullRequestHooks ++ pullRequestHooks(registry, context, settings)).foreach { pullRequestHook =>
+ registry.addPullRequestHook(pullRequestHook)
+ }
(globalMenus ++ globalMenus(registry, context, settings)).foreach { globalMenu =>
registry.addGlobalMenu(globalMenu)
}
@@ -236,6 +287,9 @@
(dashboardTabs ++ dashboardTabs(registry, context, settings)).foreach { dashboardTab =>
registry.addDashboardTab(dashboardTab)
}
+ (issueSidebars ++ issueSidebars(registry, context, settings)).foreach { issueSidebar =>
+ registry.addIssueSidebar(issueSidebar)
+ }
(assetsMappings ++ assetsMappings(registry, context, settings)).foreach { assetMapping =>
registry.addAssetsMapping((assetMapping._1, assetMapping._2, getClass.getClassLoader))
}
diff --git a/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala b/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala
index 9ab7bea..c87adbf 100644
--- a/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala
+++ b/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala
@@ -6,7 +6,7 @@
import javax.servlet.ServletContext
import gitbucket.core.controller.{Context, ControllerBase}
-import gitbucket.core.model.Account
+import gitbucket.core.model.{Account, Issue}
import gitbucket.core.service.ProtectedBranchService.ProtectedBranchReceiveHook
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.SystemSettingsService.SystemSettings
@@ -17,6 +17,7 @@
import io.github.gitbucket.solidbase.manager.JDBCVersionManager
import io.github.gitbucket.solidbase.model.Module
import org.slf4j.LoggerFactory
+import play.twirl.api.Html
import scala.collection.mutable
import scala.collection.mutable.ListBuffer
@@ -32,10 +33,17 @@
"md" -> MarkdownRenderer, "markdown" -> MarkdownRenderer
)
private val repositoryRoutings = new ListBuffer[GitRepositoryRouting]
+ private val accountHooks = new ListBuffer[AccountHook]
private val receiveHooks = new ListBuffer[ReceiveHook]
receiveHooks += new ProtectedBranchReceiveHook()
private val repositoryHooks = new ListBuffer[RepositoryHook]
+ private val issueHooks = new ListBuffer[IssueHook]
+ issueHooks += new gitbucket.core.util.Notifier.IssueHook()
+
+ private val pullRequestHooks = new ListBuffer[PullRequestHook]
+ pullRequestHooks += new gitbucket.core.util.Notifier.PullRequestHook()
+
private val globalMenus = new ListBuffer[(Context) => Option[Link]]
private val repositoryMenus = new ListBuffer[(RepositoryInfo, Context) => Option[Link]]
private val repositorySettingTabs = new ListBuffer[(RepositoryInfo, Context) => Option[Link]]
@@ -43,6 +51,7 @@
private val systemSettingMenus = new ListBuffer[(Context) => Option[Link]]
private val accountSettingMenus = new ListBuffer[(Context) => Option[Link]]
private val dashboardTabs = new ListBuffer[(Context) => Option[Link]]
+ private val issueSidebars = new ListBuffer[(Issue, RepositoryInfo, Context) => Option[Html]]
private val assetsMappings = new ListBuffer[(String, String, ClassLoader)]
private val textDecorators = new ListBuffer[TextDecorator]
@@ -99,6 +108,10 @@
}
}
+ def addAccountHook(accountHook: AccountHook): Unit = accountHooks += accountHook
+
+ def getAccountHooks: Seq[AccountHook] = accountHooks.toSeq
+
def addReceiveHook(commitHook: ReceiveHook): Unit = receiveHooks += commitHook
def getReceiveHooks: Seq[ReceiveHook] = receiveHooks.toSeq
@@ -107,6 +120,14 @@
def getRepositoryHooks: Seq[RepositoryHook] = repositoryHooks.toSeq
+ def addIssueHook(issueHook: IssueHook): Unit = issueHooks += issueHook
+
+ def getIssueHooks: Seq[IssueHook] = issueHooks.toSeq
+
+ def addPullRequestHook(pullRequestHook: PullRequestHook): Unit = pullRequestHooks += pullRequestHook
+
+ def getPullRequestHooks: Seq[PullRequestHook] = pullRequestHooks.toSeq
+
def addGlobalMenu(globalMenu: (Context) => Option[Link]): Unit = globalMenus += globalMenu
def getGlobalMenus: Seq[(Context) => Option[Link]] = globalMenus.toSeq
@@ -135,6 +156,10 @@
def getDashboardTabs: Seq[(Context) => Option[Link]] = dashboardTabs.toSeq
+ def addIssueSidebar(issueSidebar: (Issue, RepositoryInfo, Context) => Option[Html]): Unit = issueSidebars += issueSidebar
+
+ def getIssueSidebars: Seq[(Issue, RepositoryInfo, Context) => Option[Html]] = issueSidebars.toSeq
+
def addAssetsMapping(assetsMapping: (String, String, ClassLoader)): Unit = assetsMappings += assetsMapping
def getAssetsMappings: Seq[(String, String, ClassLoader)] = assetsMappings.toSeq
diff --git a/src/main/scala/gitbucket/core/service/HandleCommentService.scala b/src/main/scala/gitbucket/core/service/HandleCommentService.scala
index aaa4cf7..f7ce6ff 100644
--- a/src/main/scala/gitbucket/core/service/HandleCommentService.scala
+++ b/src/main/scala/gitbucket/core/service/HandleCommentService.scala
@@ -2,11 +2,10 @@
import gitbucket.core.controller.Context
import gitbucket.core.model.Issue
-import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._
+import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Implicits._
-import gitbucket.core.util.Notifier
trait HandleCommentService {
self: RepositoryService with IssuesService with ActivityService
@@ -21,7 +20,7 @@
defining(repository.owner, repository.name){ case (owner, name) =>
val userName = loginAccount.userName
- val (action, recordActivity) = actionOpt
+ val (action, actionActivity) = actionOpt
.collect {
case "close" if(!issue.closed) => true ->
(Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
@@ -36,54 +35,55 @@
val commentId = (content, action) match {
case (None, None) => None
- case (None, Some(action)) => Some(createComment(owner, name, userName, issue.issueId, action.capitalize, action))
- case (Some(content), _) => Some(createComment(owner, name, userName, issue.issueId, content, action.map(_+ "_comment").getOrElse("comment")))
+ case (None, Some(action)) =>
+ Some(createComment(owner, name, userName, issue.issueId, action.capitalize, action))
+ case (Some(content), _) =>
+ val id = Some(createComment(owner, name, userName, issue.issueId, content, action.map(_+ "_comment").getOrElse("comment")))
+
+ // record comment activity
+ if(issue.isPullRequest) recordCommentPullRequestActivity(owner, name, userName, issue.issueId, content)
+ else recordCommentIssueActivity(owner, name, userName, issue.issueId, content)
+
+ // extract references and create refer comment
+ createReferComment(owner, name, issue, content, loginAccount)
+
+ id
}
- // record comment activity if comment is entered
- content foreach {
- (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
- (owner, name, userName, issue.issueId, _)
- }
- recordActivity foreach ( _ (owner, name, userName, issue.issueId, issue.title) )
-
- // extract references and create refer comment
- content.map { content =>
- createReferComment(owner, name, issue, content, loginAccount)
- }
+ actionActivity foreach ( _ (owner, name, userName, issue.issueId, issue.title) )
// call web hooks
action match {
- case None => commentId.map { commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, loginAccount) }
- case Some(act) => {
+ case None => commentId foreach (callIssueCommentWebHook(repository, issue, _, loginAccount))
+ case Some(act) =>
val webHookAction = act match {
- case "open" => "opened"
- case "reopen" => "reopened"
case "close" => "closed"
- case _ => act
+ case "reopen" => "reopened"
}
- if (issue.isPullRequest) {
+ if(issue.isPullRequest)
callPullRequestWebHook(webHookAction, repository, issue.issueId, context.baseUrl, loginAccount)
- } else {
+ else
callIssuesWebHook(webHookAction, repository, issue, context.baseUrl, loginAccount)
- }
- }
}
- // notifications
- Notifier() match {
- case f =>
- content foreach {
- f.toNotify(repository, issue, _){
- Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${
- if(issue.isPullRequest) "pull" else "issues"}/${issue.issueId}#comment-${commentId.get}")
- }
- }
- action foreach {
- f.toNotify(repository, issue, _){
- Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issue.issueId}")
- }
- }
+ // call hooks
+ content foreach { x =>
+ if(issue.isPullRequest)
+ PluginRegistry().getPullRequestHooks.foreach(_.addedComment(commentId.get, x, issue, repository))
+ else
+ PluginRegistry().getIssueHooks.foreach(_.addedComment(commentId.get, x, issue, repository))
+ }
+ action foreach {
+ case "close" =>
+ if(issue.isPullRequest)
+ PluginRegistry().getPullRequestHooks.foreach(_.closed(issue, repository))
+ else
+ PluginRegistry().getIssueHooks.foreach(_.closed(issue, repository))
+ case "reopen" =>
+ if(issue.isPullRequest)
+ PluginRegistry().getPullRequestHooks.foreach(_.reopened(issue, repository))
+ else
+ PluginRegistry().getIssueHooks.foreach(_.reopened(issue, repository))
}
commentId.map( issue -> _ )
diff --git a/src/main/scala/gitbucket/core/service/IssueCreationService.scala b/src/main/scala/gitbucket/core/service/IssueCreationService.scala
index a18dad3..09e7dfb 100644
--- a/src/main/scala/gitbucket/core/service/IssueCreationService.scala
+++ b/src/main/scala/gitbucket/core/service/IssueCreationService.scala
@@ -3,11 +3,10 @@
import gitbucket.core.controller.Context
import gitbucket.core.model.{Account, Issue}
import gitbucket.core.model.Profile.profile.blockingApi._
+import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.RepositoryService.RepositoryInfo
-import gitbucket.core.util.Notifier
import gitbucket.core.util.Implicits._
-// TODO: Merged with IssuesService?
trait IssueCreationService {
self: RepositoryService with WebHookIssueCommentService with LabelsService with IssuesService with ActivityService =>
@@ -46,10 +45,9 @@
// call web hooks
callIssuesWebHook("opened", repository, issue, context.baseUrl, loginAccount)
- // notifications
- Notifier().toNotify(repository, issue, body.getOrElse("")) {
- Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
- }
+ // call hooks
+ PluginRegistry().getIssueHooks.foreach(_.created(issue, repository))
+
issue
}
diff --git a/src/main/scala/gitbucket/core/util/Notifier.scala b/src/main/scala/gitbucket/core/util/Notifier.scala
index 3c8dba5..3e8d39c 100644
--- a/src/main/scala/gitbucket/core/util/Notifier.scala
+++ b/src/main/scala/gitbucket/core/util/Notifier.scala
@@ -13,87 +13,157 @@
import org.slf4j.LoggerFactory
import gitbucket.core.controller.Context
import SystemSettingsService.Smtp
-import SyntaxSugars.defining
-trait Notifier extends RepositoryService with AccountService with IssuesService {
+/**
+ * The trait for notifications.
+ * This is used by notifications plugin, which provides notifications feature on GitBucket.
+ * Please see the plugin for details.
+ */
+trait Notifier {
- def toNotify(r: RepositoryService.RepositoryInfo, issue: Issue, content: String)
- (msg: String => String)(implicit context: Context): Unit
+ def toNotify(subject: String, msg: String)
+ (recipients: Account => Session => Seq[String])(implicit context: Context): Unit
- protected def recipients(issue: Issue, loginAccount: Account)(notify: String => Unit)(implicit session: Session) =
- (
- // individual repository's owner
- issue.userName ::
- // group members of group repository
- getGroupMembers(issue.userName).map(_.userName) :::
- // collaborators
- getCollaboratorUserNames(issue.userName, issue.repositoryName) :::
- // participants
- issue.openedUserName ::
- getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName)
- )
- .distinct
- .withFilter ( _ != loginAccount.userName ) // the operation in person is excluded
- .foreach (
- getAccountByUserName(_)
- .filterNot (_.isGroupAccount)
- .filterNot (LDAPUtil.isDummyMailAddress(_))
- .foreach (x => notify(x.mailAddress))
- )
}
object Notifier {
- // TODO We want to be able to switch to mock.
def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match {
case settings if (settings.notification && settings.useSMTP) => new Mailer(settings.smtp.get)
case _ => new MockMailer
}
- def msgIssue(url: String) = (content: String) => s"""
- |${content}
- |--
- |View it on GitBucket
- """.stripMargin
- def msgPullRequest(url: String) = (content: String) => s"""
- |${content}