this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

follows (#7)

* implement database for follows

* add migrations

* add follows to indexer

* implement api

* like tweaks

* more follows schemas

authored by hailey.at and committed by

GitHub 7d781819 27b76c4a

+1969
+68
api/server/graphgetactorfollowers.go
··· 1 + package server 2 + 3 + import ( 4 + "errors" 5 + 6 + "github.com/labstack/echo/v4" 7 + vyletdatabase "github.com/vylet-app/go/database/proto" 8 + "github.com/vylet-app/go/generated/handlers" 9 + "github.com/vylet-app/go/generated/vylet" 10 + "github.com/vylet-app/go/internal/helpers" 11 + ) 12 + 13 + func (s *Server) GraphGetActorFollowersRequiresAuth() bool { 14 + return false 15 + } 16 + 17 + func (s *Server) HandleGraphGetActorFollowers(e echo.Context, input *handlers.GraphGetActorFollowersInput) (*vylet.GraphGetActorFollowers_Output, *echo.HTTPError) { 18 + ctx := e.Request().Context() 19 + 20 + logger := s.logger.With("name", "HandleGraphGetActorFollowers") 21 + 22 + if input.Limit != nil && (*input.Limit < 1 || *input.Limit > 100) { 23 + return nil, NewValidationError("limit", "limit must be between 1 and 100") 24 + } else if input.Limit == nil { 25 + input.Limit = helpers.ToInt64Ptr(25) 26 + } 27 + 28 + did, _, err := s.fetchDidHandleFromActor(ctx, input.Actor) 29 + if err != nil { 30 + if errors.Is(err, ErrActorNotValid) { 31 + return nil, NewValidationError("author", "author must be a valid DID or handle") 32 + } 33 + logger.Error("error did from actor", "err", err) 34 + return nil, ErrInternalServerErr 35 + } 36 + 37 + resp, err := s.client.Follow.GetFollowersByActor(ctx, &vyletdatabase.GetFollowersByActorRequest{Did: did}) 38 + if err != nil { 39 + logger.Error("error getting followers", "err", err) 40 + return nil, ErrInternalServerErr 41 + } 42 + 43 + dids := make([]string, 0, len(resp.Followers)) 44 + for _, f := range resp.Followers { 45 + dids = append(dids, f.SubjectDid) 46 + } 47 + 48 + profiles, err := s.getProfiles(ctx, dids) 49 + if err != nil { 50 + logger.Error("error getting profiles", "err", err) 51 + return nil, ErrInternalServerErr 52 + } 53 + 54 + sortedProfiles := make([]*vylet.ActorDefs_ProfileView, 0, len(profiles)) 55 + for _, did := range dids { 56 + profile, ok := profiles[did] 57 + if !ok { 58 + logger.Warn("unable to find profile", "did", did) 59 + continue 60 + } 61 + sortedProfiles = append(sortedProfiles, profile) 62 + } 63 + 64 + return &vylet.GraphGetActorFollowers_Output{ 65 + Profiles: sortedProfiles, 66 + Cursor: resp.Cursor, 67 + }, nil 68 + }
+68
api/server/graphgetactorfollows.go
··· 1 + package server 2 + 3 + import ( 4 + "errors" 5 + 6 + "github.com/labstack/echo/v4" 7 + vyletdatabase "github.com/vylet-app/go/database/proto" 8 + "github.com/vylet-app/go/generated/handlers" 9 + "github.com/vylet-app/go/generated/vylet" 10 + "github.com/vylet-app/go/internal/helpers" 11 + ) 12 + 13 + func (s *Server) GraphGetActorFollowsRequiresAuth() bool { 14 + return false 15 + } 16 + 17 + func (s *Server) HandleGraphGetActorFollows(e echo.Context, input *handlers.GraphGetActorFollowsInput) (*vylet.GraphGetActorFollows_Output, *echo.HTTPError) { 18 + ctx := e.Request().Context() 19 + 20 + logger := s.logger.With("name", "HandleGraphGetActorFollows") 21 + 22 + if input.Limit != nil && (*input.Limit < 1 || *input.Limit > 100) { 23 + return nil, NewValidationError("limit", "limit must be between 1 and 100") 24 + } else if input.Limit == nil { 25 + input.Limit = helpers.ToInt64Ptr(25) 26 + } 27 + 28 + did, _, err := s.fetchDidHandleFromActor(ctx, input.Actor) 29 + if err != nil { 30 + if errors.Is(err, ErrActorNotValid) { 31 + return nil, NewValidationError("author", "author must be a valid DID or handle") 32 + } 33 + logger.Error("error did from actor", "err", err) 34 + return nil, ErrInternalServerErr 35 + } 36 + 37 + resp, err := s.client.Follow.GetFollowsByActor(ctx, &vyletdatabase.GetFollowsByActorRequest{Did: did}) 38 + if err != nil { 39 + logger.Error("error getting follows", "err", err) 40 + return nil, ErrInternalServerErr 41 + } 42 + 43 + dids := make([]string, 0, len(resp.Follows)) 44 + for _, f := range resp.Follows { 45 + dids = append(dids, f.SubjectDid) 46 + } 47 + 48 + profiles, err := s.getProfiles(ctx, dids) 49 + if err != nil { 50 + logger.Error("error getting profiles", "err", err) 51 + return nil, ErrInternalServerErr 52 + } 53 + 54 + sortedProfiles := make([]*vylet.ActorDefs_ProfileView, 0, len(profiles)) 55 + for _, did := range dids { 56 + profile, ok := profiles[did] 57 + if !ok { 58 + logger.Warn("unable to find profile", "did", did) 59 + continue 60 + } 61 + sortedProfiles = append(sortedProfiles, profile) 62 + } 63 + 64 + return &vylet.GraphGetActorFollows_Output{ 65 + Profiles: sortedProfiles, 66 + Cursor: resp.Cursor, 67 + }, nil 68 + }
+3
database/client/client.go
··· 14 14 Profile vyletdatabase.ProfileServiceClient 15 15 Post vyletdatabase.PostServiceClient 16 16 Like vyletdatabase.LikeServiceClient 17 + Follow vyletdatabase.FollowServiceClient 17 18 BlobRef vyletdatabase.BlobRefServiceClient 18 19 } 19 20 ··· 36 37 postClient := vyletdatabase.NewPostServiceClient(conn) 37 38 likeClient := vyletdatabase.NewLikeServiceClient(conn) 38 39 blobRefClient := vyletdatabase.NewBlobRefServiceClient(conn) 40 + followClient := vyletdatabase.NewFollowServiceClient(conn) 39 41 40 42 client := Client{ 41 43 client: conn, ··· 43 45 Post: postClient, 44 46 Like: likeClient, 45 47 BlobRef: blobRefClient, 48 + Follow: followClient, 46 49 } 47 50 48 51 return &client, nil
+794
database/proto/follow.pb.go
··· 1 + // Code generated by protoc-gen-go. DO NOT EDIT. 2 + // versions: 3 + // protoc-gen-go v1.36.6 4 + // protoc (unknown) 5 + // source: follow.proto 6 + 7 + package vyletdatabase 8 + 9 + import ( 10 + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 11 + protoreflect "google.golang.org/protobuf/reflect/protoreflect" 12 + protoimpl "google.golang.org/protobuf/runtime/protoimpl" 13 + timestamppb "google.golang.org/protobuf/types/known/timestamppb" 14 + reflect "reflect" 15 + sync "sync" 16 + unsafe "unsafe" 17 + ) 18 + 19 + const ( 20 + // Verify that this generated code is sufficiently up-to-date. 21 + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 22 + // Verify that runtime/protoimpl is sufficiently up-to-date. 23 + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 24 + ) 25 + 26 + type Follow struct { 27 + state protoimpl.MessageState `protogen:"open.v1"` 28 + Uri string `protobuf:"bytes,1,opt,name=uri,proto3" json:"uri,omitempty"` 29 + Cid string `protobuf:"bytes,2,opt,name=cid,proto3" json:"cid,omitempty"` 30 + SubjectDid string `protobuf:"bytes,3,opt,name=subject_did,json=subjectDid,proto3" json:"subject_did,omitempty"` 31 + AuthorDid string `protobuf:"bytes,5,opt,name=author_did,json=authorDid,proto3" json:"author_did,omitempty"` 32 + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` 33 + IndexedAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=indexed_at,json=indexedAt,proto3" json:"indexed_at,omitempty"` 34 + unknownFields protoimpl.UnknownFields 35 + sizeCache protoimpl.SizeCache 36 + } 37 + 38 + func (x *Follow) Reset() { 39 + *x = Follow{} 40 + mi := &file_follow_proto_msgTypes[0] 41 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 42 + ms.StoreMessageInfo(mi) 43 + } 44 + 45 + func (x *Follow) String() string { 46 + return protoimpl.X.MessageStringOf(x) 47 + } 48 + 49 + func (*Follow) ProtoMessage() {} 50 + 51 + func (x *Follow) ProtoReflect() protoreflect.Message { 52 + mi := &file_follow_proto_msgTypes[0] 53 + if x != nil { 54 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 55 + if ms.LoadMessageInfo() == nil { 56 + ms.StoreMessageInfo(mi) 57 + } 58 + return ms 59 + } 60 + return mi.MessageOf(x) 61 + } 62 + 63 + // Deprecated: Use Follow.ProtoReflect.Descriptor instead. 64 + func (*Follow) Descriptor() ([]byte, []int) { 65 + return file_follow_proto_rawDescGZIP(), []int{0} 66 + } 67 + 68 + func (x *Follow) GetUri() string { 69 + if x != nil { 70 + return x.Uri 71 + } 72 + return "" 73 + } 74 + 75 + func (x *Follow) GetCid() string { 76 + if x != nil { 77 + return x.Cid 78 + } 79 + return "" 80 + } 81 + 82 + func (x *Follow) GetSubjectDid() string { 83 + if x != nil { 84 + return x.SubjectDid 85 + } 86 + return "" 87 + } 88 + 89 + func (x *Follow) GetAuthorDid() string { 90 + if x != nil { 91 + return x.AuthorDid 92 + } 93 + return "" 94 + } 95 + 96 + func (x *Follow) GetCreatedAt() *timestamppb.Timestamp { 97 + if x != nil { 98 + return x.CreatedAt 99 + } 100 + return nil 101 + } 102 + 103 + func (x *Follow) GetIndexedAt() *timestamppb.Timestamp { 104 + if x != nil { 105 + return x.IndexedAt 106 + } 107 + return nil 108 + } 109 + 110 + type CreateFollowRequest struct { 111 + state protoimpl.MessageState `protogen:"open.v1"` 112 + Follow *Follow `protobuf:"bytes,1,opt,name=follow,proto3" json:"follow,omitempty"` 113 + unknownFields protoimpl.UnknownFields 114 + sizeCache protoimpl.SizeCache 115 + } 116 + 117 + func (x *CreateFollowRequest) Reset() { 118 + *x = CreateFollowRequest{} 119 + mi := &file_follow_proto_msgTypes[1] 120 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 121 + ms.StoreMessageInfo(mi) 122 + } 123 + 124 + func (x *CreateFollowRequest) String() string { 125 + return protoimpl.X.MessageStringOf(x) 126 + } 127 + 128 + func (*CreateFollowRequest) ProtoMessage() {} 129 + 130 + func (x *CreateFollowRequest) ProtoReflect() protoreflect.Message { 131 + mi := &file_follow_proto_msgTypes[1] 132 + if x != nil { 133 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 134 + if ms.LoadMessageInfo() == nil { 135 + ms.StoreMessageInfo(mi) 136 + } 137 + return ms 138 + } 139 + return mi.MessageOf(x) 140 + } 141 + 142 + // Deprecated: Use CreateFollowRequest.ProtoReflect.Descriptor instead. 143 + func (*CreateFollowRequest) Descriptor() ([]byte, []int) { 144 + return file_follow_proto_rawDescGZIP(), []int{1} 145 + } 146 + 147 + func (x *CreateFollowRequest) GetFollow() *Follow { 148 + if x != nil { 149 + return x.Follow 150 + } 151 + return nil 152 + } 153 + 154 + type CreateFollowResponse struct { 155 + state protoimpl.MessageState `protogen:"open.v1"` 156 + Error *string `protobuf:"bytes,1,opt,name=error,proto3,oneof" json:"error,omitempty"` 157 + unknownFields protoimpl.UnknownFields 158 + sizeCache protoimpl.SizeCache 159 + } 160 + 161 + func (x *CreateFollowResponse) Reset() { 162 + *x = CreateFollowResponse{} 163 + mi := &file_follow_proto_msgTypes[2] 164 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 165 + ms.StoreMessageInfo(mi) 166 + } 167 + 168 + func (x *CreateFollowResponse) String() string { 169 + return protoimpl.X.MessageStringOf(x) 170 + } 171 + 172 + func (*CreateFollowResponse) ProtoMessage() {} 173 + 174 + func (x *CreateFollowResponse) ProtoReflect() protoreflect.Message { 175 + mi := &file_follow_proto_msgTypes[2] 176 + if x != nil { 177 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 178 + if ms.LoadMessageInfo() == nil { 179 + ms.StoreMessageInfo(mi) 180 + } 181 + return ms 182 + } 183 + return mi.MessageOf(x) 184 + } 185 + 186 + // Deprecated: Use CreateFollowResponse.ProtoReflect.Descriptor instead. 187 + func (*CreateFollowResponse) Descriptor() ([]byte, []int) { 188 + return file_follow_proto_rawDescGZIP(), []int{2} 189 + } 190 + 191 + func (x *CreateFollowResponse) GetError() string { 192 + if x != nil && x.Error != nil { 193 + return *x.Error 194 + } 195 + return "" 196 + } 197 + 198 + type DeleteFollowRequest struct { 199 + state protoimpl.MessageState `protogen:"open.v1"` 200 + Uri string `protobuf:"bytes,1,opt,name=uri,proto3" json:"uri,omitempty"` 201 + unknownFields protoimpl.UnknownFields 202 + sizeCache protoimpl.SizeCache 203 + } 204 + 205 + func (x *DeleteFollowRequest) Reset() { 206 + *x = DeleteFollowRequest{} 207 + mi := &file_follow_proto_msgTypes[3] 208 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 209 + ms.StoreMessageInfo(mi) 210 + } 211 + 212 + func (x *DeleteFollowRequest) String() string { 213 + return protoimpl.X.MessageStringOf(x) 214 + } 215 + 216 + func (*DeleteFollowRequest) ProtoMessage() {} 217 + 218 + func (x *DeleteFollowRequest) ProtoReflect() protoreflect.Message { 219 + mi := &file_follow_proto_msgTypes[3] 220 + if x != nil { 221 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 222 + if ms.LoadMessageInfo() == nil { 223 + ms.StoreMessageInfo(mi) 224 + } 225 + return ms 226 + } 227 + return mi.MessageOf(x) 228 + } 229 + 230 + // Deprecated: Use DeleteFollowRequest.ProtoReflect.Descriptor instead. 231 + func (*DeleteFollowRequest) Descriptor() ([]byte, []int) { 232 + return file_follow_proto_rawDescGZIP(), []int{3} 233 + } 234 + 235 + func (x *DeleteFollowRequest) GetUri() string { 236 + if x != nil { 237 + return x.Uri 238 + } 239 + return "" 240 + } 241 + 242 + type DeleteFollowResponse struct { 243 + state protoimpl.MessageState `protogen:"open.v1"` 244 + Error *string `protobuf:"bytes,1,opt,name=error,proto3,oneof" json:"error,omitempty"` 245 + unknownFields protoimpl.UnknownFields 246 + sizeCache protoimpl.SizeCache 247 + } 248 + 249 + func (x *DeleteFollowResponse) Reset() { 250 + *x = DeleteFollowResponse{} 251 + mi := &file_follow_proto_msgTypes[4] 252 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 253 + ms.StoreMessageInfo(mi) 254 + } 255 + 256 + func (x *DeleteFollowResponse) String() string { 257 + return protoimpl.X.MessageStringOf(x) 258 + } 259 + 260 + func (*DeleteFollowResponse) ProtoMessage() {} 261 + 262 + func (x *DeleteFollowResponse) ProtoReflect() protoreflect.Message { 263 + mi := &file_follow_proto_msgTypes[4] 264 + if x != nil { 265 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 266 + if ms.LoadMessageInfo() == nil { 267 + ms.StoreMessageInfo(mi) 268 + } 269 + return ms 270 + } 271 + return mi.MessageOf(x) 272 + } 273 + 274 + // Deprecated: Use DeleteFollowResponse.ProtoReflect.Descriptor instead. 275 + func (*DeleteFollowResponse) Descriptor() ([]byte, []int) { 276 + return file_follow_proto_rawDescGZIP(), []int{4} 277 + } 278 + 279 + func (x *DeleteFollowResponse) GetError() string { 280 + if x != nil && x.Error != nil { 281 + return *x.Error 282 + } 283 + return "" 284 + } 285 + 286 + type GetFollowsByActorRequest struct { 287 + state protoimpl.MessageState `protogen:"open.v1"` 288 + Did string `protobuf:"bytes,1,opt,name=did,proto3" json:"did,omitempty"` 289 + Limit int64 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` 290 + Cursor *string `protobuf:"bytes,3,opt,name=cursor,proto3,oneof" json:"cursor,omitempty"` 291 + unknownFields protoimpl.UnknownFields 292 + sizeCache protoimpl.SizeCache 293 + } 294 + 295 + func (x *GetFollowsByActorRequest) Reset() { 296 + *x = GetFollowsByActorRequest{} 297 + mi := &file_follow_proto_msgTypes[5] 298 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 299 + ms.StoreMessageInfo(mi) 300 + } 301 + 302 + func (x *GetFollowsByActorRequest) String() string { 303 + return protoimpl.X.MessageStringOf(x) 304 + } 305 + 306 + func (*GetFollowsByActorRequest) ProtoMessage() {} 307 + 308 + func (x *GetFollowsByActorRequest) ProtoReflect() protoreflect.Message { 309 + mi := &file_follow_proto_msgTypes[5] 310 + if x != nil { 311 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 312 + if ms.LoadMessageInfo() == nil { 313 + ms.StoreMessageInfo(mi) 314 + } 315 + return ms 316 + } 317 + return mi.MessageOf(x) 318 + } 319 + 320 + // Deprecated: Use GetFollowsByActorRequest.ProtoReflect.Descriptor instead. 321 + func (*GetFollowsByActorRequest) Descriptor() ([]byte, []int) { 322 + return file_follow_proto_rawDescGZIP(), []int{5} 323 + } 324 + 325 + func (x *GetFollowsByActorRequest) GetDid() string { 326 + if x != nil { 327 + return x.Did 328 + } 329 + return "" 330 + } 331 + 332 + func (x *GetFollowsByActorRequest) GetLimit() int64 { 333 + if x != nil { 334 + return x.Limit 335 + } 336 + return 0 337 + } 338 + 339 + func (x *GetFollowsByActorRequest) GetCursor() string { 340 + if x != nil && x.Cursor != nil { 341 + return *x.Cursor 342 + } 343 + return "" 344 + } 345 + 346 + type GetFollowsByActorResponse struct { 347 + state protoimpl.MessageState `protogen:"open.v1"` 348 + Error *string `protobuf:"bytes,1,opt,name=error,proto3,oneof" json:"error,omitempty"` 349 + Follows []*Follow `protobuf:"bytes,2,rep,name=follows,proto3" json:"follows,omitempty"` 350 + Limit int64 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"` 351 + Cursor *string `protobuf:"bytes,4,opt,name=cursor,proto3,oneof" json:"cursor,omitempty"` 352 + unknownFields protoimpl.UnknownFields 353 + sizeCache protoimpl.SizeCache 354 + } 355 + 356 + func (x *GetFollowsByActorResponse) Reset() { 357 + *x = GetFollowsByActorResponse{} 358 + mi := &file_follow_proto_msgTypes[6] 359 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 360 + ms.StoreMessageInfo(mi) 361 + } 362 + 363 + func (x *GetFollowsByActorResponse) String() string { 364 + return protoimpl.X.MessageStringOf(x) 365 + } 366 + 367 + func (*GetFollowsByActorResponse) ProtoMessage() {} 368 + 369 + func (x *GetFollowsByActorResponse) ProtoReflect() protoreflect.Message { 370 + mi := &file_follow_proto_msgTypes[6] 371 + if x != nil { 372 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 373 + if ms.LoadMessageInfo() == nil { 374 + ms.StoreMessageInfo(mi) 375 + } 376 + return ms 377 + } 378 + return mi.MessageOf(x) 379 + } 380 + 381 + // Deprecated: Use GetFollowsByActorResponse.ProtoReflect.Descriptor instead. 382 + func (*GetFollowsByActorResponse) Descriptor() ([]byte, []int) { 383 + return file_follow_proto_rawDescGZIP(), []int{6} 384 + } 385 + 386 + func (x *GetFollowsByActorResponse) GetError() string { 387 + if x != nil && x.Error != nil { 388 + return *x.Error 389 + } 390 + return "" 391 + } 392 + 393 + func (x *GetFollowsByActorResponse) GetFollows() []*Follow { 394 + if x != nil { 395 + return x.Follows 396 + } 397 + return nil 398 + } 399 + 400 + func (x *GetFollowsByActorResponse) GetLimit() int64 { 401 + if x != nil { 402 + return x.Limit 403 + } 404 + return 0 405 + } 406 + 407 + func (x *GetFollowsByActorResponse) GetCursor() string { 408 + if x != nil && x.Cursor != nil { 409 + return *x.Cursor 410 + } 411 + return "" 412 + } 413 + 414 + type GetFollowersByActorRequest struct { 415 + state protoimpl.MessageState `protogen:"open.v1"` 416 + Did string `protobuf:"bytes,1,opt,name=did,proto3" json:"did,omitempty"` 417 + Limit int64 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` 418 + Cursor *string `protobuf:"bytes,3,opt,name=cursor,proto3,oneof" json:"cursor,omitempty"` 419 + unknownFields protoimpl.UnknownFields 420 + sizeCache protoimpl.SizeCache 421 + } 422 + 423 + func (x *GetFollowersByActorRequest) Reset() { 424 + *x = GetFollowersByActorRequest{} 425 + mi := &file_follow_proto_msgTypes[7] 426 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 427 + ms.StoreMessageInfo(mi) 428 + } 429 + 430 + func (x *GetFollowersByActorRequest) String() string { 431 + return protoimpl.X.MessageStringOf(x) 432 + } 433 + 434 + func (*GetFollowersByActorRequest) ProtoMessage() {} 435 + 436 + func (x *GetFollowersByActorRequest) ProtoReflect() protoreflect.Message { 437 + mi := &file_follow_proto_msgTypes[7] 438 + if x != nil { 439 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 440 + if ms.LoadMessageInfo() == nil { 441 + ms.StoreMessageInfo(mi) 442 + } 443 + return ms 444 + } 445 + return mi.MessageOf(x) 446 + } 447 + 448 + // Deprecated: Use GetFollowersByActorRequest.ProtoReflect.Descriptor instead. 449 + func (*GetFollowersByActorRequest) Descriptor() ([]byte, []int) { 450 + return file_follow_proto_rawDescGZIP(), []int{7} 451 + } 452 + 453 + func (x *GetFollowersByActorRequest) GetDid() string { 454 + if x != nil { 455 + return x.Did 456 + } 457 + return "" 458 + } 459 + 460 + func (x *GetFollowersByActorRequest) GetLimit() int64 { 461 + if x != nil { 462 + return x.Limit 463 + } 464 + return 0 465 + } 466 + 467 + func (x *GetFollowersByActorRequest) GetCursor() string { 468 + if x != nil && x.Cursor != nil { 469 + return *x.Cursor 470 + } 471 + return "" 472 + } 473 + 474 + type GetFollowersByActorResponse struct { 475 + state protoimpl.MessageState `protogen:"open.v1"` 476 + Error *string `protobuf:"bytes,1,opt,name=error,proto3,oneof" json:"error,omitempty"` 477 + Followers []*Follow `protobuf:"bytes,2,rep,name=followers,proto3" json:"followers,omitempty"` 478 + Limit int64 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"` 479 + Cursor *string `protobuf:"bytes,4,opt,name=cursor,proto3,oneof" json:"cursor,omitempty"` 480 + unknownFields protoimpl.UnknownFields 481 + sizeCache protoimpl.SizeCache 482 + } 483 + 484 + func (x *GetFollowersByActorResponse) Reset() { 485 + *x = GetFollowersByActorResponse{} 486 + mi := &file_follow_proto_msgTypes[8] 487 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 488 + ms.StoreMessageInfo(mi) 489 + } 490 + 491 + func (x *GetFollowersByActorResponse) String() string { 492 + return protoimpl.X.MessageStringOf(x) 493 + } 494 + 495 + func (*GetFollowersByActorResponse) ProtoMessage() {} 496 + 497 + func (x *GetFollowersByActorResponse) ProtoReflect() protoreflect.Message { 498 + mi := &file_follow_proto_msgTypes[8] 499 + if x != nil { 500 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 501 + if ms.LoadMessageInfo() == nil { 502 + ms.StoreMessageInfo(mi) 503 + } 504 + return ms 505 + } 506 + return mi.MessageOf(x) 507 + } 508 + 509 + // Deprecated: Use GetFollowersByActorResponse.ProtoReflect.Descriptor instead. 510 + func (*GetFollowersByActorResponse) Descriptor() ([]byte, []int) { 511 + return file_follow_proto_rawDescGZIP(), []int{8} 512 + } 513 + 514 + func (x *GetFollowersByActorResponse) GetError() string { 515 + if x != nil && x.Error != nil { 516 + return *x.Error 517 + } 518 + return "" 519 + } 520 + 521 + func (x *GetFollowersByActorResponse) GetFollowers() []*Follow { 522 + if x != nil { 523 + return x.Followers 524 + } 525 + return nil 526 + } 527 + 528 + func (x *GetFollowersByActorResponse) GetLimit() int64 { 529 + if x != nil { 530 + return x.Limit 531 + } 532 + return 0 533 + } 534 + 535 + func (x *GetFollowersByActorResponse) GetCursor() string { 536 + if x != nil && x.Cursor != nil { 537 + return *x.Cursor 538 + } 539 + return "" 540 + } 541 + 542 + type GetFollowForAuthorSubjectRequest struct { 543 + state protoimpl.MessageState `protogen:"open.v1"` 544 + AuthorDid string `protobuf:"bytes,1,opt,name=author_did,json=authorDid,proto3" json:"author_did,omitempty"` 545 + SubjectDid string `protobuf:"bytes,2,opt,name=subject_did,json=subjectDid,proto3" json:"subject_did,omitempty"` 546 + unknownFields protoimpl.UnknownFields 547 + sizeCache protoimpl.SizeCache 548 + } 549 + 550 + func (x *GetFollowForAuthorSubjectRequest) Reset() { 551 + *x = GetFollowForAuthorSubjectRequest{} 552 + mi := &file_follow_proto_msgTypes[9] 553 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 554 + ms.StoreMessageInfo(mi) 555 + } 556 + 557 + func (x *GetFollowForAuthorSubjectRequest) String() string { 558 + return protoimpl.X.MessageStringOf(x) 559 + } 560 + 561 + func (*GetFollowForAuthorSubjectRequest) ProtoMessage() {} 562 + 563 + func (x *GetFollowForAuthorSubjectRequest) ProtoReflect() protoreflect.Message { 564 + mi := &file_follow_proto_msgTypes[9] 565 + if x != nil { 566 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 567 + if ms.LoadMessageInfo() == nil { 568 + ms.StoreMessageInfo(mi) 569 + } 570 + return ms 571 + } 572 + return mi.MessageOf(x) 573 + } 574 + 575 + // Deprecated: Use GetFollowForAuthorSubjectRequest.ProtoReflect.Descriptor instead. 576 + func (*GetFollowForAuthorSubjectRequest) Descriptor() ([]byte, []int) { 577 + return file_follow_proto_rawDescGZIP(), []int{9} 578 + } 579 + 580 + func (x *GetFollowForAuthorSubjectRequest) GetAuthorDid() string { 581 + if x != nil { 582 + return x.AuthorDid 583 + } 584 + return "" 585 + } 586 + 587 + func (x *GetFollowForAuthorSubjectRequest) GetSubjectDid() string { 588 + if x != nil { 589 + return x.SubjectDid 590 + } 591 + return "" 592 + } 593 + 594 + type GetFollowForAuthorSubjectResponse struct { 595 + state protoimpl.MessageState `protogen:"open.v1"` 596 + Error *string `protobuf:"bytes,1,opt,name=error,proto3,oneof" json:"error,omitempty"` 597 + Follow *Follow `protobuf:"bytes,2,opt,name=follow,proto3,oneof" json:"follow,omitempty"` 598 + unknownFields protoimpl.UnknownFields 599 + sizeCache protoimpl.SizeCache 600 + } 601 + 602 + func (x *GetFollowForAuthorSubjectResponse) Reset() { 603 + *x = GetFollowForAuthorSubjectResponse{} 604 + mi := &file_follow_proto_msgTypes[10] 605 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 606 + ms.StoreMessageInfo(mi) 607 + } 608 + 609 + func (x *GetFollowForAuthorSubjectResponse) String() string { 610 + return protoimpl.X.MessageStringOf(x) 611 + } 612 + 613 + func (*GetFollowForAuthorSubjectResponse) ProtoMessage() {} 614 + 615 + func (x *GetFollowForAuthorSubjectResponse) ProtoReflect() protoreflect.Message { 616 + mi := &file_follow_proto_msgTypes[10] 617 + if x != nil { 618 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 619 + if ms.LoadMessageInfo() == nil { 620 + ms.StoreMessageInfo(mi) 621 + } 622 + return ms 623 + } 624 + return mi.MessageOf(x) 625 + } 626 + 627 + // Deprecated: Use GetFollowForAuthorSubjectResponse.ProtoReflect.Descriptor instead. 628 + func (*GetFollowForAuthorSubjectResponse) Descriptor() ([]byte, []int) { 629 + return file_follow_proto_rawDescGZIP(), []int{10} 630 + } 631 + 632 + func (x *GetFollowForAuthorSubjectResponse) GetError() string { 633 + if x != nil && x.Error != nil { 634 + return *x.Error 635 + } 636 + return "" 637 + } 638 + 639 + func (x *GetFollowForAuthorSubjectResponse) GetFollow() *Follow { 640 + if x != nil { 641 + return x.Follow 642 + } 643 + return nil 644 + } 645 + 646 + var File_follow_proto protoreflect.FileDescriptor 647 + 648 + const file_follow_proto_rawDesc = "" + 649 + "\n" + 650 + "\ffollow.proto\x12\rvyletdatabase\x1a\x1bbuf/validate/validate.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xfa\x01\n" + 651 + "\x06Follow\x12\x18\n" + 652 + "\x03uri\x18\x01 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\x03uri\x12\x18\n" + 653 + "\x03cid\x18\x02 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\x03cid\x12'\n" + 654 + "\vsubject_did\x18\x03 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\n" + 655 + "subjectDid\x12\x1d\n" + 656 + "\n" + 657 + "author_did\x18\x05 \x01(\tR\tauthorDid\x129\n" + 658 + "\n" + 659 + "created_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" + 660 + "\n" + 661 + "indexed_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\tindexedAt\"D\n" + 662 + "\x13CreateFollowRequest\x12-\n" + 663 + "\x06follow\x18\x01 \x01(\v2\x15.vyletdatabase.FollowR\x06follow\";\n" + 664 + "\x14CreateFollowResponse\x12\x19\n" + 665 + "\x05error\x18\x01 \x01(\tH\x00R\x05error\x88\x01\x01B\b\n" + 666 + "\x06_error\"/\n" + 667 + "\x13DeleteFollowRequest\x12\x18\n" + 668 + "\x03uri\x18\x01 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\x03uri\";\n" + 669 + "\x14DeleteFollowResponse\x12\x19\n" + 670 + "\x05error\x18\x01 \x01(\tH\x00R\x05error\x88\x01\x01B\b\n" + 671 + "\x06_error\"r\n" + 672 + "\x18GetFollowsByActorRequest\x12\x18\n" + 673 + "\x03did\x18\x01 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\x03did\x12\x14\n" + 674 + "\x05limit\x18\x02 \x01(\x03R\x05limit\x12\x1b\n" + 675 + "\x06cursor\x18\x03 \x01(\tH\x00R\x06cursor\x88\x01\x01B\t\n" + 676 + "\a_cursor\"\xaf\x01\n" + 677 + "\x19GetFollowsByActorResponse\x12\x19\n" + 678 + "\x05error\x18\x01 \x01(\tH\x00R\x05error\x88\x01\x01\x12/\n" + 679 + "\afollows\x18\x02 \x03(\v2\x15.vyletdatabase.FollowR\afollows\x12\x14\n" + 680 + "\x05limit\x18\x03 \x01(\x03R\x05limit\x12\x1b\n" + 681 + "\x06cursor\x18\x04 \x01(\tH\x01R\x06cursor\x88\x01\x01B\b\n" + 682 + "\x06_errorB\t\n" + 683 + "\a_cursor\"t\n" + 684 + "\x1aGetFollowersByActorRequest\x12\x18\n" + 685 + "\x03did\x18\x01 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\x03did\x12\x14\n" + 686 + "\x05limit\x18\x02 \x01(\x03R\x05limit\x12\x1b\n" + 687 + "\x06cursor\x18\x03 \x01(\tH\x00R\x06cursor\x88\x01\x01B\t\n" + 688 + "\a_cursor\"\xb5\x01\n" + 689 + "\x1bGetFollowersByActorResponse\x12\x19\n" + 690 + "\x05error\x18\x01 \x01(\tH\x00R\x05error\x88\x01\x01\x123\n" + 691 + "\tfollowers\x18\x02 \x03(\v2\x15.vyletdatabase.FollowR\tfollowers\x12\x14\n" + 692 + "\x05limit\x18\x03 \x01(\x03R\x05limit\x12\x1b\n" + 693 + "\x06cursor\x18\x04 \x01(\tH\x01R\x06cursor\x88\x01\x01B\b\n" + 694 + "\x06_errorB\t\n" + 695 + "\a_cursor\"r\n" + 696 + " GetFollowForAuthorSubjectRequest\x12%\n" + 697 + "\n" + 698 + "author_did\x18\x01 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\tauthorDid\x12'\n" + 699 + "\vsubject_did\x18\x02 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\n" + 700 + "subjectDid\"\x87\x01\n" + 701 + "!GetFollowForAuthorSubjectResponse\x12\x19\n" + 702 + "\x05error\x18\x01 \x01(\tH\x00R\x05error\x88\x01\x01\x122\n" + 703 + "\x06follow\x18\x02 \x01(\v2\x15.vyletdatabase.FollowH\x01R\x06follow\x88\x01\x01B\b\n" + 704 + "\x06_errorB\t\n" + 705 + "\a_follow2\x97\x04\n" + 706 + "\rFollowService\x12W\n" + 707 + "\fCreateFollow\x12\".vyletdatabase.CreateFollowRequest\x1a#.vyletdatabase.CreateFollowResponse\x12W\n" + 708 + "\fDeleteFollow\x12\".vyletdatabase.DeleteFollowRequest\x1a#.vyletdatabase.DeleteFollowResponse\x12f\n" + 709 + "\x11GetFollowsByActor\x12'.vyletdatabase.GetFollowsByActorRequest\x1a(.vyletdatabase.GetFollowsByActorResponse\x12l\n" + 710 + "\x13GetFollowersByActor\x12).vyletdatabase.GetFollowersByActorRequest\x1a*.vyletdatabase.GetFollowersByActorResponse\x12~\n" + 711 + "\x19GetFollowForAuthorSubject\x12/.vyletdatabase.GetFollowForAuthorSubjectRequest\x1a0.vyletdatabase.GetFollowForAuthorSubjectResponseB\x86\x01\n" + 712 + "\x11com.vyletdatabaseB\vFollowProtoP\x01Z\x10./;vyletdatabase\xa2\x02\x03VXX\xaa\x02\rVyletdatabase\xca\x02\rVyletdatabase\xe2\x02\x19Vyletdatabase\\GPBMetadata\xea\x02\rVyletdatabaseb\x06proto3" 713 + 714 + var ( 715 + file_follow_proto_rawDescOnce sync.Once 716 + file_follow_proto_rawDescData []byte 717 + ) 718 + 719 + func file_follow_proto_rawDescGZIP() []byte { 720 + file_follow_proto_rawDescOnce.Do(func() { 721 + file_follow_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_follow_proto_rawDesc), len(file_follow_proto_rawDesc))) 722 + }) 723 + return file_follow_proto_rawDescData 724 + } 725 + 726 + var file_follow_proto_msgTypes = make([]protoimpl.MessageInfo, 11) 727 + var file_follow_proto_goTypes = []any{ 728 + (*Follow)(nil), // 0: vyletdatabase.Follow 729 + (*CreateFollowRequest)(nil), // 1: vyletdatabase.CreateFollowRequest 730 + (*CreateFollowResponse)(nil), // 2: vyletdatabase.CreateFollowResponse 731 + (*DeleteFollowRequest)(nil), // 3: vyletdatabase.DeleteFollowRequest 732 + (*DeleteFollowResponse)(nil), // 4: vyletdatabase.DeleteFollowResponse 733 + (*GetFollowsByActorRequest)(nil), // 5: vyletdatabase.GetFollowsByActorRequest 734 + (*GetFollowsByActorResponse)(nil), // 6: vyletdatabase.GetFollowsByActorResponse 735 + (*GetFollowersByActorRequest)(nil), // 7: vyletdatabase.GetFollowersByActorRequest 736 + (*GetFollowersByActorResponse)(nil), // 8: vyletdatabase.GetFollowersByActorResponse 737 + (*GetFollowForAuthorSubjectRequest)(nil), // 9: vyletdatabase.GetFollowForAuthorSubjectRequest 738 + (*GetFollowForAuthorSubjectResponse)(nil), // 10: vyletdatabase.GetFollowForAuthorSubjectResponse 739 + (*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp 740 + } 741 + var file_follow_proto_depIdxs = []int32{ 742 + 11, // 0: vyletdatabase.Follow.created_at:type_name -> google.protobuf.Timestamp 743 + 11, // 1: vyletdatabase.Follow.indexed_at:type_name -> google.protobuf.Timestamp 744 + 0, // 2: vyletdatabase.CreateFollowRequest.follow:type_name -> vyletdatabase.Follow 745 + 0, // 3: vyletdatabase.GetFollowsByActorResponse.follows:type_name -> vyletdatabase.Follow 746 + 0, // 4: vyletdatabase.GetFollowersByActorResponse.followers:type_name -> vyletdatabase.Follow 747 + 0, // 5: vyletdatabase.GetFollowForAuthorSubjectResponse.follow:type_name -> vyletdatabase.Follow 748 + 1, // 6: vyletdatabase.FollowService.CreateFollow:input_type -> vyletdatabase.CreateFollowRequest 749 + 3, // 7: vyletdatabase.FollowService.DeleteFollow:input_type -> vyletdatabase.DeleteFollowRequest 750 + 5, // 8: vyletdatabase.FollowService.GetFollowsByActor:input_type -> vyletdatabase.GetFollowsByActorRequest 751 + 7, // 9: vyletdatabase.FollowService.GetFollowersByActor:input_type -> vyletdatabase.GetFollowersByActorRequest 752 + 9, // 10: vyletdatabase.FollowService.GetFollowForAuthorSubject:input_type -> vyletdatabase.GetFollowForAuthorSubjectRequest 753 + 2, // 11: vyletdatabase.FollowService.CreateFollow:output_type -> vyletdatabase.CreateFollowResponse 754 + 4, // 12: vyletdatabase.FollowService.DeleteFollow:output_type -> vyletdatabase.DeleteFollowResponse 755 + 6, // 13: vyletdatabase.FollowService.GetFollowsByActor:output_type -> vyletdatabase.GetFollowsByActorResponse 756 + 8, // 14: vyletdatabase.FollowService.GetFollowersByActor:output_type -> vyletdatabase.GetFollowersByActorResponse 757 + 10, // 15: vyletdatabase.FollowService.GetFollowForAuthorSubject:output_type -> vyletdatabase.GetFollowForAuthorSubjectResponse 758 + 11, // [11:16] is the sub-list for method output_type 759 + 6, // [6:11] is the sub-list for method input_type 760 + 6, // [6:6] is the sub-list for extension type_name 761 + 6, // [6:6] is the sub-list for extension extendee 762 + 0, // [0:6] is the sub-list for field type_name 763 + } 764 + 765 + func init() { file_follow_proto_init() } 766 + func file_follow_proto_init() { 767 + if File_follow_proto != nil { 768 + return 769 + } 770 + file_follow_proto_msgTypes[2].OneofWrappers = []any{} 771 + file_follow_proto_msgTypes[4].OneofWrappers = []any{} 772 + file_follow_proto_msgTypes[5].OneofWrappers = []any{} 773 + file_follow_proto_msgTypes[6].OneofWrappers = []any{} 774 + file_follow_proto_msgTypes[7].OneofWrappers = []any{} 775 + file_follow_proto_msgTypes[8].OneofWrappers = []any{} 776 + file_follow_proto_msgTypes[10].OneofWrappers = []any{} 777 + type x struct{} 778 + out := protoimpl.TypeBuilder{ 779 + File: protoimpl.DescBuilder{ 780 + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 781 + RawDescriptor: unsafe.Slice(unsafe.StringData(file_follow_proto_rawDesc), len(file_follow_proto_rawDesc)), 782 + NumEnums: 0, 783 + NumMessages: 11, 784 + NumExtensions: 0, 785 + NumServices: 1, 786 + }, 787 + GoTypes: file_follow_proto_goTypes, 788 + DependencyIndexes: file_follow_proto_depIdxs, 789 + MessageInfos: file_follow_proto_msgTypes, 790 + }.Build() 791 + File_follow_proto = out.File 792 + file_follow_proto_goTypes = nil 793 + file_follow_proto_depIdxs = nil 794 + }
+95
database/proto/follow.proto
··· 1 + syntax = "proto3"; 2 + 3 + package vyletdatabase; 4 + option go_package = "./;vyletdatabase"; 5 + 6 + import "buf/validate/validate.proto"; 7 + 8 + import "google/protobuf/timestamp.proto"; 9 + 10 + service FollowService { 11 + rpc CreateFollow(CreateFollowRequest) returns (CreateFollowResponse); 12 + rpc DeleteFollow(DeleteFollowRequest) returns (DeleteFollowResponse); 13 + 14 + rpc GetFollowsByActor(GetFollowsByActorRequest) returns (GetFollowsByActorResponse); 15 + rpc GetFollowersByActor(GetFollowersByActorRequest) returns (GetFollowersByActorResponse); 16 + 17 + rpc GetFollowForAuthorSubject(GetFollowForAuthorSubjectRequest) returns (GetFollowForAuthorSubjectResponse); 18 + } 19 + 20 + message Follow { 21 + string uri = 1 [ 22 + (buf.validate.field).required = true 23 + ]; 24 + string cid = 2 [ 25 + (buf.validate.field).required = true 26 + ]; 27 + string subject_did = 3 [ 28 + (buf.validate.field).required = true 29 + ]; 30 + string author_did = 5; 31 + google.protobuf.Timestamp created_at = 6; 32 + google.protobuf.Timestamp indexed_at = 7; 33 + } 34 + 35 + message CreateFollowRequest { 36 + Follow follow = 1; 37 + } 38 + 39 + message CreateFollowResponse { 40 + optional string error = 1; 41 + } 42 + 43 + message DeleteFollowRequest { 44 + string uri = 1 [ 45 + (buf.validate.field).required = true 46 + ]; 47 + } 48 + 49 + message DeleteFollowResponse { 50 + optional string error = 1; 51 + } 52 + 53 + message GetFollowsByActorRequest { 54 + string did = 1 [ 55 + (buf.validate.field).required = true 56 + ]; 57 + int64 limit = 2; 58 + optional string cursor = 3; 59 + } 60 + 61 + message GetFollowsByActorResponse { 62 + optional string error = 1; 63 + repeated Follow follows = 2; 64 + int64 limit = 3; 65 + optional string cursor = 4; 66 + } 67 + 68 + message GetFollowersByActorRequest { 69 + string did = 1 [ 70 + (buf.validate.field).required = true 71 + ]; 72 + int64 limit = 2; 73 + optional string cursor = 3; 74 + } 75 + 76 + message GetFollowersByActorResponse { 77 + optional string error = 1; 78 + repeated Follow followers = 2; 79 + int64 limit = 3; 80 + optional string cursor = 4; 81 + } 82 + 83 + message GetFollowForAuthorSubjectRequest { 84 + string author_did = 1 [ 85 + (buf.validate.field).required = true 86 + ]; 87 + string subject_did = 2 [ 88 + (buf.validate.field).required = true 89 + ]; 90 + } 91 + 92 + message GetFollowForAuthorSubjectResponse { 93 + optional string error = 1; 94 + optional Follow follow = 2; 95 + }
+273
database/proto/follow_grpc.pb.go
··· 1 + // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 + // versions: 3 + // - protoc-gen-go-grpc v1.6.0 4 + // - protoc (unknown) 5 + // source: follow.proto 6 + 7 + package vyletdatabase 8 + 9 + import ( 10 + context "context" 11 + grpc "google.golang.org/grpc" 12 + codes "google.golang.org/grpc/codes" 13 + status "google.golang.org/grpc/status" 14 + ) 15 + 16 + // This is a compile-time assertion to ensure that this generated file 17 + // is compatible with the grpc package it is being compiled against. 18 + // Requires gRPC-Go v1.64.0 or later. 19 + const _ = grpc.SupportPackageIsVersion9 20 + 21 + const ( 22 + FollowService_CreateFollow_FullMethodName = "/vyletdatabase.FollowService/CreateFollow" 23 + FollowService_DeleteFollow_FullMethodName = "/vyletdatabase.FollowService/DeleteFollow" 24 + FollowService_GetFollowsByActor_FullMethodName = "/vyletdatabase.FollowService/GetFollowsByActor" 25 + FollowService_GetFollowersByActor_FullMethodName = "/vyletdatabase.FollowService/GetFollowersByActor" 26 + FollowService_GetFollowForAuthorSubject_FullMethodName = "/vyletdatabase.FollowService/GetFollowForAuthorSubject" 27 + ) 28 + 29 + // FollowServiceClient is the client API for FollowService service. 30 + // 31 + // 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. 32 + type FollowServiceClient interface { 33 + CreateFollow(ctx context.Context, in *CreateFollowRequest, opts ...grpc.CallOption) (*CreateFollowResponse, error) 34 + DeleteFollow(ctx context.Context, in *DeleteFollowRequest, opts ...grpc.CallOption) (*DeleteFollowResponse, error) 35 + GetFollowsByActor(ctx context.Context, in *GetFollowsByActorRequest, opts ...grpc.CallOption) (*GetFollowsByActorResponse, error) 36 + GetFollowersByActor(ctx context.Context, in *GetFollowersByActorRequest, opts ...grpc.CallOption) (*GetFollowersByActorResponse, error) 37 + GetFollowForAuthorSubject(ctx context.Context, in *GetFollowForAuthorSubjectRequest, opts ...grpc.CallOption) (*GetFollowForAuthorSubjectResponse, error) 38 + } 39 + 40 + type followServiceClient struct { 41 + cc grpc.ClientConnInterface 42 + } 43 + 44 + func NewFollowServiceClient(cc grpc.ClientConnInterface) FollowServiceClient { 45 + return &followServiceClient{cc} 46 + } 47 + 48 + func (c *followServiceClient) CreateFollow(ctx context.Context, in *CreateFollowRequest, opts ...grpc.CallOption) (*CreateFollowResponse, error) { 49 + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 50 + out := new(CreateFollowResponse) 51 + err := c.cc.Invoke(ctx, FollowService_CreateFollow_FullMethodName, in, out, cOpts...) 52 + if err != nil { 53 + return nil, err 54 + } 55 + return out, nil 56 + } 57 + 58 + func (c *followServiceClient) DeleteFollow(ctx context.Context, in *DeleteFollowRequest, opts ...grpc.CallOption) (*DeleteFollowResponse, error) { 59 + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 60 + out := new(DeleteFollowResponse) 61 + err := c.cc.Invoke(ctx, FollowService_DeleteFollow_FullMethodName, in, out, cOpts...) 62 + if err != nil { 63 + return nil, err 64 + } 65 + return out, nil 66 + } 67 + 68 + func (c *followServiceClient) GetFollowsByActor(ctx context.Context, in *GetFollowsByActorRequest, opts ...grpc.CallOption) (*GetFollowsByActorResponse, error) { 69 + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 70 + out := new(GetFollowsByActorResponse) 71 + err := c.cc.Invoke(ctx, FollowService_GetFollowsByActor_FullMethodName, in, out, cOpts...) 72 + if err != nil { 73 + return nil, err 74 + } 75 + return out, nil 76 + } 77 + 78 + func (c *followServiceClient) GetFollowersByActor(ctx context.Context, in *GetFollowersByActorRequest, opts ...grpc.CallOption) (*GetFollowersByActorResponse, error) { 79 + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 80 + out := new(GetFollowersByActorResponse) 81 + err := c.cc.Invoke(ctx, FollowService_GetFollowersByActor_FullMethodName, in, out, cOpts...) 82 + if err != nil { 83 + return nil, err 84 + } 85 + return out, nil 86 + } 87 + 88 + func (c *followServiceClient) GetFollowForAuthorSubject(ctx context.Context, in *GetFollowForAuthorSubjectRequest, opts ...grpc.CallOption) (*GetFollowForAuthorSubjectResponse, error) { 89 + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 90 + out := new(GetFollowForAuthorSubjectResponse) 91 + err := c.cc.Invoke(ctx, FollowService_GetFollowForAuthorSubject_FullMethodName, in, out, cOpts...) 92 + if err != nil { 93 + return nil, err 94 + } 95 + return out, nil 96 + } 97 + 98 + // FollowServiceServer is the server API for FollowService service. 99 + // All implementations must embed UnimplementedFollowServiceServer 100 + // for forward compatibility. 101 + type FollowServiceServer interface { 102 + CreateFollow(context.Context, *CreateFollowRequest) (*CreateFollowResponse, error) 103 + DeleteFollow(context.Context, *DeleteFollowRequest) (*DeleteFollowResponse, error) 104 + GetFollowsByActor(context.Context, *GetFollowsByActorRequest) (*GetFollowsByActorResponse, error) 105 + GetFollowersByActor(context.Context, *GetFollowersByActorRequest) (*GetFollowersByActorResponse, error) 106 + GetFollowForAuthorSubject(context.Context, *GetFollowForAuthorSubjectRequest) (*GetFollowForAuthorSubjectResponse, error) 107 + mustEmbedUnimplementedFollowServiceServer() 108 + } 109 + 110 + // UnimplementedFollowServiceServer must be embedded to have 111 + // forward compatible implementations. 112 + // 113 + // NOTE: this should be embedded by value instead of pointer to avoid a nil 114 + // pointer dereference when methods are called. 115 + type UnimplementedFollowServiceServer struct{} 116 + 117 + func (UnimplementedFollowServiceServer) CreateFollow(context.Context, *CreateFollowRequest) (*CreateFollowResponse, error) { 118 + return nil, status.Error(codes.Unimplemented, "method CreateFollow not implemented") 119 + } 120 + func (UnimplementedFollowServiceServer) DeleteFollow(context.Context, *DeleteFollowRequest) (*DeleteFollowResponse, error) { 121 + return nil, status.Error(codes.Unimplemented, "method DeleteFollow not implemented") 122 + } 123 + func (UnimplementedFollowServiceServer) GetFollowsByActor(context.Context, *GetFollowsByActorRequest) (*GetFollowsByActorResponse, error) { 124 + return nil, status.Error(codes.Unimplemented, "method GetFollowsByActor not implemented") 125 + } 126 + func (UnimplementedFollowServiceServer) GetFollowersByActor(context.Context, *GetFollowersByActorRequest) (*GetFollowersByActorResponse, error) { 127 + return nil, status.Error(codes.Unimplemented, "method GetFollowersByActor not implemented") 128 + } 129 + func (UnimplementedFollowServiceServer) GetFollowForAuthorSubject(context.Context, *GetFollowForAuthorSubjectRequest) (*GetFollowForAuthorSubjectResponse, error) { 130 + return nil, status.Error(codes.Unimplemented, "method GetFollowForAuthorSubject not implemented") 131 + } 132 + func (UnimplementedFollowServiceServer) mustEmbedUnimplementedFollowServiceServer() {} 133 + func (UnimplementedFollowServiceServer) testEmbeddedByValue() {} 134 + 135 + // UnsafeFollowServiceServer may be embedded to opt out of forward compatibility for this service. 136 + // Use of this interface is not recommended, as added methods to FollowServiceServer will 137 + // result in compilation errors. 138 + type UnsafeFollowServiceServer interface { 139 + mustEmbedUnimplementedFollowServiceServer() 140 + } 141 + 142 + func RegisterFollowServiceServer(s grpc.ServiceRegistrar, srv FollowServiceServer) { 143 + // If the following call panics, it indicates UnimplementedFollowServiceServer was 144 + // embedded by pointer and is nil. This will cause panics if an 145 + // unimplemented method is ever invoked, so we test this at initialization 146 + // time to prevent it from happening at runtime later due to I/O. 147 + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { 148 + t.testEmbeddedByValue() 149 + } 150 + s.RegisterService(&FollowService_ServiceDesc, srv) 151 + } 152 + 153 + func _FollowService_CreateFollow_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 154 + in := new(CreateFollowRequest) 155 + if err := dec(in); err != nil { 156 + return nil, err 157 + } 158 + if interceptor == nil { 159 + return srv.(FollowServiceServer).CreateFollow(ctx, in) 160 + } 161 + info := &grpc.UnaryServerInfo{ 162 + Server: srv, 163 + FullMethod: FollowService_CreateFollow_FullMethodName, 164 + } 165 + handler := func(ctx context.Context, req interface{}) (interface{}, error) { 166 + return srv.(FollowServiceServer).CreateFollow(ctx, req.(*CreateFollowRequest)) 167 + } 168 + return interceptor(ctx, in, info, handler) 169 + } 170 + 171 + func _FollowService_DeleteFollow_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 172 + in := new(DeleteFollowRequest) 173 + if err := dec(in); err != nil { 174 + return nil, err 175 + } 176 + if interceptor == nil { 177 + return srv.(FollowServiceServer).DeleteFollow(ctx, in) 178 + } 179 + info := &grpc.UnaryServerInfo{ 180 + Server: srv, 181 + FullMethod: FollowService_DeleteFollow_FullMethodName, 182 + } 183 + handler := func(ctx context.Context, req interface{}) (interface{}, error) { 184 + return srv.(FollowServiceServer).DeleteFollow(ctx, req.(*DeleteFollowRequest)) 185 + } 186 + return interceptor(ctx, in, info, handler) 187 + } 188 + 189 + func _FollowService_GetFollowsByActor_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 190 + in := new(GetFollowsByActorRequest) 191 + if err := dec(in); err != nil { 192 + return nil, err 193 + } 194 + if interceptor == nil { 195 + return srv.(FollowServiceServer).GetFollowsByActor(ctx, in) 196 + } 197 + info := &grpc.UnaryServerInfo{ 198 + Server: srv, 199 + FullMethod: FollowService_GetFollowsByActor_FullMethodName, 200 + } 201 + handler := func(ctx context.Context, req interface{}) (interface{}, error) { 202 + return srv.(FollowServiceServer).GetFollowsByActor(ctx, req.(*GetFollowsByActorRequest)) 203 + } 204 + return interceptor(ctx, in, info, handler) 205 + } 206 + 207 + func _FollowService_GetFollowersByActor_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 208 + in := new(GetFollowersByActorRequest) 209 + if err := dec(in); err != nil { 210 + return nil, err 211 + } 212 + if interceptor == nil { 213 + return srv.(FollowServiceServer).GetFollowersByActor(ctx, in) 214 + } 215 + info := &grpc.UnaryServerInfo{ 216 + Server: srv, 217 + FullMethod: FollowService_GetFollowersByActor_FullMethodName, 218 + } 219 + handler := func(ctx context.Context, req interface{}) (interface{}, error) { 220 + return srv.(FollowServiceServer).GetFollowersByActor(ctx, req.(*GetFollowersByActorRequest)) 221 + } 222 + return interceptor(ctx, in, info, handler) 223 + } 224 + 225 + func _FollowService_GetFollowForAuthorSubject_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 226 + in := new(GetFollowForAuthorSubjectRequest) 227 + if err := dec(in); err != nil { 228 + return nil, err 229 + } 230 + if interceptor == nil { 231 + return srv.(FollowServiceServer).GetFollowForAuthorSubject(ctx, in) 232 + } 233 + info := &grpc.UnaryServerInfo{ 234 + Server: srv, 235 + FullMethod: FollowService_GetFollowForAuthorSubject_FullMethodName, 236 + } 237 + handler := func(ctx context.Context, req interface{}) (interface{}, error) { 238 + return srv.(FollowServiceServer).GetFollowForAuthorSubject(ctx, req.(*GetFollowForAuthorSubjectRequest)) 239 + } 240 + return interceptor(ctx, in, info, handler) 241 + } 242 + 243 + // FollowService_ServiceDesc is the grpc.ServiceDesc for FollowService service. 244 + // It's only intended for direct use with grpc.RegisterService, 245 + // and not to be introspected or modified (even as a copy) 246 + var FollowService_ServiceDesc = grpc.ServiceDesc{ 247 + ServiceName: "vyletdatabase.FollowService", 248 + HandlerType: (*FollowServiceServer)(nil), 249 + Methods: []grpc.MethodDesc{ 250 + { 251 + MethodName: "CreateFollow", 252 + Handler: _FollowService_CreateFollow_Handler, 253 + }, 254 + { 255 + MethodName: "DeleteFollow", 256 + Handler: _FollowService_DeleteFollow_Handler, 257 + }, 258 + { 259 + MethodName: "GetFollowsByActor", 260 + Handler: _FollowService_GetFollowsByActor_Handler, 261 + }, 262 + { 263 + MethodName: "GetFollowersByActor", 264 + Handler: _FollowService_GetFollowersByActor_Handler, 265 + }, 266 + { 267 + MethodName: "GetFollowForAuthorSubject", 268 + Handler: _FollowService_GetFollowForAuthorSubject_Handler, 269 + }, 270 + }, 271 + Streams: []grpc.StreamDesc{}, 272 + Metadata: "follow.proto", 273 + }
+394
database/server/follow.go
··· 1 + package server 2 + 3 + import ( 4 + "context" 5 + "errors" 6 + "fmt" 7 + "strings" 8 + "time" 9 + 10 + "github.com/gocql/gocql" 11 + vyletdatabase "github.com/vylet-app/go/database/proto" 12 + "github.com/vylet-app/go/internal/helpers" 13 + "google.golang.org/protobuf/types/known/timestamppb" 14 + ) 15 + 16 + func (s *Server) CreateFollow(ctx context.Context, req *vyletdatabase.CreateFollowRequest) (*vyletdatabase.CreateFollowResponse, error) { 17 + logger := s.logger.With("name", "CreateFollow", "uri", req.Follow.Uri, "did", req.Follow.AuthorDid, "subjectDid", req.Follow.SubjectDid) 18 + 19 + now := time.Now().UTC() 20 + 21 + batch := s.cqlSession.NewBatch(gocql.LoggedBatch).WithContext(ctx) 22 + 23 + args := []any{ 24 + req.Follow.Uri, 25 + req.Follow.Cid, 26 + req.Follow.SubjectDid, 27 + req.Follow.AuthorDid, 28 + req.Follow.CreatedAt.AsTime(), 29 + now, 30 + } 31 + 32 + query := ` 33 + INSERT INTO %s 34 + (uri, cid, subject_did, author_did, created_at, indexed_at) 35 + VALUES 36 + (?, ?, ?, ?, ?, ?) 37 + ` 38 + 39 + batch.Query(fmt.Sprintf(query, "follows_by_subject_did"), args...) 40 + batch.Query(fmt.Sprintf(query, "follows_by_author_did"), args...) 41 + batch.Query(fmt.Sprintf(query, "follows_by_uri"), args...) 42 + batch.Query(fmt.Sprintf(query, "follows_by_author_did_subject_did"), args...) 43 + 44 + if err := s.cqlSession.ExecuteBatch(batch); err != nil { 45 + logger.Error("failed to create follow", "err", err) 46 + return &vyletdatabase.CreateFollowResponse{ 47 + Error: helpers.ToStringPtr(err.Error()), 48 + }, nil 49 + } 50 + 51 + if err := s.cqlSession.Query(` 52 + UPDATE follow_counts 53 + SET follows_count = follows_count + 1 54 + WHERE did = ? 55 + `, req.Follow.AuthorDid).WithContext(ctx).Exec(); err != nil { 56 + logger.Error("failed to increment follows count", "err", err) 57 + return &vyletdatabase.CreateFollowResponse{ 58 + Error: helpers.ToStringPtr(err.Error()), 59 + }, nil 60 + } 61 + 62 + if err := s.cqlSession.Query(` 63 + UPDATE follow_counts 64 + SET followers_count = followers_count + 1 65 + WHERE did = ? 66 + `, req.Follow.SubjectDid).WithContext(ctx).Exec(); err != nil { 67 + logger.Error("failed to increment followers count", "err", err) 68 + return &vyletdatabase.CreateFollowResponse{ 69 + Error: helpers.ToStringPtr(err.Error()), 70 + }, nil 71 + } 72 + 73 + return &vyletdatabase.CreateFollowResponse{}, nil 74 + } 75 + 76 + func (s *Server) DeleteFollow(ctx context.Context, req *vyletdatabase.DeleteFollowRequest) (*vyletdatabase.DeleteFollowResponse, error) { 77 + logger := s.logger.With("name", "DeleteFollow", "uri", req.Uri) 78 + 79 + var ( 80 + createdAt time.Time 81 + subjectDid string 82 + authorDid string 83 + ) 84 + 85 + query := ` 86 + SELECT created_at, subject_did, author_did 87 + FROM follows_by_uri 88 + WHERE uri = ? 89 + ` 90 + if err := s.cqlSession.Query(query, req.Uri).WithContext(ctx).Scan(&createdAt, &subjectDid, &authorDid); err != nil { 91 + if err == gocql.ErrNotFound { 92 + logger.Warn("follow not found", "uri", req.Uri) 93 + return &vyletdatabase.DeleteFollowResponse{ 94 + Error: helpers.ToStringPtr("follow not found"), 95 + }, nil 96 + } 97 + logger.Error("failed to fetch follow", "uri", req.Uri, "err", err) 98 + return &vyletdatabase.DeleteFollowResponse{ 99 + Error: helpers.ToStringPtr(err.Error()), 100 + }, nil 101 + } 102 + 103 + logger = logger.With("authorDid", authorDid, "subjectDid", subjectDid) 104 + 105 + batch := s.cqlSession.NewBatch(gocql.LoggedBatch).WithContext(ctx) 106 + 107 + batch.Query(` 108 + DELETE FROM follows_by_uri 109 + WHERE uri = ? 110 + `, req.Uri) 111 + 112 + batch.Query(` 113 + DELETE FROM follows_by_subject_did 114 + WHERE subject_did = ? AND created_at = ? AND uri = ? 115 + `, subjectDid, createdAt, req.Uri) 116 + 117 + batch.Query(` 118 + DELETE FROM follows_by_author_did 119 + WHERE author_did = ? AND created_at = ? AND uri = ? 120 + `, authorDid, createdAt, req.Uri) 121 + 122 + batch.Query(` 123 + DELETE FROM follows_by_author_did_subject_did 124 + WHERE author_did = ? AND subject_did = ? 125 + `, authorDid, subjectDid) 126 + 127 + if err := s.cqlSession.ExecuteBatch(batch); err != nil { 128 + logger.Error("failed to delete follow", "uri", req.Uri, "err", err) 129 + return &vyletdatabase.DeleteFollowResponse{ 130 + Error: helpers.ToStringPtr(err.Error()), 131 + }, nil 132 + } 133 + 134 + if err := s.cqlSession.Query(` 135 + UPDATE follow_counts 136 + SET follows_count = follows_count - 1 137 + WHERE did = ? 138 + `, authorDid).WithContext(ctx).Exec(); err != nil { 139 + logger.Error("failed to decrement follows count", "err", err) 140 + return &vyletdatabase.DeleteFollowResponse{ 141 + Error: helpers.ToStringPtr(err.Error()), 142 + }, nil 143 + } 144 + 145 + if err := s.cqlSession.Query(` 146 + UPDATE follow_counts 147 + SET followers_count = followers_count - 1 148 + WHERE did = ? 149 + `, subjectDid).WithContext(ctx).Exec(); err != nil { 150 + logger.Error("failed to decrement followers count", "err", err) 151 + return &vyletdatabase.DeleteFollowResponse{ 152 + Error: helpers.ToStringPtr(err.Error()), 153 + }, nil 154 + } 155 + 156 + return &vyletdatabase.DeleteFollowResponse{}, nil 157 + } 158 + 159 + func (s *Server) GetFollowsByActor(ctx context.Context, req *vyletdatabase.GetFollowsByActorRequest) (*vyletdatabase.GetFollowsByActorResponse, error) { 160 + logger := s.logger.With("name", "GetFollowsByActor", "did", req.Did) 161 + 162 + if req.Limit <= 0 { 163 + return nil, fmt.Errorf("limit must be greater than 0") 164 + } 165 + 166 + var ( 167 + query string 168 + args []any 169 + ) 170 + 171 + if req.Cursor != nil && *req.Cursor != "" { 172 + cursorParts := strings.SplitN(*req.Cursor, "|", 2) 173 + if len(cursorParts) != 2 { 174 + logger.Error("invalid cursor format", "cursor", *req.Cursor) 175 + return &vyletdatabase.GetFollowsByActorResponse{ 176 + Error: helpers.ToStringPtr("invalid cursor format"), 177 + }, nil 178 + } 179 + 180 + cursorTime, err := time.Parse(time.RFC3339Nano, cursorParts[0]) 181 + if err != nil { 182 + logger.Error("failed to parse cursor timestamp", "cursor", *req.Cursor, "err", err) 183 + return &vyletdatabase.GetFollowsByActorResponse{ 184 + Error: helpers.ToStringPtr("invalid cursor format"), 185 + }, nil 186 + } 187 + cursorUri := cursorParts[1] 188 + 189 + query = ` 190 + SELECT uri, cid, subject_did, author_did, created_at, indexed_at 191 + FROM follows_by_author_did 192 + WHERE author_did = ? AND (created_at, uri) < (?, ?) 193 + ORDER BY created_at DESC, uri ASC 194 + LIMIT ? 195 + ` 196 + args = []any{req.Did, cursorTime, cursorUri, req.Limit + 1} 197 + } else { 198 + query = ` 199 + SELECT uri, cid, subject_did, author_did, created_at, indexed_at 200 + FROM follows_by_author_did 201 + WHERE author_did = ? 202 + ORDER BY created_at DESC, uri ASC 203 + LIMIT ? 204 + ` 205 + args = []any{req.Did, req.Limit} 206 + } 207 + 208 + iter := s.cqlSession.Query(query, args...).WithContext(ctx).Iter() 209 + defer iter.Close() 210 + 211 + var follows []*vyletdatabase.Follow 212 + 213 + var ( 214 + createdAt time.Time 215 + indexedAt time.Time 216 + ) 217 + for { 218 + follow := &vyletdatabase.Follow{} 219 + if !iter.Scan( 220 + &follow.Uri, 221 + &follow.Cid, 222 + &follow.SubjectDid, 223 + &follow.AuthorDid, 224 + &createdAt, 225 + &indexedAt, 226 + ) { 227 + break 228 + } 229 + follow.CreatedAt = timestamppb.New(createdAt) 230 + follow.IndexedAt = timestamppb.New(indexedAt) 231 + 232 + follows = append(follows, follow) 233 + } 234 + if err := iter.Close(); err != nil { 235 + logger.Error("failed to iterate follows", "err", err) 236 + return &vyletdatabase.GetFollowsByActorResponse{ 237 + Error: helpers.ToStringPtr(err.Error()), 238 + }, nil 239 + } 240 + 241 + var nextCursor *string 242 + if len(follows) > int(req.Limit) { 243 + follows = follows[:req.Limit] 244 + last := follows[len(follows)-1] 245 + cursorStr := fmt.Sprintf("%s|%s", 246 + last.CreatedAt.AsTime().Format(time.RFC3339Nano), 247 + last.Uri) 248 + nextCursor = &cursorStr 249 + } 250 + 251 + return &vyletdatabase.GetFollowsByActorResponse{ 252 + Follows: follows, 253 + Cursor: nextCursor, 254 + }, nil 255 + } 256 + 257 + func (s *Server) GetFollowersByActor(ctx context.Context, req *vyletdatabase.GetFollowersByActorRequest) (*vyletdatabase.GetFollowersByActorResponse, error) { 258 + logger := s.logger.With("name", "GetFollowersByActor", "did", req.Did) 259 + 260 + if req.Limit <= 0 { 261 + return nil, fmt.Errorf("limit must be greater than 0") 262 + } 263 + 264 + var ( 265 + query string 266 + args []any 267 + ) 268 + 269 + if req.Cursor != nil && *req.Cursor != "" { 270 + cursorParts := strings.SplitN(*req.Cursor, "|", 2) 271 + if len(cursorParts) != 2 { 272 + logger.Error("invalid cursor format", "cursor", *req.Cursor) 273 + return &vyletdatabase.GetFollowersByActorResponse{ 274 + Error: helpers.ToStringPtr("invalid cursor format"), 275 + }, nil 276 + } 277 + 278 + cursorTime, err := time.Parse(time.RFC3339Nano, cursorParts[0]) 279 + if err != nil { 280 + logger.Error("failed to parse cursor timestamp", "cursor", *req.Cursor, "err", err) 281 + return &vyletdatabase.GetFollowersByActorResponse{ 282 + Error: helpers.ToStringPtr("invalid cursor format"), 283 + }, nil 284 + } 285 + cursorUri := cursorParts[1] 286 + 287 + query = ` 288 + SELECT uri, cid, subject_did, author_did, created_at, indexed_at 289 + FROM follows_by_subject_did 290 + WHERE subject_did = ? AND (created_at, uri) < (?, ?) 291 + ORDER BY created_at DESC, uri ASC 292 + LIMIT ? 293 + ` 294 + args = []any{req.Did, cursorTime, cursorUri, req.Limit + 1} 295 + } else { 296 + query = ` 297 + SELECT uri, cid, subject_did, author_did, created_at, indexed_at 298 + FROM follows_by_subject_did 299 + WHERE subject_did = ? 300 + ORDER BY created_at DESC, uri ASC 301 + LIMIT ? 302 + ` 303 + args = []any{req.Did, req.Limit} 304 + } 305 + 306 + iter := s.cqlSession.Query(query, args...).WithContext(ctx).Iter() 307 + defer iter.Close() 308 + 309 + var follows []*vyletdatabase.Follow 310 + 311 + var ( 312 + createdAt time.Time 313 + indexedAt time.Time 314 + ) 315 + for { 316 + follow := &vyletdatabase.Follow{} 317 + if !iter.Scan( 318 + &follow.Uri, 319 + &follow.Cid, 320 + &follow.SubjectDid, 321 + &follow.AuthorDid, 322 + &createdAt, 323 + &indexedAt, 324 + ) { 325 + break 326 + } 327 + follow.CreatedAt = timestamppb.New(createdAt) 328 + follow.IndexedAt = timestamppb.New(indexedAt) 329 + 330 + follows = append(follows, follow) 331 + } 332 + if err := iter.Close(); err != nil { 333 + logger.Error("failed to iterate follows", "err", err) 334 + return &vyletdatabase.GetFollowersByActorResponse{ 335 + Error: helpers.ToStringPtr(err.Error()), 336 + }, nil 337 + } 338 + 339 + var nextCursor *string 340 + if len(follows) > int(req.Limit) { 341 + follows = follows[:req.Limit] 342 + last := follows[len(follows)-1] 343 + cursorStr := fmt.Sprintf("%s|%s", 344 + last.CreatedAt.AsTime().Format(time.RFC3339Nano), 345 + last.Uri) 346 + nextCursor = &cursorStr 347 + } 348 + 349 + return &vyletdatabase.GetFollowersByActorResponse{ 350 + Followers: follows, 351 + Cursor: nextCursor, 352 + }, nil 353 + } 354 + 355 + func (s *Server) GetFollowForAuthorSubject(ctx context.Context, req *vyletdatabase.GetFollowForAuthorSubjectRequest) (*vyletdatabase.GetFollowForAuthorSubjectResponse, error) { 356 + logger := s.logger.With("name", "GetFollowForAuthorSubject", "authorDid", req.AuthorDid, "subjectDid", req.SubjectDid) 357 + 358 + args := []any{req.AuthorDid, req.SubjectDid} 359 + 360 + query := ` 361 + SELECT uri, cid, subject_did, author_did, created_at, indexed_at 362 + FROM follows_by_author_did_subject_did 363 + WHERE author_did = ? AND subject_did = ? 364 + ` 365 + 366 + follow := &vyletdatabase.Follow{} 367 + var ( 368 + createdAt time.Time 369 + indexedAt time.Time 370 + ) 371 + if err := s.cqlSession.Query(query, args...).WithContext(ctx).Scan( 372 + &follow.Uri, 373 + &follow.Cid, 374 + &follow.SubjectDid, 375 + &follow.AuthorDid, 376 + &createdAt, 377 + &indexedAt, 378 + ); err != nil { 379 + if errors.Is(err, gocql.ErrNotFound) { 380 + return &vyletdatabase.GetFollowForAuthorSubjectResponse{}, nil 381 + } 382 + 383 + logger.Error("error finding follow", "err", err) 384 + return &vyletdatabase.GetFollowForAuthorSubjectResponse{ 385 + Error: helpers.ToStringPtr(err.Error()), 386 + }, nil 387 + } 388 + follow.CreatedAt = timestamppb.New(createdAt) 389 + follow.IndexedAt = timestamppb.New(indexedAt) 390 + 391 + return &vyletdatabase.GetFollowForAuthorSubjectResponse{ 392 + Follow: follow, 393 + }, nil 394 + }
+6
database/server/like.go
··· 46 46 batch.Query(fmt.Sprintf(likeQuery, "likes_by_subject"), likeArgs...) 47 47 batch.Query(fmt.Sprintf(likeQuery, "likes_by_actor"), likeArgs...) 48 48 batch.Query(fmt.Sprintf(likeQuery, "likes_by_uri"), likeArgs...) 49 + batch.Query(fmt.Sprintf(likeQuery, "likes_by_actor_subject"), likeArgs...) 49 50 50 51 if err := s.cqlSession.ExecuteBatch(batch); err != nil { 51 52 logger.Error("failed to create like", "uri", req.Like.Uri, "err", err) ··· 111 112 DELETE FROM likes_by_actor 112 113 WHERE author_did = ? AND created_at = ? AND uri = ? 113 114 `, authorDid, createdAt, req.Uri) 115 + 116 + batch.Query(` 117 + DELETE FROM likes_by_actor_subject 118 + WHERE author_did = ? AND subject_uri = ? 119 + `, authorDid, subjectUri) 114 120 115 121 if err := s.cqlSession.ExecuteBatch(batch); err != nil { 116 122 logger.Error("failed to delete like", "uri", req.Uri, "err", err)
+2
database/server/server.go
··· 34 34 vyletdatabase.UnimplementedPostServiceServer 35 35 vyletdatabase.UnimplementedLikeServiceServer 36 36 vyletdatabase.UnimplementedBlobRefServiceServer 37 + vyletdatabase.UnimplementedFollowServiceServer 37 38 38 39 logger *slog.Logger 39 40 ··· 161 162 vyletdatabase.RegisterPostServiceServer(s.grpcServer, s) 162 163 vyletdatabase.RegisterLikeServiceServer(s.grpcServer, s) 163 164 vyletdatabase.RegisterBlobRefServiceServer(s.grpcServer, s) 165 + vyletdatabase.RegisterFollowServiceServer(s.grpcServer, s) 164 166 reflection.Register(s.grpcServer) 165 167 } 166 168
+32
generated/handlers/graphgetactorfollowers.go
··· 1 + // GENERATED CODE - DO NOT MODIFY 2 + // Generated by vylet-app/handlergen 3 + 4 + package handlers 5 + 6 + import ( 7 + "net/http" 8 + 9 + "github.com/labstack/echo/v4" 10 + ) 11 + 12 + type GraphGetActorFollowersInput struct { 13 + Actor string `query:"actor"` 14 + Cursor *string `query:"cursor"` 15 + Limit *int64 `query:"limit"` 16 + } 17 + 18 + func (h *Handlers) HandleGraphGetActorFollowers(e echo.Context) error { 19 + var input GraphGetActorFollowersInput 20 + if err := e.Bind(&input); err != nil { 21 + logger := h.server.Logger().With("handler", "HandleGraphGetActorFollowers") 22 + logger.Error("error binding request", "err", err) 23 + return echo.NewHTTPError(http.StatusInternalServerError, "Internal Server Error") 24 + } 25 + 26 + output, err := h.server.HandleGraphGetActorFollowers(e, &input) 27 + if err != nil { 28 + return err 29 + } 30 + 31 + return e.JSON(http.StatusOK, &output) 32 + }
+32
generated/handlers/graphgetactorfollows.go
··· 1 + // GENERATED CODE - DO NOT MODIFY 2 + // Generated by vylet-app/handlergen 3 + 4 + package handlers 5 + 6 + import ( 7 + "net/http" 8 + 9 + "github.com/labstack/echo/v4" 10 + ) 11 + 12 + type GraphGetActorFollowsInput struct { 13 + Actor string `query:"actor"` 14 + Cursor *string `query:"cursor"` 15 + Limit *int64 `query:"limit"` 16 + } 17 + 18 + func (h *Handlers) HandleGraphGetActorFollows(e echo.Context) error { 19 + var input GraphGetActorFollowsInput 20 + if err := e.Bind(&input); err != nil { 21 + logger := h.server.Logger().With("handler", "HandleGraphGetActorFollows") 22 + logger.Error("error binding request", "err", err) 23 + return echo.NewHTTPError(http.StatusInternalServerError, "Internal Server Error") 24 + } 25 + 26 + output, err := h.server.HandleGraphGetActorFollows(e, &input) 27 + if err != nil { 28 + return err 29 + } 30 + 31 + return e.JSON(http.StatusOK, &output) 32 + }
+6
generated/handlers/handlers.go
··· 24 24 FeedGetPostsRequiresAuth() bool 25 25 HandleFeedGetSubjectLikes(e echo.Context, input *FeedGetSubjectLikesInput) (*vylet.FeedGetSubjectLikes_Output, *echo.HTTPError) 26 26 FeedGetSubjectLikesRequiresAuth() bool 27 + HandleGraphGetActorFollowers(e echo.Context, input *GraphGetActorFollowersInput) (*vylet.GraphGetActorFollowers_Output, *echo.HTTPError) 28 + GraphGetActorFollowersRequiresAuth() bool 29 + HandleGraphGetActorFollows(e echo.Context, input *GraphGetActorFollowsInput) (*vylet.GraphGetActorFollows_Output, *echo.HTTPError) 30 + GraphGetActorFollowsRequiresAuth() bool 27 31 } 28 32 29 33 type Handlers struct { ··· 40 44 e.GET("/xrpc/app.vylet.feed.getActorPosts", h.HandleFeedGetActorPosts, CreateAuthRequiredMiddleware(s.FeedGetActorPostsRequiresAuth())) 41 45 e.GET("/xrpc/app.vylet.feed.getPosts", h.HandleFeedGetPosts, CreateAuthRequiredMiddleware(s.FeedGetPostsRequiresAuth())) 42 46 e.GET("/xrpc/app.vylet.feed.getSubjectLikes", h.HandleFeedGetSubjectLikes, CreateAuthRequiredMiddleware(s.FeedGetSubjectLikesRequiresAuth())) 47 + e.GET("/xrpc/app.vylet.graph.getActorFollowers", h.HandleGraphGetActorFollowers, CreateAuthRequiredMiddleware(s.GraphGetActorFollowersRequiresAuth())) 48 + e.GET("/xrpc/app.vylet.graph.getActorFollows", h.HandleGraphGetActorFollows, CreateAuthRequiredMiddleware(s.GraphGetActorFollowsRequiresAuth())) 43 49 } 44 50 45 51 func AuthRequiredMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
+38
generated/vylet/graphgetActorFollowers.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + // Lexicon schema: app.vylet.graph.getActorFollowers 4 + 5 + package vylet 6 + 7 + import ( 8 + "context" 9 + 10 + lexutil "github.com/bluesky-social/indigo/lex/util" 11 + ) 12 + 13 + // GraphGetActorFollowers_Output is the output of a app.vylet.graph.getActorFollowers call. 14 + type GraphGetActorFollowers_Output struct { 15 + Cursor *string `json:"cursor,omitempty" cborgen:"cursor,omitempty"` 16 + Profiles []*ActorDefs_ProfileView `json:"profiles" cborgen:"profiles"` 17 + } 18 + 19 + // GraphGetActorFollowers calls the XRPC method "app.vylet.graph.getActorFollowers". 20 + // 21 + // actor: Handle or DID of account to fetch the followers of. 22 + func GraphGetActorFollowers(ctx context.Context, c lexutil.LexClient, actor string, cursor string, limit int64) (*GraphGetActorFollowers_Output, error) { 23 + var out GraphGetActorFollowers_Output 24 + 25 + params := map[string]interface{}{} 26 + params["actor"] = actor 27 + if cursor != "" { 28 + params["cursor"] = cursor 29 + } 30 + if limit != 0 { 31 + params["limit"] = limit 32 + } 33 + if err := c.LexDo(ctx, lexutil.Query, "", "app.vylet.graph.getActorFollowers", params, nil, &out); err != nil { 34 + return nil, err 35 + } 36 + 37 + return &out, nil 38 + }
+38
generated/vylet/graphgetActorFollows.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + // Lexicon schema: app.vylet.graph.getActorFollows 4 + 5 + package vylet 6 + 7 + import ( 8 + "context" 9 + 10 + lexutil "github.com/bluesky-social/indigo/lex/util" 11 + ) 12 + 13 + // GraphGetActorFollows_Output is the output of a app.vylet.graph.getActorFollows call. 14 + type GraphGetActorFollows_Output struct { 15 + Cursor *string `json:"cursor,omitempty" cborgen:"cursor,omitempty"` 16 + Profiles []*ActorDefs_ProfileView `json:"profiles" cborgen:"profiles"` 17 + } 18 + 19 + // GraphGetActorFollows calls the XRPC method "app.vylet.graph.getActorFollows". 20 + // 21 + // actor: Handle or DID of account to fetch the follows of. 22 + func GraphGetActorFollows(ctx context.Context, c lexutil.LexClient, actor string, cursor string, limit int64) (*GraphGetActorFollows_Output, error) { 23 + var out GraphGetActorFollows_Output 24 + 25 + params := map[string]interface{}{} 26 + params["actor"] = actor 27 + if cursor != "" { 28 + params["cursor"] = cursor 29 + } 30 + if limit != 0 { 31 + params["limit"] = limit 32 + } 33 + if err := c.LexDo(ctx, lexutil.Query, "", "app.vylet.graph.getActorFollows", params, nil, &out); err != nil { 34 + return nil, err 35 + } 36 + 37 + return &out, nil 38 + }
+62
indexer/graphfollow.go
··· 1 + package indexer 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + "time" 8 + 9 + vyletkafka "github.com/vylet-app/go/bus/proto" 10 + vyletdatabase "github.com/vylet-app/go/database/proto" 11 + "github.com/vylet-app/go/generated/vylet" 12 + "google.golang.org/protobuf/types/known/timestamppb" 13 + ) 14 + 15 + func (s *Server) handleGraphFollow(ctx context.Context, evt *vyletkafka.FirehoseEvent) error { 16 + var rec vylet.GraphFollow 17 + op := evt.Commit 18 + uri := firehoseEventToUri(evt) 19 + switch op.Operation { 20 + case vyletkafka.CommitOperation_COMMIT_OPERATION_CREATE: 21 + if err := json.Unmarshal(op.Record, &rec); err != nil { 22 + return fmt.Errorf("failed to unmarshal follow record: %w", err) 23 + } 24 + 25 + createdAtTime, err := time.Parse(time.RFC3339Nano, rec.CreatedAt) 26 + if err != nil { 27 + return fmt.Errorf("failed to parse time from record: %w", err) 28 + } 29 + 30 + req := vyletdatabase.CreateFollowRequest{ 31 + Follow: &vyletdatabase.Follow{ 32 + Uri: uri, 33 + Cid: evt.Commit.Cid, 34 + SubjectDid: rec.Subject, 35 + AuthorDid: evt.Did, 36 + CreatedAt: timestamppb.New(createdAtTime), 37 + }, 38 + } 39 + 40 + resp, err := s.db.Follow.CreateFollow(ctx, &req) 41 + if err != nil { 42 + return fmt.Errorf("failed to create create follow request: %w", err) 43 + } 44 + if resp.Error != nil { 45 + return fmt.Errorf("error creating follow: %s", *resp.Error) 46 + } 47 + case vyletkafka.CommitOperation_COMMIT_OPERATION_UPDATE: 48 + return fmt.Errorf("unsupported follow update event") 49 + case vyletkafka.CommitOperation_COMMIT_OPERATION_DELETE: 50 + resp, err := s.db.Follow.DeleteFollow(ctx, &vyletdatabase.DeleteFollowRequest{ 51 + Uri: uri, 52 + }) 53 + if err != nil { 54 + return fmt.Errorf("failed to create delete follow request: %w", err) 55 + } 56 + if resp.Error != nil { 57 + return fmt.Errorf("error deleting follow %s", *resp.Error) 58 + } 59 + } 60 + 61 + return nil 62 + }
+2
indexer/handler.go
··· 22 22 return s.handleFeedPost(ctx, evt) 23 23 case "app.vylet.feed.like": 24 24 return s.handleFeedLike(ctx, evt) 25 + case "app.vylet.graph.follow": 26 + return s.handleGraphFollow(ctx, evt) 25 27 } 26 28 27 29 return nil
+1
migrations/1765253661_create_follows_by_author_did_table.down.cql
··· 1 + DROP TABLE IF EXISTS follows_by_author_did;
+9
migrations/1765253661_create_follows_by_author_did_table.up.cql
··· 1 + CREATE TABLE IF NOT EXISTS follows_by_author_did ( 2 + uri TEXT, 3 + cid TEXT, 4 + subject_did TEXT, 5 + author_did TEXT, 6 + created_at TIMESTAMP, 7 + indexed_at TIMESTAMP, 8 + PRIMARY KEY (author_did, created_at, uri) 9 + ) WITH CLUSTERING ORDER BY (created_at DESC, uri ASC);
+1
migrations/1765253666_create_follows_by_subject_did_table.down.cql
··· 1 + DROP TABLE IF EXISTS follows_by_subject_did;
+9
migrations/1765253666_create_follows_by_subject_did_table.up.cql
··· 1 + CREATE TABLE IF NOT EXISTS follows_by_subject_did ( 2 + uri TEXT, 3 + cid TEXT, 4 + subject_did TEXT, 5 + author_did TEXT, 6 + created_at TIMESTAMP, 7 + indexed_at TIMESTAMP, 8 + PRIMARY KEY (subject_did, created_at, uri) 9 + ) WITH CLUSTERING ORDER BY (created_at DESC, uri ASC);
+1
migrations/1765253671_create_follows_by_uri_table.down.cql
··· 1 + DROP TABLE IF EXISTS follows_by_uri;
+8
migrations/1765253671_create_follows_by_uri_table.up.cql
··· 1 + CREATE TABLE IF NOT EXISTS follows_by_uri ( 2 + uri TEXT PRIMARY KEY, 3 + cid TEXT, 4 + subject_did TEXT, 5 + author_did TEXT, 6 + created_at TIMESTAMP, 7 + indexed_at TIMESTAMP, 8 + );
+1
migrations/1765256303_create_likes_by_actor_subject_table.down.cql
··· 1 + DROP TABLE IF EXISTS likes_by_actor_subject;
+10
migrations/1765256303_create_likes_by_actor_subject_table.up.cql
··· 1 + CREATE TABLE IF NOT EXISTS likes_by_actor_subject ( 2 + uri TEXT, 3 + cid TEXT, 4 + subject_uri TEXT, 5 + subject_cid TEXT, 6 + author_did TEXT, 7 + created_at TIMESTAMP, 8 + indexed_at TIMESTAMP, 9 + PRIMARY KEY ((author_did, subject_uri)) 10 + );
+1
migrations/1765257958_create_follows_by_author_did_subject_did_table.down.cql
··· 1 + DROP TABLE IF EXISTS follows_by_author_did_subject_did;
+9
migrations/1765257958_create_follows_by_author_did_subject_did_table.up.cql
··· 1 + CREATE TABLE IF NOT EXISTS follows_by_author_did_subject_did ( 2 + uri TEXT, 3 + cid TEXT, 4 + subject_did TEXT, 5 + author_did TEXT, 6 + created_at TIMESTAMP, 7 + indexed_at TIMESTAMP, 8 + PRIMARY KEY ((author_did, subject_did)) 9 + );
+1
migrations/1765259681_create_follow_counts_table.down.cql
··· 1 + DROP TABLE IF EXISTS follow_counts;
+5
migrations/1765259681_create_follow_counts_table.up.cql
··· 1 + CREATE TABLE IF NOT EXISTS follow_counts ( 2 + did TEXT PRIMARY KEY, 3 + follows_count COUNTER, 4 + followers_count COUNTER, 5 + );