Beispiel #1
0
func webhookHandler(c *integram.Context, wc *integram.WebhookContext) (err error) {

	wh := webhook{Mrkdwn: true}
	err = wc.JSON(&wh)

	if err != nil {
		return
	}

	if len(wh.Attachments) > 0 {
		if wh.Text != "" {
			wh.Text += "\n"
		}
		wp := c.WebPreview(wh.Attachments[0].Title, wh.Attachments[0].AuthorName, wh.Attachments[0].Pretext, wh.Attachments[0].TitleLink, wh.Attachments[0].ThumbURL)
		text := m.URL(" ", wp) + " " + wh.Text
		for i, attachment := range wh.Attachments {
			if i > 0 {
				text += "\n"
			}
			text += m.URL(attachment.Title, attachment.TitleLink) + " " + attachment.Pretext
		}
		return c.NewMessage().SetText(text).EnableAntiFlood().EnableHTML().Send()
	}

	if wh.Text != "" {
		m := c.NewMessage().SetText(wh.Text + " " + wh.Channel).EnableAntiFlood()
		if wh.Mrkdwn {
			m.EnableMarkdown()
		}
		return m.Send()
	}

	return errors.New("Text and Attachments not found")
}
Beispiel #2
0
func webhookHandler(c *integram.Context, wc *integram.WebhookContext) (err error) {
	eventKey := wc.Header("X-Event-Key")

	if eventKey == "" {
		// try the old one Bitbucket POST service

		return oldWebhookHandler(c, wc)
	}

	if _, ok := eventTypeMap[eventKey]; !ok {

		return errors.New("Bad X-Event-Key: " + eventKey)
	}

	c.Log().Debugf("eventKey=%v", eventKey)

	if err != nil {
		return errors.New("JSON deserialization error: " + err.Error())
	}

	switch eventKey {
	case "repo:push":
		event := api.RepoPushEvent{}
		wc.JSON(&event)

		for _, change := range event.Push.Changes {

			msg := c.NewMessage()
			commits := 0
			text := ""

			if len(change.Commits) > 1 {
				anyOherPersonCommits := false
				for _, commit := range change.Commits {
					if commit.Author.User.UUID != event.Actor.UUID {
						anyOherPersonCommits = true
						break
					}
				}
				for _, commit := range change.Commits {
					commits++
					if anyOherPersonCommits {
						text += mention(c, &commit.Author.User) + ": "
					}
					text += m.URL(commit.Message, commit.Links.HTML.Href) + "\n"
				}
				if change.Truncated {
					text += m.URL("... See all", change.Links.Commits.Href) + "\n"
				}
			} else if len(change.Commits) == 1 {
				commits++
				commit := &change.Commits[0]
				if commit.Author.User.UUID != event.Actor.UUID {
					text += mention(c, &commit.Author.User) + ": "
				}

				text += commit.Message + "\n"
			}

			change.New.Target.Author.User.Links.Avatar.Href = strings.Replace(change.New.Target.Author.User.Links.Avatar.Href, "/32/", "/128/", 1)
			wp := ""
			if change.Truncated {
				wp = c.WebPreview("> 5 commits", "@"+commitShort(change.Old.Target.Hash)+" ... @"+commitShort(change.New.Target.Hash), "", change.Links.HTML.Href, change.New.Target.Author.User.Links.Avatar.Href)
			} else if commits > 1 {
				wp = c.WebPreview(fmt.Sprintf("%d commits", commits), "@"+commitShort(change.Old.Target.Hash)+" ... @"+commitShort(change.New.Target.Hash), "", change.Links.HTML.Href, change.New.Target.Author.User.Links.Avatar.Href)
			} else if commits == 1 {
				wp = c.WebPreview("Commit", "@"+commitShort(change.New.Target.Hash), "", change.Commits[0].Links.HTML.Href, change.New.Target.Author.User.Links.Avatar.Href)
			}

			if commits > 0 {
				pushedText := ""
				if change.Forced {
					pushedText = m.URL("❗️ forcibly pushed", wp)
				} else {
					pushedText = m.URL("pushed", wp)
				}
				err = msg.SetTextFmt("%s %s to %s/%s\n%s",
					mention(c, &change.New.Target.Author.User),
					pushedText,
					m.URL(event.Repository.Name, event.Repository.Links.HTML.Href),
					m.URL(change.New.Name, change.New.Links.HTML.Href),
					text).
					AddEventID(commitUniqueID(change.Commits[0].Hash)).
					EnableHTML().
					Send()
			}
		}
	case "issue:created":
		event := api.IssueCreatedEvent{}
		err := wc.JSON(&event)
		if err != nil {
			return err
		}
		event.Issue.Repository = &event.Repository

		c.SetServiceCache(issueUniqueID(event.Repository.FullName, event.Issue.ID), event.Issue, time.Hour*24*365)

		return c.NewMessage().AddEventID(issueUniqueID(event.Repository.FullName, event.Issue.ID)).
			SetInlineKeyboard(issueInlineKeyboard(&event.Issue)).
			SetText(issueText(c, &event.Issue)).
			SetCallbackAction(issueInlineButtonPressed, event.Repository.FullName, event.Issue.ID).
			EnableHTML().Send()
	case "issue:comment_created":
		event := api.IssueCommentCreatedEvent{}
		err := wc.JSON(&event)
		if err != nil {
			return err
		}

		var rm *integram.Message
		if event.Comment.Parent.ID > 0 {
			// actually bitbucket doesn't provide parent id for issue comments for now
			rm, _ = c.FindMessageByEventID(issueCommentUniqueID(event.Repository.FullName, event.Issue.ID, event.Comment.Parent.ID))
		}
		if rm == nil {
			rm, _ = c.FindMessageByEventID(issueUniqueID(event.Repository.FullName, event.Issue.ID))
		}

		msg := c.NewMessage().AddEventID(issueCommentUniqueID(event.Repository.FullName, event.Issue.ID, event.Comment.ID)).EnableHTML()

		if rm != nil {
			return msg.SetReplyToMsgID(rm.MsgID).SetText(fmt.Sprintf("%s: %s", mention(c, &event.Actor), event.Comment.Content.Raw)).Send()
		}
		wp := c.WebPreview("Issue", event.Issue.Title, event.Repository.FullName, event.Comment.Links.HTML.Href, "")
		return msg.SetText(fmt.Sprintf("%s %s: %s", m.URL("💬", wp), mention(c, &event.Actor), event.Comment.Content.Raw)).Send()

	case "repo:commit_comment_created":
		event := api.RepoCommitCommentCreatedEvent{}
		err := wc.JSON(&event)
		if err != nil {
			return err
		}

		var rm *integram.Message
		if event.Comment.Parent.ID > 0 {
			// actually bitbucket doesn't provide parent id for issue comments for now
			rm, _ = c.FindMessageByEventID(commitCommentUniqueID(event.Commit.Hash, event.Comment.Parent.ID))
		}
		if rm == nil {
			rm, _ = c.FindMessageByEventID(commitUniqueID(event.Commit.Hash))
		}

		msg := c.NewMessage().AddEventID(commitCommentUniqueID(event.Commit.Hash, event.Comment.Parent.ID)).EnableHTML()

		if rm != nil {
			return msg.SetReplyToMsgID(rm.MsgID).SetText(fmt.Sprintf("%s: %s", mention(c, &event.Actor), event.Comment.Content.Raw)).Send()
		}
		wp := c.WebPreview("Commit", "@"+event.Commit.Hash[0:10], event.Repository.FullName, event.Comment.Links.HTML.Href, "")
		return msg.SetText(fmt.Sprintf("%s %s: %s", m.URL("💬", wp), mention(c, &event.Actor), event.Comment.Content.Raw)).Send()

	case "issue:updated":
		event := api.IssueUpdatedEvent{}
		err := wc.JSON(&event)
		if err != nil {
			return err
		}
		eventID := issueUniqueID(event.Repository.FullName, event.Issue.ID)
		rm, _ := c.FindMessageByEventID(eventID)

		msg := c.NewMessage().AddEventID(issueCommentUniqueID(event.Repository.FullName, event.Issue.ID, event.Comment.ID)).EnableHTML()

		if event.Comment.Content.Raw != "" {
			event.Comment.Content.Raw = ": " + event.Comment.Content.Raw
		}

		if rm != nil {
			c.EditMessagesTextWithEventID(eventID, issueText(c, &event.Issue))
			// if last Issue message just posted
			if err == nil && time.Now().Sub(rm.Date).Seconds() < 60 {
				return nil
			}

			return msg.SetReplyToMsgID(rm.MsgID).SetText(fmt.Sprintf("%s update the issue%s", mention(c, &event.Actor), event.Comment.Content.Raw)).Send()
		}
		return msg.SetText(fmt.Sprintf("%s updated an issue%s\n%s", mention(c, &event.Actor), event.Comment.Content.Raw, issueText(c, &event.Issue))).Send()

	case "pullrequest:updated":
		event := api.PullRequestCreatedEvent{}
		err := wc.JSON(&event)
		if err != nil {
			return err
		}

		prText := prText(c, &event.PullRequest)

		eventID := prUniqueID(event.Repository.FullName, event.PullRequest.ID)
		rm, _ := c.FindMessageByEventID(eventID)

		msg := c.NewMessage()

		if rm != nil {
			c.EditMessagesTextWithEventID(eventID, prText)
			// if last PR message just posted
			if err == nil && time.Now().Sub(rm.Date).Seconds() < 60 {
				return nil
			}

			msg.SetReplyToMsgID(rm.MsgID)
		} else {
			msg.AddEventID(prUniqueID(event.Repository.FullName, event.PullRequest.ID))
		}

		return msg.
			SetText("✏️ " + prText).
			EnableHTML().Send()
	case "pullrequest:created":
		event := api.PullRequestCreatedEvent{}
		err := wc.JSON(&event)
		if err != nil {
			return err
		}

		c.SetServiceCache(prUniqueID(event.Repository.FullName, event.PullRequest.ID), event.PullRequest, time.Hour*24*365)

		return c.NewMessage().AddEventID(prUniqueID(event.Repository.FullName, event.PullRequest.ID)).
			SetText(prText(c, &event.PullRequest)).
			EnableHTML().Send()

	case "pullrequest:approved":
		event := api.PullRequestApprovedEvent{}
		err := wc.JSON(&event)
		if err != nil {
			return err
		}

		rm, _ := c.FindMessageByEventID(prUniqueID(event.Repository.FullName, event.PullRequest.ID))
		msg := c.NewMessage().EnableHTML()

		if rm != nil {
			return msg.SetReplyToMsgID(rm.MsgID).SetText(fmt.Sprintf("✅ Approved by %s", mention(c, &event.Actor))).Send()
		}
		wp := c.WebPreview("Pull Request", event.PullRequest.Title, "by "+event.PullRequest.Author.DisplayName+" in "+event.Repository.FullName, event.PullRequest.Links.HTML.Href, "")
		return msg.SetText(fmt.Sprintf("✅ %s by %s", m.URL("Approved", wp), mention(c, &event.Actor))).Send()

	case "pullrequest:unapproved":
		event := api.PullRequestApprovalRemovedEvent{}
		err := wc.JSON(&event)
		if err != nil {
			return err
		}

		rm, _ := c.FindMessageByEventID(prUniqueID(event.Repository.FullName, event.PullRequest.ID))
		msg := c.NewMessage().EnableHTML()

		if rm != nil {
			return msg.SetReplyToMsgID(rm.MsgID).SetText(fmt.Sprintf("❌ %s removed approval", mention(c, &event.Actor))).Send()
		}
		wp := c.WebPreview("Pull Request", event.PullRequest.Title, "by "+event.PullRequest.Author.DisplayName+" in "+event.Repository.FullName, event.PullRequest.Links.HTML.Href, "")
		return msg.SetText(fmt.Sprintf("❌ %s %s", mention(c, &event.Actor), m.URL("removed approval", wp))).Send()

	case "pullrequest:fulfilled":
		event := api.PullRequestMergedEvent{}
		err := wc.JSON(&event)
		if err != nil {
			return err
		}

		rm, _ := c.FindMessageByEventID(prUniqueID(event.Repository.FullName, event.PullRequest.ID))
		msg := c.NewMessage().EnableHTML()

		if rm != nil {
			return msg.SetReplyToMsgID(rm.MsgID).SetText(fmt.Sprintf("✅ Merged by %s", mention(c, &event.Actor))).Send()
		}
		wp := c.WebPreview("Pull Request", event.PullRequest.Title, "by "+event.PullRequest.Author.DisplayName+" in "+event.Repository.FullName, event.PullRequest.Links.HTML.Href, "")
		return msg.SetText(fmt.Sprintf("✅ %s by %s", m.URL("Merged", wp), mention(c, &event.Actor))).Send()

	case "pullrequest:rejected":
		event := api.PullRequestDeclinedEvent{}
		err := wc.JSON(&event)
		if err != nil {
			return err
		}

		rm, _ := c.FindMessageByEventID(prUniqueID(event.Repository.FullName, event.PullRequest.ID))
		msg := c.NewMessage().EnableHTML()

		if rm != nil {
			return msg.SetReplyToMsgID(rm.MsgID).SetText(fmt.Sprintf("❌ Declined by %s: %s", mention(c, &event.Actor), event.PullRequest.Reason)).Send()
		}
		wp := c.WebPreview("Pull Request", event.PullRequest.Title, "by "+event.PullRequest.Author.DisplayName+" in "+event.Repository.FullName, event.PullRequest.Links.HTML.Href, "")
		return msg.SetText(fmt.Sprintf("❌ %s by %s", m.URL("Declined", wp), mention(c, &event.Actor))).Send()

	case "pullrequest:comment_created":
		event := api.PullRequestCommentCreatedEvent{}
		err := wc.JSON(&event)
		if err != nil {
			return err
		}

		rm, _ := c.FindMessageByEventID(prUniqueID(event.Repository.FullName, event.PullRequest.ID))

		msg := c.NewMessage().AddEventID(prCommentUniqueID(event.Repository.FullName, event.PullRequest.ID, event.Comment.ID)).EnableHTML()

		if rm != nil {
			return msg.SetReplyToMsgID(rm.MsgID).SetText(fmt.Sprintf("%s: %s", mention(c, &event.Actor), event.Comment.Content.Raw)).Send()
		}
		wp := c.WebPreview("Pull Request", event.PullRequest.Title, event.Repository.FullName, event.PullRequest.Links.HTML.Href, "")
		return msg.SetText(fmt.Sprintf("%s %s: %s", m.URL("💬", wp), mention(c, &event.Actor), event.Comment.Content.Raw)).Send()

	}
	return err

}
Beispiel #3
0
func oldWebhookHandler(c *integram.Context, wc *integram.WebhookContext) (err error) {

	wh := oldWebhook{}

	payload := wc.FormValue("payload")

	if payload == "" {
		return errors.New("X-Event-Key header missed and old-style webhook data not found")
	}

	json.Unmarshal([]byte(payload), &wh)

	if wh.CanonURL == "" {
		return errors.New("Error decoding payload for old webhook")
	}

	msg := c.NewMessage()
	commits := 0
	text := ""
	wp := ""

	if len(wh.Commits) == 0 {
		return nil
	}

	headCommit := wh.Commits[len(wh.Commits)-1]

	if len(wh.Commits) > 1 {

		wp = c.WebPreview(fmt.Sprintf("%d commits", len(wh.Commits)), "@"+wh.Commits[0].Node[0:10]+" ... @"+headCommit.Node[0:10], "", wh.CanonURL+wh.Repository.AbsoluteURL+"/compare/"+headCommit.Node+".."+wh.Commits[0].Parents[0], "")

		anyOherPersonCommits := false
		for _, commit := range wh.Commits {
			if commit.Author != wh.User {
				anyOherPersonCommits = true
				break
			}
		}
		for _, commit := range wh.Commits {
			commits++
			if anyOherPersonCommits {
				text += m.Bold(commit.Author) + ": "
			}
			text += m.URL(commit.Message, wh.CanonURL+wh.Repository.AbsoluteURL+"commits/"+headCommit.Node[0:10]) + "\n"
		}

	} else if len(wh.Commits) == 1 {
		wp = c.WebPreview("Commit", "@"+headCommit.Node[0:10], "", wh.CanonURL+wh.Repository.AbsoluteURL+"commits/"+headCommit.Node[0:10], "")

		commit := &wh.Commits[0]
		if commit.Author != wh.User {
			text += m.Bold(commit.Author) + ": "
		}

		text += commit.Message + "\n"
	}

	if len(wh.Commits) > 0 {

		return msg.SetTextFmt("%s %s to %s/%s\n%s",
			m.Bold(wh.User),
			m.URL("pushed", wp),
			m.URL(wh.Repository.Name, wh.CanonURL+wh.Repository.AbsoluteURL),
			m.URL(headCommit.Branch, wh.CanonURL+wh.Repository.AbsoluteURL+"branch/"+headCommit.Branch),
			text).
			EnableHTML().
			Send()
	}
	return nil

}
Beispiel #4
0
func webhookHandler(c *integram.Context, request *integram.WebhookContext) (err error) {
	wh := &webhook{}

	err = request.JSON(wh)

	if err != nil {
		return
	}

	msg := c.NewMessage()

	if wh.Repository.Homepage != "" {
		c.SetServiceBaseURL(wh.Repository.Homepage)
	} else if wh.ObjectAttributes != nil {
		if wh.ObjectAttributes.URL == "" {
			c.Log().WithField("wh", wh).Error("gitlab webhook empty url")
		}
		c.SetServiceBaseURL(wh.ObjectAttributes.URL)
	}

	switch wh.ObjectKind {
	case "push":
		s := strings.Split(wh.Ref, "/")
		branch := s[len(s)-1]
		text := ""

		added := 0
		removed := 0
		modified := 0
		anyOherPersonCommits := false
		for _, commit := range wh.Commits {
			if commit.Author.Email != wh.UserEmail && commit.Author.Name != wh.UserName {
				anyOherPersonCommits = true
			}
		}
		for _, commit := range wh.Commits {

			commit.Message = strings.TrimSuffix(commit.Message, "\n")
			if anyOherPersonCommits {
				text += mention(c, commit.Author.Name, commit.Author.Email) + ": "
			}
			text += m.URL(commit.Message, commit.URL) + "\n"
			added += len(commit.Added)
			removed += len(commit.Removed)
			modified += len(commit.Modified)
		}
		f := ""
		if modified > 0 {
			f += strconv.Itoa(modified) + " files modified"
		}

		if added > 0 {
			if f == "" {
				f += strconv.Itoa(added) + " files added"
			} else {
				f += " " + strconv.Itoa(added) + " added"
			}
		}

		if removed > 0 {
			if f == "" {
				f += strconv.Itoa(removed) + " files removed"
			} else {
				f += " " + strconv.Itoa(removed) + " removed"
			}
		}
		wp := ""
		if len(wh.Commits) > 1 {
			wp = c.WebPreview(fmt.Sprintf("%d commits", len(wh.Commits)), "@"+wh.Before[0:10]+" ... @"+wh.After[0:10], f, compareURL(wh.Repository.Homepage, wh.Before, wh.After), "")
		} else if len(wh.Commits) == 1 {
			wp = c.WebPreview("Commit", "@"+wh.After[0:10], f, wh.Commits[0].URL, "")
		}

		var err error

		if len(wh.Commits) > 0 {
			if len(wh.Commits) == 1 {
				msg.SetReplyAction(commitReplied, c.ServiceBaseURL.String(), wh.ProjectID, wh.Commits[0].ID)
			} else {
				msg.SetReplyAction(commitsReplied, c.ServiceBaseURL.String(), wh.ProjectID, wh.Commits)
			}
			text := fmt.Sprintf("%s %s to %s\n%s", mention(c, wh.UserName, wh.UserEmail), m.URL("pushed", wp), m.URL(wh.Repository.Name+"/"+branch, wh.Repository.Homepage+"/tree/"+url.QueryEscape(branch)), text)
			c.Chat.SetCache("commit_"+wh.Commits[len(wh.Commits)-1].ID, text, time.Hour*24*30)

			err = msg.AddEventID("commit_" + wh.Commits[len(wh.Commits)-1].ID).SetText(text).
				EnableHTML().
				Send()

		} else {
			if wh.After != "0000000000000000000000000000000000000000" && wh.After != "" {
				err = msg.SetText(fmt.Sprintf("%s created branch %s\n%s", mention(c, wh.UserName, wh.UserEmail), m.URL(wh.Repository.Name+"/"+branch, wh.Repository.Homepage+"/tree/"+url.QueryEscape(branch)), text)).
					EnableHTML().
					Send()
			} else {
				err = msg.SetText(fmt.Sprintf("%s deleted branch %s\n%s", mention(c, wh.UserName, wh.UserEmail), m.Bold(wh.Repository.Name+"/"+branch), text)).
					EnableHTML().
					Send()
			}
		}

		return err
	case "tag_push":
		s := strings.Split(wh.Ref, "/")
		itemType := s[len(s)-2]
		if itemType == "tags" {
			itemType = "tag"
		} else if itemType == "heads" {
			itemType = "branch"
		}

		return msg.SetText(fmt.Sprintf("%s pushed new %s at %s", mention(c, wh.UserName, wh.UserEmail), m.URL(itemType+" "+s[len(s)-1], wh.Repository.Homepage+"/tree/"+s[len(s)-1]), m.URL(wh.UserName+" / "+wh.Repository.Name, wh.Repository.Homepage))).
			EnableHTML().DisableWebPreview().Send()
	case "issue":
		if wh.ObjectAttributes.MilestoneID > 0 {
			// Todo: need an API access to fetch milestones
		}

		msg.SetReplyAction(issueReplied, c.ServiceBaseURL.String(), wh.ObjectAttributes.ProjectID, wh.ObjectAttributes.ID)

		if wh.ObjectAttributes.Action == "open" {
			return msg.AddEventID("issue_" + strconv.Itoa(wh.ObjectAttributes.ID)).SetText(fmt.Sprintf("%s %s %s at %s:\n%s\n%s", mention(c, wh.User.Username, wh.UserEmail), wh.ObjectAttributes.State, m.URL("issue", wh.ObjectAttributes.URL), m.URL(wh.User.Username+" / "+wh.Repository.Name, wh.Repository.Homepage), m.Bold(wh.ObjectAttributes.Title), wh.ObjectAttributes.Description)).
				EnableHTML().DisableWebPreview().Send()
		}
		action := "updated"
		if wh.ObjectAttributes.Action == "reopen" {
			action = "reopened"
		} else if wh.ObjectAttributes.Action == "close" {
			action = "closed"
		}

		id := issueMessageID(c, wh.ObjectAttributes.ID)

		if id > 0 {
			// reply to existing message
			return msg.SetText(fmt.Sprintf("%s by %s", m.Bold(action), mention(c, wh.User.Username, ""))).
				EnableHTML().DisableWebPreview().SetReplyToMsgID(id).Send()
		}
		// original message not found. Send WebPreview
		wp := c.WebPreview("Issue", wh.ObjectAttributes.Title, wh.User.Username+" / "+wh.Repository.Name, wh.ObjectAttributes.URL, "")

		return msg.SetText(fmt.Sprintf("%s by %s", m.URL(action, wp), mention(c, wh.User.Username, ""))).EnableHTML().Send()

	case "note":
		wp := ""
		noteType := ""
		originMsg := &integram.Message{}
		noteID := strconv.Itoa(wh.ObjectAttributes.ID)
		if wh.ObjectAttributes.Note == "Commit" {
			// collisions by date are unlikely here
			noteID = wh.ObjectAttributes.CreatedAt
		}
		if msg, _ := c.FindMessageByEventID(noteUniqueID(wh.ObjectAttributes.ProjectID, noteID)); msg != nil {
			return nil
		}

		switch wh.ObjectAttributes.NoteableType {
		case "Commit":
			noteType = "commit"
			originMsg, _ = c.FindMessageByEventID(fmt.Sprintf("commit_%s", wh.ObjectAttributes.CommitID))
			if originMsg != nil {
				break
			}
			wp = c.WebPreview("Commit", "@"+wh.ObjectAttributes.CommitID[0:10], wh.User.Username+" / "+wh.Repository.Name, wh.ObjectAttributes.URL, "")
		case "MergeRequest":
			noteType = "merge request"
			originMsg, _ = c.FindMessageByEventID(fmt.Sprintf("mr_%d", wh.MergeRequest.ID))
			if originMsg != nil {
				break
			}
			wp = c.WebPreview("Merge Request", wh.MergeRequest.Title, wh.User.Username+" / "+wh.Repository.Name, wh.ObjectAttributes.URL, "")
		case "Issue":
			noteType = "issue"
			originMsg, _ = c.FindMessageByEventID(fmt.Sprintf("issue_%d", wh.Issue.ID))
			if originMsg != nil {
				break
			}
			wp = c.WebPreview("Issue", wh.Issue.Title, wh.User.Username+" / "+wh.Repository.Name, wh.ObjectAttributes.URL, "")
		case "Snippet":
			noteType = "snippet"
			originMsg, _ = c.FindMessageByEventID(fmt.Sprintf("snippet_%d", wh.Snippet.ID))
			if originMsg != nil {
				break
			}
			wp = c.WebPreview("Snippet", wh.Snippet.Title, wh.User.Username+" / "+wh.Repository.Name, wh.ObjectAttributes.URL, "")
		}

		if originMsg == nil {
			if wp == "" {
				wp = wh.ObjectAttributes.URL
			}

			if noteType == "" {
				noteType = strings.ToLower(wh.ObjectAttributes.NoteableType)
			}

			return msg.SetTextFmt("%s commented on %s: %s", mention(c, wh.User.Username, ""), m.URL(noteType, wp), wh.ObjectAttributes.Note).
				EnableHTML().
				Send()
		}
		return msg.SetText(fmt.Sprintf("%s: %s", mention(c, wh.User.Username, ""), wh.ObjectAttributes.Note)).
			DisableWebPreview().EnableHTML().SetReplyToMsgID(originMsg.MsgID).Send()
	case "merge_request":

		cs := chatSettings(c)
		if !cs.MR.Open && (wh.ObjectAttributes.Action == "open" || wh.ObjectAttributes.Action == "reopen") {
			return nil
		}

		if !cs.MR.Close && (wh.ObjectAttributes.Action == "close") {
			return nil
		}

		if !cs.MR.Update && (wh.ObjectAttributes.Action == "update") {
			return nil
		}

		if !cs.MR.Merge && (wh.ObjectAttributes.Action == "merge") {
			return nil
		}

		if wh.ObjectAttributes.Action == "open" {
			if wh.ObjectAttributes.Description != "" {
				wh.ObjectAttributes.Description = "\n" + wh.ObjectAttributes.Description
			}

			err := msg.AddEventID("mr_" + strconv.Itoa(wh.ObjectAttributes.ID)).SetText(fmt.Sprintf("%s %s %s at %s:\n%s%s", mention(c, wh.User.Username, wh.UserEmail), wh.ObjectAttributes.State, m.URL("merge request", wh.ObjectAttributes.URL), m.URL(wh.UserName+" / "+wh.Repository.Name, wh.Repository.Homepage), m.Bold(wh.ObjectAttributes.Title), wh.ObjectAttributes.Description)).
				EnableHTML().DisableWebPreview().Send()

			return err
		}
		originMsg, _ := c.FindMessageByEventID(fmt.Sprintf("mr_%d", wh.ObjectAttributes.ID))

		if originMsg != nil {
			return msg.SetText(fmt.Sprintf("%s %s by %s", m.URL("merge request", wh.ObjectAttributes.URL), wh.ObjectAttributes.State, mention(c, wh.User.Username, wh.UserEmail))).
				EnableHTML().SetReplyToMsgID(originMsg.MsgID).DisableWebPreview().Send()
		}
		wp := c.WebPreview("Merge Request", wh.ObjectAttributes.Title, wh.ObjectAttributes.Description, wh.ObjectAttributes.URL, "")

		return msg.SetText(fmt.Sprintf("%s %s by %s", m.URL("Merge request", wp), wh.ObjectAttributes.State, mention(c, wh.User.Username, wh.UserEmail))).
			EnableHTML().Send()
	case "build":
		time.Sleep(time.Second)
		// workaround for simultaneously push/build webhooks
		// todo: replace with job?

		commitMsg, _ := c.FindMessageByEventID(fmt.Sprintf("commit_%s", wh.SHA))

		text := ""
		commit := ""

		build := m.URL(strings.ToUpper(wh.BuildStage[0:1])+wh.BuildStage[1:], fmt.Sprintf("%s/builds/%d", wh.Repository.Homepage, wh.BuildID))

		if commitMsg == nil {
			hpURL := strings.Split(wh.Repository.Homepage, "/")
			username := hpURL[len(hpURL)-2]
			commit = m.URL("Commit", c.WebPreview("Commit", "@"+wh.SHA[0:10], username+" / "+wh.Repository.Name, wh.Repository.URL+"/commit/"+wh.SHA, "")) + " "
			build = m.URL(wh.BuildStage, fmt.Sprintf("%s/builds/%d", wh.Repository.Homepage, wh.BuildID))
		} else {
			msg.SetReplyToMsgID(commitMsg.MsgID).DisableWebPreview()
		}

		if strings.ToLower(wh.BuildStage) != strings.ToLower(wh.BuildName) {
			build += " #" + wh.BuildName
		}

		if wh.BuildStatus == "pending" {
			text = "⏳ CI: " + commit + build + " is pending"
		} else if wh.BuildStatus == "running" {
			text = "⚙ CI: " + commit + build + " is running"
		} else if wh.BuildStatus == "success" {
			text = fmt.Sprintf("✅ CI: "+commit+build+" succeeded after %.1f sec", wh.BuildDuration)
		} else if wh.BuildStatus == "failed" {
			text = fmt.Sprintf("‼️ CI: "+commit+build+" failed after %.1f sec", wh.BuildDuration)
		} else if wh.BuildStatus == "canceled" {
			text = fmt.Sprintf("🔚 CI: "+commit+build+" canceled by %s after %.1f sec", mention(c, wh.User.Name, ""), wh.BuildDuration)
		}
		if commitMsg != nil {
			var commitMsgText string
			c.Chat.Cache("commit_"+wh.Commit.SHA, &commitMsgText)

			if commitMsgText != "" {
				_, err = c.EditMessagesTextWithEventID(commitMsg.EventID[0], commitMsgText+"\n"+text)
			}
		}

		cs := chatSettings(c)
		if cs.CI.Success && (wh.BuildStatus == "success") || cs.CI.Cancel && (wh.BuildStatus == "canceled") || cs.CI.Fail && (wh.BuildStatus == "failed") {
			return msg.SetText(text).
				EnableHTML().Send()
		}
	}
	return
}