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