code.gitea.io/gitea@v1.21.7/models/webhook/hooktask.go (about) 1 // Copyright 2017 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package webhook 5 6 import ( 7 "context" 8 "time" 9 10 "code.gitea.io/gitea/models/db" 11 "code.gitea.io/gitea/modules/json" 12 "code.gitea.io/gitea/modules/log" 13 "code.gitea.io/gitea/modules/setting" 14 api "code.gitea.io/gitea/modules/structs" 15 "code.gitea.io/gitea/modules/timeutil" 16 webhook_module "code.gitea.io/gitea/modules/webhook" 17 18 gouuid "github.com/google/uuid" 19 ) 20 21 // ___ ___ __ ___________ __ 22 // / | \ ____ ____ | | _\__ ___/____ _____| | __ 23 // / ~ \/ _ \ / _ \| |/ / | | \__ \ / ___/ |/ / 24 // \ Y ( <_> | <_> ) < | | / __ \_\___ \| < 25 // \___|_ / \____/ \____/|__|_ \ |____| (____ /____ >__|_ \ 26 // \/ \/ \/ \/ \/ 27 28 // HookRequest represents hook task request information. 29 type HookRequest struct { 30 URL string `json:"url"` 31 HTTPMethod string `json:"http_method"` 32 Headers map[string]string `json:"headers"` 33 } 34 35 // HookResponse represents hook task response information. 36 type HookResponse struct { 37 Status int `json:"status"` 38 Headers map[string]string `json:"headers"` 39 Body string `json:"body"` 40 } 41 42 // HookTask represents a hook task. 43 type HookTask struct { 44 ID int64 `xorm:"pk autoincr"` 45 HookID int64 `xorm:"index"` 46 UUID string `xorm:"unique"` 47 api.Payloader `xorm:"-"` 48 PayloadContent string `xorm:"LONGTEXT"` 49 EventType webhook_module.HookEventType 50 IsDelivered bool 51 Delivered timeutil.TimeStampNano 52 53 // History info. 54 IsSucceed bool 55 RequestContent string `xorm:"LONGTEXT"` 56 RequestInfo *HookRequest `xorm:"-"` 57 ResponseContent string `xorm:"LONGTEXT"` 58 ResponseInfo *HookResponse `xorm:"-"` 59 } 60 61 func init() { 62 db.RegisterModel(new(HookTask)) 63 } 64 65 // BeforeUpdate will be invoked by XORM before updating a record 66 // representing this object 67 func (t *HookTask) BeforeUpdate() { 68 if t.RequestInfo != nil { 69 t.RequestContent = t.simpleMarshalJSON(t.RequestInfo) 70 } 71 if t.ResponseInfo != nil { 72 t.ResponseContent = t.simpleMarshalJSON(t.ResponseInfo) 73 } 74 } 75 76 // AfterLoad updates the webhook object upon setting a column 77 func (t *HookTask) AfterLoad() { 78 if len(t.RequestContent) == 0 { 79 return 80 } 81 82 t.RequestInfo = &HookRequest{} 83 if err := json.Unmarshal([]byte(t.RequestContent), t.RequestInfo); err != nil { 84 log.Error("Unmarshal RequestContent[%d]: %v", t.ID, err) 85 } 86 87 if len(t.ResponseContent) > 0 { 88 t.ResponseInfo = &HookResponse{} 89 if err := json.Unmarshal([]byte(t.ResponseContent), t.ResponseInfo); err != nil { 90 log.Error("Unmarshal ResponseContent[%d]: %v", t.ID, err) 91 } 92 } 93 } 94 95 func (t *HookTask) simpleMarshalJSON(v any) string { 96 p, err := json.Marshal(v) 97 if err != nil { 98 log.Error("Marshal [%d]: %v", t.ID, err) 99 } 100 return string(p) 101 } 102 103 // HookTasks returns a list of hook tasks by given conditions. 104 func HookTasks(hookID int64, page int) ([]*HookTask, error) { 105 tasks := make([]*HookTask, 0, setting.Webhook.PagingNum) 106 return tasks, db.GetEngine(db.DefaultContext). 107 Limit(setting.Webhook.PagingNum, (page-1)*setting.Webhook.PagingNum). 108 Where("hook_id=?", hookID). 109 Desc("id"). 110 Find(&tasks) 111 } 112 113 // CreateHookTask creates a new hook task, 114 // it handles conversion from Payload to PayloadContent. 115 func CreateHookTask(ctx context.Context, t *HookTask) (*HookTask, error) { 116 t.UUID = gouuid.New().String() 117 if t.Payloader != nil { 118 data, err := t.Payloader.JSONPayload() 119 if err != nil { 120 return nil, err 121 } 122 t.PayloadContent = string(data) 123 } 124 if t.Delivered == 0 { 125 t.Delivered = timeutil.TimeStampNanoNow() 126 } 127 return t, db.Insert(ctx, t) 128 } 129 130 func GetHookTaskByID(ctx context.Context, id int64) (*HookTask, error) { 131 t := &HookTask{} 132 133 has, err := db.GetEngine(ctx).ID(id).Get(t) 134 if err != nil { 135 return nil, err 136 } 137 if !has { 138 return nil, ErrHookTaskNotExist{ 139 TaskID: id, 140 } 141 } 142 return t, nil 143 } 144 145 // UpdateHookTask updates information of hook task. 146 func UpdateHookTask(t *HookTask) error { 147 _, err := db.GetEngine(db.DefaultContext).ID(t.ID).AllCols().Update(t) 148 return err 149 } 150 151 // ReplayHookTask copies a hook task to get re-delivered 152 func ReplayHookTask(ctx context.Context, hookID int64, uuid string) (*HookTask, error) { 153 task := &HookTask{ 154 HookID: hookID, 155 UUID: uuid, 156 } 157 has, err := db.GetByBean(ctx, task) 158 if err != nil { 159 return nil, err 160 } else if !has { 161 return nil, ErrHookTaskNotExist{ 162 HookID: hookID, 163 UUID: uuid, 164 } 165 } 166 167 return CreateHookTask(ctx, &HookTask{ 168 HookID: task.HookID, 169 PayloadContent: task.PayloadContent, 170 EventType: task.EventType, 171 }) 172 } 173 174 // FindUndeliveredHookTaskIDs will find the next 100 undelivered hook tasks with ID greater than the provided lowerID 175 func FindUndeliveredHookTaskIDs(ctx context.Context, lowerID int64) ([]int64, error) { 176 const batchSize = 100 177 178 tasks := make([]int64, 0, batchSize) 179 return tasks, db.GetEngine(ctx). 180 Select("id"). 181 Table(new(HookTask)). 182 Where("is_delivered=?", false). 183 And("id > ?", lowerID). 184 Asc("id"). 185 Limit(batchSize). 186 Find(&tasks) 187 } 188 189 func MarkTaskDelivered(ctx context.Context, task *HookTask) (bool, error) { 190 count, err := db.GetEngine(ctx).ID(task.ID).Where("is_delivered = ?", false).Cols("is_delivered").Update(&HookTask{ 191 ID: task.ID, 192 IsDelivered: true, 193 }) 194 195 return count != 0, err 196 } 197 198 // CleanupHookTaskTable deletes rows from hook_task as needed. 199 func CleanupHookTaskTable(ctx context.Context, cleanupType HookTaskCleanupType, olderThan time.Duration, numberToKeep int) error { 200 log.Trace("Doing: CleanupHookTaskTable") 201 202 if cleanupType == OlderThan { 203 deleteOlderThan := time.Now().Add(-olderThan).UnixNano() 204 deletes, err := db.GetEngine(ctx). 205 Where("is_delivered = ? and delivered < ?", true, deleteOlderThan). 206 Delete(new(HookTask)) 207 if err != nil { 208 return err 209 } 210 log.Trace("Deleted %d rows from hook_task", deletes) 211 } else if cleanupType == PerWebhook { 212 hookIDs := make([]int64, 0, 10) 213 err := db.GetEngine(ctx). 214 Table("webhook"). 215 Where("id > 0"). 216 Cols("id"). 217 Find(&hookIDs) 218 if err != nil { 219 return err 220 } 221 for _, hookID := range hookIDs { 222 select { 223 case <-ctx.Done(): 224 return db.ErrCancelledf("Before deleting hook_task records for hook id %d", hookID) 225 default: 226 } 227 if err = deleteDeliveredHookTasksByWebhook(ctx, hookID, numberToKeep); err != nil { 228 return err 229 } 230 } 231 } 232 log.Trace("Finished: CleanupHookTaskTable") 233 return nil 234 } 235 236 func deleteDeliveredHookTasksByWebhook(ctx context.Context, hookID int64, numberDeliveriesToKeep int) error { 237 log.Trace("Deleting hook_task rows for webhook %d, keeping the most recent %d deliveries", hookID, numberDeliveriesToKeep) 238 deliveryDates := make([]int64, 0, 10) 239 err := db.GetEngine(ctx).Table("hook_task"). 240 Where("hook_task.hook_id = ? AND hook_task.is_delivered = ? AND hook_task.delivered is not null", hookID, true). 241 Cols("hook_task.delivered"). 242 Join("INNER", "webhook", "hook_task.hook_id = webhook.id"). 243 OrderBy("hook_task.delivered desc"). 244 Limit(1, numberDeliveriesToKeep). 245 Find(&deliveryDates) 246 if err != nil { 247 return err 248 } 249 250 if len(deliveryDates) > 0 { 251 deletes, err := db.GetEngine(ctx). 252 Where("hook_id = ? and is_delivered = ? and delivered <= ?", hookID, true, deliveryDates[0]). 253 Delete(new(HookTask)) 254 if err != nil { 255 return err 256 } 257 log.Trace("Deleted %d hook_task rows for webhook %d", deletes, hookID) 258 } else { 259 log.Trace("No hook_task rows to delete for webhook %d", hookID) 260 } 261 262 return nil 263 }