mirror of
https://github.com/usememos/memos.git
synced 2024-12-27 15:42:51 +08:00
chore: implement webhook dispatch in api v1
This commit is contained in:
parent
db95b94c9a
commit
bc965f6afa
4 changed files with 202 additions and 1 deletions
|
@ -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
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
112
plugin/webhook/webhook.go
Normal 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
|
||||||
|
}
|
1
plugin/webhook/webhook_test.go
Normal file
1
plugin/webhook/webhook_test.go
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package webhook
|
Loading…
Reference in a new issue