chore: implement webhook dispatch in v2 api

This commit is contained in:
Steven 2023-12-23 11:17:35 +08:00
parent c6ebb5552e
commit eefce6ade3
6 changed files with 298 additions and 149 deletions

View file

@ -0,0 +1,91 @@
package v2
import (
"context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
"github.com/usememos/memos/store"
)
func (s *APIV2Service) SetMemoRelations(ctx context.Context, request *apiv2pb.SetMemoRelationsRequest) (*apiv2pb.SetMemoRelationsResponse, error) {
referenceType := store.MemoRelationReference
// Delete all reference relations first.
if err := s.Store.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{
MemoID: &request.Id,
Type: &referenceType,
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete memo relation")
}
for _, relation := range request.Relations {
if _, err := s.Store.UpsertMemoRelation(ctx, &store.MemoRelation{
MemoID: request.Id,
RelatedMemoID: relation.RelatedMemoId,
Type: convertMemoRelationTypeToStore(relation.Type),
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to upsert memo relation")
}
}
return &apiv2pb.SetMemoRelationsResponse{}, nil
}
func (s *APIV2Service) ListMemoRelations(ctx context.Context, request *apiv2pb.ListMemoRelationsRequest) (*apiv2pb.ListMemoRelationsResponse, error) {
relationList := []*apiv2pb.MemoRelation{}
tempList, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{
MemoID: &request.Id,
})
if err != nil {
return nil, err
}
for _, relation := range tempList {
relationList = append(relationList, convertMemoRelationFromStore(relation))
}
tempList, err = s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{
RelatedMemoID: &request.Id,
})
if err != nil {
return nil, err
}
for _, relation := range tempList {
relationList = append(relationList, convertMemoRelationFromStore(relation))
}
response := &apiv2pb.ListMemoRelationsResponse{
Relations: relationList,
}
return response, nil
}
func convertMemoRelationFromStore(memoRelation *store.MemoRelation) *apiv2pb.MemoRelation {
return &apiv2pb.MemoRelation{
MemoId: memoRelation.MemoID,
RelatedMemoId: memoRelation.RelatedMemoID,
Type: convertMemoRelationTypeFromStore(memoRelation.Type),
}
}
func convertMemoRelationTypeFromStore(relationType store.MemoRelationType) apiv2pb.MemoRelation_Type {
switch relationType {
case store.MemoRelationReference:
return apiv2pb.MemoRelation_REFERENCE
case store.MemoRelationComment:
return apiv2pb.MemoRelation_COMMENT
default:
return apiv2pb.MemoRelation_TYPE_UNSPECIFIED
}
}
func convertMemoRelationTypeToStore(relationType apiv2pb.MemoRelation_Type) store.MemoRelationType {
switch relationType {
case apiv2pb.MemoRelation_REFERENCE:
return store.MemoRelationReference
case apiv2pb.MemoRelation_COMMENT:
return store.MemoRelationComment
default:
return store.MemoRelationReference
}
}

View file

@ -0,0 +1,66 @@
package v2
import (
"context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
"github.com/usememos/memos/store"
)
func (s *APIV2Service) SetMemoResources(ctx context.Context, request *apiv2pb.SetMemoResourcesRequest) (*apiv2pb.SetMemoResourcesResponse, error) {
resources, err := s.Store.ListResources(ctx, &store.FindResource{MemoID: &request.Id})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list resources")
}
// Delete resources that are not in the request.
for _, resource := range resources {
found := false
for _, requestResource := range request.Resources {
if resource.ID == int32(requestResource.Id) {
found = true
break
}
}
if !found {
if err = s.Store.DeleteResource(ctx, &store.DeleteResource{
ID: int32(resource.ID),
MemoID: &request.Id,
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete resource")
}
}
}
// Update resources' memo_id in the request.
for _, resource := range request.Resources {
if _, err := s.Store.UpdateResource(ctx, &store.UpdateResource{
ID: resource.Id,
MemoID: &request.Id,
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to update resource")
}
}
return &apiv2pb.SetMemoResourcesResponse{}, nil
}
func (s *APIV2Service) ListMemoResources(ctx context.Context, request *apiv2pb.ListMemoResourcesRequest) (*apiv2pb.ListMemoResourcesResponse, error) {
resources, err := s.Store.ListResources(ctx, &store.FindResource{
MemoID: &request.Id,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list resources")
}
response := &apiv2pb.ListMemoResourcesResponse{
Resources: []*apiv2pb.Resource{},
}
for _, resource := range resources {
response.Resources = append(response.Resources, s.convertResourceFromStore(ctx, resource))
}
return response, nil
}

View file

@ -8,15 +8,20 @@ import (
"github.com/google/cel-go/cel"
"github.com/pkg/errors"
"go.uber.org/zap"
expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
apiv1 "github.com/usememos/memos/api/v1"
"github.com/usememos/memos/internal/log"
"github.com/usememos/memos/plugin/gomark/parser"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
"github.com/usememos/memos/plugin/webhook"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/server/service/metric"
"github.com/usememos/memos/store"
)
@ -38,11 +43,17 @@ func (s *APIV2Service) CreateMemo(ctx context.Context, request *apiv2pb.CreateMe
if err != nil {
return nil, err
}
metric.Enqueue("memo create")
memoMessage, err := s.convertMemoFromStore(ctx, memo)
if err != nil {
return nil, errors.Wrap(err, "failed to convert memo")
}
// Try to dispatch webhook when memo is created.
if err := s.DispatchMemoCreatedWebhook(ctx, memoMessage); err != nil {
log.Warn("Failed to dispatch memo created webhook", zap.Error(err))
}
response := &apiv2pb.CreateMemoResponse{
Memo: memoMessage,
}
@ -222,6 +233,11 @@ func (s *APIV2Service) UpdateMemo(ctx context.Context, request *apiv2pb.UpdateMe
if err != nil {
return nil, errors.Wrap(err, "failed to convert memo")
}
// Try to dispatch webhook when memo is updated.
if err := s.DispatchMemoUpdatedWebhook(ctx, memoMessage); err != nil {
log.Warn("Failed to dispatch memo updated webhook", zap.Error(err))
}
return &apiv2pb.UpdateMemoResponse{
Memo: memoMessage,
}, nil
@ -252,112 +268,12 @@ func (s *APIV2Service) DeleteMemo(ctx context.Context, request *apiv2pb.DeleteMe
return &apiv2pb.DeleteMemoResponse{}, nil
}
func (s *APIV2Service) SetMemoResources(ctx context.Context, request *apiv2pb.SetMemoResourcesRequest) (*apiv2pb.SetMemoResourcesResponse, error) {
resources, err := s.Store.ListResources(ctx, &store.FindResource{MemoID: &request.Id})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list resources")
}
// Delete resources that are not in the request.
for _, resource := range resources {
found := false
for _, requestResource := range request.Resources {
if resource.ID == int32(requestResource.Id) {
found = true
break
}
}
if !found {
if err = s.Store.DeleteResource(ctx, &store.DeleteResource{
ID: int32(resource.ID),
MemoID: &request.Id,
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete resource")
}
}
}
// Update resources' memo_id in the request.
for _, resource := range request.Resources {
if _, err := s.Store.UpdateResource(ctx, &store.UpdateResource{
ID: resource.Id,
MemoID: &request.Id,
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to update resource")
}
}
return &apiv2pb.SetMemoResourcesResponse{}, nil
}
func (s *APIV2Service) ListMemoResources(ctx context.Context, request *apiv2pb.ListMemoResourcesRequest) (*apiv2pb.ListMemoResourcesResponse, error) {
resources, err := s.Store.ListResources(ctx, &store.FindResource{
MemoID: &request.Id,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list resources")
}
response := &apiv2pb.ListMemoResourcesResponse{
Resources: []*apiv2pb.Resource{},
}
for _, resource := range resources {
response.Resources = append(response.Resources, s.convertResourceFromStore(ctx, resource))
}
return response, nil
}
func (s *APIV2Service) SetMemoRelations(ctx context.Context, request *apiv2pb.SetMemoRelationsRequest) (*apiv2pb.SetMemoRelationsResponse, error) {
referenceType := store.MemoRelationReference
// Delete all reference relations first.
if err := s.Store.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{
MemoID: &request.Id,
Type: &referenceType,
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete memo relation")
}
for _, relation := range request.Relations {
if _, err := s.Store.UpsertMemoRelation(ctx, &store.MemoRelation{
MemoID: request.Id,
RelatedMemoID: relation.RelatedMemoId,
Type: convertMemoRelationTypeToStore(relation.Type),
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to upsert memo relation")
}
}
return &apiv2pb.SetMemoRelationsResponse{}, nil
}
func (s *APIV2Service) ListMemoRelations(ctx context.Context, request *apiv2pb.ListMemoRelationsRequest) (*apiv2pb.ListMemoRelationsResponse, error) {
relationList := []*apiv2pb.MemoRelation{}
tempList, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{
MemoID: &request.Id,
})
if err != nil {
return nil, err
}
for _, relation := range tempList {
relationList = append(relationList, convertMemoRelationFromStore(relation))
}
tempList, err = s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{
RelatedMemoID: &request.Id,
})
if err != nil {
return nil, err
}
for _, relation := range tempList {
relationList = append(relationList, convertMemoRelationFromStore(relation))
}
response := &apiv2pb.ListMemoRelationsResponse{
Relations: relationList,
}
return response, nil
}
func (s *APIV2Service) CreateMemoComment(ctx context.Context, request *apiv2pb.CreateMemoCommentRequest) (*apiv2pb.CreateMemoCommentResponse, error) {
relatedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{ID: &request.Id})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get memo")
}
// Create the comment memo first.
createMemoResponse, err := s.CreateMemo(ctx, request.Create)
if err != nil {
@ -374,6 +290,34 @@ func (s *APIV2Service) CreateMemoComment(ctx context.Context, request *apiv2pb.C
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to create memo relation")
}
if memo.Visibility != apiv2pb.Visibility_PRIVATE {
activity, err := s.Store.CreateActivity(ctx, &store.Activity{
CreatorID: memo.CreatorId,
Type: store.ActivityTypeMemoComment,
Level: store.ActivityLevelInfo,
Payload: &storepb.ActivityPayload{
MemoComment: &storepb.ActivityMemoCommentPayload{
MemoId: memo.Id,
RelatedMemoId: request.Id,
},
},
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to create activity")
}
if _, err := s.Store.CreateInbox(ctx, &store.Inbox{
SenderID: memo.CreatorId,
ReceiverID: relatedMemo.CreatorID,
Status: store.UNREAD,
Message: &storepb.InboxMessage{
Type: storepb.InboxMessage_TYPE_MEMO_COMMENT,
ActivityId: &activity.ID,
},
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to create inbox")
}
}
metric.Enqueue("memo comment create")
response := &apiv2pb.CreateMemoCommentResponse{
Memo: memo,
@ -473,36 +417,6 @@ func (s *APIV2Service) getMemoDisplayWithUpdatedTsSettingValue(ctx context.Conte
return memoDisplayWithUpdatedTs, nil
}
func convertMemoRelationFromStore(memoRelation *store.MemoRelation) *apiv2pb.MemoRelation {
return &apiv2pb.MemoRelation{
MemoId: memoRelation.MemoID,
RelatedMemoId: memoRelation.RelatedMemoID,
Type: convertMemoRelationTypeFromStore(memoRelation.Type),
}
}
func convertMemoRelationTypeFromStore(relationType store.MemoRelationType) apiv2pb.MemoRelation_Type {
switch relationType {
case store.MemoRelationReference:
return apiv2pb.MemoRelation_REFERENCE
case store.MemoRelationComment:
return apiv2pb.MemoRelation_COMMENT
default:
return apiv2pb.MemoRelation_TYPE_UNSPECIFIED
}
}
func convertMemoRelationTypeToStore(relationType apiv2pb.MemoRelation_Type) store.MemoRelationType {
switch relationType {
case apiv2pb.MemoRelation_REFERENCE:
return store.MemoRelationReference
case apiv2pb.MemoRelation_COMMENT:
return store.MemoRelationComment
default:
return store.MemoRelationReference
}
}
func convertVisibilityFromStore(visibility store.Visibility) apiv2pb.Visibility {
switch visibility {
case store.Private:
@ -613,3 +527,73 @@ func findField(callExpr *expr.Expr_Call, filter *ListMemosFilter) {
}
}
}
// DispatchMemoCreatedWebhook dispatches webhook when memo is created.
func (s *APIV2Service) DispatchMemoCreatedWebhook(ctx context.Context, memo *apiv2pb.Memo) error {
return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.created")
}
// DispatchMemoUpdatedWebhook dispatches webhook when memo is updated.
func (s *APIV2Service) DispatchMemoUpdatedWebhook(ctx context.Context, memo *apiv2pb.Memo) error {
return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.updated")
}
func (s *APIV2Service) dispatchMemoRelatedWebhook(ctx context.Context, memo *apiv2pb.Memo, activityType string) error {
webhooks, err := s.Store.ListWebhooks(ctx, &store.FindWebhook{
CreatorID: &memo.CreatorId,
})
if err != nil {
return err
}
metric.Enqueue("webhook dispatch")
for _, hook := range webhooks {
payload := convertMemoToWebhookPayload(memo)
payload.ActivityType = activityType
payload.URL = hook.Url
err := webhook.Post(*payload)
if err != nil {
return errors.Wrap(err, "failed to post webhook")
}
}
return nil
}
func convertMemoToWebhookPayload(memo *apiv2pb.Memo) *webhook.WebhookPayload {
return &webhook.WebhookPayload{
CreatorID: memo.CreatorId,
CreatedTs: time.Now().Unix(),
Memo: &webhook.Memo{
ID: memo.Id,
CreatorID: memo.CreatorId,
CreatedTs: memo.CreateTime.Seconds,
UpdatedTs: memo.UpdateTime.Seconds,
Content: memo.Content,
Visibility: memo.Visibility.String(),
Pinned: memo.Pinned,
ResourceList: func() []*webhook.Resource {
resources := []*webhook.Resource{}
for _, resource := range memo.Resources {
resources = append(resources, &webhook.Resource{
ID: resource.Id,
Filename: resource.Filename,
ExternalLink: resource.ExternalLink,
Type: resource.Type,
Size: resource.Size,
})
}
return resources
}(),
RelationList: func() []*webhook.MemoRelation {
relations := []*webhook.MemoRelation{}
for _, relation := range memo.Relations {
relations = append(relations, &webhook.MemoRelation{
MemoID: relation.MemoId,
RelatedMemoID: relation.RelatedMemoId,
Type: relation.Type.String(),
})
}
return relations
}(),
},
}
}

View file

@ -12,7 +12,7 @@ import (
var Version = "0.18.1"
// DevVersion is the service current development version.
var DevVersion = "0.18.1"
var DevVersion = "0.18.2"
func GetCurrentVersion(mode string) string {
if mode == "dev" || mode == "demo" {

View file

@ -9,7 +9,7 @@ import { TAB_SPACE_WIDTH, UNKNOWN_ID } from "@/helpers/consts";
import { useGlobalStore, useResourceStore } from "@/store/module";
import { useMemoStore, useUserStore } from "@/store/v1";
import { MemoRelation, MemoRelation_Type } from "@/types/proto/api/v2/memo_relation_service";
import { Visibility } from "@/types/proto/api/v2/memo_service";
import { Memo, Visibility } from "@/types/proto/api/v2/memo_service";
import { Resource } from "@/types/proto/api/v2/resource_service";
import { UserSetting } from "@/types/proto/api/v2/user_service";
import { useTranslate } from "@/utils/i18n";
@ -28,6 +28,7 @@ interface Props {
editorClassName?: string;
cacheKey?: string;
memoId?: number;
parentMemoId?: number;
relationList?: MemoRelation[];
onConfirm?: (memoId: number) => void;
}
@ -41,7 +42,7 @@ interface State {
}
const MemoEditor = (props: Props) => {
const { className, editorClassName, cacheKey, memoId, onConfirm } = props;
const { className, editorClassName, cacheKey, memoId, parentMemoId, onConfirm } = props;
const { i18n } = useTranslation();
const t = useTranslate();
const contentCacheKey = `memo-editor-${cacheKey}`;
@ -260,6 +261,7 @@ const MemoEditor = (props: Props) => {
});
const content = editorRef.current?.getContent() ?? "";
try {
// Update memo.
if (memoId && memoId !== UNKNOWN_ID) {
const prevMemo = await memoStore.getOrFetchMemoById(memoId ?? UNKNOWN_ID);
if (prevMemo) {
@ -284,10 +286,22 @@ const MemoEditor = (props: Props) => {
}
}
} else {
const memo = await memoStore.createMemo({
content,
visibility: state.memoVisibility,
});
// Create memo or memo comment.
const request = !parentMemoId
? memoStore.createMemo({
content,
visibility: state.memoVisibility,
})
: memoServiceClient
.createMemoComment({
id: parentMemoId,
create: {
content,
visibility: state.memoVisibility,
},
})
.then(({ memo }) => memo as Memo);
const memo = await request;
await memoServiceClient.setMemoResources({
id: memo.id,
resources: state.resourceList,

View file

@ -15,7 +15,6 @@ import MobileHeader from "@/components/MobileHeader";
import showShareMemoDialog from "@/components/ShareMemoDialog";
import UserAvatar from "@/components/UserAvatar";
import VisibilityIcon from "@/components/VisibilityIcon";
import { UNKNOWN_ID } from "@/helpers/consts";
import { getDateTimeString } from "@/helpers/datetime";
import useCurrentUser from "@/hooks/useCurrentUser";
import useNavigateTo from "@/hooks/useNavigateTo";
@ -214,12 +213,7 @@ const MemoDetail = () => {
{/* Only show comment editor when user login */}
{currentUser && (
<MemoEditor
key={memo.id}
cacheKey={`comment-editor-${memo.id}`}
relationList={[{ memoId: UNKNOWN_ID, relatedMemoId: memo.id, type: MemoRelation_Type.COMMENT }]}
onConfirm={handleCommentCreated}
/>
<MemoEditor key={memo.id} cacheKey={`comment-editor-${memo.id}`} parentMemoId={memo.id} onConfirm={handleCommentCreated} />
)}
</div>
</div>