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

webhook.go 17KB

10 years ago
10 years ago
7 years ago
7 years ago
7 years ago
10 years ago
10 years ago
8 years ago
10 years ago
8 years ago
8 years ago
8 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
7 years ago
8 years ago
10 years ago
10 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2017 The Gitea Authors. All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package models
  6. import (
  7. "crypto/tls"
  8. "encoding/json"
  9. "fmt"
  10. "io/ioutil"
  11. "strings"
  12. "time"
  13. "github.com/go-xorm/xorm"
  14. gouuid "github.com/satori/go.uuid"
  15. api "code.gitea.io/sdk/gitea"
  16. "code.gitea.io/gitea/modules/httplib"
  17. "code.gitea.io/gitea/modules/log"
  18. "code.gitea.io/gitea/modules/setting"
  19. "code.gitea.io/gitea/modules/sync"
  20. )
  21. // HookQueue is a global queue of web hooks
  22. var HookQueue = sync.NewUniqueQueue(setting.Webhook.QueueLength)
  23. // HookContentType is the content type of a web hook
  24. type HookContentType int
  25. const (
  26. // ContentTypeJSON is a JSON payload for web hooks
  27. ContentTypeJSON HookContentType = iota + 1
  28. // ContentTypeForm is an url-encoded form payload for web hook
  29. ContentTypeForm
  30. )
  31. var hookContentTypes = map[string]HookContentType{
  32. "json": ContentTypeJSON,
  33. "form": ContentTypeForm,
  34. }
  35. // ToHookContentType returns HookContentType by given name.
  36. func ToHookContentType(name string) HookContentType {
  37. return hookContentTypes[name]
  38. }
  39. // Name returns the name of a given web hook's content type
  40. func (t HookContentType) Name() string {
  41. switch t {
  42. case ContentTypeJSON:
  43. return "json"
  44. case ContentTypeForm:
  45. return "form"
  46. }
  47. return ""
  48. }
  49. // IsValidHookContentType returns true if given name is a valid hook content type.
  50. func IsValidHookContentType(name string) bool {
  51. _, ok := hookContentTypes[name]
  52. return ok
  53. }
  54. // HookEvents is a set of web hook events
  55. type HookEvents struct {
  56. Create bool `json:"create"`
  57. Push bool `json:"push"`
  58. PullRequest bool `json:"pull_request"`
  59. }
  60. // HookEvent represents events that will delivery hook.
  61. type HookEvent struct {
  62. PushOnly bool `json:"push_only"`
  63. SendEverything bool `json:"send_everything"`
  64. ChooseEvents bool `json:"choose_events"`
  65. HookEvents `json:"events"`
  66. }
  67. // HookStatus is the status of a web hook
  68. type HookStatus int
  69. // Possible statuses of a web hook
  70. const (
  71. HookStatusNone = iota
  72. HookStatusSucceed
  73. HookStatusFail
  74. )
  75. // Webhook represents a web hook object.
  76. type Webhook struct {
  77. ID int64 `xorm:"pk autoincr"`
  78. RepoID int64 `xorm:"INDEX"`
  79. OrgID int64 `xorm:"INDEX"`
  80. URL string `xorm:"url TEXT"`
  81. ContentType HookContentType
  82. Secret string `xorm:"TEXT"`
  83. Events string `xorm:"TEXT"`
  84. *HookEvent `xorm:"-"`
  85. IsSSL bool `xorm:"is_ssl"`
  86. IsActive bool `xorm:"INDEX"`
  87. HookTaskType HookTaskType
  88. Meta string `xorm:"TEXT"` // store hook-specific attributes
  89. LastStatus HookStatus // Last delivery status
  90. Created time.Time `xorm:"-"`
  91. CreatedUnix int64 `xorm:"INDEX"`
  92. Updated time.Time `xorm:"-"`
  93. UpdatedUnix int64 `xorm:"INDEX"`
  94. }
  95. // BeforeInsert will be invoked by XORM before inserting a record
  96. // representing this object
  97. func (w *Webhook) BeforeInsert() {
  98. w.CreatedUnix = time.Now().Unix()
  99. w.UpdatedUnix = w.CreatedUnix
  100. }
  101. // BeforeUpdate will be invoked by XORM before updating a record
  102. // representing this object
  103. func (w *Webhook) BeforeUpdate() {
  104. w.UpdatedUnix = time.Now().Unix()
  105. }
  106. // AfterSet updates the webhook object upon setting a column
  107. func (w *Webhook) AfterSet(colName string, _ xorm.Cell) {
  108. var err error
  109. switch colName {
  110. case "events":
  111. w.HookEvent = &HookEvent{}
  112. if err = json.Unmarshal([]byte(w.Events), w.HookEvent); err != nil {
  113. log.Error(3, "Unmarshal[%d]: %v", w.ID, err)
  114. }
  115. case "created_unix":
  116. w.Created = time.Unix(w.CreatedUnix, 0).Local()
  117. case "updated_unix":
  118. w.Updated = time.Unix(w.UpdatedUnix, 0).Local()
  119. }
  120. }
  121. // GetSlackHook returns slack metadata
  122. func (w *Webhook) GetSlackHook() *SlackMeta {
  123. s := &SlackMeta{}
  124. if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
  125. log.Error(4, "webhook.GetSlackHook(%d): %v", w.ID, err)
  126. }
  127. return s
  128. }
  129. // History returns history of webhook by given conditions.
  130. func (w *Webhook) History(page int) ([]*HookTask, error) {
  131. return HookTasks(w.ID, page)
  132. }
  133. // UpdateEvent handles conversion from HookEvent to Events.
  134. func (w *Webhook) UpdateEvent() error {
  135. data, err := json.Marshal(w.HookEvent)
  136. w.Events = string(data)
  137. return err
  138. }
  139. // HasCreateEvent returns true if hook enabled create event.
  140. func (w *Webhook) HasCreateEvent() bool {
  141. return w.SendEverything ||
  142. (w.ChooseEvents && w.HookEvents.Create)
  143. }
  144. // HasPushEvent returns true if hook enabled push event.
  145. func (w *Webhook) HasPushEvent() bool {
  146. return w.PushOnly || w.SendEverything ||
  147. (w.ChooseEvents && w.HookEvents.Push)
  148. }
  149. // HasPullRequestEvent returns true if hook enabled pull request event.
  150. func (w *Webhook) HasPullRequestEvent() bool {
  151. return w.SendEverything ||
  152. (w.ChooseEvents && w.HookEvents.PullRequest)
  153. }
  154. // EventsArray returns an array of hook events
  155. func (w *Webhook) EventsArray() []string {
  156. events := make([]string, 0, 3)
  157. if w.HasCreateEvent() {
  158. events = append(events, "create")
  159. }
  160. if w.HasPushEvent() {
  161. events = append(events, "push")
  162. }
  163. if w.HasPullRequestEvent() {
  164. events = append(events, "pull_request")
  165. }
  166. return events
  167. }
  168. // CreateWebhook creates a new web hook.
  169. func CreateWebhook(w *Webhook) error {
  170. _, err := x.Insert(w)
  171. return err
  172. }
  173. // getWebhook uses argument bean as query condition,
  174. // ID must be specified and do not assign unnecessary fields.
  175. func getWebhook(bean *Webhook) (*Webhook, error) {
  176. has, err := x.Get(bean)
  177. if err != nil {
  178. return nil, err
  179. } else if !has {
  180. return nil, ErrWebhookNotExist{bean.ID}
  181. }
  182. return bean, nil
  183. }
  184. // GetWebhookByRepoID returns webhook of repository by given ID.
  185. func GetWebhookByRepoID(repoID, id int64) (*Webhook, error) {
  186. return getWebhook(&Webhook{
  187. ID: id,
  188. RepoID: repoID,
  189. })
  190. }
  191. // GetWebhookByOrgID returns webhook of organization by given ID.
  192. func GetWebhookByOrgID(orgID, id int64) (*Webhook, error) {
  193. return getWebhook(&Webhook{
  194. ID: id,
  195. OrgID: orgID,
  196. })
  197. }
  198. // GetActiveWebhooksByRepoID returns all active webhooks of repository.
  199. func GetActiveWebhooksByRepoID(repoID int64) ([]*Webhook, error) {
  200. webhooks := make([]*Webhook, 0, 5)
  201. return webhooks, x.Where("is_active=?", true).
  202. Find(&webhooks, &Webhook{RepoID: repoID})
  203. }
  204. // GetWebhooksByRepoID returns all webhooks of a repository.
  205. func GetWebhooksByRepoID(repoID int64) ([]*Webhook, error) {
  206. webhooks := make([]*Webhook, 0, 5)
  207. return webhooks, x.Find(&webhooks, &Webhook{RepoID: repoID})
  208. }
  209. // GetActiveWebhooksByOrgID returns all active webhooks for an organization.
  210. func GetActiveWebhooksByOrgID(orgID int64) (ws []*Webhook, err error) {
  211. err = x.
  212. Where("org_id=?", orgID).
  213. And("is_active=?", true).
  214. Find(&ws)
  215. return ws, err
  216. }
  217. // GetWebhooksByOrgID returns all webhooks for an organization.
  218. func GetWebhooksByOrgID(orgID int64) (ws []*Webhook, err error) {
  219. err = x.Find(&ws, &Webhook{OrgID: orgID})
  220. return ws, err
  221. }
  222. // UpdateWebhook updates information of webhook.
  223. func UpdateWebhook(w *Webhook) error {
  224. _, err := x.Id(w.ID).AllCols().Update(w)
  225. return err
  226. }
  227. // deleteWebhook uses argument bean as query condition,
  228. // ID must be specified and do not assign unnecessary fields.
  229. func deleteWebhook(bean *Webhook) (err error) {
  230. sess := x.NewSession()
  231. defer sessionRelease(sess)
  232. if err = sess.Begin(); err != nil {
  233. return err
  234. }
  235. if count, err := sess.Delete(bean); err != nil {
  236. return err
  237. } else if count == 0 {
  238. return ErrWebhookNotExist{ID: bean.ID}
  239. } else if _, err = sess.Delete(&HookTask{HookID: bean.ID}); err != nil {
  240. return err
  241. }
  242. return sess.Commit()
  243. }
  244. // DeleteWebhookByRepoID deletes webhook of repository by given ID.
  245. func DeleteWebhookByRepoID(repoID, id int64) error {
  246. return deleteWebhook(&Webhook{
  247. ID: id,
  248. RepoID: repoID,
  249. })
  250. }
  251. // DeleteWebhookByOrgID deletes webhook of organization by given ID.
  252. func DeleteWebhookByOrgID(orgID, id int64) error {
  253. return deleteWebhook(&Webhook{
  254. ID: id,
  255. OrgID: orgID,
  256. })
  257. }
  258. // ___ ___ __ ___________ __
  259. // / | \ ____ ____ | | _\__ ___/____ _____| | __
  260. // / ~ \/ _ \ / _ \| |/ / | | \__ \ / ___/ |/ /
  261. // \ Y ( <_> | <_> ) < | | / __ \_\___ \| <
  262. // \___|_ / \____/ \____/|__|_ \ |____| (____ /____ >__|_ \
  263. // \/ \/ \/ \/ \/
  264. // HookTaskType is the type of an hook task
  265. type HookTaskType int
  266. // Types of hook tasks
  267. const (
  268. GOGS HookTaskType = iota + 1
  269. SLACK
  270. GITEA
  271. )
  272. var hookTaskTypes = map[string]HookTaskType{
  273. "gitea": GITEA,
  274. "gogs": GOGS,
  275. "slack": SLACK,
  276. }
  277. // ToHookTaskType returns HookTaskType by given name.
  278. func ToHookTaskType(name string) HookTaskType {
  279. return hookTaskTypes[name]
  280. }
  281. // Name returns the name of an hook task type
  282. func (t HookTaskType) Name() string {
  283. switch t {
  284. case GITEA:
  285. return "gitea"
  286. case GOGS:
  287. return "gogs"
  288. case SLACK:
  289. return "slack"
  290. }
  291. return ""
  292. }
  293. // IsValidHookTaskType returns true if given name is a valid hook task type.
  294. func IsValidHookTaskType(name string) bool {
  295. _, ok := hookTaskTypes[name]
  296. return ok
  297. }
  298. // HookEventType is the type of an hook event
  299. type HookEventType string
  300. // Types of hook events
  301. const (
  302. HookEventCreate HookEventType = "create"
  303. HookEventPush HookEventType = "push"
  304. HookEventPullRequest HookEventType = "pull_request"
  305. )
  306. // HookRequest represents hook task request information.
  307. type HookRequest struct {
  308. Headers map[string]string `json:"headers"`
  309. }
  310. // HookResponse represents hook task response information.
  311. type HookResponse struct {
  312. Status int `json:"status"`
  313. Headers map[string]string `json:"headers"`
  314. Body string `json:"body"`
  315. }
  316. // HookTask represents a hook task.
  317. type HookTask struct {
  318. ID int64 `xorm:"pk autoincr"`
  319. RepoID int64 `xorm:"INDEX"`
  320. HookID int64
  321. UUID string
  322. Type HookTaskType
  323. URL string `xorm:"TEXT"`
  324. api.Payloader `xorm:"-"`
  325. PayloadContent string `xorm:"TEXT"`
  326. ContentType HookContentType
  327. EventType HookEventType
  328. IsSSL bool
  329. IsDelivered bool
  330. Delivered int64
  331. DeliveredString string `xorm:"-"`
  332. // History info.
  333. IsSucceed bool
  334. RequestContent string `xorm:"TEXT"`
  335. RequestInfo *HookRequest `xorm:"-"`
  336. ResponseContent string `xorm:"TEXT"`
  337. ResponseInfo *HookResponse `xorm:"-"`
  338. }
  339. // BeforeUpdate will be invoked by XORM before updating a record
  340. // representing this object
  341. func (t *HookTask) BeforeUpdate() {
  342. if t.RequestInfo != nil {
  343. t.RequestContent = t.simpleMarshalJSON(t.RequestInfo)
  344. }
  345. if t.ResponseInfo != nil {
  346. t.ResponseContent = t.simpleMarshalJSON(t.ResponseInfo)
  347. }
  348. }
  349. // AfterSet updates the webhook object upon setting a column
  350. func (t *HookTask) AfterSet(colName string, _ xorm.Cell) {
  351. var err error
  352. switch colName {
  353. case "delivered":
  354. t.DeliveredString = time.Unix(0, t.Delivered).Format("2006-01-02 15:04:05 MST")
  355. case "request_content":
  356. if len(t.RequestContent) == 0 {
  357. return
  358. }
  359. t.RequestInfo = &HookRequest{}
  360. if err = json.Unmarshal([]byte(t.RequestContent), t.RequestInfo); err != nil {
  361. log.Error(3, "Unmarshal[%d]: %v", t.ID, err)
  362. }
  363. case "response_content":
  364. if len(t.ResponseContent) == 0 {
  365. return
  366. }
  367. t.ResponseInfo = &HookResponse{}
  368. if err = json.Unmarshal([]byte(t.ResponseContent), t.ResponseInfo); err != nil {
  369. log.Error(3, "Unmarshal [%d]: %v", t.ID, err)
  370. }
  371. }
  372. }
  373. func (t *HookTask) simpleMarshalJSON(v interface{}) string {
  374. p, err := json.Marshal(v)
  375. if err != nil {
  376. log.Error(3, "Marshal [%d]: %v", t.ID, err)
  377. }
  378. return string(p)
  379. }
  380. // HookTasks returns a list of hook tasks by given conditions.
  381. func HookTasks(hookID int64, page int) ([]*HookTask, error) {
  382. tasks := make([]*HookTask, 0, setting.Webhook.PagingNum)
  383. return tasks, x.
  384. Limit(setting.Webhook.PagingNum, (page-1)*setting.Webhook.PagingNum).
  385. Where("hook_id=?", hookID).
  386. Desc("id").
  387. Find(&tasks)
  388. }
  389. // CreateHookTask creates a new hook task,
  390. // it handles conversion from Payload to PayloadContent.
  391. func CreateHookTask(t *HookTask) error {
  392. data, err := t.Payloader.JSONPayload()
  393. if err != nil {
  394. return err
  395. }
  396. t.UUID = gouuid.NewV4().String()
  397. t.PayloadContent = string(data)
  398. _, err = x.Insert(t)
  399. return err
  400. }
  401. // UpdateHookTask updates information of hook task.
  402. func UpdateHookTask(t *HookTask) error {
  403. _, err := x.Id(t.ID).AllCols().Update(t)
  404. return err
  405. }
  406. // PrepareWebhooks adds new webhooks to task queue for given payload.
  407. func PrepareWebhooks(repo *Repository, event HookEventType, p api.Payloader) error {
  408. ws, err := GetActiveWebhooksByRepoID(repo.ID)
  409. if err != nil {
  410. return fmt.Errorf("GetActiveWebhooksByRepoID: %v", err)
  411. }
  412. // check if repo belongs to org and append additional webhooks
  413. if repo.MustOwner().IsOrganization() {
  414. // get hooks for org
  415. orgHooks, err := GetActiveWebhooksByOrgID(repo.OwnerID)
  416. if err != nil {
  417. return fmt.Errorf("GetActiveWebhooksByOrgID: %v", err)
  418. }
  419. ws = append(ws, orgHooks...)
  420. }
  421. if len(ws) == 0 {
  422. return nil
  423. }
  424. var payloader api.Payloader
  425. for _, w := range ws {
  426. switch event {
  427. case HookEventCreate:
  428. if !w.HasCreateEvent() {
  429. continue
  430. }
  431. case HookEventPush:
  432. if !w.HasPushEvent() {
  433. continue
  434. }
  435. case HookEventPullRequest:
  436. if !w.HasPullRequestEvent() {
  437. continue
  438. }
  439. }
  440. // Use separate objects so modifications won't be made on payload on non-Gogs/Gitea type hooks.
  441. switch w.HookTaskType {
  442. case SLACK:
  443. payloader, err = GetSlackPayload(p, event, w.Meta)
  444. if err != nil {
  445. return fmt.Errorf("GetSlackPayload: %v", err)
  446. }
  447. default:
  448. p.SetSecret(w.Secret)
  449. payloader = p
  450. }
  451. if err = CreateHookTask(&HookTask{
  452. RepoID: repo.ID,
  453. HookID: w.ID,
  454. Type: w.HookTaskType,
  455. URL: w.URL,
  456. Payloader: payloader,
  457. ContentType: w.ContentType,
  458. EventType: event,
  459. IsSSL: w.IsSSL,
  460. }); err != nil {
  461. return fmt.Errorf("CreateHookTask: %v", err)
  462. }
  463. }
  464. return nil
  465. }
  466. func (t *HookTask) deliver() {
  467. t.IsDelivered = true
  468. timeout := time.Duration(setting.Webhook.DeliverTimeout) * time.Second
  469. req := httplib.Post(t.URL).SetTimeout(timeout, timeout).
  470. Header("X-Gitea-Delivery", t.UUID).
  471. Header("X-Gitea-Event", string(t.EventType)).
  472. Header("X-Gogs-Delivery", t.UUID).
  473. Header("X-Gogs-Event", string(t.EventType)).
  474. Header("X-GitHub-Delivery", t.UUID).
  475. Header("X-GitHub-Event", string(t.EventType)).
  476. SetTLSClientConfig(&tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify})
  477. switch t.ContentType {
  478. case ContentTypeJSON:
  479. req = req.Header("Content-Type", "application/json").Body(t.PayloadContent)
  480. case ContentTypeForm:
  481. req.Param("payload", t.PayloadContent)
  482. }
  483. // Record delivery information.
  484. t.RequestInfo = &HookRequest{
  485. Headers: map[string]string{},
  486. }
  487. for k, vals := range req.Headers() {
  488. t.RequestInfo.Headers[k] = strings.Join(vals, ",")
  489. }
  490. t.ResponseInfo = &HookResponse{
  491. Headers: map[string]string{},
  492. }
  493. defer func() {
  494. t.Delivered = time.Now().UnixNano()
  495. if t.IsSucceed {
  496. log.Trace("Hook delivered: %s", t.UUID)
  497. } else {
  498. log.Trace("Hook delivery failed: %s", t.UUID)
  499. }
  500. // Update webhook last delivery status.
  501. w, err := GetWebhookByRepoID(t.RepoID, t.HookID)
  502. if err != nil {
  503. log.Error(5, "GetWebhookByID: %v", err)
  504. return
  505. }
  506. if t.IsSucceed {
  507. w.LastStatus = HookStatusSucceed
  508. } else {
  509. w.LastStatus = HookStatusFail
  510. }
  511. if err = UpdateWebhook(w); err != nil {
  512. log.Error(5, "UpdateWebhook: %v", err)
  513. return
  514. }
  515. }()
  516. resp, err := req.Response()
  517. if err != nil {
  518. t.ResponseInfo.Body = fmt.Sprintf("Delivery: %v", err)
  519. return
  520. }
  521. defer resp.Body.Close()
  522. // Status code is 20x can be seen as succeed.
  523. t.IsSucceed = resp.StatusCode/100 == 2
  524. t.ResponseInfo.Status = resp.StatusCode
  525. for k, vals := range resp.Header {
  526. t.ResponseInfo.Headers[k] = strings.Join(vals, ",")
  527. }
  528. p, err := ioutil.ReadAll(resp.Body)
  529. if err != nil {
  530. t.ResponseInfo.Body = fmt.Sprintf("read body: %s", err)
  531. return
  532. }
  533. t.ResponseInfo.Body = string(p)
  534. }
  535. // DeliverHooks checks and delivers undelivered hooks.
  536. // TODO: shoot more hooks at same time.
  537. func DeliverHooks() {
  538. tasks := make([]*HookTask, 0, 10)
  539. err := x.Where("is_delivered=?", false).Find(&tasks)
  540. if err != nil {
  541. log.Error(4, "DeliverHooks: %v", err)
  542. return
  543. }
  544. // Update hook task status.
  545. for _, t := range tasks {
  546. t.deliver()
  547. if err := UpdateHookTask(t); err != nil {
  548. log.Error(4, "UpdateHookTask [%d]: %v", t.ID, err)
  549. }
  550. }
  551. // Start listening on new hook requests.
  552. for repoID := range HookQueue.Queue() {
  553. log.Trace("DeliverHooks [repo_id: %v]", repoID)
  554. HookQueue.Remove(repoID)
  555. tasks = make([]*HookTask, 0, 5)
  556. if err := x.Where("repo_id=? AND is_delivered=?", repoID, false).Find(&tasks); err != nil {
  557. log.Error(4, "Get repository [%s] hook tasks: %v", repoID, err)
  558. continue
  559. }
  560. for _, t := range tasks {
  561. t.deliver()
  562. if err := UpdateHookTask(t); err != nil {
  563. log.Error(4, "UpdateHookTask [%d]: %v", t.ID, err)
  564. continue
  565. }
  566. }
  567. }
  568. }
  569. // InitDeliverHooks starts the hooks delivery thread
  570. func InitDeliverHooks() {
  571. go DeliverHooks()
  572. }