You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

discord.go 18 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  1. // Copyright 2017 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package webhook
  5. import (
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "strconv"
  10. "strings"
  11. "code.gitea.io/gitea/models"
  12. "code.gitea.io/gitea/modules/git"
  13. "code.gitea.io/gitea/modules/log"
  14. "code.gitea.io/gitea/modules/setting"
  15. api "code.gitea.io/gitea/modules/structs"
  16. )
  17. type (
  18. // DiscordEmbedFooter for Embed Footer Structure.
  19. DiscordEmbedFooter struct {
  20. Text string `json:"text"`
  21. }
  22. // DiscordEmbedAuthor for Embed Author Structure
  23. DiscordEmbedAuthor struct {
  24. Name string `json:"name"`
  25. URL string `json:"url"`
  26. IconURL string `json:"icon_url"`
  27. }
  28. // DiscordEmbedField for Embed Field Structure
  29. DiscordEmbedField struct {
  30. Name string `json:"name"`
  31. Value string `json:"value"`
  32. }
  33. // DiscordEmbed is for Embed Structure
  34. DiscordEmbed struct {
  35. Title string `json:"title"`
  36. Description string `json:"description"`
  37. URL string `json:"url"`
  38. Color int `json:"color"`
  39. Footer DiscordEmbedFooter `json:"footer"`
  40. Author DiscordEmbedAuthor `json:"author"`
  41. Fields []DiscordEmbedField `json:"fields"`
  42. }
  43. // DiscordPayload represents
  44. DiscordPayload struct {
  45. Wait bool `json:"wait"`
  46. Content string `json:"content"`
  47. Username string `json:"username"`
  48. AvatarURL string `json:"avatar_url"`
  49. TTS bool `json:"tts"`
  50. Embeds []DiscordEmbed `json:"embeds"`
  51. }
  52. // DiscordMeta contains the discord metadata
  53. DiscordMeta struct {
  54. Username string `json:"username"`
  55. IconURL string `json:"icon_url"`
  56. }
  57. )
  58. // GetDiscordHook returns discord metadata
  59. func GetDiscordHook(w *models.Webhook) *DiscordMeta {
  60. s := &DiscordMeta{}
  61. if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
  62. log.Error("webhook.GetDiscordHook(%d): %v", w.ID, err)
  63. }
  64. return s
  65. }
  66. func color(clr string) int {
  67. if clr != "" {
  68. clr = strings.TrimLeft(clr, "#")
  69. if s, err := strconv.ParseInt(clr, 16, 32); err == nil {
  70. return int(s)
  71. }
  72. }
  73. return 0
  74. }
  75. var (
  76. greenColor = color("1ac600")
  77. greenColorLight = color("bfe5bf")
  78. yellowColor = color("ffd930")
  79. greyColor = color("4f545c")
  80. purpleColor = color("7289da")
  81. orangeColor = color("eb6420")
  82. orangeColorLight = color("e68d60")
  83. redColor = color("ff3232")
  84. )
  85. // SetSecret sets the discord secret
  86. func (p *DiscordPayload) SetSecret(_ string) {}
  87. // JSONPayload Marshals the DiscordPayload to json
  88. func (p *DiscordPayload) JSONPayload() ([]byte, error) {
  89. data, err := json.MarshalIndent(p, "", " ")
  90. if err != nil {
  91. return []byte{}, err
  92. }
  93. return data, nil
  94. }
  95. func getDiscordCreatePayload(p *api.CreatePayload, meta *DiscordMeta) (*DiscordPayload, error) {
  96. // created tag/branch
  97. refName := git.RefEndName(p.Ref)
  98. title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
  99. return &DiscordPayload{
  100. Username: meta.Username,
  101. AvatarURL: meta.IconURL,
  102. Embeds: []DiscordEmbed{
  103. {
  104. Title: title,
  105. URL: p.Repo.HTMLURL + "/src/" + refName,
  106. Color: greenColor,
  107. Author: DiscordEmbedAuthor{
  108. Name: p.Sender.UserName,
  109. URL: setting.AppURL + p.Sender.UserName,
  110. IconURL: p.Sender.AvatarURL,
  111. },
  112. },
  113. },
  114. }, nil
  115. }
  116. func getDiscordDeletePayload(p *api.DeletePayload, meta *DiscordMeta) (*DiscordPayload, error) {
  117. // deleted tag/branch
  118. refName := git.RefEndName(p.Ref)
  119. title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
  120. return &DiscordPayload{
  121. Username: meta.Username,
  122. AvatarURL: meta.IconURL,
  123. Embeds: []DiscordEmbed{
  124. {
  125. Title: title,
  126. URL: p.Repo.HTMLURL + "/src/" + refName,
  127. Color: redColor,
  128. Author: DiscordEmbedAuthor{
  129. Name: p.Sender.UserName,
  130. URL: setting.AppURL + p.Sender.UserName,
  131. IconURL: p.Sender.AvatarURL,
  132. },
  133. },
  134. },
  135. }, nil
  136. }
  137. func getDiscordForkPayload(p *api.ForkPayload, meta *DiscordMeta) (*DiscordPayload, error) {
  138. // fork
  139. title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
  140. return &DiscordPayload{
  141. Username: meta.Username,
  142. AvatarURL: meta.IconURL,
  143. Embeds: []DiscordEmbed{
  144. {
  145. Title: title,
  146. URL: p.Repo.HTMLURL,
  147. Color: greenColor,
  148. Author: DiscordEmbedAuthor{
  149. Name: p.Sender.UserName,
  150. URL: setting.AppURL + p.Sender.UserName,
  151. IconURL: p.Sender.AvatarURL,
  152. },
  153. },
  154. },
  155. }, nil
  156. }
  157. func getDiscordPushPayload(p *api.PushPayload, meta *DiscordMeta) (*DiscordPayload, error) {
  158. var (
  159. branchName = git.RefEndName(p.Ref)
  160. commitDesc string
  161. )
  162. var titleLink string
  163. if len(p.Commits) == 1 {
  164. commitDesc = "1 new commit"
  165. titleLink = p.Commits[0].URL
  166. } else {
  167. commitDesc = fmt.Sprintf("%d new commits", len(p.Commits))
  168. titleLink = p.CompareURL
  169. }
  170. if titleLink == "" {
  171. titleLink = p.Repo.HTMLURL + "/src/" + branchName
  172. }
  173. title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc)
  174. var text string
  175. // for each commit, generate attachment text
  176. for i, commit := range p.Commits {
  177. text += fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL,
  178. strings.TrimRight(commit.Message, "\r\n"), commit.Author.Name)
  179. // add linebreak to each commit but the last
  180. if i < len(p.Commits)-1 {
  181. text += "\n"
  182. }
  183. }
  184. return &DiscordPayload{
  185. Username: meta.Username,
  186. AvatarURL: meta.IconURL,
  187. Embeds: []DiscordEmbed{
  188. {
  189. Title: title,
  190. Description: text,
  191. URL: titleLink,
  192. Color: greenColor,
  193. Author: DiscordEmbedAuthor{
  194. Name: p.Sender.UserName,
  195. URL: setting.AppURL + p.Sender.UserName,
  196. IconURL: p.Sender.AvatarURL,
  197. },
  198. },
  199. },
  200. }, nil
  201. }
  202. func getDiscordIssuesPayload(p *api.IssuePayload, meta *DiscordMeta) (*DiscordPayload, error) {
  203. var text, title string
  204. var color int
  205. url := fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index)
  206. switch p.Action {
  207. case api.HookIssueOpened:
  208. title = fmt.Sprintf("[%s] Issue opened: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
  209. text = p.Issue.Body
  210. color = orangeColor
  211. case api.HookIssueClosed:
  212. title = fmt.Sprintf("[%s] Issue closed: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
  213. color = redColor
  214. text = p.Issue.Body
  215. case api.HookIssueReOpened:
  216. title = fmt.Sprintf("[%s] Issue re-opened: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
  217. text = p.Issue.Body
  218. color = yellowColor
  219. case api.HookIssueEdited:
  220. title = fmt.Sprintf("[%s] Issue edited: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
  221. text = p.Issue.Body
  222. color = yellowColor
  223. case api.HookIssueAssigned:
  224. title = fmt.Sprintf("[%s] Issue assigned to %s: #%d %s", p.Repository.FullName,
  225. p.Issue.Assignee.UserName, p.Index, p.Issue.Title)
  226. text = p.Issue.Body
  227. color = greenColor
  228. case api.HookIssueUnassigned:
  229. title = fmt.Sprintf("[%s] Issue unassigned: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
  230. text = p.Issue.Body
  231. color = yellowColor
  232. case api.HookIssueLabelUpdated:
  233. title = fmt.Sprintf("[%s] Issue labels updated: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
  234. text = p.Issue.Body
  235. color = yellowColor
  236. case api.HookIssueLabelCleared:
  237. title = fmt.Sprintf("[%s] Issue labels cleared: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
  238. text = p.Issue.Body
  239. color = yellowColor
  240. case api.HookIssueSynchronized:
  241. title = fmt.Sprintf("[%s] Issue synchronized: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
  242. text = p.Issue.Body
  243. color = yellowColor
  244. case api.HookIssueMilestoned:
  245. title = fmt.Sprintf("[%s] Issue milestone: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
  246. text = p.Issue.Body
  247. color = yellowColor
  248. case api.HookIssueDemilestoned:
  249. title = fmt.Sprintf("[%s] Issue clear milestone: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
  250. text = p.Issue.Body
  251. color = yellowColor
  252. }
  253. return &DiscordPayload{
  254. Username: meta.Username,
  255. AvatarURL: meta.IconURL,
  256. Embeds: []DiscordEmbed{
  257. {
  258. Title: title,
  259. Description: text,
  260. URL: url,
  261. Color: color,
  262. Author: DiscordEmbedAuthor{
  263. Name: p.Sender.UserName,
  264. URL: setting.AppURL + p.Sender.UserName,
  265. IconURL: p.Sender.AvatarURL,
  266. },
  267. },
  268. },
  269. }, nil
  270. }
  271. func getDiscordIssueCommentPayload(p *api.IssueCommentPayload, discord *DiscordMeta) (*DiscordPayload, error) {
  272. title := fmt.Sprintf("#%d: %s", p.Issue.Index, p.Issue.Title)
  273. url := fmt.Sprintf("%s/issues/%d#%s", p.Repository.HTMLURL, p.Issue.Index, models.CommentHashTag(p.Comment.ID))
  274. content := ""
  275. var color int
  276. switch p.Action {
  277. case api.HookIssueCommentCreated:
  278. if p.IsPull {
  279. title = "New comment on pull request " + title
  280. color = greenColorLight
  281. } else {
  282. title = "New comment on issue " + title
  283. color = orangeColorLight
  284. }
  285. content = p.Comment.Body
  286. case api.HookIssueCommentEdited:
  287. if p.IsPull {
  288. title = "Comment edited on pull request " + title
  289. } else {
  290. title = "Comment edited on issue " + title
  291. }
  292. content = p.Comment.Body
  293. color = yellowColor
  294. case api.HookIssueCommentDeleted:
  295. if p.IsPull {
  296. title = "Comment deleted on pull request " + title
  297. } else {
  298. title = "Comment deleted on issue " + title
  299. }
  300. url = fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index)
  301. content = p.Comment.Body
  302. color = redColor
  303. }
  304. title = fmt.Sprintf("[%s] %s", p.Repository.FullName, title)
  305. return &DiscordPayload{
  306. Username: discord.Username,
  307. AvatarURL: discord.IconURL,
  308. Embeds: []DiscordEmbed{
  309. {
  310. Title: title,
  311. Description: content,
  312. URL: url,
  313. Color: color,
  314. Author: DiscordEmbedAuthor{
  315. Name: p.Sender.UserName,
  316. URL: setting.AppURL + p.Sender.UserName,
  317. IconURL: p.Sender.AvatarURL,
  318. },
  319. },
  320. },
  321. }, nil
  322. }
  323. func getDiscordPullRequestPayload(p *api.PullRequestPayload, meta *DiscordMeta) (*DiscordPayload, error) {
  324. var text, title string
  325. var color int
  326. switch p.Action {
  327. case api.HookIssueOpened:
  328. title = fmt.Sprintf("[%s] Pull request opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
  329. text = p.PullRequest.Body
  330. color = greenColor
  331. case api.HookIssueClosed:
  332. if p.PullRequest.HasMerged {
  333. title = fmt.Sprintf("[%s] Pull request merged: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
  334. color = purpleColor
  335. } else {
  336. title = fmt.Sprintf("[%s] Pull request closed: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
  337. color = redColor
  338. }
  339. text = p.PullRequest.Body
  340. case api.HookIssueReOpened:
  341. title = fmt.Sprintf("[%s] Pull request re-opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
  342. text = p.PullRequest.Body
  343. color = yellowColor
  344. case api.HookIssueEdited:
  345. title = fmt.Sprintf("[%s] Pull request edited: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
  346. text = p.PullRequest.Body
  347. color = yellowColor
  348. case api.HookIssueAssigned:
  349. list := make([]string, len(p.PullRequest.Assignees))
  350. for i, user := range p.PullRequest.Assignees {
  351. list[i] = user.UserName
  352. }
  353. title = fmt.Sprintf("[%s] Pull request assigned to %s: #%d by %s", p.Repository.FullName,
  354. strings.Join(list, ", "),
  355. p.Index, p.PullRequest.Title)
  356. text = p.PullRequest.Body
  357. color = greenColor
  358. case api.HookIssueUnassigned:
  359. title = fmt.Sprintf("[%s] Pull request unassigned: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
  360. text = p.PullRequest.Body
  361. color = yellowColor
  362. case api.HookIssueLabelUpdated:
  363. title = fmt.Sprintf("[%s] Pull request labels updated: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
  364. text = p.PullRequest.Body
  365. color = yellowColor
  366. case api.HookIssueLabelCleared:
  367. title = fmt.Sprintf("[%s] Pull request labels cleared: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
  368. text = p.PullRequest.Body
  369. color = yellowColor
  370. case api.HookIssueSynchronized:
  371. title = fmt.Sprintf("[%s] Pull request synchronized: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
  372. text = p.PullRequest.Body
  373. color = yellowColor
  374. case api.HookIssueMilestoned:
  375. title = fmt.Sprintf("[%s] Pull request milestone: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
  376. text = p.PullRequest.Body
  377. color = yellowColor
  378. case api.HookIssueDemilestoned:
  379. title = fmt.Sprintf("[%s] Pull request clear milestone: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
  380. text = p.PullRequest.Body
  381. color = yellowColor
  382. }
  383. return &DiscordPayload{
  384. Username: meta.Username,
  385. AvatarURL: meta.IconURL,
  386. Embeds: []DiscordEmbed{
  387. {
  388. Title: title,
  389. Description: text,
  390. URL: p.PullRequest.HTMLURL,
  391. Color: color,
  392. Author: DiscordEmbedAuthor{
  393. Name: p.Sender.UserName,
  394. URL: setting.AppURL + p.Sender.UserName,
  395. IconURL: p.Sender.AvatarURL,
  396. },
  397. },
  398. },
  399. }, nil
  400. }
  401. func getDiscordPullRequestApprovalPayload(p *api.PullRequestPayload, meta *DiscordMeta, event models.HookEventType) (*DiscordPayload, error) {
  402. var text, title string
  403. var color int
  404. switch p.Action {
  405. case api.HookIssueSynchronized:
  406. action, err := parseHookPullRequestEventType(event)
  407. if err != nil {
  408. return nil, err
  409. }
  410. title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
  411. text = p.Review.Content
  412. switch event {
  413. case models.HookEventPullRequestApproved:
  414. color = greenColor
  415. case models.HookEventPullRequestRejected:
  416. color = redColor
  417. case models.HookEventPullRequestComment:
  418. color = greyColor
  419. default:
  420. color = yellowColor
  421. }
  422. }
  423. return &DiscordPayload{
  424. Username: meta.Username,
  425. AvatarURL: meta.IconURL,
  426. Embeds: []DiscordEmbed{
  427. {
  428. Title: title,
  429. Description: text,
  430. URL: p.PullRequest.HTMLURL,
  431. Color: color,
  432. Author: DiscordEmbedAuthor{
  433. Name: p.Sender.UserName,
  434. URL: setting.AppURL + p.Sender.UserName,
  435. IconURL: p.Sender.AvatarURL,
  436. },
  437. },
  438. },
  439. }, nil
  440. }
  441. func getDiscordRepositoryPayload(p *api.RepositoryPayload, meta *DiscordMeta) (*DiscordPayload, error) {
  442. var title, url string
  443. var color int
  444. switch p.Action {
  445. case api.HookRepoCreated:
  446. title = fmt.Sprintf("[%s] Repository created", p.Repository.FullName)
  447. url = p.Repository.HTMLURL
  448. color = greenColor
  449. case api.HookRepoDeleted:
  450. title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
  451. color = redColor
  452. }
  453. return &DiscordPayload{
  454. Username: meta.Username,
  455. AvatarURL: meta.IconURL,
  456. Embeds: []DiscordEmbed{
  457. {
  458. Title: title,
  459. URL: url,
  460. Color: color,
  461. Author: DiscordEmbedAuthor{
  462. Name: p.Sender.UserName,
  463. URL: setting.AppURL + p.Sender.UserName,
  464. IconURL: p.Sender.AvatarURL,
  465. },
  466. },
  467. },
  468. }, nil
  469. }
  470. func getDiscordReleasePayload(p *api.ReleasePayload, meta *DiscordMeta) (*DiscordPayload, error) {
  471. var title, url string
  472. var color int
  473. switch p.Action {
  474. case api.HookReleasePublished:
  475. title = fmt.Sprintf("[%s] Release created", p.Release.TagName)
  476. url = p.Release.URL
  477. color = greenColor
  478. case api.HookReleaseUpdated:
  479. title = fmt.Sprintf("[%s] Release updated", p.Release.TagName)
  480. url = p.Release.URL
  481. color = yellowColor
  482. case api.HookReleaseDeleted:
  483. title = fmt.Sprintf("[%s] Release deleted", p.Release.TagName)
  484. url = p.Release.URL
  485. color = redColor
  486. }
  487. return &DiscordPayload{
  488. Username: meta.Username,
  489. AvatarURL: meta.IconURL,
  490. Embeds: []DiscordEmbed{
  491. {
  492. Title: title,
  493. Description: p.Release.Note,
  494. URL: url,
  495. Color: color,
  496. Author: DiscordEmbedAuthor{
  497. Name: p.Sender.UserName,
  498. URL: setting.AppURL + p.Sender.UserName,
  499. IconURL: p.Sender.AvatarURL,
  500. },
  501. },
  502. },
  503. }, nil
  504. }
  505. // GetDiscordPayload converts a discord webhook into a DiscordPayload
  506. func GetDiscordPayload(p api.Payloader, event models.HookEventType, meta string) (*DiscordPayload, error) {
  507. s := new(DiscordPayload)
  508. discord := &DiscordMeta{}
  509. if err := json.Unmarshal([]byte(meta), &discord); err != nil {
  510. return s, errors.New("GetDiscordPayload meta json:" + err.Error())
  511. }
  512. switch event {
  513. case models.HookEventCreate:
  514. return getDiscordCreatePayload(p.(*api.CreatePayload), discord)
  515. case models.HookEventDelete:
  516. return getDiscordDeletePayload(p.(*api.DeletePayload), discord)
  517. case models.HookEventFork:
  518. return getDiscordForkPayload(p.(*api.ForkPayload), discord)
  519. case models.HookEventIssues:
  520. return getDiscordIssuesPayload(p.(*api.IssuePayload), discord)
  521. case models.HookEventIssueComment:
  522. return getDiscordIssueCommentPayload(p.(*api.IssueCommentPayload), discord)
  523. case models.HookEventPush:
  524. return getDiscordPushPayload(p.(*api.PushPayload), discord)
  525. case models.HookEventPullRequest:
  526. return getDiscordPullRequestPayload(p.(*api.PullRequestPayload), discord)
  527. case models.HookEventPullRequestRejected, models.HookEventPullRequestApproved, models.HookEventPullRequestComment:
  528. return getDiscordPullRequestApprovalPayload(p.(*api.PullRequestPayload), discord, event)
  529. case models.HookEventRepository:
  530. return getDiscordRepositoryPayload(p.(*api.RepositoryPayload), discord)
  531. case models.HookEventRelease:
  532. return getDiscordReleasePayload(p.(*api.ReleasePayload), discord)
  533. }
  534. return s, nil
  535. }
  536. func parseHookPullRequestEventType(event models.HookEventType) (string, error) {
  537. switch event {
  538. case models.HookEventPullRequestApproved:
  539. return "approved", nil
  540. case models.HookEventPullRequestRejected:
  541. return "rejected", nil
  542. case models.HookEventPullRequestComment:
  543. return "comment", nil
  544. default:
  545. return "", errors.New("unknown event type")
  546. }
  547. }