Unverified Commit 04f239a2 authored by boojack's avatar boojack Committed by GitHub

fix(api): remove public activity service (#5734)

parent 89c69028
syntax = "proto3";
package memos.api.v1;
import "google/api/annotations.proto";
import "google/api/client.proto";
import "google/api/field_behavior.proto";
import "google/api/resource.proto";
import "google/protobuf/timestamp.proto";
option go_package = "gen/api/v1";
service ActivityService {
// ListActivities returns a list of activities.
rpc ListActivities(ListActivitiesRequest) returns (ListActivitiesResponse) {
option (google.api.http) = {get: "/api/v1/activities"};
}
// GetActivity returns the activity with the given id.
rpc GetActivity(GetActivityRequest) returns (Activity) {
option (google.api.http) = {get: "/api/v1/{name=activities/*}"};
option (google.api.method_signature) = "name";
}
}
message Activity {
option (google.api.resource) = {
type: "memos.api.v1/Activity"
pattern: "activities/{activity}"
name_field: "name"
singular: "activity"
plural: "activities"
};
// The name of the activity.
// Format: activities/{id}
string name = 1 [
(google.api.field_behavior) = OUTPUT_ONLY,
(google.api.field_behavior) = IDENTIFIER
];
// The name of the creator.
// Format: users/{user}
string creator = 2 [(google.api.field_behavior) = OUTPUT_ONLY];
// The type of the activity.
Type type = 3 [(google.api.field_behavior) = OUTPUT_ONLY];
// The level of the activity.
Level level = 4 [(google.api.field_behavior) = OUTPUT_ONLY];
// The create time of the activity.
google.protobuf.Timestamp create_time = 5 [(google.api.field_behavior) = OUTPUT_ONLY];
// The payload of the activity.
ActivityPayload payload = 6 [(google.api.field_behavior) = OUTPUT_ONLY];
// Activity types.
enum Type {
// Unspecified type.
TYPE_UNSPECIFIED = 0;
// Memo comment activity.
MEMO_COMMENT = 1;
}
// Activity levels.
enum Level {
// Unspecified level.
LEVEL_UNSPECIFIED = 0;
// Info level.
INFO = 1;
// Warn level.
WARN = 2;
// Error level.
ERROR = 3;
}
}
message ActivityPayload {
oneof payload {
// Memo comment activity payload.
ActivityMemoCommentPayload memo_comment = 1;
}
}
// ActivityMemoCommentPayload represents the payload of a memo comment activity.
message ActivityMemoCommentPayload {
// The memo name of comment.
// Format: memos/{memo}
string memo = 1;
// The name of related memo.
// Format: memos/{memo}
string related_memo = 2;
}
message ListActivitiesRequest {
// The maximum number of activities to return.
// The service may return fewer than this value.
// If unspecified, at most 100 activities will be returned.
// The maximum value is 1000; values above 1000 will be coerced to 1000.
int32 page_size = 1;
// A page token, received from a previous `ListActivities` call.
// Provide this to retrieve the subsequent page.
string page_token = 2;
}
message ListActivitiesResponse {
// The activities.
repeated Activity activities = 1;
// A token to retrieve the next page of results.
// Pass this value in the page_token field in the subsequent call to `ListActivities`
// method to retrieve the next page of results.
string next_page_token = 2;
}
message GetActivityRequest {
// The name of the activity.
// Format: activities/{id}, id is the system generated auto-incremented id.
string name = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {type: "memos.api.v1/Activity"}
];
}
......@@ -626,8 +626,19 @@ message UserNotification {
// The type of the notification.
Type type = 5 [(google.api.field_behavior) = OUTPUT_ONLY];
// The activity ID associated with this notification.
optional int32 activity_id = 6 [(google.api.field_behavior) = OPTIONAL];
oneof payload {
MemoCommentPayload memo_comment = 6 [(google.api.field_behavior) = OUTPUT_ONLY];
}
message MemoCommentPayload {
// The memo name of comment.
// Format: memos/{memo}
string memo = 1;
// The name of related memo.
// Format: memos/{memo}
string related_memo = 2;
}
enum Status {
STATUS_UNSPECIFIED = 0;
......
This diff is collapsed.
This diff is collapsed.
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc (unknown)
// source: api/v1/activity_service.proto
package apiv1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
ActivityService_ListActivities_FullMethodName = "/memos.api.v1.ActivityService/ListActivities"
ActivityService_GetActivity_FullMethodName = "/memos.api.v1.ActivityService/GetActivity"
)
// ActivityServiceClient is the client API for ActivityService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type ActivityServiceClient interface {
// ListActivities returns a list of activities.
ListActivities(ctx context.Context, in *ListActivitiesRequest, opts ...grpc.CallOption) (*ListActivitiesResponse, error)
// GetActivity returns the activity with the given id.
GetActivity(ctx context.Context, in *GetActivityRequest, opts ...grpc.CallOption) (*Activity, error)
}
type activityServiceClient struct {
cc grpc.ClientConnInterface
}
func NewActivityServiceClient(cc grpc.ClientConnInterface) ActivityServiceClient {
return &activityServiceClient{cc}
}
func (c *activityServiceClient) ListActivities(ctx context.Context, in *ListActivitiesRequest, opts ...grpc.CallOption) (*ListActivitiesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListActivitiesResponse)
err := c.cc.Invoke(ctx, ActivityService_ListActivities_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *activityServiceClient) GetActivity(ctx context.Context, in *GetActivityRequest, opts ...grpc.CallOption) (*Activity, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Activity)
err := c.cc.Invoke(ctx, ActivityService_GetActivity_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// ActivityServiceServer is the server API for ActivityService service.
// All implementations must embed UnimplementedActivityServiceServer
// for forward compatibility.
type ActivityServiceServer interface {
// ListActivities returns a list of activities.
ListActivities(context.Context, *ListActivitiesRequest) (*ListActivitiesResponse, error)
// GetActivity returns the activity with the given id.
GetActivity(context.Context, *GetActivityRequest) (*Activity, error)
mustEmbedUnimplementedActivityServiceServer()
}
// UnimplementedActivityServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedActivityServiceServer struct{}
func (UnimplementedActivityServiceServer) ListActivities(context.Context, *ListActivitiesRequest) (*ListActivitiesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListActivities not implemented")
}
func (UnimplementedActivityServiceServer) GetActivity(context.Context, *GetActivityRequest) (*Activity, error) {
return nil, status.Error(codes.Unimplemented, "method GetActivity not implemented")
}
func (UnimplementedActivityServiceServer) mustEmbedUnimplementedActivityServiceServer() {}
func (UnimplementedActivityServiceServer) testEmbeddedByValue() {}
// UnsafeActivityServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ActivityServiceServer will
// result in compilation errors.
type UnsafeActivityServiceServer interface {
mustEmbedUnimplementedActivityServiceServer()
}
func RegisterActivityServiceServer(s grpc.ServiceRegistrar, srv ActivityServiceServer) {
// If the following call panics, it indicates UnimplementedActivityServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&ActivityService_ServiceDesc, srv)
}
func _ActivityService_ListActivities_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListActivitiesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ActivityServiceServer).ListActivities(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ActivityService_ListActivities_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ActivityServiceServer).ListActivities(ctx, req.(*ListActivitiesRequest))
}
return interceptor(ctx, in, info, handler)
}
func _ActivityService_GetActivity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetActivityRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ActivityServiceServer).GetActivity(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ActivityService_GetActivity_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ActivityServiceServer).GetActivity(ctx, req.(*GetActivityRequest))
}
return interceptor(ctx, in, info, handler)
}
// ActivityService_ServiceDesc is the grpc.ServiceDesc for ActivityService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var ActivityService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "memos.api.v1.ActivityService",
HandlerType: (*ActivityServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "ListActivities",
Handler: _ActivityService_ListActivities_Handler,
},
{
MethodName: "GetActivity",
Handler: _ActivityService_GetActivity_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "api/v1/activity_service.proto",
}
// Code generated by protoc-gen-connect-go. DO NOT EDIT.
//
// Source: api/v1/activity_service.proto
package apiv1connect
import (
connect "connectrpc.com/connect"
context "context"
errors "errors"
v1 "github.com/usememos/memos/proto/gen/api/v1"
http "net/http"
strings "strings"
)
// This is a compile-time assertion to ensure that this generated file and the connect package are
// compatible. If you get a compiler error that this constant is not defined, this code was
// generated with a version of connect newer than the one compiled into your binary. You can fix the
// problem by either regenerating this code with an older version of connect or updating the connect
// version compiled into your binary.
const _ = connect.IsAtLeastVersion1_13_0
const (
// ActivityServiceName is the fully-qualified name of the ActivityService service.
ActivityServiceName = "memos.api.v1.ActivityService"
)
// These constants are the fully-qualified names of the RPCs defined in this package. They're
// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
//
// Note that these are different from the fully-qualified method names used by
// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
// period.
const (
// ActivityServiceListActivitiesProcedure is the fully-qualified name of the ActivityService's
// ListActivities RPC.
ActivityServiceListActivitiesProcedure = "/memos.api.v1.ActivityService/ListActivities"
// ActivityServiceGetActivityProcedure is the fully-qualified name of the ActivityService's
// GetActivity RPC.
ActivityServiceGetActivityProcedure = "/memos.api.v1.ActivityService/GetActivity"
)
// ActivityServiceClient is a client for the memos.api.v1.ActivityService service.
type ActivityServiceClient interface {
// ListActivities returns a list of activities.
ListActivities(context.Context, *connect.Request[v1.ListActivitiesRequest]) (*connect.Response[v1.ListActivitiesResponse], error)
// GetActivity returns the activity with the given id.
GetActivity(context.Context, *connect.Request[v1.GetActivityRequest]) (*connect.Response[v1.Activity], error)
}
// NewActivityServiceClient constructs a client for the memos.api.v1.ActivityService service. By
// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses,
// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the
// connect.WithGRPC() or connect.WithGRPCWeb() options.
//
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
// http://api.acme.com or https://acme.com/grpc).
func NewActivityServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ActivityServiceClient {
baseURL = strings.TrimRight(baseURL, "/")
activityServiceMethods := v1.File_api_v1_activity_service_proto.Services().ByName("ActivityService").Methods()
return &activityServiceClient{
listActivities: connect.NewClient[v1.ListActivitiesRequest, v1.ListActivitiesResponse](
httpClient,
baseURL+ActivityServiceListActivitiesProcedure,
connect.WithSchema(activityServiceMethods.ByName("ListActivities")),
connect.WithClientOptions(opts...),
),
getActivity: connect.NewClient[v1.GetActivityRequest, v1.Activity](
httpClient,
baseURL+ActivityServiceGetActivityProcedure,
connect.WithSchema(activityServiceMethods.ByName("GetActivity")),
connect.WithClientOptions(opts...),
),
}
}
// activityServiceClient implements ActivityServiceClient.
type activityServiceClient struct {
listActivities *connect.Client[v1.ListActivitiesRequest, v1.ListActivitiesResponse]
getActivity *connect.Client[v1.GetActivityRequest, v1.Activity]
}
// ListActivities calls memos.api.v1.ActivityService.ListActivities.
func (c *activityServiceClient) ListActivities(ctx context.Context, req *connect.Request[v1.ListActivitiesRequest]) (*connect.Response[v1.ListActivitiesResponse], error) {
return c.listActivities.CallUnary(ctx, req)
}
// GetActivity calls memos.api.v1.ActivityService.GetActivity.
func (c *activityServiceClient) GetActivity(ctx context.Context, req *connect.Request[v1.GetActivityRequest]) (*connect.Response[v1.Activity], error) {
return c.getActivity.CallUnary(ctx, req)
}
// ActivityServiceHandler is an implementation of the memos.api.v1.ActivityService service.
type ActivityServiceHandler interface {
// ListActivities returns a list of activities.
ListActivities(context.Context, *connect.Request[v1.ListActivitiesRequest]) (*connect.Response[v1.ListActivitiesResponse], error)
// GetActivity returns the activity with the given id.
GetActivity(context.Context, *connect.Request[v1.GetActivityRequest]) (*connect.Response[v1.Activity], error)
}
// NewActivityServiceHandler builds an HTTP handler from the service implementation. It returns the
// path on which to mount the handler and the handler itself.
//
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
// and JSON codecs. They also support gzip compression.
func NewActivityServiceHandler(svc ActivityServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
activityServiceMethods := v1.File_api_v1_activity_service_proto.Services().ByName("ActivityService").Methods()
activityServiceListActivitiesHandler := connect.NewUnaryHandler(
ActivityServiceListActivitiesProcedure,
svc.ListActivities,
connect.WithSchema(activityServiceMethods.ByName("ListActivities")),
connect.WithHandlerOptions(opts...),
)
activityServiceGetActivityHandler := connect.NewUnaryHandler(
ActivityServiceGetActivityProcedure,
svc.GetActivity,
connect.WithSchema(activityServiceMethods.ByName("GetActivity")),
connect.WithHandlerOptions(opts...),
)
return "/memos.api.v1.ActivityService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case ActivityServiceListActivitiesProcedure:
activityServiceListActivitiesHandler.ServeHTTP(w, r)
case ActivityServiceGetActivityProcedure:
activityServiceGetActivityHandler.ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
})
}
// UnimplementedActivityServiceHandler returns CodeUnimplemented from all methods.
type UnimplementedActivityServiceHandler struct{}
func (UnimplementedActivityServiceHandler) ListActivities(context.Context, *connect.Request[v1.ListActivitiesRequest]) (*connect.Response[v1.ListActivitiesResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.ActivityService.ListActivities is not implemented"))
}
func (UnimplementedActivityServiceHandler) GetActivity(context.Context, *connect.Request[v1.GetActivityRequest]) (*connect.Response[v1.Activity], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.ActivityService.GetActivity is not implemented"))
}
This diff is collapsed.
......@@ -6,69 +6,6 @@ info:
title: ""
version: 0.0.1
paths:
/api/v1/activities:
get:
tags:
- ActivityService
description: ListActivities returns a list of activities.
operationId: ActivityService_ListActivities
parameters:
- name: pageSize
in: query
description: |-
The maximum number of activities to return.
The service may return fewer than this value.
If unspecified, at most 100 activities will be returned.
The maximum value is 1000; values above 1000 will be coerced to 1000.
schema:
type: integer
format: int32
- name: pageToken
in: query
description: |-
A page token, received from a previous `ListActivities` call.
Provide this to retrieve the subsequent page.
schema:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/ListActivitiesResponse'
default:
description: Default error response
content:
application/json:
schema:
$ref: '#/components/schemas/Status'
/api/v1/activities/{activity}:
get:
tags:
- ActivityService
description: GetActivity returns the activity with the given id.
operationId: ActivityService_GetActivity
parameters:
- name: activity
in: path
description: The activity id.
required: true
schema:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Activity'
default:
description: Default error response
content:
application/json:
schema:
$ref: '#/components/schemas/Status'
/api/v1/attachments:
get:
tags:
......@@ -1927,70 +1864,6 @@ paths:
$ref: '#/components/schemas/Status'
components:
schemas:
Activity:
type: object
properties:
name:
readOnly: true
type: string
description: |-
The name of the activity.
Format: activities/{id}
creator:
readOnly: true
type: string
description: |-
The name of the creator.
Format: users/{user}
type:
readOnly: true
enum:
- TYPE_UNSPECIFIED
- MEMO_COMMENT
type: string
description: The type of the activity.
format: enum
level:
readOnly: true
enum:
- LEVEL_UNSPECIFIED
- INFO
- WARN
- ERROR
type: string
description: The level of the activity.
format: enum
createTime:
readOnly: true
type: string
description: The create time of the activity.
format: date-time
payload:
readOnly: true
allOf:
- $ref: '#/components/schemas/ActivityPayload'
description: The payload of the activity.
ActivityMemoCommentPayload:
type: object
properties:
memo:
type: string
description: |-
The memo name of comment.
Format: memos/{memo}
relatedMemo:
type: string
description: |-
The name of related memo.
Format: memos/{memo}
description: ActivityMemoCommentPayload represents the payload of a memo comment activity.
ActivityPayload:
type: object
properties:
memoComment:
allOf:
- $ref: '#/components/schemas/ActivityMemoCommentPayload'
description: Memo comment activity payload.
Attachment:
required:
- filename
......@@ -2243,20 +2116,6 @@ components:
- $ref: '#/components/schemas/StorageSetting_S3Config'
description: The S3 config.
description: Storage configuration settings for instance attachments.
ListActivitiesResponse:
type: object
properties:
activities:
type: array
items:
$ref: '#/components/schemas/Activity'
description: The activities.
nextPageToken:
type: string
description: |-
A token to retrieve the next page of results.
Pass this value in the page_token field in the subsequent call to `ListActivities`
method to retrieve the next page of results.
ListAllUserStatsResponse:
type: object
properties:
......@@ -2942,10 +2801,23 @@ components:
type: string
description: The type of the notification.
format: enum
activityId:
type: integer
description: The activity ID associated with this notification.
format: int32
memoComment:
readOnly: true
allOf:
- $ref: '#/components/schemas/UserNotification_MemoCommentPayload'
UserNotification_MemoCommentPayload:
type: object
properties:
memo:
type: string
description: |-
The memo name of comment.
Format: memos/{memo}
relatedMemo:
type: string
description: |-
The name of related memo.
Format: memos/{memo}
UserSetting:
type: object
properties:
......@@ -3061,7 +2933,6 @@ components:
format: date-time
description: UserWebhook represents a webhook owned by a user.
tags:
- name: ActivityService
- name: AttachmentService
- name: AuthService
- name: IdentityProviderService
......
......@@ -60,8 +60,6 @@ func TestProtectedMethodsRequireAuth(t *testing.T) {
"/memos.api.v1.ShortcutService/ListShortcuts",
"/memos.api.v1.ShortcutService/UpdateShortcut",
"/memos.api.v1.ShortcutService/DeleteShortcut",
// Activity Service
"/memos.api.v1.ActivityService/GetActivity",
}
for _, method := range protectedMethods {
......
package v1
import (
"context"
"fmt"
"time"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
v1pb "github.com/usememos/memos/proto/gen/api/v1"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
)
func (s *APIV1Service) ListActivities(ctx context.Context, request *v1pb.ListActivitiesRequest) (*v1pb.ListActivitiesResponse, error) {
var limit, offset int
if request.PageToken != "" {
var pageToken v1pb.PageToken
if err := unmarshalPageToken(request.PageToken, &pageToken); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid page token: %v", err)
}
limit = int(pageToken.Limit)
offset = int(pageToken.Offset)
} else {
limit = int(request.PageSize)
}
if limit <= 0 {
limit = DefaultPageSize
}
if limit > MaxPageSize {
limit = MaxPageSize
}
limitPlusOne := limit + 1
activities, err := s.Store.ListActivities(ctx, &store.FindActivity{
Limit: &limitPlusOne,
Offset: &offset,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list activities: %v", err)
}
var activityMessages []*v1pb.Activity
nextPageToken := ""
if len(activities) == limitPlusOne {
activities = activities[:limit]
nextPageToken, err = getPageToken(limit, offset+limit)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get next page token, error: %v", err)
}
}
for _, activity := range activities {
activityMessage, err := s.convertActivityFromStore(ctx, activity)
if err != nil {
// Skip activities that reference deleted memos instead of failing the entire list
continue
}
if activityMessage != nil {
activityMessages = append(activityMessages, activityMessage)
}
}
return &v1pb.ListActivitiesResponse{
Activities: activityMessages,
NextPageToken: nextPageToken,
}, nil
}
func (s *APIV1Service) GetActivity(ctx context.Context, request *v1pb.GetActivityRequest) (*v1pb.Activity, error) {
activityID, err := ExtractActivityIDFromName(request.Name)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid activity name: %v", err)
}
activity, err := s.Store.GetActivity(ctx, &store.FindActivity{
ID: &activityID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get activity: %v", err)
}
activityMessage, err := s.convertActivityFromStore(ctx, activity)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to convert activity from store: %v", err)
}
if activityMessage == nil {
return nil, status.Errorf(codes.NotFound, "activity references deleted content")
}
return activityMessage, nil
}
// convertActivityFromStore converts a storage-layer activity to an API activity.
// This handles the mapping between internal activity representation and the public API,
// including proper type and level conversions.
// Returns nil if the activity references deleted content (to allow graceful skipping).
func (s *APIV1Service) convertActivityFromStore(ctx context.Context, activity *store.Activity) (*v1pb.Activity, error) {
payload, err := s.convertActivityPayloadFromStore(ctx, activity.Payload)
if err != nil {
return nil, err
}
// Skip activities that reference deleted memos
if payload == nil {
return nil, nil
}
// Convert store activity type to proto enum
var activityType v1pb.Activity_Type
switch activity.Type {
case store.ActivityTypeMemoComment:
activityType = v1pb.Activity_MEMO_COMMENT
default:
activityType = v1pb.Activity_TYPE_UNSPECIFIED
}
// Convert store activity level to proto enum
var activityLevel v1pb.Activity_Level
switch activity.Level {
case store.ActivityLevelInfo:
activityLevel = v1pb.Activity_INFO
default:
activityLevel = v1pb.Activity_LEVEL_UNSPECIFIED
}
return &v1pb.Activity{
Name: fmt.Sprintf("%s%d", ActivityNamePrefix, activity.ID),
Creator: fmt.Sprintf("%s%d", UserNamePrefix, activity.CreatorID),
Type: activityType,
Level: activityLevel,
CreateTime: timestamppb.New(time.Unix(activity.CreatedTs, 0)),
Payload: payload,
}, nil
}
// convertActivityPayloadFromStore converts a storage-layer activity payload to an API payload.
// This resolves references (e.g., memo IDs) to resource names for the API.
// Returns nil if the activity references deleted content (to allow graceful skipping).
func (s *APIV1Service) convertActivityPayloadFromStore(ctx context.Context, payload *storepb.ActivityPayload) (*v1pb.ActivityPayload, error) {
v2Payload := &v1pb.ActivityPayload{}
if payload.MemoComment != nil {
// Fetch the comment memo
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
ID: &payload.MemoComment.MemoId,
ExcludeContent: true,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get memo: %v", err)
}
// If the comment memo was deleted, skip this activity gracefully
if memo == nil {
return nil, nil
}
// Fetch the related memo (the one being commented on)
relatedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{
ID: &payload.MemoComment.RelatedMemoId,
ExcludeContent: true,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get related memo: %v", err)
}
// If the related memo was deleted, skip this activity gracefully
if relatedMemo == nil {
return nil, nil
}
v2Payload.Payload = &v1pb.ActivityPayload_MemoComment{
MemoComment: &v1pb.ActivityMemoCommentPayload{
Memo: fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID),
RelatedMemo: fmt.Sprintf("%s%s", MemoNamePrefix, relatedMemo.UID),
},
}
}
return v2Payload, nil
}
......@@ -40,7 +40,6 @@ func (s *ConnectServiceHandler) RegisterConnectHandlers(mux *http.ServeMux, opts
wrap(apiv1connect.NewMemoServiceHandler(s, opts...)),
wrap(apiv1connect.NewAttachmentServiceHandler(s, opts...)),
wrap(apiv1connect.NewShortcutServiceHandler(s, opts...)),
wrap(apiv1connect.NewActivityServiceHandler(s, opts...)),
wrap(apiv1connect.NewIdentityProviderServiceHandler(s, opts...)),
}
......
......@@ -429,24 +429,6 @@ func (s *ConnectServiceHandler) DeleteShortcut(ctx context.Context, req *connect
return connect.NewResponse(resp), nil
}
// ActivityService
func (s *ConnectServiceHandler) ListActivities(ctx context.Context, req *connect.Request[v1pb.ListActivitiesRequest]) (*connect.Response[v1pb.ListActivitiesResponse], error) {
resp, err := s.APIV1Service.ListActivities(ctx, req.Msg)
if err != nil {
return nil, convertGRPCError(err)
}
return connect.NewResponse(resp), nil
}
func (s *ConnectServiceHandler) GetActivity(ctx context.Context, req *connect.Request[v1pb.GetActivityRequest]) (*connect.Response[v1pb.Activity], error) {
resp, err := s.APIV1Service.GetActivity(ctx, req.Msg)
if err != nil {
return nil, convertGRPCError(err)
}
return connect.NewResponse(resp), nil
}
// IdentityProviderService
func (s *ConnectServiceHandler) ListIdentityProviders(ctx context.Context, req *connect.Request[v1pb.ListIdentityProvidersRequest]) (*connect.Response[v1pb.ListIdentityProvidersResponse], error) {
......
......@@ -21,7 +21,6 @@ const (
ReactionNamePrefix = "reactions/"
InboxNamePrefix = "inboxes/"
IdentityProviderNamePrefix = "identity-providers/"
ActivityNamePrefix = "activities/"
WebhookNamePrefix = "webhooks/"
)
......@@ -145,18 +144,6 @@ func ExtractIdentityProviderUIDFromName(name string) (string, error) {
return tokens[0], nil
}
func ExtractActivityIDFromName(name string) (int32, error) {
tokens, err := GetNameParentTokens(name, ActivityNamePrefix)
if err != nil {
return 0, err
}
id, err := util.ConvertStringToInt32(tokens[0])
if err != nil {
return 0, errors.Errorf("invalid activity ID %q", tokens[0])
}
return id, nil
}
// ValidateAndGenerateUID validates a user-provided UID or generates a new one.
// If provided is empty, a new shortuuid is generated.
// If provided is non-empty, it is validated against base.UIDMatcher.
......
package test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
apiv1 "github.com/usememos/memos/proto/gen/api/v1"
v1 "github.com/usememos/memos/server/router/api/v1" //nolint:revive
"github.com/usememos/memos/store"
)
// TestListActivitiesWithDeletedMemos verifies that ListActivities gracefully handles
// activities that reference deleted memos instead of crashing the entire request.
func TestListActivitiesWithDeletedMemos(t *testing.T) {
ctx := context.Background()
ts := NewTestService(t)
defer ts.Cleanup()
// Create two users - one to create memo, one to comment
userOne, err := ts.CreateRegularUser(ctx, "test-user-1")
require.NoError(t, err)
userOneCtx := ts.CreateUserContext(ctx, userOne.ID)
userTwo, err := ts.CreateRegularUser(ctx, "test-user-2")
require.NoError(t, err)
userTwoCtx := ts.CreateUserContext(ctx, userTwo.ID)
// Create a memo by userOne
memo1, err := ts.Service.CreateMemo(userOneCtx, &apiv1.CreateMemoRequest{
Memo: &apiv1.Memo{
Content: "Original memo",
Visibility: apiv1.Visibility_PUBLIC,
},
})
require.NoError(t, err)
require.NotNil(t, memo1)
// Create a comment on the memo by userTwo (this will create an activity for userOne)
comment, err := ts.Service.CreateMemoComment(userTwoCtx, &apiv1.CreateMemoCommentRequest{
Name: memo1.Name,
Comment: &apiv1.Memo{
Content: "This is a comment",
Visibility: apiv1.Visibility_PUBLIC,
},
})
require.NoError(t, err)
require.NotNil(t, comment)
// Verify activity was created for the comment (check from userOne's perspective - they receive the notification)
activities, err := ts.Service.ListActivities(userOneCtx, &apiv1.ListActivitiesRequest{})
require.NoError(t, err)
initialActivityCount := len(activities.Activities)
require.Greater(t, initialActivityCount, 0, "Should have at least one activity")
// Delete the original memo (this deletes the comment too)
_, err = ts.Service.DeleteMemo(userOneCtx, &apiv1.DeleteMemoRequest{
Name: memo1.Name,
})
require.NoError(t, err)
// List activities again - should succeed even though the memo is deleted
activities, err = ts.Service.ListActivities(userOneCtx, &apiv1.ListActivitiesRequest{})
require.NoError(t, err)
// Activities list should be empty or not contain the deleted memo activity
for _, activity := range activities.Activities {
if activity.Payload != nil && activity.Payload.GetMemoComment() != nil {
require.NotEqual(t, memo1.Name, activity.Payload.GetMemoComment().Memo,
"Activity should not reference deleted memo")
}
}
// After deletion, there should be fewer activities
require.LessOrEqual(t, len(activities.Activities), initialActivityCount-1,
"Should have filtered out the activity for the deleted memo")
}
// TestGetActivityWithDeletedMemo verifies that GetActivity returns a proper error
// when trying to fetch an activity that references a deleted memo.
func TestGetActivityWithDeletedMemo(t *testing.T) {
ctx := context.Background()
ts := NewTestService(t)
defer ts.Cleanup()
// Create two users
userOne, err := ts.CreateRegularUser(ctx, "test-user-1")
require.NoError(t, err)
userOneCtx := ts.CreateUserContext(ctx, userOne.ID)
userTwo, err := ts.CreateRegularUser(ctx, "test-user-2")
require.NoError(t, err)
userTwoCtx := ts.CreateUserContext(ctx, userTwo.ID)
// Create a memo by userOne
memo1, err := ts.Service.CreateMemo(userOneCtx, &apiv1.CreateMemoRequest{
Memo: &apiv1.Memo{
Content: "Original memo",
Visibility: apiv1.Visibility_PUBLIC,
},
})
require.NoError(t, err)
require.NotNil(t, memo1)
// Create a comment to trigger activity creation by userTwo
comment, err := ts.Service.CreateMemoComment(userTwoCtx, &apiv1.CreateMemoCommentRequest{
Name: memo1.Name,
Comment: &apiv1.Memo{
Content: "Comment",
Visibility: apiv1.Visibility_PUBLIC,
},
})
require.NoError(t, err)
require.NotNil(t, comment)
// Get the activity ID by listing activities from userOne's perspective
activities, err := ts.Service.ListActivities(userOneCtx, &apiv1.ListActivitiesRequest{})
require.NoError(t, err)
require.Greater(t, len(activities.Activities), 0)
activityName := activities.Activities[0].Name
// Delete the memo
_, err = ts.Service.DeleteMemo(userOneCtx, &apiv1.DeleteMemoRequest{
Name: memo1.Name,
})
require.NoError(t, err)
// Try to get the specific activity - should return NotFound error
_, err = ts.Service.GetActivity(userOneCtx, &apiv1.GetActivityRequest{
Name: activityName,
})
require.Error(t, err)
require.Contains(t, err.Error(), "activity references deleted content")
}
// TestActivitiesWithPartiallyDeletedMemos verifies that when some memos are deleted,
// other valid activities are still returned.
func TestActivitiesWithPartiallyDeletedMemos(t *testing.T) {
ctx := context.Background()
ts := NewTestService(t)
defer ts.Cleanup()
// Create two users
userOne, err := ts.CreateRegularUser(ctx, "test-user-1")
require.NoError(t, err)
userOneCtx := ts.CreateUserContext(ctx, userOne.ID)
userTwo, err := ts.CreateRegularUser(ctx, "test-user-2")
require.NoError(t, err)
userTwoCtx := ts.CreateUserContext(ctx, userTwo.ID)
// Create two memos by userOne
memo1, err := ts.Service.CreateMemo(userOneCtx, &apiv1.CreateMemoRequest{
Memo: &apiv1.Memo{
Content: "First memo",
Visibility: apiv1.Visibility_PUBLIC,
},
})
require.NoError(t, err)
memo2, err := ts.Service.CreateMemo(userOneCtx, &apiv1.CreateMemoRequest{
Memo: &apiv1.Memo{
Content: "Second memo",
Visibility: apiv1.Visibility_PUBLIC,
},
})
require.NoError(t, err)
// Create comments on both by userTwo (creates activities for userOne)
_, err = ts.Service.CreateMemoComment(userTwoCtx, &apiv1.CreateMemoCommentRequest{
Name: memo1.Name,
Comment: &apiv1.Memo{
Content: "Comment on first",
Visibility: apiv1.Visibility_PUBLIC,
},
})
require.NoError(t, err)
_, err = ts.Service.CreateMemoComment(userTwoCtx, &apiv1.CreateMemoCommentRequest{
Name: memo2.Name,
Comment: &apiv1.Memo{
Content: "Comment on second",
Visibility: apiv1.Visibility_PUBLIC,
},
})
require.NoError(t, err)
// Should have 2 activities from userOne's perspective
activities, err := ts.Service.ListActivities(userOneCtx, &apiv1.ListActivitiesRequest{})
require.NoError(t, err)
require.Equal(t, 2, len(activities.Activities))
// Delete first memo
_, err = ts.Service.DeleteMemo(userOneCtx, &apiv1.DeleteMemoRequest{
Name: memo1.Name,
})
require.NoError(t, err)
// List activities - should still work and return only the second memo's activity
activities, err = ts.Service.ListActivities(userOneCtx, &apiv1.ListActivitiesRequest{})
require.NoError(t, err)
require.Equal(t, 1, len(activities.Activities), "Should have 1 activity remaining")
// Verify the remaining activity relates to a valid memo
require.NotNil(t, activities.Activities[0].Payload.GetMemoComment())
require.Contains(t, activities.Activities[0].Payload.GetMemoComment().RelatedMemo, "memos/")
}
// TestActivityStoreDirectDeletion tests the scenario where a memo is deleted directly
// from the store (simulating database-level deletion or migration).
func TestActivityStoreDirectDeletion(t *testing.T) {
ctx := context.Background()
ts := NewTestService(t)
defer ts.Cleanup()
user, err := ts.CreateRegularUser(ctx, "test-user")
require.NoError(t, err)
userCtx := ts.CreateUserContext(ctx, user.ID)
// Create a memo
memo1, err := ts.Service.CreateMemo(userCtx, &apiv1.CreateMemoRequest{
Memo: &apiv1.Memo{
Content: "Test memo",
Visibility: apiv1.Visibility_PUBLIC,
},
})
require.NoError(t, err)
// Create a comment
comment, err := ts.Service.CreateMemoComment(userCtx, &apiv1.CreateMemoCommentRequest{
Name: memo1.Name,
Comment: &apiv1.Memo{
Content: "Test comment",
Visibility: apiv1.Visibility_PUBLIC,
},
})
require.NoError(t, err)
// Extract memo UID from the comment name
commentMemoUID, err := v1.ExtractMemoUIDFromName(comment.Name)
require.NoError(t, err)
commentMemo, err := ts.Store.GetMemo(ctx, &store.FindMemo{
UID: &commentMemoUID,
})
require.NoError(t, err)
require.NotNil(t, commentMemo)
// Delete the comment memo directly from store (simulating orphaned activity)
err = ts.Store.DeleteMemo(ctx, &store.DeleteMemo{ID: commentMemo.ID})
require.NoError(t, err)
// List activities should still succeed even with orphaned activity
activities, err := ts.Service.ListActivities(userCtx, &apiv1.ListActivitiesRequest{})
require.NoError(t, err)
// Activities should be empty or not include the orphaned one
for _, activity := range activities.Activities {
if activity.Payload != nil && activity.Payload.GetMemoComment() != nil {
require.NotEqual(t, comment.Name, activity.Payload.GetMemoComment().Memo,
"Should not return activity with deleted memo")
}
}
}
package test
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/require"
apiv1 "github.com/usememos/memos/proto/gen/api/v1"
)
func TestListActivities(t *testing.T) {
ctx := context.Background()
ts := NewTestService(t)
defer ts.Cleanup()
// Create userOne
userOne, err := ts.CreateRegularUser(ctx, "test-user-1")
require.NoError(t, err)
userOneCtx := ts.CreateUserContext(ctx, userOne.ID)
// Create userTwo
userTwo, err := ts.CreateRegularUser(ctx, "test-user-2")
require.NoError(t, err)
userTwoCtx := ts.CreateUserContext(ctx, userTwo.ID)
// UserOne creates a memo
memo, err := ts.Service.CreateMemo(userOneCtx, &apiv1.CreateMemoRequest{
Memo: &apiv1.Memo{
Content: "Base memo",
Visibility: apiv1.Visibility_PUBLIC,
},
})
require.NoError(t, err)
// UserTwo creates 15 comments on the memo to generate 15 activities
for i := 0; i < 15; i++ {
_, err := ts.Service.CreateMemoComment(userTwoCtx, &apiv1.CreateMemoCommentRequest{
Name: memo.Name,
Comment: &apiv1.Memo{
Content: fmt.Sprintf("Comment %d", i),
Visibility: apiv1.Visibility_PUBLIC,
},
})
require.NoError(t, err)
}
// List activities with page size 10 (as admin or userOne)
// Activities are visible to the receiver (UserOne)
resp, err := ts.Service.ListActivities(userOneCtx, &apiv1.ListActivitiesRequest{
PageSize: 10,
})
require.NoError(t, err)
require.Len(t, resp.Activities, 10)
require.NotEmpty(t, resp.NextPageToken)
// List next page
resp, err = ts.Service.ListActivities(userOneCtx, &apiv1.ListActivitiesRequest{
PageSize: 10,
PageToken: resp.NextPageToken,
})
require.NoError(t, err)
require.Len(t, resp.Activities, 5)
require.Empty(t, resp.NextPageToken)
}
package test
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/require"
apiv1 "github.com/usememos/memos/proto/gen/api/v1"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
)
func TestListUserNotificationsIncludesMemoCommentPayload(t *testing.T) {
ctx := context.Background()
ts := NewTestService(t)
defer ts.Cleanup()
owner, err := ts.CreateRegularUser(ctx, "notification-owner")
require.NoError(t, err)
ownerCtx := ts.CreateUserContext(ctx, owner.ID)
commenter, err := ts.CreateRegularUser(ctx, "notification-commenter")
require.NoError(t, err)
commenterCtx := ts.CreateUserContext(ctx, commenter.ID)
memo, err := ts.Service.CreateMemo(ownerCtx, &apiv1.CreateMemoRequest{
Memo: &apiv1.Memo{
Content: "Base memo",
Visibility: apiv1.Visibility_PUBLIC,
},
})
require.NoError(t, err)
comment, err := ts.Service.CreateMemoComment(commenterCtx, &apiv1.CreateMemoCommentRequest{
Name: memo.Name,
Comment: &apiv1.Memo{
Content: "Comment content",
Visibility: apiv1.Visibility_PUBLIC,
},
})
require.NoError(t, err)
resp, err := ts.Service.ListUserNotifications(ownerCtx, &apiv1.ListUserNotificationsRequest{
Parent: fmt.Sprintf("users/%d", owner.ID),
})
require.NoError(t, err)
require.Len(t, resp.Notifications, 1)
notification := resp.Notifications[0]
require.Equal(t, apiv1.UserNotification_MEMO_COMMENT, notification.Type)
require.NotNil(t, notification.GetMemoComment())
require.Equal(t, comment.Name, notification.GetMemoComment().Memo)
require.Equal(t, memo.Name, notification.GetMemoComment().RelatedMemo)
}
func TestListUserNotificationsOmitsPayloadWhenActivityMissing(t *testing.T) {
ctx := context.Background()
ts := NewTestService(t)
defer ts.Cleanup()
owner, err := ts.CreateRegularUser(ctx, "notification-owner")
require.NoError(t, err)
ownerCtx := ts.CreateUserContext(ctx, owner.ID)
commenter, err := ts.CreateRegularUser(ctx, "notification-commenter")
require.NoError(t, err)
commenterCtx := ts.CreateUserContext(ctx, commenter.ID)
memo, err := ts.Service.CreateMemo(ownerCtx, &apiv1.CreateMemoRequest{
Memo: &apiv1.Memo{
Content: "Base memo",
Visibility: apiv1.Visibility_PUBLIC,
},
})
require.NoError(t, err)
_, err = ts.Service.CreateMemoComment(commenterCtx, &apiv1.CreateMemoCommentRequest{
Name: memo.Name,
Comment: &apiv1.Memo{
Content: "Comment content",
Visibility: apiv1.Visibility_PUBLIC,
},
})
require.NoError(t, err)
messageType := storepb.InboxMessage_MEMO_COMMENT
inboxes, err := ts.Store.ListInboxes(ctx, &store.FindInbox{
ReceiverID: &owner.ID,
MessageType: &messageType,
})
require.NoError(t, err)
require.Len(t, inboxes, 1)
require.NotNil(t, inboxes[0].Message)
require.NotNil(t, inboxes[0].Message.ActivityId)
_, err = ts.Store.GetDriver().GetDB().ExecContext(ctx, "DELETE FROM activity WHERE id = ?", *inboxes[0].Message.ActivityId)
require.NoError(t, err)
resp, err := ts.Service.ListUserNotifications(ownerCtx, &apiv1.ListUserNotificationsRequest{
Parent: fmt.Sprintf("users/%d", owner.ID),
})
require.NoError(t, err)
require.Len(t, resp.Notifications, 1)
require.Nil(t, resp.Notifications[0].GetMemoComment())
}
func TestListUserNotificationsOmitsPayloadWhenMemosDeleted(t *testing.T) {
ctx := context.Background()
ts := NewTestService(t)
defer ts.Cleanup()
owner, err := ts.CreateRegularUser(ctx, "notification-owner")
require.NoError(t, err)
ownerCtx := ts.CreateUserContext(ctx, owner.ID)
commenter, err := ts.CreateRegularUser(ctx, "notification-commenter")
require.NoError(t, err)
commenterCtx := ts.CreateUserContext(ctx, commenter.ID)
memo, err := ts.Service.CreateMemo(ownerCtx, &apiv1.CreateMemoRequest{
Memo: &apiv1.Memo{
Content: "Base memo",
Visibility: apiv1.Visibility_PUBLIC,
},
})
require.NoError(t, err)
_, err = ts.Service.CreateMemoComment(commenterCtx, &apiv1.CreateMemoCommentRequest{
Name: memo.Name,
Comment: &apiv1.Memo{
Content: "Comment content",
Visibility: apiv1.Visibility_PUBLIC,
},
})
require.NoError(t, err)
_, err = ts.Service.DeleteMemo(ownerCtx, &apiv1.DeleteMemoRequest{
Name: memo.Name,
})
require.NoError(t, err)
resp, err := ts.Service.ListUserNotifications(ownerCtx, &apiv1.ListUserNotificationsRequest{
Parent: fmt.Sprintf("users/%d", owner.ID),
})
require.NoError(t, err)
require.Len(t, resp.Notifications, 1)
require.Equal(t, apiv1.UserNotification_MEMO_COMMENT, resp.Notifications[0].Type)
require.Nil(t, resp.Notifications[0].GetMemoComment())
}
......@@ -1399,7 +1399,7 @@ func (s *APIV1Service) DeleteUserNotification(ctx context.Context, request *v1pb
// convertInboxToUserNotification converts a storage-layer inbox to an API notification.
// This handles the mapping between the internal inbox representation and the public API.
func (*APIV1Service) convertInboxToUserNotification(_ context.Context, inbox *store.Inbox) (*v1pb.UserNotification, error) {
func (s *APIV1Service) convertInboxToUserNotification(ctx context.Context, inbox *store.Inbox) (*v1pb.UserNotification, error) {
notification := &v1pb.UserNotification{
Name: fmt.Sprintf("users/%d/notifications/%d", inbox.ReceiverID, inbox.ID),
Sender: fmt.Sprintf("%s%d", UserNamePrefix, inbox.SenderID),
......@@ -1425,14 +1425,63 @@ func (*APIV1Service) convertInboxToUserNotification(_ context.Context, inbox *st
notification.Type = v1pb.UserNotification_TYPE_UNSPECIFIED
}
if inbox.Message.ActivityId != nil {
notification.ActivityId = inbox.Message.ActivityId
payload, err := s.convertUserNotificationPayload(ctx, inbox.Message)
if err != nil {
return nil, err
}
if payload != nil {
notification.Payload = &v1pb.UserNotification_MemoComment{
MemoComment: payload,
}
}
}
return notification, nil
}
func (s *APIV1Service) convertUserNotificationPayload(ctx context.Context, message *storepb.InboxMessage) (*v1pb.UserNotification_MemoCommentPayload, error) {
if message == nil || message.Type != storepb.InboxMessage_MEMO_COMMENT || message.ActivityId == nil {
return nil, nil
}
activity, err := s.Store.GetActivity(ctx, &store.FindActivity{
ID: message.ActivityId,
})
if err != nil {
return nil, errors.Wrap(err, "failed to get activity")
}
if activity == nil || activity.Payload == nil || activity.Payload.MemoComment == nil {
return nil, nil
}
commentMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{
ID: &activity.Payload.MemoComment.MemoId,
ExcludeContent: true,
})
if err != nil {
return nil, errors.Wrap(err, "failed to get comment memo")
}
if commentMemo == nil {
return nil, nil
}
relatedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{
ID: &activity.Payload.MemoComment.RelatedMemoId,
ExcludeContent: true,
})
if err != nil {
return nil, errors.Wrap(err, "failed to get related memo")
}
if relatedMemo == nil {
return nil, nil
}
return &v1pb.UserNotification_MemoCommentPayload{
Memo: fmt.Sprintf("%s%s", MemoNamePrefix, commentMemo.UID),
RelatedMemo: fmt.Sprintf("%s%s", MemoNamePrefix, relatedMemo.UID),
}, nil
}
// ExtractNotificationIDFromName extracts the notification ID from a resource name.
// Expected format: users/{user_id}/notifications/{notification_id}.
func ExtractNotificationIDFromName(name string) (int32, error) {
......
......@@ -24,7 +24,6 @@ type APIV1Service struct {
v1pb.UnimplementedMemoServiceServer
v1pb.UnimplementedAttachmentServiceServer
v1pb.UnimplementedShortcutServiceServer
v1pb.UnimplementedActivityServiceServer
v1pb.UnimplementedIdentityProviderServiceServer
Secret string
......@@ -107,9 +106,6 @@ func (s *APIV1Service) RegisterGateway(ctx context.Context, echoServer *echo.Ech
if err := v1pb.RegisterShortcutServiceHandlerServer(ctx, gwMux, s); err != nil {
return err
}
if err := v1pb.RegisterActivityServiceHandlerServer(ctx, gwMux, s); err != nil {
return err
}
if err := v1pb.RegisterIdentityProviderServiceHandlerServer(ctx, gwMux, s); err != nil {
return err
}
......
......@@ -4,8 +4,7 @@ import { CheckIcon, MessageCircleIcon, TrashIcon, XIcon } from "lucide-react";
import { useState } from "react";
import toast from "react-hot-toast";
import UserAvatar from "@/components/UserAvatar";
import { activityServiceClient, memoServiceClient, userServiceClient } from "@/connect";
import { activityNamePrefix } from "@/helpers/resource-names";
import { memoServiceClient, userServiceClient } from "@/connect";
import useAsyncEffect from "@/hooks/useAsyncEffect";
import useNavigateTo from "@/hooks/useNavigateTo";
import { useUser } from "@/hooks/useUserQueries";
......@@ -31,17 +30,13 @@ function MemoCommentMessage({ notification }: Props) {
const { data: sender } = useUser(senderName || "", { enabled: !!senderName });
useAsyncEffect(async () => {
if (!notification.activityId) {
if (notification.payload?.case !== "memoComment") {
setHasError(true);
return;
}
try {
const activity = await activityServiceClient.getActivity({
name: `${activityNamePrefix}${notification.activityId}`,
});
if (activity.payload?.payload?.case === "memoComment") {
const memoCommentPayload = activity.payload.payload.value;
const memoCommentPayload = notification.payload.value;
const memo = await memoServiceClient.getMemo({
name: memoCommentPayload.relatedMemo,
});
......@@ -54,15 +49,14 @@ function MemoCommentMessage({ notification }: Props) {
setSenderName(notification.sender);
setInitialized(true);
}
} catch (error) {
handleError(error, () => {}, {
context: "Failed to fetch activity",
context: "Failed to fetch memo comment notification",
onError: () => setHasError(true),
});
return;
}
}, [notification.activityId]);
}, [notification.payload, notification.sender]);
const handleNavigateToMemo = async () => {
if (!relatedMemo) {
......
......@@ -2,7 +2,6 @@ import { timestampDate } from "@bufbuild/protobuf/wkt";
import { Code, ConnectError, createClient, type Interceptor } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { getAccessToken, hasStoredToken, isTokenExpired, REQUEST_TOKEN_EXPIRY_BUFFER_MS, setAccessToken } from "./auth-state";
import { ActivityService } from "./types/proto/api/v1/activity_service_pb";
import { AttachmentService } from "./types/proto/api/v1/attachment_service_pb";
import { AuthService } from "./types/proto/api/v1/auth_service_pb";
import { IdentityProviderService } from "./types/proto/api/v1/idp_service_pb";
......@@ -197,7 +196,6 @@ export const userServiceClient = createClient(UserService, transport);
export const memoServiceClient = createClient(MemoService, transport);
export const attachmentServiceClient = createClient(AttachmentService, transport);
export const shortcutServiceClient = createClient(ShortcutService, transport);
export const activityServiceClient = createClient(ActivityService, transport);
// Configuration service clients
export const identityProviderServiceClient = createClient(IdentityProviderService, transport);
......@@ -6,7 +6,6 @@ export const instanceSettingNamePrefix = "instance/settings/";
export const userNamePrefix = "users/";
export const memoNamePrefix = "memos/";
export const identityProviderNamePrefix = "identity-providers/";
export const activityNamePrefix = "activities/";
export const extractUserIdFromName = (name: string) => {
return name.split(userNamePrefix).pop() || "";
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment