2021-11-10 08:13:16 +03:00
// Copyright 2017 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2021-11-10 08:13:16 +03:00
package webhook
import (
"context"
2024-03-08 01:18:38 +03:00
"errors"
2021-11-10 08:13:16 +03:00
"time"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
2023-05-04 02:53:43 +03:00
"code.gitea.io/gitea/modules/timeutil"
2023-01-01 18:23:15 +03:00
webhook_module "code.gitea.io/gitea/modules/webhook"
2021-11-10 08:13:16 +03:00
gouuid "github.com/google/uuid"
2023-12-07 10:27:36 +03:00
"xorm.io/builder"
2021-11-10 08:13:16 +03:00
)
// ___ ___ __ ___________ __
// / | \ ____ ____ | | _\__ ___/____ _____| | __
// / ~ \/ _ \ / _ \| |/ / | | \__ \ / ___/ |/ /
// \ 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" `
2024-03-08 01:18:38 +03:00
Body string ` json:"body" `
2021-11-10 08:13:16 +03:00
}
// 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 {
2023-05-04 02:53:43 +03:00
ID int64 ` xorm:"pk autoincr" `
HookID int64 ` xorm:"index" `
UUID string ` xorm:"unique" `
PayloadContent string ` xorm:"LONGTEXT" `
2024-03-08 01:18:38 +03:00
// 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
2021-11-10 08:13:16 +03:00
// History info.
IsSucceed bool
2022-06-19 21:47:04 +03:00
RequestContent string ` xorm:"LONGTEXT" `
2021-11-10 08:13:16 +03:00
RequestInfo * HookRequest ` xorm:"-" `
2022-06-19 21:47:04 +03:00
ResponseContent string ` xorm:"LONGTEXT" `
2021-11-10 08:13:16 +03:00
ResponseInfo * HookResponse ` xorm:"-" `
}
func init ( ) {
db . RegisterModel ( new ( HookTask ) )
}
// BeforeUpdate will be invoked by XORM before updating a record
// representing this object
func ( 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 column
func ( 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 )
}
}
}
2023-07-04 21:36:08 +03:00
func ( t * HookTask ) simpleMarshalJSON ( v any ) string {
2021-11-10 08:13:16 +03:00
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.
2023-10-14 11:37:24 +03:00
func HookTasks ( ctx context . Context , hookID int64 , page int ) ( [ ] * HookTask , error ) {
2021-11-10 08:13:16 +03:00
tasks := make ( [ ] * HookTask , 0 , setting . Webhook . PagingNum )
2023-10-14 11:37:24 +03:00
return tasks , db . GetEngine ( ctx ) .
2021-11-10 08:13:16 +03:00
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.
2022-10-21 19:21:56 +03:00
func CreateHookTask ( ctx context . Context , t * HookTask ) ( * HookTask , error ) {
2021-11-10 08:13:16 +03:00
t . UUID = gouuid . New ( ) . String ( )
2023-05-04 02:53:43 +03:00
if t . Delivered == 0 {
t . Delivered = timeutil . TimeStampNanoNow ( )
}
2024-03-08 01:18:38 +03:00
if t . PayloadVersion == 0 {
return nil , errors . New ( "missing HookTask.PayloadVersion" )
}
2022-10-21 19:21:56 +03:00
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
2021-11-10 08:13:16 +03:00
}
// UpdateHookTask updates information of hook task.
2023-10-14 11:37:24 +03:00
func UpdateHookTask ( ctx context . Context , t * HookTask ) error {
_ , err := db . GetEngine ( ctx ) . ID ( t . ID ) . AllCols ( ) . Update ( t )
2021-11-10 08:13:16 +03:00
return err
}
2022-01-06 00:00:20 +03:00
// ReplayHookTask copies a hook task to get re-delivered
2022-10-21 19:21:56 +03:00
func ReplayHookTask ( ctx context . Context , hookID int64 , uuid string ) ( * HookTask , error ) {
2023-12-07 10:27:36 +03:00
task , exist , err := db . Get [ HookTask ] ( ctx , builder . Eq { "hook_id" : hookID , "uuid" : uuid } )
2022-10-21 19:21:56 +03:00
if err != nil {
return nil , err
2023-12-07 10:27:36 +03:00
} else if ! exist {
2022-10-21 19:21:56 +03:00
return nil , ErrHookTaskNotExist {
2022-01-06 00:00:20 +03:00
HookID : hookID ,
UUID : uuid ,
}
2022-10-21 19:21:56 +03:00
}
2022-01-06 00:00:20 +03:00
2023-05-04 02:53:43 +03:00
return CreateHookTask ( ctx , & HookTask {
2022-10-21 19:21:56 +03:00
HookID : task . HookID ,
PayloadContent : task . PayloadContent ,
EventType : task . EventType ,
2024-03-08 01:18:38 +03:00
PayloadVersion : task . PayloadVersion ,
2023-05-04 02:53:43 +03:00
} )
2022-01-06 00:00:20 +03:00
}
2022-11-23 17:10:04 +03:00
// FindUndeliveredHookTaskIDs will find the next 100 undelivered hook tasks with ID greater than the provided lowerID
func FindUndeliveredHookTaskIDs ( ctx context . Context , lowerID int64 ) ( [ ] int64 , error ) {
const batchSize = 100
tasks := make ( [ ] int64 , 0 , batchSize )
2022-10-21 19:21:56 +03:00
return tasks , db . GetEngine ( ctx ) .
2022-11-23 17:10:04 +03:00
Select ( "id" ) .
Table ( new ( HookTask ) ) .
2022-10-21 19:21:56 +03:00
Where ( "is_delivered=?" , false ) .
2022-11-23 17:10:04 +03:00
And ( "id > ?" , lowerID ) .
Asc ( "id" ) .
Limit ( batchSize ) .
2022-10-21 19:21:56 +03:00
Find ( & tasks )
2021-11-10 08:13:16 +03:00
}
2022-11-23 17:10:04 +03:00
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
}
2021-11-10 08:13:16 +03:00
// 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" )
if cleanupType == OlderThan {
deleteOlderThan := time . Now ( ) . Add ( - olderThan ) . UnixNano ( )
2022-10-21 19:21:56 +03:00
deletes , err := db . GetEngine ( ctx ) .
2021-11-10 08:13:16 +03:00
Where ( "is_delivered = ? and delivered < ?" , true , deleteOlderThan ) .
Delete ( new ( HookTask ) )
if err != nil {
return err
}
log . Trace ( "Deleted %d rows from hook_task" , deletes )
} else if cleanupType == PerWebhook {
hookIDs := make ( [ ] int64 , 0 , 10 )
2022-10-21 19:21:56 +03:00
err := db . GetEngine ( ctx ) .
Table ( "webhook" ) .
2021-11-10 08:13:16 +03:00
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 :
}
2022-10-28 14:05:39 +03:00
if err = deleteDeliveredHookTasksByWebhook ( ctx , hookID , numberToKeep ) ; err != nil {
2021-11-10 08:13:16 +03:00
return err
}
}
}
log . Trace ( "Finished: CleanupHookTaskTable" )
return nil
}
2022-10-28 14:05:39 +03:00
func deleteDeliveredHookTasksByWebhook ( ctx context . Context , hookID int64 , numberDeliveriesToKeep int ) error {
2021-11-10 08:13:16 +03:00
log . Trace ( "Deleting hook_task rows for webhook %d, keeping the most recent %d deliveries" , hookID , numberDeliveriesToKeep )
deliveryDates := make ( [ ] int64 , 0 , 10 )
2022-10-28 14:05:39 +03:00
err := db . GetEngine ( ctx ) . Table ( "hook_task" ) .
2021-11-10 08:13:16 +03:00
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" ) .
2022-06-20 13:02:49 +03:00
Limit ( 1 , numberDeliveriesToKeep ) .
2021-11-10 08:13:16 +03:00
Find ( & deliveryDates )
if err != nil {
return err
}
if len ( deliveryDates ) > 0 {
2022-10-28 14:05:39 +03:00
deletes , err := db . GetEngine ( ctx ) .
2021-11-10 08:13:16 +03:00
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
}