add organization webhook support #14784

Open
a1012112796 wants to merge 11 commits from a1012112796/hooks into main
18 changed files with 592 additions and 25 deletions

@ -325,6 +325,8 @@ var migrations = []Migration{
NewMigration("Create protected tag table", createProtectedTagTable),
// v187 -> v188
NewMigration("Drop unneeded webhook related columns", dropWebhookColumns),
// v188 -> v189
NewMigration("Add org_id to hook_task table", addOrgIDHookTaskColumn),
}
// GetCurrentDBVersion returns the current db version

22
models/migrations/v188.go Normal file

@ -0,0 +1,22 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package migrations
import (
"fmt"
"xorm.io/xorm"
)
func addOrgIDHookTaskColumn(x *xorm.Engine) error {
type HookTask struct {
OrgID int64 `xorm:"INDEX NOT NULL DEFAULT 0"`
}
if err := x.Sync2(new(HookTask)); err != nil {
return fmt.Errorf("Sync2: %v", err)
}
return nil
}

@ -97,6 +97,9 @@ type HookEvents struct {
PullRequestSync bool `json:"pull_request_sync"`
Repository bool `json:"repository"`
Release bool `json:"release"`
Organization bool `json:"organization"`
Team bool `json:"team"`
TeamMember bool `json:"membership"`
}
// HookEvent represents events that will delivery hook.
@ -299,6 +302,33 @@ func (w *Webhook) HasRepositoryEvent() bool {
(w.ChooseEvents && w.HookEvents.Repository)
}
// HasOrgEvent returns if hook enabled org event.
func (w *Webhook) HasOrgEvent() bool {
if w.RepoID != 0 {
return false
}
return w.SendEverything ||
(w.ChooseEvents && w.HookEvents.Organization)
}
// HasTeamEvent returns if hook enabled team event.
func (w *Webhook) HasTeamEvent() bool {
if w.RepoID != 0 {
return false
}
return w.SendEverything ||
(w.ChooseEvents && w.HookEvents.Team)
}
// HasTeamMemberEvent returns if hook enabled team member event.
func (w *Webhook) HasTeamMemberEvent() bool {
if w.RepoID != 0 {
return false
}
return w.SendEverything ||
(w.ChooseEvents && w.HookEvents.TeamMember)
}
// EventCheckers returns event checkers
func (w *Webhook) EventCheckers() []struct {
Has func() bool
@ -328,6 +358,9 @@ func (w *Webhook) EventCheckers() []struct {
{w.HasPullRequestSyncEvent, HookEventPullRequestSync},
{w.HasRepositoryEvent, HookEventRepository},
{w.HasReleaseEvent, HookEventRelease},
{w.HasOrgEvent, HookEventOrg},
{w.HasTeamEvent, HookEventTeam},
{w.HasTeamMemberEvent, HookEventTeamMember},
}
}
@ -597,6 +630,9 @@ const (
HookEventPullRequestSync HookEventType = "pull_request_sync"
HookEventRepository HookEventType = "repository"
HookEventRelease HookEventType = "release"
HookEventOrg HookEventType = "organization"
HookEventTeam HookEventType = "team"
HookEventTeamMember HookEventType = "membership"
)
// Event returns the HookEventType as an event string
@ -627,10 +663,38 @@ func (h HookEventType) Event() string {
return "repository"
case HookEventRelease:
Review
  HookEventLevelRepo HookEventLevel = iota + 1

iota along makes it zero and it feels kind of weird seeing a valid state as 0

```suggestion HookEventLevelRepo HookEventLevel = iota + 1 ``` iota along makes it zero and it feels kind of weird seeing a valid state as 0
return "release"
case HookEventOrg:
return "organization"
case HookEventTeam:
return "team"
case HookEventTeamMember:
return "membership"
}
return ""
}
// HookEventLevel leve of a hook event
type HookEventLevel int64
const (
// HookEventLevelRepo all hook types can be used
HookEventLevelRepo HookEventLevel = iota + 1
// HookEventLevelOrg org and system hook can be used
HookEventLevelOrg
// HookEventLevelSys only system hook can be used
// HookEventLevelSys
)
// EventLevel got last leve of this event
func (h HookEventType) EventLevel() HookEventLevel {
if h == HookEventOrg ||
h == HookEventTeam ||
h == HookEventTeamMember {
return HookEventLevelOrg
}
return HookEventLevelRepo
}
// HookRequest represents hook task request information.
type HookRequest struct {
URL string `json:"url"`
@ -649,6 +713,7 @@ type HookResponse struct {
type HookTask struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX"`
OrgID int64 `xorm:"INDEX NOT NULL DEFAULT 0"`
HookID int64
UUID string
api.Payloader `xorm:"-"`
@ -759,6 +824,15 @@ func FindRepoUndeliveredHookTasks(repoID int64) ([]*HookTask, error) {
return tasks, nil
}
// FindOrgUndeliveredHookTasks finds the undelivered hook tasks of an organisation
func FindOrgUndeliveredHookTasks(orgID int64) ([]*HookTask, error) {
tasks := make([]*HookTask, 0, 5)
Review
// FindOrgUndeliveredHookTasks finds the undelivered hook tasks of an organisation
```suggestion // FindOrgUndeliveredHookTasks finds the undelivered hook tasks of an organisation ```
if err := x.Where("org_id=? AND is_delivered=?", orgID, false).Find(&tasks); err != nil {
return nil, err
}
return tasks, nil
}
// CleanupHookTaskTable deletes rows from hook_task as needed.
func CleanupHookTaskTable(ctx context.Context, cleanupType HookTaskCleanupType, olderThan time.Duration, numberToKeep int) error {
log.Trace("Doing: CleanupHookTaskTable")

@ -70,8 +70,22 @@ func TestWebhook_EventsArray(t *testing.T) {
"pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone",
"pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected",
"pull_request_review_comment", "pull_request_sync", "repository", "release",
},
"organization", "team", "membership"},
(&Webhook{
RepoID: 0,
OrgID: 3,
HookEvent: &HookEvent{SendEverything: true},
}).EventsArray(),
)
assert.Equal(t, []string{"create", "delete", "fork", "push",
"issues", "issue_assign", "issue_label", "issue_milestone", "issue_comment",
"pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone",
"pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected",
"pull_request_review_comment", "pull_request_sync", "repository", "release"},
(&Webhook{
RepoID: 3,
OrgID: 0,
HookEvent: &HookEvent{SendEverything: true},
}).EventsArray(),
)

@ -59,4 +59,12 @@ type Notifier interface {
NotifySyncDeleteRef(doer *models.User, repo *models.Repository, refType, refFullName string)
NotifyRepoPendingTransfer(doer, newOwner *models.User, repo *models.Repository)
// org
NotifyAddOrgMember(doer, org, member *models.User)
NotifyRemoveOrgMember(doer, org, member *models.User)
NotifyAddOrgTeam(doer, org *models.User, team *models.Team)
NotifyRemoveOrgTeam(doer, org *models.User, team *models.Team)
NotifyAddTeamMember(doer, org, member *models.User, team *models.Team)
NotifyRemoveTeamMember(doer, org, member *models.User, team *models.Team)
}

@ -167,6 +167,30 @@ func (*NullNotifier) NotifySyncCreateRef(doer *models.User, repo *models.Reposit
func (*NullNotifier) NotifySyncDeleteRef(doer *models.User, repo *models.Repository, refType, refFullName string) {
}
// NotifyAddOrgMember notify new member added into an org
func (*NullNotifier) NotifyAddOrgMember(doer, org, member *models.User) {
}
// NotifyRemoveOrgMember notify member leave or be removed from an org
func (*NullNotifier) NotifyRemoveOrgMember(doer, org, member *models.User) {
}
// NotifyAddOrgTeam notify a new team created in an org
func (*NullNotifier) NotifyAddOrgTeam(doer, org *models.User, team *models.Team) {
}
// NotifyRemoveOrgTeam notify a team removed from an org
func (*NullNotifier) NotifyRemoveOrgTeam(doer, org *models.User, team *models.Team) {
}
// NotifyAddTeamMember notify add new member in a team
func (*NullNotifier) NotifyAddTeamMember(doer, org, member *models.User, team *models.Team) {
}
// NotifyRemoveTeamMember notify a member be removed from a team
func (*NullNotifier) NotifyRemoveTeamMember(doer, org, member *models.User, team *models.Team) {
}
// NotifyRepoPendingTransfer places a place holder function
func (*NullNotifier) NotifyRepoPendingTransfer(doer, newOwner *models.User, repo *models.Repository) {
}

@ -291,6 +291,48 @@ func NotifySyncDeleteRef(pusher *models.User, repo *models.Repository, refType,
}
}
// NotifyAddOrgMember notify new member added into an org
func NotifyAddOrgMember(doer, org, member *models.User) {
for _, notifier := range notifiers {
notifier.NotifyAddOrgMember(doer, org, member)
}
}
// NotifyRemoveOrgMember notify member leave or be removed from an org
func NotifyRemoveOrgMember(doer, org, member *models.User) {
for _, notifier := range notifiers {
notifier.NotifyRemoveOrgMember(doer, org, member)
}
}
// NotifyAddOrgTeam notify a new team created in an org
func NotifyAddOrgTeam(doer, org *models.User, team *models.Team) {
for _, notifier := range notifiers {
notifier.NotifyAddOrgTeam(doer, org, team)
}
}
// NotifyRemoveOrgTeam notify a team removed from an org
func NotifyRemoveOrgTeam(doer, org *models.User, team *models.Team) {
for _, notifier := range notifiers {
notifier.NotifyRemoveOrgTeam(doer, org, team)
}
}
// NotifyAddTeamMember notify add new member in a team
func NotifyAddTeamMember(doer, org, member *models.User, team *models.Team) {
for _, notifier := range notifiers {
notifier.NotifyAddTeamMember(doer, org, member, team)
}
}
// NotifyRemoveTeamMember notify a member be removed from a team
func NotifyRemoveTeamMember(doer, org, member *models.User, team *models.Team) {
for _, notifier := range notifiers {
notifier.NotifyRemoveTeamMember(doer, org, member, team)
}
}
// NotifyRepoPendingTransfer notifies creation of pending transfer to notifiers
func NotifyRepoPendingTransfer(doer, newOwner *models.User, repo *models.Repository) {
for _, notifier := range notifiers {

@ -819,3 +819,97 @@ func (m *webhookNotifier) NotifySyncCreateRef(pusher *models.User, repo *models.
func (m *webhookNotifier) NotifySyncDeleteRef(pusher *models.User, repo *models.Repository, refType, refFullName string) {
m.NotifyDeleteRef(pusher, repo, refType, refFullName)
}
func (*webhookNotifier) NotifyAddOrgMember(doer, org, member *models.User) {
apiDoer := convert.ToUser(doer, doer)
apiMember := convert.ToUser(member, doer)
apiOrg := convert.ToOrganization(org)
if err := webhook_services.PrepareOrgWebhooks(org, models.HookEventOrg, &api.OrganizationPayload{
Action: api.OrganizationActionTypeAddMember,
Member: apiMember,
Organization: apiOrg,
Sender: apiDoer,
}); err != nil {
log.Error("PrepareWebhooks: %v", err)
}
}
func (*webhookNotifier) NotifyRemoveOrgMember(doer, org, member *models.User) {
apiDoer := convert.ToUser(doer, doer)
apiMember := convert.ToUser(member, doer)
apiOrg := convert.ToOrganization(org)
if err := webhook_services.PrepareOrgWebhooks(org, models.HookEventOrg, &api.OrganizationPayload{
Action: api.OrganizationActionTypeRemoveMember,
Member: apiMember,
Organization: apiOrg,
Sender: apiDoer,
}); err != nil {
log.Error("PrepareWebhooks: %v", err)
}
}
func (*webhookNotifier) NotifyAddOrgTeam(doer, org *models.User, team *models.Team) {
apiDoer := convert.ToUser(doer, doer)
apiOrg := convert.ToOrganization(org)
apiTeam := convert.ToTeam(team)
if err := webhook_services.PrepareOrgWebhooks(org, models.HookEventTeam, &api.TeamPayload{
Action: api.TeamActionTypeAdd,
Team: apiTeam,
Organization: apiOrg,
Sender: apiDoer,
}); err != nil {
log.Error("PrepareWebhooks: %v", err)
}
}
func (*webhookNotifier) NotifyRemoveOrgTeam(doer, org *models.User, team *models.Team) {
apiDoer := convert.ToUser(doer, doer)
apiOrg := convert.ToOrganization(org)
apiTeam := convert.ToTeam(team)
if err := webhook_services.PrepareOrgWebhooks(org, models.HookEventTeam, &api.TeamPayload{
Action: api.TeamActionTypeRemove,
Team: apiTeam,
Organization: apiOrg,
Sender: apiDoer,
}); err != nil {
log.Error("PrepareWebhooks: %v", err)
}
}
func (*webhookNotifier) NotifyAddTeamMember(doer, org, member *models.User, team *models.Team) {
apiDoer := convert.ToUser(doer, doer)
apiMember := convert.ToUser(member, doer)
apiOrg := convert.ToOrganization(org)
apiTeam := convert.ToTeam(team)
if err := webhook_services.PrepareOrgWebhooks(org, models.HookEventTeamMember, &api.MembershipPayload{
Action: api.MembershipActionTypeAdd,
Team: apiTeam,
Organization: apiOrg,
Member: apiMember,
Sender: apiDoer,
}); err != nil {
log.Error("PrepareWebhooks: %v", err)
}
}
func (*webhookNotifier) NotifyRemoveTeamMember(doer, org, member *models.User, team *models.Team) {
apiDoer := convert.ToUser(doer, doer)
apiMember := convert.ToUser(member, doer)
apiOrg := convert.ToOrganization(org)
apiTeam := convert.ToTeam(team)
if err := webhook_services.PrepareOrgWebhooks(org, models.HookEventTeamMember, &api.MembershipPayload{
Action: api.MembershipActionTypeRemove,
Team: apiTeam,
Organization: apiOrg,
Member: apiMember,
Sender: apiDoer,
}); err != nil {
log.Error("PrepareWebhooks: %v", err)
}
}

@ -6,6 +6,7 @@
package structs
import (
"encoding/json"
"errors"
"strings"
"time"
@ -438,3 +439,95 @@ func (p *RepositoryPayload) JSONPayload() ([]byte, error) {
json := jsoniter.ConfigCompatibleWithStandardLibrary
return json.MarshalIndent(p, "", " ")
}
// organization
// OrganizationActionType org action type
type OrganizationActionType string
const (
// OrganizationActionTypeAddMember add member in an org
OrganizationActionTypeAddMember OrganizationActionType = "member_added"
// OrganizationActionTypeRemoveMember member removed in an org
OrganizationActionTypeRemoveMember OrganizationActionType = "member_removed"
)
// OrganizationPayload Activity related to an organization and its members.
type OrganizationPayload struct {
Secret string `json:"secret"`
Action OrganizationActionType `json:"action"`
Member *User `json:"member"`
Organization *Organization `json:"organization"`
Sender *User `json:"sender"`
}
// SetSecret modifies the secret of the RepositoryPayload
func (p *OrganizationPayload) SetSecret(secret string) {
p.Secret = secret
}
// JSONPayload JSON representation of the payload
func (p *OrganizationPayload) JSONPayload() ([]byte, error) {
return json.MarshalIndent(p, "", " ")
}
// TeamActionType action types of team
type TeamActionType string
const (
// TeamActionTypeAdd created a new team
TeamActionTypeAdd TeamActionType = "created"
// TeamActionTypeRemove deleted a team
TeamActionTypeRemove TeamActionType = "deleted"
)
// TeamPayload Activity related to an organization's team.
// The type of activity is specified in the action property of the payload object
type TeamPayload struct {
Secret string `json:"secret"`
Action TeamActionType `json:"action"`
Team *Team `json:"team"`
Organization *Organization `json:"organization"`
Sender *User `json:"sender"`
}
// SetSecret modifies the secret of the RepositoryPayload
func (p *TeamPayload) SetSecret(secret string) {
p.Secret = secret
}
// JSONPayload JSON representation of the payload
func (p *TeamPayload) JSONPayload() ([]byte, error) {
return json.MarshalIndent(p, "", " ")
}
// MembershipActionType type of membership
type MembershipActionType string
const (
// MembershipActionTypeAdd add new member
MembershipActionTypeAdd MembershipActionType = "added"
// MembershipActionTypeRemove remove member
MembershipActionTypeRemove MembershipActionType = "removed"
)
// MembershipPayload Activity related to team membership.
// The type of activity is specified in the action property of the payload object
type MembershipPayload struct {
Secret string `json:"secret"`
Action MembershipActionType `json:"action"`
Member *User `json:"member"`
Team *Team `json:"team"`
Organization *Organization `json:"organization"`
Sender *User `json:"sender"`
}
// SetSecret modifies the secret of the RepositoryPayload
func (p *MembershipPayload) SetSecret(secret string) {
p.Secret = secret
}
// JSONPayload JSON representation of the payload
func (p *MembershipPayload) JSONPayload() ([]byte, error) {
return json.MarshalIndent(p, "", " ")
}

@ -1812,6 +1812,13 @@ settings.event_pull_request_review = Pull Request Reviewed
settings.event_pull_request_review_desc = Pull request approved, rejected, or review comment.
settings.event_pull_request_sync = Pull Request Synchronized
settings.event_pull_request_sync_desc = Pull request synchronized.
settings.event_header_org = Organization
settings.event_organization = Organization
settings.event_organization_desc = added or removed member in organization
settings.event_team = team
settings.event_team_desc = added or removed team in organization
settings.event_team_member = team member (membership)
settings.event_team_member_desc = added or removed member in a team
settings.branch_filter = Branch filter
settings.branch_filter_desc = Branch whitelist for push, branch creation and branch deletion events, specified as glob pattern. If empty or <code>*</code>, events for all branches are reported. See <a href="https://pkg.go.dev/github.com/gobwas/glob#Compile">github.com/gobwas/glob</a> documentation for syntax. Examples: <code>master</code>, <code>{master,release*}</code>.
settings.active = Active

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/notification"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user"
@ -187,6 +188,7 @@ func CreateTeam(ctx *context.APIContext) {
return
}
notification.NotifyAddOrgTeam(ctx.User, ctx.Org.Organization, team)
ctx.JSON(http.StatusCreated, convert.ToTeam(team))
}
@ -286,11 +288,19 @@ func DeleteTeam(ctx *context.APIContext) {
// responses:
// "204":
// description: team deleted
if err := models.DeleteTeam(ctx.Org.Team); err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteTeam", err)
return
}
var err error
ctx.Org.Organization, err = models.GetUserByID(ctx.Org.Team.OrgID)
if err != nil {
log.Error("GetUserByID: %v", err)
ctx.Status(http.StatusNoContent)
return
}
notification.NotifyRemoveOrgTeam(ctx.User, ctx.Org.Organization, ctx.Org.Team)
ctx.Status(http.StatusNoContent)
}
@ -412,10 +422,25 @@ func AddTeamMember(ctx *context.APIContext) {
if ctx.Written() {
return
}
var err error
ctx.Org.Organization, err = models.GetUserByID(ctx.Org.Team.OrgID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "AddMember", err)
return
}
isOrgMember, err2 := models.IsOrganizationMember(ctx.Org.Organization.ID, u.ID)
if err2 != nil {
log.Error("IsOrganizationMember : %v", err2)
}
if err := ctx.Org.Team.AddMember(u.ID); err != nil {
ctx.Error(http.StatusInternalServerError, "AddMember", err)
return
}
notification.NotifyAddTeamMember(ctx.User, ctx.Org.Organization, u, ctx.Org.Team)
if !isOrgMember {
notification.NotifyAddOrgMember(ctx.User, ctx.Org.Organization, u)
}
ctx.Status(http.StatusNoContent)
}
@ -453,6 +478,21 @@ func RemoveTeamMember(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "RemoveMember", err)
return
}
var err error
ctx.Org.Organization, err = models.GetUserByID(ctx.Org.Team.OrgID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "RemoveMember", err)
return
}
isOrgMember, err2 := models.IsOrganizationMember(ctx.Org.Organization.ID, u.ID)
if err2 != nil {
log.Error("IsOrganizationMember : %v", err2)
}
notification.NotifyRemoveTeamMember(ctx.User, ctx.Org.Organization, u, ctx.Org.Team)
if !isOrgMember {
notification.NotifyRemoveOrgMember(ctx.User, ctx.Org.Organization, u)
}
ctx.Status(http.StatusNoContent)
}

@ -129,6 +129,9 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, orgID, repoID
PullRequestSync: pullHook(form.Events, string(models.HookEventPullRequestSync)),
Repository: util.IsStringInSlice(string(models.HookEventRepository), form.Events, true),
Release: util.IsStringInSlice(string(models.HookEventRelease), form.Events, true),
Organization: util.IsStringInSlice(string(models.HookEventOrg), form.Events, true),
Team: util.IsStringInSlice(string(models.HookEventTeam), form.Events, true),
TeamMember: util.IsStringInSlice(string(models.HookEventTeamMember), form.Events, true),
},
BranchFilter: form.BranchFilter,
},

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/notification"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/utils"
"code.gitea.io/gitea/services/forms"
@ -63,9 +64,25 @@ func TeamsAction(ctx *context.Context) {
ctx.Error(http.StatusNotFound)
return
}
isOrgMember, err2 := models.IsOrganizationMember(ctx.Org.Organization.ID, ctx.User.ID)
if err2 != nil {
log.Error("IsOrganizationMember: %v", err2)
}
err = ctx.Org.Team.AddMember(ctx.User.ID)
if err == nil {
notification.NotifyAddTeamMember(ctx.User, ctx.Org.Organization, ctx.User, ctx.Org.Team)
if !isOrgMember {
notification.NotifyAddOrgMember(ctx.User, ctx.Org.Organization, ctx.User)
}
}
case "leave":
err = ctx.Org.Team.RemoveMember(ctx.User.ID)
if err == nil {
notification.NotifyRemoveTeamMember(ctx.User, ctx.Org.Organization, ctx.User, ctx.Org.Team)
if isOrgMember, err := models.IsOrganizationMember(ctx.Org.Organization.ID, ctx.User.ID); err == nil && !isOrgMember {
notification.NotifyRemoveOrgMember(ctx.User, ctx.Org.Organization, ctx.User)
}
}
case "remove":
if !ctx.Org.IsOwner {
ctx.Error(http.StatusNotFound)
@ -73,6 +90,15 @@ func TeamsAction(ctx *context.Context) {
}
err = ctx.Org.Team.RemoveMember(uid)
page = "team"
if err == nil {
u, err2 := models.GetUserByID(uid)
if err2 == nil {
notification.NotifyRemoveTeamMember(ctx.User, ctx.Org.Organization, u, ctx.Org.Team)
if isOrgMember, err3 := models.IsOrganizationMember(ctx.Org.Organization.ID, u.ID); err3 == nil && !isOrgMember {
notification.NotifyRemoveOrgMember(ctx.User, ctx.Org.Organization, u)
}
}
}
case "add":
if !ctx.Org.IsOwner {
ctx.Error(http.StatusNotFound)
@ -97,12 +123,24 @@ func TeamsAction(ctx *context.Context) {
return
}
isOrgMember, err2 := models.IsOrganizationMember(ctx.Org.Organization.ID, u.ID)
if err2 != nil {
log.Error("IsOrganizationMember: %v", err2)
}
if ctx.Org.Team.IsMember(u.ID) {
ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users"))
} else {
err = ctx.Org.Team.AddMember(u.ID)
}
if err == nil {
notification.NotifyAddTeamMember(ctx.User, ctx.Org.Organization, u, ctx.Org.Team)
if !isOrgMember {
notification.NotifyAddOrgMember(ctx.User, ctx.Org.Organization, u)
}
}
page = "team"
}
@ -237,6 +275,7 @@ func NewTeamPost(ctx *context.Context) {
}
return
}
notification.NotifyAddOrgTeam(ctx.User, ctx.Org.Organization, t)
log.Trace("Team created: %s/%s", ctx.Org.Organization.Name, t.Name)
ctx.Redirect(ctx.Org.OrgLink + "/teams/" + t.LowerName)
}
@ -351,6 +390,7 @@ func DeleteTeam(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("org.teams.delete_team_success"))
}
notification.NotifyRemoveOrgTeam(ctx.User, ctx.Org.Organization, ctx.Org.Team)
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": ctx.Org.OrgLink + "/teams",
})

@ -136,6 +136,10 @@ func WebhooksNew(ctx *context.Context) {
ctx.Data["PageIsSettingsHooksNew"] = true
}
if orCtx.OrgID > 0 {
ctx.Data["PageIsOrgHooks"] = true
}
hookType := checkHookType(ctx)
ctx.Data["HookType"] = hookType
if ctx.Written() {
@ -177,6 +181,9 @@ func ParseHookEvent(form forms.WebhookForm) *models.HookEvent {
PullRequestReview: form.PullRequestReview,
PullRequestSync: form.PullRequestSync,
Repository: form.Repository,
Organization: form.Organization,
Team: form.Team,
TeamMember: form.TeamMember,
},
BranchFilter: form.BranchFilter,
}
@ -696,6 +703,10 @@ func WebHooksEdit(ctx *context.Context) {
}
ctx.Data["Webhook"] = w
if orCtx.OrgID > 0 {
ctx.Data["PageIsOrgHooks"] = true
}
ctx.HTML(http.StatusOK, orCtx.NewTemplate)
}

@ -235,6 +235,9 @@ type WebhookForm struct {
PullRequestSync bool
Repository bool
Active bool
Organization bool
Team bool
TeamMember bool
BranchFilter string `binding:"GlobPattern"`
}

@ -226,15 +226,26 @@ func DeliverHooks(ctx context.Context) {
log.Trace("DeliverHooks [repo_id: %v]", repoIDStr)
hookQueue.Remove(repoIDStr)
repoID, err := strconv.ParseInt(repoIDStr, 10, 64)
keyID, err := strconv.ParseInt(repoIDStr, 10, 64)
if err != nil {
log.Error("Invalid repo ID: %s", repoIDStr)
continue
}
tasks, err := models.FindRepoUndeliveredHookTasks(repoID)
// keyID > 0 : repo id
// keyID < 0 : org id
// keyID = 0 : system only (not used now)
var tasks []*models.HookTask
if keyID < 0 {
orgID := -keyID
tasks, err = models.FindOrgUndeliveredHookTasks(orgID)
} else {
repoID := keyID
tasks, err = models.FindRepoUndeliveredHookTasks(repoID)
}
if err != nil {
log.Error("Get repository [%d] hook tasks: %v", repoID, err)
log.Error("Get [%d] hook tasks: %v", keyID, err)
continue
}
for _, t := range tasks {

@ -91,12 +91,21 @@ func getPayloadBranch(p api.Payloader) string {
return ""
}
// PrepareWebhook adds special webhook to task queue for given payload.
// PrepareWebhook adds special repository webhook to task queue for given payload.
Review

It probably would be simpler to have:

func PrepareWebhook(w *models.Webhook, repo *models.Repository, event models.HookEventType, p api.Payloader) error {
  return PrepareRepoOrgWebhook(w, repo, nil, event, p)
}

func PrepareOrgWebhook(w *models.Webhook, org *models.User, event models.HookEventType, p api.Payloader) error {
  return PrepareRepoOrgWebhook(w, nil, org, event, p)
}

func PrepareRepoOrgWebhook(w *models.Webhook, repo *models.Repository, org *models.User, event models.HookEventType, p api.Payloader) error {
 ...
}

Then you would need most of the changes in modules/notification/webhook/webhook.go and elsewhere.

It probably would be simpler to have: ```go func PrepareWebhook(w *models.Webhook, repo *models.Repository, event models.HookEventType, p api.Payloader) error { return PrepareRepoOrgWebhook(w, repo, nil, event, p) } func PrepareOrgWebhook(w *models.Webhook, org *models.User, event models.HookEventType, p api.Payloader) error { return PrepareRepoOrgWebhook(w, nil, org, event, p) } func PrepareRepoOrgWebhook(w *models.Webhook, repo *models.Repository, org *models.User, event models.HookEventType, p api.Payloader) error { ... } ``` Then you would need most of the changes in modules/notification/webhook/webhook.go and elsewhere.
func PrepareWebhook(w *models.Webhook, repo *models.Repository, event models.HookEventType, p api.Payloader) error {
if err := prepareWebhook(w, repo, event, p); err != nil {
return prepareRepoOrgWebhook(w, repo, nil, event, p)
}
func prepareRepoOrgWebhook(w *models.Webhook, repo *models.Repository, org *models.User, event models.HookEventType, p api.Payloader) error {
if err := prepareWebhook(w, repo, org, event, p); err != nil {
return err
}
if org != nil {
go hookQueue.Add(-org.ID)
return nil
}
go hookQueue.Add(repo.ID)
return nil
}
@ -116,7 +125,7 @@ func checkBranch(w *models.Webhook, branch string) bool {
return g.Match(branch)
}
func prepareWebhook(w *models.Webhook, repo *models.Repository, event models.HookEventType, p api.Payloader) error {
func prepareWebhook(w *models.Webhook, repo *models.Repository, org *models.User, event models.HookEventType, p api.Payloader) error {
// Skip sending if webhooks are disabled.
if setting.DisableWebhooks {
return nil
@ -161,56 +170,90 @@ func prepareWebhook(w *models.Webhook, repo *models.Repository, event models.Hoo
payloader = p
}
if err = models.CreateHookTask(&models.HookTask{
RepoID: repo.ID,
task := &models.HookTask{
HookID: w.ID,
Payloader: payloader,
EventType: event,
}); err != nil {
}
if repo != nil {
task.RepoID = repo.ID
}
if org != nil {
task.OrgID = org.ID
}
if err = models.CreateHookTask(task); err != nil {
return fmt.Errorf("CreateHookTask: %v", err)
}
return nil
}
// PrepareWebhooks adds new webhooks to task queue for given payload.
// PrepareWebhooks adds new repository webhooks to task queue for given payload.
func PrepareWebhooks(repo *models.Repository, event models.HookEventType, p api.Payloader) error {
if err := prepareWebhooks(repo, event, p); err != nil {
return prepareRepoOrgWebhooks(repo, nil, event, p)
}
// PrepareOrgWebhooks adds new orgnization webhooks to task queue for given payload.
func PrepareOrgWebhooks(org *models.User, event models.HookEventType, p api.Payloader) error {
return prepareRepoOrgWebhooks(nil, org, event, p)
}
func prepareRepoOrgWebhooks(repo *models.Repository, org *models.User, event models.HookEventType, p api.Payloader) error {
if err := prepareWebhooks(repo, org, event, p); err != nil {
return err
}
if org != nil {
go hookQueue.Add(-org.ID)
return nil
}
go hookQueue.Add(repo.ID)
return nil
}
func prepareWebhooks(repo *models.Repository, event models.HookEventType, p api.Payloader) error {
ws, err := models.GetActiveWebhooksByRepoID(repo.ID)
func prepareWebhooks(repo *models.Repository, org *models.User, event models.HookEventType, p api.Payloader) error {
level := event.EventLevel()
// 1. Add any admin-defined system webhooks
ws, err := models.GetSystemWebhooks()
if err != nil {
return fmt.Errorf("GetActiveWebhooksByRepoID: %v", err)
return fmt.Errorf("GetSystemWebhooks: %v", err)
}
// check if repo belongs to org and append additional webhooks
if repo.MustOwner().IsOrganization() {
// 2. check if repo belongs to org and append additional webhooks
if org != nil || repo.MustOwner().IsOrganization() {
// get hooks for org
orgHooks, err := models.GetActiveWebhooksByOrgID(repo.OwnerID)
orgHooks, err := models.GetActiveWebhooksByOrgID(func() int64 {
if org != nil {
return org.ID
}
return repo.OwnerID
}())
if err != nil {
return fmt.Errorf("GetActiveWebhooksByOrgID: %v", err)
}
ws = append(ws, orgHooks...)
}
// Add any admin-defined system webhooks
systemHooks, err := models.GetSystemWebhooks()
if err != nil {
return fmt.Errorf("GetSystemWebhooks: %v", err)
// 3. add repo hooks if needed
if level == models.HookEventLevelRepo && repo != nil {
repoHooks, err := models.GetActiveWebhooksByRepoID(repo.ID)
if err != nil {
return fmt.Errorf("GetActiveWebhooksByRepoID: %v", err)
}
ws = append(ws, repoHooks...)
}
ws = append(ws, systemHooks...)
if len(ws) == 0 {
return nil
}
for _, w := range ws {
if err = prepareWebhook(w, repo, event, p); err != nil {
if err = prepareWebhook(w, repo, org, event, p); err != nil {
return err
}
}

@ -217,6 +217,42 @@
</div>
</div>
</div>
{{if or .PageIsOrgHooks .PageIsAdminSystemHooks}}
<!-- org hooks -->
<div class="fourteen wide column">
<label>{{.i18n.Tr "repo.settings.event_header_org"}}</label>
</div>
<!-- organization hook -->
<div class="seven wide column">
<div class="field">
<div class="ui checkbox">
<input class="hidden" name="organization" type="checkbox" tabindex="0" {{if .Webhook.Organization}}checked{{end}}>
<label>{{.i18n.Tr "repo.settings.event_organization"}}</label>
<span class="help">{{.i18n.Tr "repo.settings.event_organization_desc"}}</span>
</div>
</div>
</div>
<!-- team hook -->
<div class="seven wide column">
<div class="field">
<div class="ui checkbox">
<input class="hidden" name="team" type="checkbox" tabindex="0" {{if .Webhook.Team}}checked{{end}}>
<label>{{.i18n.Tr "repo.settings.event_team"}}</label>
<span class="help">{{.i18n.Tr "repo.settings.event_team_desc"}}</span>
</div>
</div>
</div>
<!-- team member -->
<div class="seven wide column">
<div class="field">
<div class="ui checkbox">
<input class="hidden" name="team_member" type="checkbox" tabindex="0" {{if .Webhook.TeamMember}}checked{{end}}>
<label>{{.i18n.Tr "repo.settings.event_team_member"}}</label>
<span class="help">{{.i18n.Tr "repo.settings.event_team_member_desc"}}</span>
</div>
</div>
</div>
{{end}}
</div>
</div>