webhook_discord.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. package models
  2. import (
  3. "fmt"
  4. "gitote/gitote/pkg/setting"
  5. "strconv"
  6. "strings"
  7. "github.com/json-iterator/go"
  8. "gitlab.com/gitote/git-module"
  9. api "gitlab.com/gitote/go-gitote-client"
  10. )
  11. type DiscordEmbedFooterObject struct {
  12. Text string `json:"text"`
  13. }
  14. type DiscordEmbedAuthorObject struct {
  15. Name string `json:"name"`
  16. URL string `json:"url"`
  17. IconURL string `json:"icon_url"`
  18. }
  19. type DiscordEmbedFieldObject struct {
  20. Name string `json:"name"`
  21. Value string `json:"value"`
  22. }
  23. type DiscordEmbedObject struct {
  24. Title string `json:"title"`
  25. Description string `json:"description"`
  26. URL string `json:"url"`
  27. Color int `json:"color"`
  28. Footer *DiscordEmbedFooterObject `json:"footer"`
  29. Author *DiscordEmbedAuthorObject `json:"author"`
  30. Fields []*DiscordEmbedFieldObject `json:"fields"`
  31. }
  32. type DiscordPayload struct {
  33. Content string `json:"content"`
  34. Username string `json:"username"`
  35. AvatarURL string `json:"avatar_url"`
  36. Embeds []*DiscordEmbedObject `json:"embeds"`
  37. }
  38. func (p *DiscordPayload) JSONPayload() ([]byte, error) {
  39. data, err := jsoniter.MarshalIndent(p, "", " ")
  40. if err != nil {
  41. return []byte{}, err
  42. }
  43. return data, nil
  44. }
  45. func DiscordTextFormatter(s string) string {
  46. return strings.Split(s, "\n")[0]
  47. }
  48. func DiscordLinkFormatter(url string, text string) string {
  49. return fmt.Sprintf("[%s](%s)", text, url)
  50. }
  51. func DiscordSHALinkFormatter(url string, text string) string {
  52. return fmt.Sprintf("[`%s`](%s)", text, url)
  53. }
  54. // getDiscordCreatePayload composes Discord payload for create new branch or tag.
  55. func getDiscordCreatePayload(p *api.CreatePayload) (*DiscordPayload, error) {
  56. refName := git.RefEndName(p.Ref)
  57. repoLink := DiscordLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
  58. refLink := DiscordLinkFormatter(p.Repo.HTMLURL+"/src/"+refName, refName)
  59. content := fmt.Sprintf("Created new %s: %s/%s", p.RefType, repoLink, refLink)
  60. return &DiscordPayload{
  61. Embeds: []*DiscordEmbedObject{{
  62. Description: content,
  63. URL: setting.AppURL + p.Sender.UserName,
  64. Author: &DiscordEmbedAuthorObject{
  65. Name: p.Sender.UserName,
  66. IconURL: p.Sender.AvatarUrl,
  67. },
  68. }},
  69. }, nil
  70. }
  71. // getDiscordDeletePayload composes Discord payload for delete a branch or tag.
  72. func getDiscordDeletePayload(p *api.DeletePayload) (*DiscordPayload, error) {
  73. refName := git.RefEndName(p.Ref)
  74. repoLink := DiscordLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
  75. content := fmt.Sprintf("Deleted %s: %s/%s", p.RefType, repoLink, refName)
  76. return &DiscordPayload{
  77. Embeds: []*DiscordEmbedObject{{
  78. Description: content,
  79. URL: setting.AppURL + p.Sender.UserName,
  80. Author: &DiscordEmbedAuthorObject{
  81. Name: p.Sender.UserName,
  82. IconURL: p.Sender.AvatarUrl,
  83. },
  84. }},
  85. }, nil
  86. }
  87. // getDiscordForkPayload composes Discord payload for forked by a repository.
  88. func getDiscordForkPayload(p *api.ForkPayload) (*DiscordPayload, error) {
  89. baseLink := DiscordLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
  90. forkLink := DiscordLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName)
  91. content := fmt.Sprintf("%s is forked to %s", baseLink, forkLink)
  92. return &DiscordPayload{
  93. Embeds: []*DiscordEmbedObject{{
  94. Description: content,
  95. URL: setting.AppURL + p.Sender.UserName,
  96. Author: &DiscordEmbedAuthorObject{
  97. Name: p.Sender.UserName,
  98. IconURL: p.Sender.AvatarUrl,
  99. },
  100. }},
  101. }, nil
  102. }
  103. func getDiscordPushPayload(p *api.PushPayload, slack *SlackMeta) (*DiscordPayload, error) {
  104. // n new commits
  105. var (
  106. branchName = git.RefEndName(p.Ref)
  107. commitDesc string
  108. commitString string
  109. )
  110. if len(p.Commits) == 1 {
  111. commitDesc = "1 new commit"
  112. } else {
  113. commitDesc = fmt.Sprintf("%d new commits", len(p.Commits))
  114. }
  115. if len(p.CompareURL) > 0 {
  116. commitString = DiscordLinkFormatter(p.CompareURL, commitDesc)
  117. } else {
  118. commitString = commitDesc
  119. }
  120. repoLink := DiscordLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
  121. branchLink := DiscordLinkFormatter(p.Repo.HTMLURL+"/src/"+branchName, branchName)
  122. content := fmt.Sprintf("Pushed %s to %s/%s\n", commitString, repoLink, branchLink)
  123. // for each commit, generate attachment text
  124. for i, commit := range p.Commits {
  125. content += fmt.Sprintf("%s %s - %s", DiscordSHALinkFormatter(commit.URL, commit.ID[:7]), DiscordTextFormatter(commit.Message), commit.Author.Name)
  126. // add linebreak to each commit but the last
  127. if i < len(p.Commits)-1 {
  128. content += "\n"
  129. }
  130. }
  131. color, _ := strconv.ParseInt(strings.TrimLeft(slack.Color, "#"), 16, 32)
  132. return &DiscordPayload{
  133. Username: slack.Username,
  134. AvatarURL: slack.IconURL,
  135. Embeds: []*DiscordEmbedObject{{
  136. Description: content,
  137. URL: setting.AppURL + p.Sender.UserName,
  138. Color: int(color),
  139. Author: &DiscordEmbedAuthorObject{
  140. Name: p.Sender.UserName,
  141. IconURL: p.Sender.AvatarUrl,
  142. },
  143. }},
  144. }, nil
  145. }
  146. func getDiscordIssuesPayload(p *api.IssuesPayload, slack *SlackMeta) (*DiscordPayload, error) {
  147. title := fmt.Sprintf("#%d %s", p.Index, p.Issue.Title)
  148. url := fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Index)
  149. content := ""
  150. fields := make([]*DiscordEmbedFieldObject, 0, 1)
  151. switch p.Action {
  152. case api.HOOK_ISSUE_OPENED:
  153. title = "New issue: " + title
  154. content = p.Issue.Body
  155. case api.HOOK_ISSUE_CLOSED:
  156. title = "Issue closed: " + title
  157. case api.HOOK_ISSUE_REOPENED:
  158. title = "Issue re-opened: " + title
  159. case api.HOOK_ISSUE_EDITED:
  160. title = "Issue edited: " + title
  161. content = p.Issue.Body
  162. case api.HOOK_ISSUE_ASSIGNED:
  163. title = "Issue assigned: " + title
  164. fields = []*DiscordEmbedFieldObject{{
  165. Name: "New Assignee",
  166. Value: p.Issue.Assignee.UserName,
  167. }}
  168. case api.HOOK_ISSUE_UNASSIGNED:
  169. title = "Issue unassigned: " + title
  170. case api.HOOK_ISSUE_LABEL_UPDATED:
  171. title = "Issue labels updated: " + title
  172. labels := make([]string, len(p.Issue.Labels))
  173. for i := range p.Issue.Labels {
  174. labels[i] = p.Issue.Labels[i].Name
  175. }
  176. if len(labels) == 0 {
  177. labels = []string{"<empty>"}
  178. }
  179. fields = []*DiscordEmbedFieldObject{{
  180. Name: "Labels",
  181. Value: strings.Join(labels, ", "),
  182. }}
  183. case api.HOOK_ISSUE_LABEL_CLEARED:
  184. title = "Issue labels cleared: " + title
  185. case api.HOOK_ISSUE_SYNCHRONIZED:
  186. title = "Issue synchronized: " + title
  187. case api.HOOK_ISSUE_MILESTONED:
  188. title = "Issue milestoned: " + title
  189. fields = []*DiscordEmbedFieldObject{{
  190. Name: "New Milestone",
  191. Value: p.Issue.Milestone.Title,
  192. }}
  193. case api.HOOK_ISSUE_DEMILESTONED:
  194. title = "Issue demilestoned: " + title
  195. }
  196. color, _ := strconv.ParseInt(strings.TrimLeft(slack.Color, "#"), 16, 32)
  197. return &DiscordPayload{
  198. Username: slack.Username,
  199. AvatarURL: slack.IconURL,
  200. Embeds: []*DiscordEmbedObject{{
  201. Title: title,
  202. Description: content,
  203. URL: url,
  204. Color: int(color),
  205. Footer: &DiscordEmbedFooterObject{
  206. Text: p.Repository.FullName,
  207. },
  208. Author: &DiscordEmbedAuthorObject{
  209. Name: p.Sender.UserName,
  210. IconURL: p.Sender.AvatarUrl,
  211. },
  212. Fields: fields,
  213. }},
  214. }, nil
  215. }
  216. func getDiscordIssueCommentPayload(p *api.IssueCommentPayload, slack *SlackMeta) (*DiscordPayload, error) {
  217. title := fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title)
  218. url := fmt.Sprintf("%s/issues/%d#%s", p.Repository.HTMLURL, p.Issue.Index, CommentHashTag(p.Comment.ID))
  219. content := ""
  220. fields := make([]*DiscordEmbedFieldObject, 0, 1)
  221. switch p.Action {
  222. case api.HOOK_ISSUE_COMMENT_CREATED:
  223. title = "New comment: " + title
  224. content = p.Comment.Body
  225. case api.HOOK_ISSUE_COMMENT_EDITED:
  226. title = "Comment edited: " + title
  227. content = p.Comment.Body
  228. case api.HOOK_ISSUE_COMMENT_DELETED:
  229. title = "Comment deleted: " + title
  230. url = fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index)
  231. content = p.Comment.Body
  232. }
  233. color, _ := strconv.ParseInt(strings.TrimLeft(slack.Color, "#"), 16, 32)
  234. return &DiscordPayload{
  235. Username: slack.Username,
  236. AvatarURL: slack.IconURL,
  237. Embeds: []*DiscordEmbedObject{{
  238. Title: title,
  239. Description: content,
  240. URL: url,
  241. Color: int(color),
  242. Footer: &DiscordEmbedFooterObject{
  243. Text: p.Repository.FullName,
  244. },
  245. Author: &DiscordEmbedAuthorObject{
  246. Name: p.Sender.UserName,
  247. IconURL: p.Sender.AvatarUrl,
  248. },
  249. Fields: fields,
  250. }},
  251. }, nil
  252. }
  253. func getDiscordPullRequestPayload(p *api.PullRequestPayload, slack *SlackMeta) (*DiscordPayload, error) {
  254. title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title)
  255. url := fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index)
  256. content := ""
  257. fields := make([]*DiscordEmbedFieldObject, 0, 1)
  258. switch p.Action {
  259. case api.HOOK_ISSUE_OPENED:
  260. title = "New pull request: " + title
  261. content = p.PullRequest.Body
  262. case api.HOOK_ISSUE_CLOSED:
  263. if p.PullRequest.HasMerged {
  264. title = "Pull request merged: " + title
  265. } else {
  266. title = "Pull request closed: " + title
  267. }
  268. case api.HOOK_ISSUE_REOPENED:
  269. title = "Pull request re-opened: " + title
  270. case api.HOOK_ISSUE_EDITED:
  271. title = "Pull request edited: " + title
  272. content = p.PullRequest.Body
  273. case api.HOOK_ISSUE_ASSIGNED:
  274. title = "Pull request assigned: " + title
  275. fields = []*DiscordEmbedFieldObject{{
  276. Name: "New Assignee",
  277. Value: p.PullRequest.Assignee.UserName,
  278. }}
  279. case api.HOOK_ISSUE_UNASSIGNED:
  280. title = "Pull request unassigned: " + title
  281. case api.HOOK_ISSUE_LABEL_UPDATED:
  282. title = "Pull request labels updated: " + title
  283. labels := make([]string, len(p.PullRequest.Labels))
  284. for i := range p.PullRequest.Labels {
  285. labels[i] = p.PullRequest.Labels[i].Name
  286. }
  287. fields = []*DiscordEmbedFieldObject{{
  288. Name: "Labels",
  289. Value: strings.Join(labels, ", "),
  290. }}
  291. case api.HOOK_ISSUE_LABEL_CLEARED:
  292. title = "Pull request labels cleared: " + title
  293. case api.HOOK_ISSUE_SYNCHRONIZED:
  294. title = "Pull request synchronized: " + title
  295. case api.HOOK_ISSUE_MILESTONED:
  296. title = "Pull request milestoned: " + title
  297. fields = []*DiscordEmbedFieldObject{{
  298. Name: "New Milestone",
  299. Value: p.PullRequest.Milestone.Title,
  300. }}
  301. case api.HOOK_ISSUE_DEMILESTONED:
  302. title = "Pull request demilestoned: " + title
  303. }
  304. color, _ := strconv.ParseInt(strings.TrimLeft(slack.Color, "#"), 16, 32)
  305. return &DiscordPayload{
  306. Username: slack.Username,
  307. AvatarURL: slack.IconURL,
  308. Embeds: []*DiscordEmbedObject{{
  309. Title: title,
  310. Description: content,
  311. URL: url,
  312. Color: int(color),
  313. Footer: &DiscordEmbedFooterObject{
  314. Text: p.Repository.FullName,
  315. },
  316. Author: &DiscordEmbedAuthorObject{
  317. Name: p.Sender.UserName,
  318. IconURL: p.Sender.AvatarUrl,
  319. },
  320. Fields: fields,
  321. }},
  322. }, nil
  323. }
  324. func getDiscordReleasePayload(p *api.ReleasePayload) (*DiscordPayload, error) {
  325. repoLink := DiscordLinkFormatter(p.Repository.HTMLURL, p.Repository.Name)
  326. refLink := DiscordLinkFormatter(p.Repository.HTMLURL+"/src/"+p.Release.TagName, p.Release.TagName)
  327. content := fmt.Sprintf("Published new release %s of %s", refLink, repoLink)
  328. return &DiscordPayload{
  329. Embeds: []*DiscordEmbedObject{{
  330. Description: content,
  331. URL: setting.AppURL + p.Sender.UserName,
  332. Author: &DiscordEmbedAuthorObject{
  333. Name: p.Sender.UserName,
  334. IconURL: p.Sender.AvatarUrl,
  335. },
  336. }},
  337. }, nil
  338. }
  339. func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (payload *DiscordPayload, err error) {
  340. slack := &SlackMeta{}
  341. if err := jsoniter.Unmarshal([]byte(meta), &slack); err != nil {
  342. return nil, fmt.Errorf("jsoniter.Unmarshal: %v", err)
  343. }
  344. switch event {
  345. case HOOK_EVENT_CREATE:
  346. payload, err = getDiscordCreatePayload(p.(*api.CreatePayload))
  347. case HOOK_EVENT_DELETE:
  348. payload, err = getDiscordDeletePayload(p.(*api.DeletePayload))
  349. case HOOK_EVENT_FORK:
  350. payload, err = getDiscordForkPayload(p.(*api.ForkPayload))
  351. case HOOK_EVENT_PUSH:
  352. payload, err = getDiscordPushPayload(p.(*api.PushPayload), slack)
  353. case HOOK_EVENT_ISSUES:
  354. payload, err = getDiscordIssuesPayload(p.(*api.IssuesPayload), slack)
  355. case HOOK_EVENT_ISSUE_COMMENT:
  356. payload, err = getDiscordIssueCommentPayload(p.(*api.IssueCommentPayload), slack)
  357. case HOOK_EVENT_PULL_REQUEST:
  358. payload, err = getDiscordPullRequestPayload(p.(*api.PullRequestPayload), slack)
  359. case HOOK_EVENT_RELEASE:
  360. payload, err = getDiscordReleasePayload(p.(*api.ReleasePayload))
  361. }
  362. if err != nil {
  363. return nil, fmt.Errorf("event '%s': %v", event, err)
  364. }
  365. payload.Username = slack.Username
  366. payload.AvatarURL = slack.IconURL
  367. if len(payload.Embeds) > 0 {
  368. color, _ := strconv.ParseInt(strings.TrimLeft(slack.Color, "#"), 16, 32)
  369. payload.Embeds[0].Color = int(color)
  370. }
  371. return payload, nil
  372. }