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  }