263 lines · 7.6 KB · Go
RawDownload
// Copyright 2017 The Gitea Authors. All rights reserved.// SPDX-License-Identifier: MIT package webhook import (	"context"	"errors"	"time" 	"forgejo.org/models/db"	"forgejo.org/modules/json"	"forgejo.org/modules/log"	"forgejo.org/modules/setting"	"forgejo.org/modules/timeutil"	webhook_module "forgejo.org/modules/webhook" 	gouuid "github.com/google/uuid"	"xorm.io/builder") //   ___ ___                __   ___________              __//  /   |   \  ____   ____ |  | _\__    ___/____    _____|  | __// /    ~    \/  _ \ /  _ \|  |/ / |    |  \__  \  /  ___/  |/ /// \    Y    (  <_> |  <_> )    <  |    |   / __ \_\___ \|    <//  \___|_  / \____/ \____/|__|_ \ |____|  (____  /____  >__|_ \//        \/                    \/              \/     \/     \/ // HookRequest represents hook task request information.type HookRequest struct {	URL        string            `json:"url"`	HTTPMethod string            `json:"http_method"`	Headers    map[string]string `json:"headers"`	Body       string            `json:"body"`} // HookResponse represents hook task response information.type HookResponse struct {	Status  int               `json:"status"`	Headers map[string]string `json:"headers"`	Body    string            `json:"body"`} // HookTask represents a hook task.type HookTask struct {	ID             int64  `xorm:"pk autoincr"`	HookID         int64  `xorm:"index"`	UUID           string `xorm:"unique"`	PayloadContent string `xorm:"LONGTEXT"`	// PayloadVersion number to allow for smooth version upgrades:	//  - PayloadVersion 1: PayloadContent contains the JSON as sent to the URL	//  - PayloadVersion 2: PayloadContent contains the original event	PayloadVersion int `xorm:"DEFAULT 1"` 	EventType   webhook_module.HookEventType	IsDelivered bool	Delivered   timeutil.TimeStampNano 	// History info.	IsSucceed       bool	RequestContent  string        `xorm:"LONGTEXT"`	RequestInfo     *HookRequest  `xorm:"-"`	ResponseContent string        `xorm:"LONGTEXT"`	ResponseInfo    *HookResponse `xorm:"-"`} func init() {	db.RegisterModel(new(HookTask))} // BeforeUpdate will be invoked by XORM before updating a record// representing this objectfunc (t *HookTask) BeforeUpdate() {	if t.RequestInfo != nil {		t.RequestContent = t.simpleMarshalJSON(t.RequestInfo)	}	if t.ResponseInfo != nil {		t.ResponseContent = t.simpleMarshalJSON(t.ResponseInfo)	}} // AfterLoad updates the webhook object upon setting a columnfunc (t *HookTask) AfterLoad() {	if len(t.RequestContent) == 0 {		return	} 	t.RequestInfo = &HookRequest{}	if err := json.Unmarshal([]byte(t.RequestContent), t.RequestInfo); err != nil {		log.Error("Unmarshal RequestContent[%d]: %v", t.ID, err)	} 	if len(t.ResponseContent) > 0 {		t.ResponseInfo = &HookResponse{}		if err := json.Unmarshal([]byte(t.ResponseContent), t.ResponseInfo); err != nil {			log.Error("Unmarshal ResponseContent[%d]: %v", t.ID, err)		}	}} func (t *HookTask) simpleMarshalJSON(v any) string {	p, err := json.Marshal(v)	if err != nil {		log.Error("Marshal [%d]: %v", t.ID, err)	}	return string(p)} // HookTasks returns a list of hook tasks by given conditions, order by ID desc.func HookTasks(ctx context.Context, hookID int64, page int) ([]*HookTask, error) {	tasks := make([]*HookTask, 0, setting.Webhook.PagingNum)	return tasks, db.GetEngine(ctx).		Limit(setting.Webhook.PagingNum, (page-1)*setting.Webhook.PagingNum).		Where("hook_id=?", hookID).		Desc("id").		Find(&tasks)} // CreateHookTask creates a new hook task,// it handles conversion from Payload to PayloadContent.func CreateHookTask(ctx context.Context, t *HookTask) (*HookTask, error) {	t.UUID = gouuid.New().String()	if t.Delivered == 0 {		t.Delivered = timeutil.TimeStampNanoNow()	}	if t.PayloadVersion == 0 {		return nil, errors.New("missing HookTask.PayloadVersion")	}	return t, db.Insert(ctx, t)} func GetHookTaskByID(ctx context.Context, id int64) (*HookTask, error) {	t := &HookTask{} 	has, err := db.GetEngine(ctx).ID(id).Get(t)	if err != nil {		return nil, err	}	if !has {		return nil, ErrHookTaskNotExist{			TaskID: id,		}	}	return t, nil} // UpdateHookTask updates information of hook task.func UpdateHookTask(ctx context.Context, t *HookTask) error {	_, err := db.GetEngine(ctx).ID(t.ID).AllCols().Update(t)	return err} // ReplayHookTask copies a hook task to get re-deliveredfunc ReplayHookTask(ctx context.Context, hookID int64, uuid string) (*HookTask, error) {	task, exist, err := db.Get[HookTask](ctx, builder.Eq{"hook_id": hookID, "uuid": uuid})	if err != nil {		return nil, err	} else if !exist {		return nil, ErrHookTaskNotExist{			HookID: hookID,			UUID:   uuid,		}	} 	return CreateHookTask(ctx, &HookTask{		HookID:         task.HookID,		PayloadContent: task.PayloadContent,		EventType:      task.EventType,		PayloadVersion: task.PayloadVersion,	})} // FindUndeliveredHookTaskIDs will find the next 100 undelivered hook tasks with ID greater than the provided lowerIDfunc FindUndeliveredHookTaskIDs(ctx context.Context, lowerID int64) ([]int64, error) {	const batchSize = 100 	tasks := make([]int64, 0, batchSize)	return tasks, db.GetEngine(ctx).		Select("id").		Table(new(HookTask)).		Where("is_delivered=?", false).		And("id > ?", lowerID).		Asc("id").		Limit(batchSize).		Find(&tasks)} func MarkTaskDelivered(ctx context.Context, task *HookTask) (bool, error) {	count, err := db.GetEngine(ctx).ID(task.ID).Where("is_delivered = ?", false).Cols("is_delivered").Update(&HookTask{		ID:          task.ID,		IsDelivered: true,	}) 	return count != 0, err} // CleanupHookTaskTable deletes rows from hook_task as needed.func CleanupHookTaskTable(ctx context.Context, cleanupType HookTaskCleanupType, olderThan time.Duration, numberToKeep int) error {	log.Trace("Doing: CleanupHookTaskTable") 	switch cleanupType {	case OlderThan:		deleteOlderThan := time.Now().Add(-olderThan).UnixNano()		deletes, err := db.GetEngine(ctx).			Where("is_delivered = ? and delivered < ?", true, deleteOlderThan).			Delete(new(HookTask))		if err != nil {			return err		}		log.Trace("Deleted %d rows from hook_task", deletes)	case PerWebhook:		hookIDs := make([]int64, 0, 10)		err := db.GetEngine(ctx).			Table("webhook").			Where("id > 0").			Cols("id").			Find(&hookIDs)		if err != nil {			return err		}		for _, hookID := range hookIDs {			select {			case <-ctx.Done():				return db.ErrCancelledf("Before deleting hook_task records for hook id %d", hookID)			default:			}			if err = deleteDeliveredHookTasksByWebhook(ctx, hookID, numberToKeep); err != nil {				return err			}		}	}	log.Trace("Finished: CleanupHookTaskTable")	return nil} func deleteDeliveredHookTasksByWebhook(ctx context.Context, hookID int64, numberDeliveriesToKeep int) error {	log.Trace("Deleting hook_task rows for webhook %d, keeping the most recent %d deliveries", hookID, numberDeliveriesToKeep)	deliveryDates := make([]int64, 0, 10)	err := db.GetEngine(ctx).Table("hook_task").		Where("hook_task.hook_id = ? AND hook_task.is_delivered = ? AND hook_task.delivered is not null", hookID, true).		Cols("hook_task.delivered").		Join("INNER", "webhook", "hook_task.hook_id = webhook.id").		OrderBy("hook_task.delivered desc").		Limit(1, numberDeliveriesToKeep).		Find(&deliveryDates)	if err != nil {		return err	} 	if len(deliveryDates) > 0 {		deletes, err := db.GetEngine(ctx).			Where("hook_id = ? and is_delivered = ? and delivered <= ?", hookID, true, deliveryDates[0]).			Delete(new(HookTask))		if err != nil {			return err		}		log.Trace("Deleted %d hook_task rows for webhook %d", deletes, hookID)	} else {		log.Trace("No hook_task rows to delete for webhook %d", hookID)	} 	return nil}