chore: implement webhook dispatch in api v1

This commit is contained in:
Steven 2023-11-25 10:31:58 +08:00
parent db95b94c9a
commit bc965f6afa
4 changed files with 202 additions and 1 deletions

View file

@ -14,6 +14,7 @@ import (
"github.com/usememos/memos/internal/log" "github.com/usememos/memos/internal/log"
"github.com/usememos/memos/internal/util" "github.com/usememos/memos/internal/util"
"github.com/usememos/memos/plugin/webhook"
storepb "github.com/usememos/memos/proto/gen/store" storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/server/service/metric" "github.com/usememos/memos/server/service/metric"
"github.com/usememos/memos/store" "github.com/usememos/memos/store"
@ -392,7 +393,7 @@ func (s *APIV1Service) CreateMemo(c echo.Context) error {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
} }
// send notification by telegram bot if memo is not Private // Send notification to telegram if memo is not private.
if memoResponse.Visibility != Private { if memoResponse.Visibility != Private {
// fetch all telegram UserID // fetch all telegram UserID
userSettings, err := s.Store.ListUserSettings(ctx, &store.FindUserSetting{Key: UserSettingTelegramUserIDKey.String()}) userSettings, err := s.Store.ListUserSettings(ctx, &store.FindUserSetting{Key: UserSettingTelegramUserIDKey.String()})
@ -423,6 +424,11 @@ func (s *APIV1Service) CreateMemo(c echo.Context) error {
} }
} }
} }
// Try to dispatch webhook when memo is created.
if err := s.DispatchMemoCreatedWebhook(ctx, memoResponse); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to dispatch memo created webhook").SetInternal(err)
}
metric.Enqueue("memo create") metric.Enqueue("memo create")
return c.JSON(http.StatusOK, memoResponse) return c.JSON(http.StatusOK, memoResponse)
} }
@ -795,6 +801,11 @@ func (s *APIV1Service) UpdateMemo(c echo.Context) error {
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
} }
// Try to dispatch webhook when memo is created.
if err := s.DispatchMemoCreatedWebhook(ctx, memoResponse); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to dispatch memo created webhook").SetInternal(err)
}
return c.JSON(http.StatusOK, memoResponse) return c.JSON(http.StatusOK, memoResponse)
} }
@ -946,3 +957,76 @@ func getIDListDiff(oldList, newList []int32) (addedList, removedList []int32) {
} }
return addedList, removedList return addedList, removedList
} }
// DispatchMemoCreatedWebhook dispatches webhook when memo is created.
func (s *APIV1Service) DispatchMemoCreatedWebhook(ctx context.Context, memo *Memo) error {
return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.created")
}
// DispatchMemoUpdatedWebhook dispatches webhook when memo is updated.
func (s *APIV1Service) DispatchMemoUpdatedWebhook(ctx context.Context, memo *Memo) error {
return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.updated")
}
func (s *APIV1Service) dispatchMemoRelatedWebhook(ctx context.Context, memo *Memo, activityType string) error {
webhooks, err := s.Store.ListWebhooks(ctx, &store.FindWebhook{
CreatorID: &memo.CreatorID,
})
if err != nil {
return err
}
for _, wb := range webhooks {
payload := convertMemoToWebhookPayload(memo)
payload.ActivityType = activityType
payload.URL = wb.Url
err := webhook.Post(*payload)
if err != nil {
return errors.Wrap(err, "failed to post webhook")
}
}
return nil
}
func convertMemoToWebhookPayload(memo *Memo) *webhook.WebhookPayload {
return &webhook.WebhookPayload{
CreatorID: memo.CreatorID,
CreatedTs: time.Now().Unix(),
Memo: &webhook.Memo{
ID: memo.ID,
CreatorID: memo.CreatorID,
CreatedTs: memo.CreatedTs,
UpdatedTs: memo.UpdatedTs,
Content: memo.Content,
Visibility: memo.Visibility.String(),
Pinned: memo.Pinned,
ResourceList: func() []*webhook.Resource {
resources := []*webhook.Resource{}
for _, resource := range memo.ResourceList {
resources = append(resources, &webhook.Resource{
ID: resource.ID,
CreatorID: resource.CreatorID,
CreatedTs: resource.CreatedTs,
UpdatedTs: resource.UpdatedTs,
Filename: resource.Filename,
InternalPath: resource.InternalPath,
ExternalLink: resource.ExternalLink,
Type: resource.Type,
Size: resource.Size,
})
}
return resources
}(),
RelationList: func() []*webhook.MemoRelation {
relations := []*webhook.MemoRelation{}
for _, relation := range memo.RelationList {
relations = append(relations, &webhook.MemoRelation{
MemoID: relation.MemoID,
RelatedMemoID: relation.RelatedMemoID,
Type: relation.Type.String(),
})
}
return relations
}(),
},
}
}

View file

@ -18,6 +18,10 @@ const (
MemoRelationComment MemoRelationType = "COMMENT" MemoRelationComment MemoRelationType = "COMMENT"
) )
func (t MemoRelationType) String() string {
return string(t)
}
type MemoRelation struct { type MemoRelation struct {
MemoID int32 `json:"memoId"` MemoID int32 `json:"memoId"`
RelatedMemoID int32 `json:"relatedMemoId"` RelatedMemoID int32 `json:"relatedMemoId"`

112
plugin/webhook/webhook.go Normal file
View file

@ -0,0 +1,112 @@
package webhook
import (
"bytes"
"encoding/json"
"io"
"net/http"
"time"
"github.com/pkg/errors"
)
var (
// timeout is the timeout for webhook request. Default to 30 seconds.
timeout = 30 * time.Second
)
type Memo struct {
ID int32 `json:"id"`
CreatorID int32 `json:"creatorId"`
CreatedTs int64 `json:"createdTs"`
UpdatedTs int64 `json:"updatedTs"`
// Domain specific fields
Content string `json:"content"`
Visibility string `json:"visibility"`
Pinned bool `json:"pinned"`
ResourceList []*Resource `json:"resourceList"`
RelationList []*MemoRelation `json:"relationList"`
}
type Resource struct {
ID int32 `json:"id"`
// Standard fields
CreatorID int32 `json:"creatorId"`
CreatedTs int64 `json:"createdTs"`
UpdatedTs int64 `json:"updatedTs"`
// Domain specific fields
Filename string `json:"filename"`
InternalPath string `json:"internalPath"`
ExternalLink string `json:"externalLink"`
Type string `json:"type"`
Size int64 `json:"size"`
}
type MemoRelation struct {
MemoID int32 `json:"memoId"`
RelatedMemoID int32 `json:"relatedMemoId"`
Type string `json:"type"`
}
// WebhookPayload is the payload of webhook request.
// nolint
type WebhookPayload struct {
URL string `json:"url"`
ActivityType string `json:"activityType"`
CreatorID int32 `json:"creatorId"`
CreatedTs int64 `json:"createdTs"`
Memo *Memo `json:"memo"`
}
// WebhookResponse is the response of webhook request.
// nolint
type WebhookResponse struct {
Code int `json:"code"`
Message string `json:"message"`
}
// Post posts the message to webhook endpoint.
func Post(payload WebhookPayload) error {
body, err := json.Marshal(&payload)
if err != nil {
return errors.Wrapf(err, "failed to marshal webhook request to %s", payload.URL)
}
req, err := http.NewRequest("POST",
payload.URL, bytes.NewBuffer(body))
if err != nil {
return errors.Wrapf(err, "failed to construct webhook request to %s", payload.URL)
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{
Timeout: timeout,
}
resp, err := client.Do(req)
if err != nil {
return errors.Wrapf(err, "failed to post webhook to %s", payload.URL)
}
b, err := io.ReadAll(resp.Body)
if err != nil {
return errors.Wrapf(err, "failed to read webhook response from %s", payload.URL)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.Errorf("failed to post webhook %s, status code: %d, response body: %s", payload.URL, resp.StatusCode, b)
}
response := &WebhookResponse{}
if err := json.Unmarshal(b, response); err != nil {
return errors.Wrapf(err, "failed to unmarshal webhook response from %s", payload.URL)
}
if response.Code != 0 {
return errors.Errorf("receive error code sent by webhook server, code %d, msg: %s", response.Code, response.Message)
}
return nil
}

View file

@ -0,0 +1 @@
package webhook