1
0
Fork 0
mirror of https://github.com/anyproto/anytype-heart.git synced 2025-06-07 21:37:04 +09:00

GO-3938 Merge main 2

This commit is contained in:
kirillston 2025-06-02 19:13:11 +02:00
commit e0dee12ff0
No known key found for this signature in database
GPG key ID: BE4BF014F0ECDFE8
171 changed files with 23299 additions and 6949 deletions

View file

@ -49,6 +49,11 @@ linters-settings:
excludes:
- G602
- G108
tagliatelle:
case:
use-field-name: true
rules:
json: snake
linters:
disable-all: true
@ -70,6 +75,7 @@ linters:
- govet
- unconvert
- errorlint
- tagliatelle
severity:
default-severity: error

View file

@ -120,9 +120,6 @@ packages:
github.com/anyproto/anytype-heart/space/internal/components/participantwatcher:
interfaces:
ParticipantWatcher:
github.com/anyproto/anytype-heart/space/internal/components/invitemigrator:
interfaces:
InviteMigrator:
github.com/anyproto/anytype-heart/space/internal/components/aclnotifications:
interfaces:
AclNotification:
@ -242,7 +239,7 @@ packages:
github.com/anyproto/anytype-heart/core/api/core:
interfaces:
AccountService:
ExportService:
EventService:
ClientCommands:
github.com/anyproto/anytype-heart/core/block/template:
interfaces:
@ -253,3 +250,6 @@ packages:
github.com/anyproto/anytype-heart/core/block/editor/chatobject:
interfaces:
StoreObject:
github.com/anyproto/anytype-heart/core/block/chats/chatsubscription:
interfaces:
Service:

View file

@ -25,368 +25,373 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
func init() { proto.RegisterFile("pb/protos/service/service.proto", fileDescriptor_93a29dc403579097) }
var fileDescriptor_93a29dc403579097 = []byte{
// 5766 bytes of a gzipped FileDescriptorProto
// 5854 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x9d, 0xdd, 0x6f, 0x24, 0x49,
0x52, 0xc0, 0xd7, 0x2f, 0x2c, 0xd4, 0x71, 0x0b, 0xf4, 0xc2, 0xb2, 0xb7, 0xdc, 0xcd, 0xcc, 0xce,
0x87, 0x3d, 0x33, 0x1e, 0xb7, 0x67, 0x67, 0xf6, 0x8b, 0x3b, 0x24, 0xe8, 0xb1, 0xc7, 0xde, 0xbe,
0xb5, 0xbd, 0xc6, 0xdd, 0x9e, 0x11, 0x2b, 0x21, 0x51, 0xee, 0x4a, 0xb7, 0x0b, 0x57, 0x57, 0xd6,
0x55, 0x65, 0x7b, 0xa6, 0x0f, 0x81, 0x40, 0x20, 0x10, 0x08, 0xc4, 0x89, 0x2f, 0xc1, 0x13, 0x12,
0x7f, 0x01, 0x7f, 0x06, 0x8f, 0xf7, 0xc8, 0x23, 0xda, 0xfd, 0x33, 0x78, 0x00, 0x55, 0x66, 0x56,
0x7e, 0x44, 0x45, 0x64, 0x95, 0xf7, 0x9e, 0x66, 0xe4, 0xf8, 0x45, 0x44, 0x7e, 0x44, 0x66, 0x46,
0x66, 0x65, 0x55, 0x47, 0x37, 0x8b, 0xb3, 0xed, 0xa2, 0xe4, 0x82, 0x57, 0xdb, 0x15, 0x2b, 0xaf,
0xd2, 0x19, 0x6b, 0xfe, 0x1d, 0xca, 0x3f, 0x0f, 0xde, 0x8c, 0xf3, 0x95, 0x58, 0x15, 0xec, 0xbd,
0x77, 0x2d, 0x39, 0xe3, 0x8b, 0x45, 0x9c, 0x27, 0x95, 0x42, 0xde, 0x7b, 0xc7, 0x4a, 0xd8, 0x15,
0xcb, 0x85, 0xfe, 0xfb, 0x93, 0xff, 0xfd, 0xbf, 0xb5, 0xe8, 0xad, 0x9d, 0x2c, 0x65, 0xb9, 0xd8,
0xd1, 0x1a, 0x83, 0x2f, 0xa3, 0x6f, 0x8f, 0x8a, 0x62, 0x9f, 0x89, 0x17, 0xac, 0xac, 0x52, 0x9e,
0x0f, 0xee, 0x0c, 0xb5, 0x83, 0xe1, 0x49, 0x31, 0x1b, 0x8e, 0x8a, 0x62, 0x68, 0x85, 0xc3, 0x13,
0xf6, 0xa3, 0x25, 0xab, 0xc4, 0x7b, 0x77, 0xc3, 0x50, 0x55, 0xf0, 0xbc, 0x62, 0x83, 0xf3, 0xe8,
0x57, 0x46, 0x45, 0x31, 0x61, 0x62, 0x97, 0xd5, 0x15, 0x98, 0x88, 0x58, 0xb0, 0xc1, 0x46, 0x4b,
0xd5, 0x07, 0x8c, 0x8f, 0xfb, 0xdd, 0xa0, 0xf6, 0x33, 0x8d, 0xbe, 0x55, 0xfb, 0xb9, 0x58, 0x8a,
0x84, 0xbf, 0xca, 0x07, 0xef, 0xb7, 0x15, 0xb5, 0xc8, 0xd8, 0xbe, 0x1d, 0x42, 0xb4, 0xd5, 0x97,
0xd1, 0x2f, 0xbe, 0x8c, 0xb3, 0x8c, 0x89, 0x9d, 0x92, 0xd5, 0x05, 0xf7, 0x75, 0x94, 0x68, 0xa8,
0x64, 0xc6, 0xee, 0x9d, 0x20, 0xa3, 0x0d, 0x7f, 0x19, 0x7d, 0x5b, 0x49, 0x4e, 0xd8, 0x8c, 0x5f,
0xb1, 0x72, 0x80, 0x6a, 0x69, 0x21, 0xd1, 0xe4, 0x2d, 0x08, 0xda, 0xde, 0xe1, 0xf9, 0x15, 0x2b,
0x05, 0x6e, 0x5b, 0x0b, 0xc3, 0xb6, 0x2d, 0xa4, 0x6d, 0xff, 0xf5, 0x5a, 0xf4, 0xdd, 0xd1, 0x6c,
0xc6, 0x97, 0xb9, 0x38, 0xe0, 0xb3, 0x38, 0x3b, 0x48, 0xf3, 0xcb, 0x23, 0xf6, 0x6a, 0xe7, 0xa2,
0xe6, 0xf3, 0x39, 0x1b, 0x3c, 0xf5, 0x5b, 0x55, 0xa1, 0x43, 0xc3, 0x0e, 0x5d, 0xd8, 0xf8, 0xfe,
0xf0, 0x7a, 0x4a, 0xba, 0x2c, 0x7f, 0xbf, 0x16, 0xdd, 0x80, 0x65, 0x99, 0xf0, 0xec, 0x8a, 0xd9,
0xd2, 0x7c, 0xd4, 0x61, 0xd8, 0xc7, 0x4d, 0x79, 0x3e, 0xbe, 0xae, 0x9a, 0x2e, 0x51, 0x16, 0xbd,
0xed, 0x86, 0xcb, 0x84, 0x55, 0x72, 0x38, 0x3d, 0xa0, 0x23, 0x42, 0x23, 0xc6, 0xf3, 0xc3, 0x3e,
0xa8, 0xf6, 0x96, 0x46, 0x03, 0xed, 0x2d, 0xe3, 0x95, 0x71, 0x76, 0x1f, 0xb5, 0xe0, 0x10, 0xc6,
0xd7, 0x83, 0x1e, 0xa4, 0x76, 0xf5, 0x07, 0xd1, 0x2f, 0xbd, 0xe4, 0xe5, 0x65, 0x55, 0xc4, 0x33,
0xa6, 0x87, 0xc2, 0x3d, 0x5f, 0xbb, 0x91, 0xc2, 0xd1, 0xb0, 0xde, 0x85, 0x39, 0x41, 0xdb, 0x08,
0xbf, 0x28, 0x18, 0x9c, 0x83, 0xac, 0x62, 0x2d, 0xa4, 0x82, 0x16, 0x42, 0xda, 0xf6, 0x65, 0x34,
0xb0, 0xb6, 0xcf, 0xfe, 0x90, 0xcd, 0xc4, 0x28, 0x49, 0x60, 0xaf, 0x58, 0x5d, 0x49, 0x0c, 0x47,
0x49, 0x42, 0xf5, 0x0a, 0x8e, 0x6a, 0x67, 0xaf, 0xa2, 0x77, 0x80, 0xb3, 0x83, 0xb4, 0x92, 0x0e,
0xb7, 0xc2, 0x56, 0x34, 0x66, 0x9c, 0x0e, 0xfb, 0xe2, 0xda, 0xf1, 0x9f, 0xae, 0x45, 0xdf, 0x41,
0x3c, 0x9f, 0xb0, 0x05, 0xbf, 0x62, 0x83, 0xc7, 0xdd, 0xd6, 0x14, 0x69, 0xfc, 0x7f, 0x70, 0x0d,
0x0d, 0x24, 0x4c, 0x26, 0x2c, 0x63, 0x33, 0x41, 0x86, 0x89, 0x12, 0x77, 0x86, 0x89, 0xc1, 0x9c,
0x11, 0xd6, 0x08, 0xf7, 0x99, 0xd8, 0x59, 0x96, 0x25, 0xcb, 0x05, 0xd9, 0x97, 0x16, 0xe9, 0xec,
0x4b, 0x0f, 0x45, 0xea, 0xb3, 0xcf, 0xc4, 0x28, 0xcb, 0xc8, 0xfa, 0x28, 0x71, 0x67, 0x7d, 0x0c,
0xa6, 0x3d, 0xcc, 0xa2, 0x5f, 0x76, 0x5a, 0x4c, 0x8c, 0xf3, 0x73, 0x3e, 0xa0, 0xdb, 0x42, 0xca,
0x8d, 0x8f, 0x8d, 0x4e, 0x0e, 0xa9, 0xc6, 0xf3, 0xd7, 0x05, 0x2f, 0xe9, 0x6e, 0x51, 0xe2, 0xce,
0x6a, 0x18, 0x4c, 0x7b, 0xf8, 0xfd, 0xe8, 0x2d, 0x3d, 0x4b, 0x36, 0xeb, 0xd9, 0x5d, 0x74, 0x0a,
0x85, 0x0b, 0xda, 0xbd, 0x0e, 0xaa, 0x65, 0xfe, 0x30, 0x9d, 0x97, 0xf5, 0xec, 0x83, 0x9b, 0xd7,
0xd2, 0x0e, 0xf3, 0x96, 0xd2, 0xe6, 0x79, 0xf4, 0xab, 0xbe, 0xf9, 0x9d, 0x38, 0x9f, 0xb1, 0x6c,
0xf0, 0x30, 0xa4, 0xae, 0x18, 0xe3, 0x6a, 0xb3, 0x17, 0x6b, 0x27, 0x3b, 0x4d, 0xe8, 0xc9, 0xf4,
0x0e, 0xaa, 0x0d, 0xa6, 0xd2, 0xbb, 0x61, 0xa8, 0x65, 0x7b, 0x97, 0x65, 0x8c, 0xb4, 0xad, 0x84,
0x1d, 0xb6, 0x0d, 0xa4, 0x6d, 0x97, 0xd1, 0xaf, 0x99, 0x6e, 0xae, 0xf3, 0x02, 0x29, 0xaf, 0x17,
0x9d, 0x4d, 0xa2, 0x1f, 0x5d, 0xc8, 0xf8, 0x7a, 0xd4, 0x0f, 0x6e, 0xd5, 0x47, 0xcf, 0x28, 0x78,
0x7d, 0xc0, 0x7c, 0x72, 0x37, 0x0c, 0x69, 0xdb, 0x7f, 0xb3, 0x16, 0x7d, 0x4f, 0xcb, 0x9e, 0xe7,
0xf1, 0x59, 0xc6, 0xe4, 0x12, 0x7f, 0xc4, 0xc4, 0x2b, 0x5e, 0x5e, 0x4e, 0x56, 0xf9, 0x8c, 0x48,
0x67, 0x70, 0xb8, 0x23, 0x9d, 0x21, 0x95, 0x74, 0x61, 0xfe, 0x28, 0x7a, 0xb7, 0x09, 0x8a, 0x8b,
0x38, 0x9f, 0xb3, 0x1f, 0x56, 0x3c, 0x1f, 0x15, 0xe9, 0x28, 0x49, 0xca, 0xc1, 0x10, 0xef, 0x7a,
0xc8, 0x99, 0x12, 0x6c, 0xf7, 0xe6, 0x9d, 0xf4, 0x59, 0xb7, 0xb2, 0xe0, 0x05, 0x4c, 0x9f, 0x9b,
0xe6, 0x13, 0xbc, 0xa0, 0xd2, 0x67, 0x1f, 0x69, 0x59, 0x3d, 0xac, 0xd7, 0x20, 0xdc, 0xea, 0xa1,
0xbb, 0xe8, 0xdc, 0x0e, 0x21, 0x76, 0x0d, 0x68, 0x1a, 0x8a, 0xe7, 0xe7, 0xe9, 0xfc, 0xb4, 0x48,
0xea, 0x31, 0xf4, 0x00, 0xaf, 0xb3, 0x83, 0x10, 0x6b, 0x00, 0x81, 0x6a, 0x6f, 0x7f, 0x67, 0xb3,
0x4c, 0x3d, 0x2f, 0xed, 0x95, 0x7c, 0x71, 0xc0, 0xe6, 0xf1, 0x6c, 0xa5, 0x27, 0xd3, 0x0f, 0x43,
0xb3, 0x18, 0xa4, 0x4d, 0x21, 0x3e, 0xba, 0xa6, 0x96, 0x2e, 0xcf, 0xbf, 0xaf, 0x45, 0x77, 0xbd,
0x38, 0xd1, 0xc1, 0xa4, 0x4a, 0x3f, 0xca, 0x93, 0x13, 0x56, 0x89, 0xb8, 0x14, 0x83, 0xef, 0x07,
0x62, 0x80, 0xd0, 0x31, 0x65, 0xfb, 0xc1, 0x37, 0xd2, 0xb5, 0xbd, 0x3e, 0xa9, 0x57, 0x09, 0x3d,
0xff, 0xf8, 0xbd, 0x2e, 0x25, 0x70, 0xf6, 0xb9, 0x1d, 0x42, 0x6c, 0xaf, 0x4b, 0xc1, 0x38, 0xbf,
0x4a, 0x05, 0xdb, 0x67, 0x39, 0x2b, 0xdb, 0xbd, 0xae, 0x54, 0x7d, 0x84, 0xe8, 0x75, 0x02, 0xb5,
0x33, 0x9d, 0xe7, 0xcd, 0x64, 0x1a, 0x9b, 0x01, 0x23, 0xad, 0x5c, 0xe3, 0x51, 0x3f, 0x98, 0xa8,
0xa1, 0xd8, 0xaf, 0x8d, 0x04, 0x6b, 0xa8, 0x90, 0x5e, 0x35, 0x34, 0xa8, 0xdd, 0x98, 0x3b, 0xde,
0x4e, 0xd8, 0x15, 0xbf, 0x84, 0x1b, 0x73, 0xd7, 0x80, 0x02, 0x88, 0x8d, 0x39, 0x0a, 0xda, 0xe4,
0xc3, 0xf1, 0xf3, 0x22, 0x65, 0xaf, 0x40, 0xf2, 0xe1, 0x2a, 0xd7, 0x62, 0x22, 0xf9, 0x40, 0x30,
0xed, 0xe1, 0x28, 0xfa, 0x05, 0x29, 0xfc, 0x21, 0x4f, 0xf3, 0xc1, 0x4d, 0x44, 0xa9, 0x16, 0x18,
0xab, 0xb7, 0x68, 0x00, 0x94, 0xb8, 0xfe, 0xab, 0xce, 0x04, 0xee, 0x11, 0x4a, 0x20, 0x09, 0x58,
0xef, 0xc2, 0x6c, 0xd6, 0x27, 0x85, 0xf5, 0x6c, 0x39, 0xb9, 0x88, 0xcb, 0x34, 0x9f, 0x0f, 0x30,
0x5d, 0x47, 0x4e, 0x64, 0x7d, 0x18, 0x07, 0xc2, 0x49, 0x2b, 0x8e, 0x8a, 0xa2, 0xac, 0x27, 0x61,
0x2c, 0x9c, 0x7c, 0x24, 0x18, 0x4e, 0x2d, 0x14, 0xf7, 0xb6, 0xcb, 0x66, 0x59, 0x9a, 0x07, 0xbd,
0x69, 0xa4, 0x8f, 0x37, 0x8b, 0x82, 0xe0, 0x3d, 0x60, 0xf1, 0x15, 0x6b, 0x6a, 0x86, 0xb5, 0x8c,
0x0b, 0x04, 0x83, 0x17, 0x80, 0x76, 0x8b, 0x2d, 0xc5, 0x87, 0xf1, 0x25, 0xab, 0x1b, 0x98, 0xd5,
0x4b, 0xf8, 0x00, 0xd3, 0xf7, 0x08, 0x62, 0x8b, 0x8d, 0x93, 0xda, 0xd5, 0x32, 0x7a, 0x47, 0xca,
0x8f, 0xe3, 0x52, 0xa4, 0xb3, 0xb4, 0x88, 0xf3, 0x66, 0xeb, 0x86, 0xcd, 0x22, 0x2d, 0xca, 0xb8,
0xdc, 0xea, 0x49, 0x6b, 0xb7, 0xff, 0xb2, 0x16, 0xbd, 0x0f, 0xfd, 0x1e, 0xb3, 0x72, 0x91, 0xca,
0x13, 0x80, 0x4a, 0x4d, 0xf9, 0x83, 0x4f, 0xc2, 0x46, 0x5b, 0x0a, 0xa6, 0x34, 0x9f, 0x5e, 0x5f,
0xd1, 0xe6, 0x7d, 0x13, 0xbd, 0x2b, 0xfa, 0xa2, 0x4c, 0x5a, 0x27, 0x64, 0x93, 0x66, 0xab, 0x23,
0x85, 0x44, 0xde, 0xd7, 0x82, 0xc0, 0x08, 0x3f, 0xcd, 0xab, 0xc6, 0x3a, 0x36, 0xc2, 0xad, 0x38,
0x38, 0xc2, 0x3d, 0xcc, 0x8e, 0xf0, 0xe3, 0xe5, 0x59, 0x96, 0x56, 0x17, 0x69, 0x3e, 0xd7, 0x49,
0xbe, 0xaf, 0x6b, 0xc5, 0x30, 0xcf, 0xdf, 0xe8, 0xe4, 0x30, 0x27, 0x3a, 0x58, 0x48, 0x27, 0x20,
0x4c, 0x36, 0x3a, 0x39, 0xbb, 0xf7, 0xb2, 0xd2, 0x7a, 0xd3, 0x0f, 0xf6, 0x5e, 0x8e, 0x6a, 0x2d,
0x25, 0xf6, 0x5e, 0x6d, 0xca, 0xee, 0xbd, 0xdc, 0x3a, 0x54, 0x3c, 0xbb, 0x62, 0xa7, 0x65, 0x0a,
0xf6, 0x5e, 0x5e, 0xf9, 0x1a, 0x86, 0xd8, 0x7b, 0x51, 0xac, 0x9d, 0xa8, 0x2c, 0xb1, 0xcf, 0xc4,
0x44, 0xc4, 0x62, 0x59, 0x81, 0x89, 0xca, 0xb1, 0x61, 0x10, 0x62, 0xa2, 0x22, 0x50, 0xed, 0xed,
0x77, 0xa3, 0x48, 0x9d, 0x97, 0xc8, 0x33, 0x2d, 0x7f, 0xed, 0xd1, 0x07, 0x29, 0xde, 0x81, 0xd6,
0xfb, 0x01, 0xc2, 0xa6, 0x57, 0xea, 0xef, 0xf2, 0xa8, 0x6e, 0x80, 0x6a, 0x48, 0x11, 0x91, 0x5e,
0x01, 0x04, 0x16, 0x74, 0x72, 0xc1, 0x5f, 0xe1, 0x05, 0xad, 0x25, 0xe1, 0x82, 0x6a, 0xc2, 0x1e,
0x9e, 0xeb, 0x82, 0x62, 0x87, 0xe7, 0x4d, 0x31, 0x42, 0x87, 0xe7, 0x90, 0xb1, 0x31, 0xe3, 0x1a,
0x7e, 0xc6, 0xf9, 0xe5, 0x22, 0x2e, 0x2f, 0x41, 0xcc, 0x78, 0xca, 0x0d, 0x43, 0xc4, 0x0c, 0xc5,
0xda, 0x98, 0x71, 0x1d, 0xd6, 0xc9, 0xf9, 0x69, 0x99, 0x81, 0x98, 0xf1, 0x6c, 0x68, 0x84, 0x88,
0x19, 0x02, 0xb5, 0xb3, 0x93, 0xeb, 0x6d, 0xc2, 0xe0, 0x71, 0x8d, 0xa7, 0x3e, 0x61, 0xd4, 0x71,
0x0d, 0x82, 0xc1, 0x10, 0xda, 0x2f, 0xe3, 0xe2, 0x02, 0x0f, 0x21, 0x29, 0x0a, 0x87, 0x50, 0x83,
0xc0, 0xfe, 0x9e, 0xb0, 0xb8, 0x9c, 0x5d, 0xe0, 0xfd, 0xad, 0x64, 0xe1, 0xfe, 0x36, 0x0c, 0xec,
0x6f, 0x25, 0x78, 0x99, 0x8a, 0x8b, 0x43, 0x26, 0x62, 0xbc, 0xbf, 0x7d, 0x26, 0xdc, 0xdf, 0x2d,
0xd6, 0x66, 0xff, 0xae, 0xc3, 0xc9, 0xf2, 0xac, 0x9a, 0x95, 0xe9, 0x19, 0x1b, 0x04, 0xac, 0x18,
0x88, 0xc8, 0xfe, 0x49, 0x58, 0xfb, 0xfc, 0xc9, 0x5a, 0x74, 0xb3, 0xe9, 0x76, 0x5e, 0x55, 0x7a,
0xed, 0xf3, 0xdd, 0x7f, 0x84, 0xf7, 0x2f, 0x81, 0x13, 0x8f, 0x33, 0x7a, 0xa8, 0x39, 0xb9, 0x01,
0x5e, 0xa4, 0xd3, 0xbc, 0x32, 0x85, 0xfa, 0xa4, 0x8f, 0x75, 0x47, 0x81, 0xc8, 0x0d, 0x7a, 0x29,
0xda, 0xb4, 0x4c, 0xf7, 0x4f, 0x23, 0x1b, 0x27, 0x15, 0x48, 0xcb, 0x9a, 0xf6, 0x76, 0x08, 0x22,
0x2d, 0xc3, 0x49, 0x18, 0x0a, 0xfb, 0x25, 0x5f, 0x16, 0x55, 0x47, 0x28, 0x00, 0x28, 0x1c, 0x0a,
0x6d, 0x58, 0xfb, 0x7c, 0x1d, 0xfd, 0xba, 0x1b, 0x7e, 0x6e, 0x63, 0x6f, 0xd1, 0x31, 0x85, 0x35,
0xf1, 0xb0, 0x2f, 0x6e, 0x33, 0x8a, 0xc6, 0xb3, 0xd8, 0x65, 0x22, 0x4e, 0xb3, 0x6a, 0xb0, 0x8e,
0xdb, 0x68, 0xe4, 0x44, 0x46, 0x81, 0x71, 0x70, 0x7e, 0xdb, 0x5d, 0x16, 0x59, 0x3a, 0x6b, 0x3f,
0x4c, 0xd2, 0xba, 0x46, 0x1c, 0x9e, 0xdf, 0x5c, 0x0c, 0xce, 0xd7, 0x75, 0xea, 0x27, 0xff, 0x33,
0x5d, 0x15, 0x0c, 0x9f, 0xaf, 0x3d, 0x24, 0x3c, 0x5f, 0x43, 0x14, 0xd6, 0x67, 0xc2, 0xc4, 0x41,
0xbc, 0xe2, 0x4b, 0x62, 0xbe, 0x36, 0xe2, 0x70, 0x7d, 0x5c, 0xcc, 0xee, 0x0d, 0x8c, 0x87, 0x71,
0x2e, 0x58, 0x99, 0xc7, 0xd9, 0x5e, 0x16, 0xcf, 0xab, 0x01, 0x31, 0xc7, 0xf8, 0x14, 0xb1, 0x37,
0xa0, 0x69, 0xa4, 0x19, 0xc7, 0xd5, 0x5e, 0x7c, 0xc5, 0xcb, 0x54, 0xd0, 0xcd, 0x68, 0x91, 0xce,
0x66, 0xf4, 0x50, 0xd4, 0xdb, 0xa8, 0x9c, 0x5d, 0xa4, 0x57, 0x2c, 0x09, 0x78, 0x6b, 0x90, 0x1e,
0xde, 0x1c, 0x14, 0xe9, 0xb4, 0x09, 0x5f, 0x96, 0x33, 0x46, 0x76, 0x9a, 0x12, 0x77, 0x76, 0x9a,
0xc1, 0xb4, 0x87, 0xbf, 0x58, 0x8b, 0x7e, 0x43, 0x49, 0xdd, 0x27, 0x3c, 0xbb, 0x71, 0x75, 0x71,
0xc6, 0xe3, 0x32, 0x19, 0x7c, 0x80, 0xd9, 0x41, 0x51, 0xe3, 0xfa, 0xc9, 0x75, 0x54, 0x60, 0xb3,
0xd6, 0x79, 0xb7, 0x1d, 0x71, 0x68, 0xb3, 0x7a, 0x48, 0xb8, 0x59, 0x21, 0x0a, 0x27, 0x10, 0x29,
0x57, 0x07, 0x80, 0xeb, 0xa4, 0xbe, 0x7f, 0x0a, 0xb8, 0xd1, 0xc9, 0xc1, 0xf9, 0xb1, 0x16, 0xfa,
0xd1, 0xb2, 0x45, 0xd9, 0xc0, 0x23, 0x66, 0xd8, 0x17, 0x27, 0x3d, 0x9b, 0x51, 0x11, 0xf6, 0xdc,
0x1a, 0x19, 0xc3, 0xbe, 0x38, 0xe1, 0xd9, 0x99, 0xd6, 0x42, 0x9e, 0x91, 0xa9, 0x6d, 0xd8, 0x17,
0x87, 0xd9, 0x97, 0x66, 0x9a, 0x75, 0xe1, 0x61, 0xc0, 0x0e, 0x5c, 0x1b, 0x36, 0x7b, 0xb1, 0xda,
0xe1, 0x5f, 0xad, 0x45, 0xdf, 0xb5, 0x1e, 0x0f, 0x79, 0x92, 0x9e, 0xaf, 0x14, 0xf4, 0x22, 0xce,
0x96, 0xac, 0x1a, 0x3c, 0xa1, 0xac, 0xb5, 0x59, 0x53, 0x82, 0xa7, 0xd7, 0xd2, 0x81, 0x63, 0x67,
0x54, 0x14, 0xd9, 0x6a, 0xca, 0x16, 0x45, 0x46, 0x8e, 0x1d, 0x0f, 0x09, 0x8f, 0x1d, 0x88, 0xc2,
0xac, 0x7c, 0xca, 0xeb, 0x9c, 0x1f, 0xcd, 0xca, 0xa5, 0x28, 0x9c, 0x95, 0x37, 0x08, 0xcc, 0x95,
0xa6, 0x7c, 0x87, 0x67, 0x19, 0x9b, 0x89, 0xf6, 0x2d, 0x11, 0xa3, 0x69, 0x89, 0x70, 0xae, 0x04,
0x48, 0x7b, 0x2a, 0xd7, 0xec, 0x21, 0xe3, 0x92, 0x3d, 0x5b, 0x1d, 0xa4, 0xf9, 0xe5, 0x00, 0x4f,
0x0b, 0x2c, 0x40, 0x9c, 0xca, 0xa1, 0x20, 0xdc, 0xab, 0x9e, 0xe6, 0x09, 0xc7, 0xf7, 0xaa, 0xb5,
0x24, 0xbc, 0x57, 0xd5, 0x04, 0x34, 0x79, 0xc2, 0x28, 0x93, 0xb5, 0x24, 0x6c, 0x52, 0x13, 0xd8,
0x54, 0xa8, 0x9f, 0x14, 0x91, 0x53, 0x21, 0x78, 0x36, 0xb4, 0xd1, 0xc9, 0xc1, 0x3d, 0x97, 0x76,
0x80, 0x46, 0x04, 0x30, 0x7e, 0x27, 0xc8, 0xc0, 0xd0, 0x6f, 0x76, 0xc3, 0x7b, 0x4c, 0xcc, 0x2e,
0xf0, 0xd0, 0xf7, 0x90, 0x70, 0xe8, 0x43, 0x14, 0xb6, 0xd5, 0x94, 0x9b, 0xdd, 0xfc, 0x3a, 0x1e,
0x78, 0xad, 0x9d, 0xfc, 0x46, 0x27, 0x07, 0xdb, 0x6a, 0xbc, 0xa0, 0xdb, 0x4a, 0xc9, 0xc2, 0x6d,
0x65, 0x18, 0x58, 0x7a, 0x25, 0x90, 0x87, 0x64, 0xeb, 0xb4, 0xa2, 0x77, 0x4c, 0xb6, 0xd1, 0xc9,
0x69, 0x27, 0xff, 0x64, 0xf6, 0x87, 0x4a, 0x7a, 0xc4, 0xeb, 0xc1, 0xf7, 0x22, 0xce, 0xd2, 0x24,
0x16, 0x6c, 0xca, 0x2f, 0x59, 0x8e, 0x6f, 0xc5, 0x74, 0x69, 0x15, 0x3f, 0xf4, 0x14, 0xc2, 0x5b,
0xb1, 0xb0, 0x22, 0x8c, 0x13, 0x45, 0x9f, 0x56, 0x6c, 0x27, 0xae, 0x88, 0x29, 0xd2, 0x43, 0xc2,
0x71, 0x02, 0x51, 0x98, 0x08, 0x2b, 0xf9, 0xf3, 0xd7, 0x05, 0x2b, 0x53, 0x96, 0xcf, 0x18, 0x9e,
0x08, 0x43, 0x2a, 0x9c, 0x08, 0x23, 0x34, 0xdc, 0x04, 0xee, 0xc6, 0x82, 0x3d, 0x5b, 0x4d, 0xd3,
0x05, 0xab, 0x44, 0xbc, 0x28, 0xf0, 0x4d, 0x20, 0x80, 0xc2, 0x9b, 0xc0, 0x36, 0xdc, 0x3a, 0x73,
0x32, 0x33, 0x6d, 0xfb, 0xd6, 0x1a, 0x24, 0x02, 0xb7, 0xd6, 0x08, 0x14, 0x36, 0xac, 0x05, 0xd0,
0xa7, 0x0f, 0x2d, 0x2b, 0xc1, 0xa7, 0x0f, 0x34, 0xdd, 0x3a, 0xc9, 0x33, 0xcc, 0xa4, 0x1e, 0x9a,
0x1d, 0x45, 0x9f, 0xb8, 0x43, 0x74, 0xb3, 0x17, 0x8b, 0x1f, 0x1d, 0x9e, 0xb0, 0x2c, 0x96, 0xeb,
0x61, 0xe0, 0x7c, 0xae, 0x61, 0xfa, 0x1c, 0x1d, 0x3a, 0xac, 0x76, 0xf8, 0x67, 0x6b, 0xd1, 0x7b,
0x98, 0xc7, 0x2f, 0x0a, 0xe9, 0xf7, 0x71, 0xb7, 0x2d, 0x45, 0x12, 0xd7, 0xf2, 0xc2, 0x1a, 0xf6,
0x66, 0x49, 0x23, 0xb2, 0xb7, 0xf6, 0x74, 0x01, 0xfc, 0x6c, 0xd0, 0x94, 0x1f, 0x72, 0xc4, 0xcd,
0x92, 0x10, 0x6f, 0x37, 0x5a, 0x7e, 0xb9, 0x2a, 0xb0, 0xd1, 0x32, 0x36, 0xb4, 0x98, 0xd8, 0x68,
0x21, 0x98, 0x1d, 0x9d, 0x6e, 0xf5, 0x5e, 0xa6, 0xe2, 0x42, 0x26, 0x72, 0x60, 0x74, 0x7a, 0x65,
0x35, 0x10, 0x31, 0x3a, 0x49, 0x18, 0xa6, 0x3a, 0x0d, 0x58, 0x8f, 0x4d, 0x6c, 0x2e, 0x37, 0x86,
0xdc, 0x91, 0x79, 0xbf, 0x1b, 0x84, 0xf1, 0xda, 0x88, 0xf5, 0x9e, 0xea, 0x61, 0xc8, 0x02, 0xd8,
0x57, 0x6d, 0xf6, 0x62, 0xb5, 0xc3, 0x3f, 0x89, 0xbe, 0xd3, 0xaa, 0xd8, 0x1e, 0x8b, 0xc5, 0xb2,
0x64, 0xc9, 0x60, 0xbb, 0xa3, 0xdc, 0x0d, 0x68, 0x5c, 0x3f, 0xee, 0xaf, 0xd0, 0x4a, 0xfe, 0x1b,
0x4e, 0x85, 0x95, 0x29, 0xc3, 0x93, 0x90, 0x49, 0x9f, 0x0d, 0x26, 0xff, 0xb4, 0x4e, 0x6b, 0xff,
0xee, 0x46, 0xd7, 0xe8, 0x2a, 0x4e, 0x33, 0xf9, 0x14, 0xf8, 0x83, 0x90, 0x51, 0x0f, 0x0d, 0xee,
0xdf, 0x49, 0x95, 0xd6, 0xcc, 0x2c, 0xc7, 0xb8, 0xb3, 0xef, 0x7b, 0x44, 0xcf, 0x04, 0xc8, 0xb6,
0x6f, 0xab, 0x27, 0xad, 0xdd, 0x8a, 0x66, 0xc9, 0xab, 0xff, 0xec, 0x06, 0x39, 0xe6, 0x55, 0xab,
0x22, 0x91, 0xbe, 0xd5, 0x93, 0xd6, 0x5e, 0xff, 0x38, 0x7a, 0xb7, 0xed, 0x55, 0x2f, 0x44, 0xdb,
0x9d, 0xa6, 0xc0, 0x5a, 0xf4, 0xb8, 0xbf, 0x82, 0x76, 0xff, 0xaf, 0xe6, 0xc0, 0x5b, 0xf9, 0x9f,
0xf1, 0xc5, 0x82, 0xe5, 0x09, 0x4b, 0x1a, 0x8d, 0xaa, 0xde, 0x98, 0x7d, 0x4a, 0xdb, 0x35, 0x0a,
0x43, 0x57, 0xc3, 0x94, 0xe8, 0x37, 0xbf, 0x81, 0xa6, 0x2e, 0xda, 0x7f, 0xae, 0x45, 0x0f, 0xd0,
0xa2, 0x35, 0x81, 0xeb, 0x15, 0xf1, 0x77, 0xfa, 0x38, 0xc2, 0x34, 0x4d, 0x51, 0x47, 0x3f, 0x83,
0x05, 0x5d, 0xe4, 0x7f, 0x5b, 0x8b, 0x6e, 0x5b, 0xc5, 0x3a, 0xbc, 0x77, 0x78, 0x7e, 0x9e, 0xa5,
0x33, 0x21, 0x1f, 0xf5, 0x6a, 0x15, 0xba, 0x39, 0x29, 0x8d, 0xee, 0xe6, 0x0c, 0x68, 0xea, 0xb2,
0xfd, 0xe3, 0x5a, 0x74, 0xcb, 0x6d, 0x4e, 0xf9, 0x9c, 0x58, 0x1d, 0xbb, 0x36, 0x8a, 0xd5, 0xe0,
0x63, 0xba, 0x0d, 0x30, 0xde, 0x94, 0xeb, 0x93, 0x6b, 0xeb, 0xd9, 0xbd, 0xfa, 0x67, 0x69, 0x25,
0x78, 0xb9, 0x9a, 0x5c, 0xf0, 0x57, 0xcd, 0xdb, 0x58, 0xfe, 0x6a, 0xa1, 0x81, 0xa1, 0x43, 0x10,
0x7b, 0x75, 0x9c, 0x6c, 0xb9, 0xb2, 0x6f, 0x6d, 0x55, 0x84, 0x2b, 0x87, 0xe8, 0x70, 0xe5, 0x93,
0x76, 0xad, 0x6c, 0x6a, 0x65, 0x5f, 0x31, 0xdb, 0xc0, 0x8b, 0xda, 0x7e, 0xcd, 0xec, 0x7e, 0x37,
0x68, 0x33, 0x66, 0x2d, 0xde, 0x4d, 0xcf, 0xcf, 0x4d, 0x9d, 0xf0, 0x92, 0xba, 0x08, 0x91, 0x31,
0x13, 0xa8, 0xdd, 0xf4, 0xed, 0xa5, 0x19, 0x93, 0x8f, 0xaa, 0xbe, 0x38, 0x3f, 0xcf, 0x78, 0x9c,
0x80, 0x4d, 0x5f, 0x2d, 0x1e, 0xba, 0x72, 0x62, 0xd3, 0x87, 0x71, 0xf6, 0x12, 0x4c, 0x2d, 0xad,
0xc7, 0x5c, 0x3e, 0x4b, 0x33, 0x78, 0x99, 0x5b, 0x6a, 0x1a, 0x21, 0x71, 0x09, 0xa6, 0x05, 0xd9,
0xc4, 0xac, 0x16, 0xd5, 0x63, 0xa5, 0x29, 0xff, 0xbd, 0xb6, 0xa2, 0x23, 0x26, 0x12, 0x33, 0x04,
0xb3, 0x87, 0x2a, 0xb5, 0xf0, 0xb4, 0x90, 0xc6, 0x6f, 0xb5, 0xb5, 0x94, 0x84, 0x38, 0x54, 0xf1,
0x09, 0xbb, 0x87, 0xaf, 0xff, 0xbe, 0xcb, 0x5f, 0xe5, 0xd2, 0xe8, 0xed, 0xb6, 0x4a, 0x23, 0x23,
0xf6, 0xf0, 0x90, 0xd1, 0x86, 0x3f, 0x8f, 0x7e, 0x5e, 0x1a, 0x2e, 0x79, 0x31, 0xb8, 0x81, 0x28,
0x94, 0xce, 0xd5, 0xe7, 0x9b, 0xa4, 0xdc, 0xde, 0x99, 0x31, 0xb1, 0x71, 0x5a, 0xc5, 0x73, 0xf8,
0xbe, 0x82, 0xed, 0x71, 0x29, 0x25, 0xee, 0xcc, 0xb4, 0x29, 0x3f, 0x2a, 0x8e, 0x78, 0xa2, 0xad,
0x23, 0x35, 0x34, 0xc2, 0x50, 0x54, 0xb8, 0x90, 0x4d, 0xa6, 0x8f, 0xe2, 0xab, 0x74, 0x6e, 0x12,
0x1e, 0x35, 0x7d, 0x55, 0x20, 0x99, 0xb6, 0xcc, 0xd0, 0x81, 0x88, 0x64, 0x9a, 0x84, 0x9d, 0xc9,
0xd8, 0x32, 0xfb, 0xcd, 0x31, 0xf4, 0x38, 0x3f, 0xe7, 0x75, 0xea, 0x7d, 0x90, 0xe6, 0x97, 0x70,
0x32, 0x76, 0x4c, 0xe2, 0x3c, 0x31, 0x19, 0xf7, 0xd1, 0xb3, 0xbb, 0xa6, 0xe6, 0x8c, 0xd6, 0x5e,
0xd4, 0x50, 0x1a, 0x60, 0xd7, 0x64, 0x8e, 0x72, 0x21, 0x47, 0xec, 0x9a, 0x42, 0xbc, 0xed, 0x62,
0xe3, 0x3c, 0xe3, 0x39, 0xec, 0x62, 0x6b, 0xa1, 0x16, 0x12, 0x5d, 0xdc, 0x82, 0xec, 0x7c, 0xdc,
0x88, 0xd4, 0xa9, 0xdf, 0x28, 0xcb, 0xc0, 0x7c, 0x6c, 0x54, 0x0d, 0x40, 0xcc, 0xc7, 0x28, 0xa8,
0xfd, 0x9c, 0x44, 0xdf, 0xaa, 0x9b, 0xf4, 0xb8, 0x64, 0x57, 0x29, 0x83, 0x77, 0x8a, 0x1c, 0x09,
0x31, 0xfe, 0x7d, 0xc2, 0x8e, 0xac, 0xd3, 0xbc, 0x2a, 0xb2, 0xb8, 0xba, 0xd0, 0xb7, 0x4c, 0xfc,
0x3a, 0x37, 0x42, 0x78, 0xcf, 0xe4, 0x5e, 0x07, 0x65, 0x27, 0xf5, 0x46, 0x66, 0xa6, 0x98, 0x75,
0x5c, 0xb5, 0x35, 0xcd, 0x6c, 0x74, 0x72, 0xf6, 0x51, 0xce, 0x7e, 0x9c, 0x65, 0xac, 0x5c, 0x35,
0xb2, 0xc3, 0x38, 0x4f, 0xcf, 0x59, 0x25, 0xc0, 0xa3, 0x1c, 0x4d, 0x0d, 0x21, 0x46, 0x3c, 0xca,
0x09, 0xe0, 0x76, 0x37, 0x09, 0x3c, 0x8f, 0xf3, 0x84, 0xbd, 0x06, 0xbb, 0x49, 0x68, 0x47, 0x32,
0xc4, 0x6e, 0x92, 0x62, 0xed, 0x23, 0x8d, 0x67, 0x19, 0x9f, 0x5d, 0xea, 0x25, 0xc0, 0xef, 0x60,
0x29, 0x81, 0x6b, 0xc0, 0xed, 0x10, 0x62, 0x17, 0x01, 0x29, 0x38, 0x61, 0x45, 0x16, 0xcf, 0xe0,
0xc5, 0x32, 0xa5, 0xa3, 0x65, 0xc4, 0x22, 0x00, 0x19, 0x50, 0x5c, 0x7d, 0x61, 0x0d, 0x2b, 0x2e,
0xb8, 0xaf, 0x76, 0x3b, 0x84, 0xd8, 0x65, 0x50, 0x0a, 0x26, 0x45, 0x96, 0x0a, 0x30, 0x0c, 0x94,
0x86, 0x94, 0x10, 0xc3, 0xc0, 0x27, 0x80, 0xc9, 0x43, 0x56, 0xce, 0x19, 0x6a, 0x52, 0x4a, 0x82,
0x26, 0x1b, 0xc2, 0xde, 0xa2, 0x57, 0x75, 0xe7, 0xc5, 0x0a, 0xdc, 0xa2, 0xd7, 0xd5, 0xe2, 0xc5,
0x8a, 0xb8, 0x45, 0xef, 0x01, 0xa0, 0x88, 0xc7, 0x71, 0x25, 0xf0, 0x22, 0x4a, 0x49, 0xb0, 0x88,
0x0d, 0x61, 0xd7, 0x68, 0x55, 0xc4, 0xa5, 0x00, 0x6b, 0xb4, 0x2e, 0x80, 0x73, 0xb5, 0xe2, 0x26,
0x29, 0xb7, 0x33, 0x89, 0xea, 0x15, 0x26, 0xf6, 0x52, 0x96, 0x25, 0x15, 0x98, 0x49, 0x74, 0xbb,
0x37, 0x52, 0x62, 0x26, 0x69, 0x53, 0x20, 0x94, 0xf4, 0x73, 0x19, 0xac, 0x76, 0xe0, 0xb1, 0xcc,
0xed, 0x10, 0x62, 0xe7, 0xa7, 0xa6, 0xd0, 0x3b, 0x71, 0x59, 0xa6, 0xf5, 0xe2, 0xbf, 0x8e, 0x17,
0xa8, 0x91, 0x13, 0xf3, 0x13, 0xc6, 0x81, 0xe1, 0xd5, 0x4c, 0xdc, 0x58, 0xc1, 0xe0, 0xd4, 0x7d,
0x27, 0xc8, 0xd8, 0x8c, 0x53, 0x4a, 0x9c, 0xbb, 0x01, 0x58, 0x6b, 0x22, 0x57, 0x03, 0xd6, 0xbb,
0x30, 0xe7, 0x85, 0x3e, 0xe3, 0xe2, 0x90, 0x5f, 0xb1, 0x29, 0x7f, 0xfe, 0x3a, 0xad, 0xea, 0x4d,
0xa0, 0x5e, 0xb9, 0x9f, 0x12, 0x96, 0x30, 0x98, 0x78, 0xa1, 0xaf, 0x53, 0xc9, 0x26, 0x10, 0xa0,
0x2c, 0x47, 0xec, 0x15, 0x9a, 0x40, 0x40, 0x8b, 0x86, 0x23, 0x12, 0x88, 0x10, 0x6f, 0xcf, 0xf1,
0x8c, 0x73, 0xfd, 0x15, 0x87, 0x29, 0x6f, 0x72, 0x39, 0xca, 0x1a, 0x04, 0x89, 0xa3, 0x94, 0xa0,
0x82, 0xdd, 0x5f, 0x1a, 0xff, 0x76, 0x88, 0xdd, 0x27, 0xec, 0xb4, 0x87, 0xd9, 0x83, 0x1e, 0x24,
0xe2, 0xca, 0x5e, 0x70, 0xa1, 0x5c, 0xb5, 0xef, 0xb7, 0x3c, 0xe8, 0x41, 0x3a, 0x67, 0x82, 0x6e,
0xb5, 0x9e, 0xc5, 0xb3, 0xcb, 0x79, 0xc9, 0x97, 0x79, 0xb2, 0xc3, 0x33, 0x5e, 0x82, 0x33, 0x41,
0xaf, 0xd4, 0x00, 0x25, 0xce, 0x04, 0x3b, 0x54, 0x6c, 0x06, 0xe7, 0x96, 0x62, 0x94, 0xa5, 0x73,
0xb8, 0xa3, 0xf6, 0x0c, 0x49, 0x80, 0xc8, 0xe0, 0x50, 0x10, 0x09, 0x22, 0xb5, 0xe3, 0x16, 0xe9,
0x2c, 0xce, 0x94, 0xbf, 0x6d, 0xda, 0x8c, 0x07, 0x76, 0x06, 0x11, 0xa2, 0x80, 0xd4, 0x73, 0xba,
0x2c, 0xf3, 0x71, 0x2e, 0x38, 0x59, 0xcf, 0x06, 0xe8, 0xac, 0xa7, 0x03, 0x82, 0x69, 0x75, 0xca,
0x5e, 0xd7, 0xa5, 0xa9, 0xff, 0xc1, 0xa6, 0xd5, 0xfa, 0xef, 0x43, 0x2d, 0x0f, 0x4d, 0xab, 0x80,
0x03, 0x95, 0xd1, 0x4e, 0x54, 0xc0, 0x04, 0xb4, 0xfd, 0x30, 0xb9, 0xdf, 0x0d, 0xe2, 0x7e, 0x26,
0x62, 0x95, 0xb1, 0x90, 0x1f, 0x09, 0xf4, 0xf1, 0xd3, 0x80, 0xf6, 0xb8, 0xc5, 0xab, 0xcf, 0x05,
0x9b, 0x5d, 0xb6, 0xee, 0xeb, 0xf9, 0x05, 0x55, 0x08, 0x71, 0xdc, 0x42, 0xa0, 0x78, 0x17, 0x8d,
0x67, 0x3c, 0x0f, 0x75, 0x51, 0x2d, 0xef, 0xd3, 0x45, 0x9a, 0xb3, 0x9b, 0x5f, 0x23, 0xd5, 0x91,
0xa9, 0xba, 0x69, 0x93, 0xb0, 0xe0, 0x42, 0xc4, 0xe6, 0x97, 0x84, 0x6d, 0x4e, 0x0e, 0x7d, 0x1e,
0xb6, 0x5f, 0x66, 0x68, 0x59, 0x39, 0xa4, 0x5f, 0x66, 0xa0, 0x58, 0xba, 0x92, 0x2a, 0x46, 0x3a,
0xac, 0xf8, 0x71, 0xf2, 0xa8, 0x1f, 0x6c, 0xb7, 0x3c, 0x9e, 0xcf, 0x9d, 0x8c, 0xc5, 0xa5, 0xf2,
0xba, 0x15, 0x30, 0x64, 0x31, 0x62, 0xcb, 0x13, 0xc0, 0xc1, 0x14, 0xe6, 0x79, 0xde, 0xe1, 0xb9,
0x60, 0xb9, 0xc0, 0xa6, 0x30, 0xdf, 0x98, 0x06, 0x43, 0x53, 0x18, 0xa5, 0x00, 0xe2, 0x56, 0x9e,
0x07, 0x31, 0x71, 0x14, 0x2f, 0xd0, 0x8c, 0x4d, 0x9d, 0xf5, 0x28, 0x79, 0x28, 0x6e, 0x01, 0xe7,
0x3c, 0x64, 0x76, 0xbd, 0x4c, 0xe3, 0x72, 0x6e, 0x4e, 0x37, 0x92, 0xc1, 0x63, 0xda, 0x8e, 0x4f,
0x12, 0x0f, 0x99, 0xc3, 0x1a, 0x60, 0xda, 0x19, 0x2f, 0xe2, 0xb9, 0xa9, 0x29, 0x52, 0x03, 0x29,
0x6f, 0x55, 0xf5, 0x7e, 0x37, 0x08, 0xfc, 0xbc, 0x48, 0x13, 0xc6, 0x03, 0x7e, 0xa4, 0xbc, 0x8f,
0x1f, 0x08, 0x82, 0xec, 0xad, 0xae, 0xb7, 0xda, 0xd1, 0x8d, 0xf2, 0x44, 0xef, 0x63, 0x87, 0x44,
0xf3, 0x00, 0x2e, 0x94, 0xbd, 0x11, 0x3c, 0x18, 0xa3, 0xcd, 0x01, 0x6d, 0x68, 0x8c, 0x9a, 0xf3,
0xd7, 0x3e, 0x63, 0x14, 0x83, 0xb5, 0xcf, 0x1f, 0xeb, 0x31, 0xba, 0x1b, 0x8b, 0xb8, 0xce, 0xdb,
0x5f, 0xa4, 0xec, 0x95, 0xde, 0x08, 0x23, 0xf5, 0x6d, 0xa8, 0xa1, 0x7c, 0x17, 0x1b, 0xec, 0x8a,
0xb7, 0x7b, 0xf3, 0x01, 0xdf, 0x7a, 0x87, 0xd0, 0xe9, 0x1b, 0x6c, 0x15, 0xb6, 0x7b, 0xf3, 0x01,
0xdf, 0xfa, 0x93, 0x12, 0x9d, 0xbe, 0xc1, 0x77, 0x25, 0xb6, 0x7b, 0xf3, 0xda, 0xf7, 0x9f, 0x37,
0x03, 0xd7, 0x75, 0x5e, 0xe7, 0x61, 0x33, 0x91, 0x5e, 0x31, 0x2c, 0x9d, 0xf4, 0xed, 0x19, 0x34,
0x94, 0x4e, 0xd2, 0x2a, 0xce, 0x47, 0xdd, 0xb0, 0x52, 0x1c, 0xf3, 0x2a, 0x95, 0x97, 0x44, 0x9e,
0xf6, 0x30, 0xda, 0xc0, 0xa1, 0x4d, 0x53, 0x48, 0xc9, 0x3e, 0xee, 0xf6, 0x50, 0x7b, 0x3d, 0xff,
0x51, 0xc0, 0x5e, 0xfb, 0x96, 0xfe, 0x56, 0x4f, 0xda, 0x3e, 0x78, 0xf6, 0x98, 0xe6, 0x91, 0xe1,
0x84, 0xa1, 0xab, 0x84, 0x31, 0x65, 0x1e, 0x25, 0xbb, 0xcf, 0x4e, 0x1f, 0xf7, 0x57, 0xe8, 0x70,
0x3f, 0x4a, 0x92, 0x7e, 0xee, 0xdd, 0x67, 0xee, 0x8f, 0xfb, 0x2b, 0x68, 0xf7, 0x7f, 0xd9, 0x6c,
0x6b, 0xa0, 0x7f, 0x3d, 0x06, 0x9f, 0xf4, 0xb1, 0x08, 0xc6, 0xe1, 0xd3, 0x6b, 0xe9, 0xe8, 0x82,
0xfc, 0x6d, 0xb3, 0x7f, 0x6f, 0x50, 0xf9, 0x8e, 0x94, 0x7c, 0xb7, 0x5a, 0x0f, 0xc9, 0x50, 0x54,
0x59, 0x18, 0x0e, 0xcc, 0x8f, 0xae, 0xa9, 0xe5, 0x7c, 0x61, 0xd0, 0x83, 0xf5, 0xbb, 0xbc, 0x4e,
0x79, 0x42, 0x96, 0x1d, 0x1a, 0x16, 0xe8, 0xe3, 0xeb, 0xaa, 0x51, 0x43, 0xd5, 0x81, 0xe5, 0x37,
0x76, 0x9e, 0xf6, 0x34, 0xec, 0x7d, 0x75, 0xe7, 0xc3, 0xeb, 0x29, 0xe9, 0xb2, 0xfc, 0xc7, 0x5a,
0x74, 0xcf, 0x63, 0xed, 0xe3, 0x0c, 0x70, 0xe8, 0xf2, 0x83, 0x80, 0x7d, 0x4a, 0xc9, 0x14, 0xee,
0xb7, 0xbe, 0x99, 0xb2, 0xfd, 0x1c, 0x9f, 0xa7, 0xb2, 0x97, 0x66, 0x82, 0x95, 0xed, 0xcf, 0xf1,
0xf9, 0x76, 0x15, 0x35, 0xa4, 0x3f, 0xc7, 0x17, 0xc0, 0x9d, 0xcf, 0xf1, 0x21, 0x9e, 0xd1, 0xcf,
0xf1, 0xa1, 0xd6, 0x82, 0x9f, 0xe3, 0x0b, 0x6b, 0x50, 0xab, 0x4b, 0x53, 0x04, 0x75, 0x6c, 0xde,
0xcb, 0xa2, 0x7f, 0x8a, 0xfe, 0xe4, 0x3a, 0x2a, 0xc4, 0xfa, 0xaa, 0x38, 0x79, 0xcd, 0xb3, 0x47,
0x9b, 0x7a, 0x57, 0x3d, 0xb7, 0x7b, 0xf3, 0xda, 0xf7, 0x8f, 0xf4, 0xe6, 0xca, 0xac, 0x26, 0xbc,
0x94, 0x9f, 0x62, 0xdc, 0x0c, 0xad, 0x0e, 0xb5, 0x05, 0xb7, 0xe7, 0x1f, 0xf5, 0x83, 0x89, 0xea,
0xd6, 0x84, 0xee, 0xf4, 0x61, 0x97, 0x21, 0xd0, 0xe5, 0xdb, 0xbd, 0x79, 0x62, 0x19, 0x51, 0xbe,
0x55, 0x6f, 0xf7, 0x30, 0xe6, 0xf7, 0xf5, 0xe3, 0xfe, 0x0a, 0xda, 0xfd, 0x95, 0xce, 0x5a, 0x5d,
0xf7, 0xb2, 0x9f, 0xb7, 0xba, 0x4c, 0x4d, 0xbc, 0x6e, 0x1e, 0xf6, 0xc5, 0x43, 0xf9, 0x8b, 0xbb,
0x84, 0x76, 0xe5, 0x2f, 0xe8, 0x32, 0xfa, 0xe1, 0xf5, 0x94, 0x74, 0x59, 0xfe, 0x61, 0x2d, 0xba,
0x49, 0x96, 0x45, 0xc7, 0xc1, 0xc7, 0x7d, 0x2d, 0x83, 0x78, 0xf8, 0xe4, 0xda, 0x7a, 0xba, 0x50,
0xff, 0xbc, 0x16, 0xdd, 0x0a, 0x14, 0x4a, 0x05, 0xc8, 0x35, 0xac, 0xfb, 0x81, 0xf2, 0xe9, 0xf5,
0x15, 0xa9, 0xe5, 0xde, 0xc5, 0x27, 0xed, 0x4f, 0xab, 0x05, 0x6c, 0x4f, 0xe8, 0x4f, 0xab, 0x75,
0x6b, 0xc1, 0x33, 0xa6, 0xf8, 0xac, 0xd9, 0xf3, 0xa1, 0x67, 0x4c, 0xf2, 0x82, 0x66, 0xf0, 0xa3,
0x2d, 0x18, 0x87, 0x39, 0x79, 0xfe, 0xba, 0x88, 0xf3, 0x84, 0x76, 0xa2, 0xe4, 0xdd, 0x4e, 0x0c,
0x07, 0xcf, 0xe6, 0x6a, 0xe9, 0x09, 0x6f, 0xf6, 0x71, 0x0f, 0x28, 0x7d, 0x83, 0x04, 0xcf, 0xe6,
0x5a, 0x28, 0xe1, 0x4d, 0x67, 0x8d, 0x21, 0x6f, 0x20, 0x59, 0x7c, 0xd8, 0x07, 0x05, 0x3b, 0x04,
0xe3, 0xcd, 0x1c, 0xf9, 0x3f, 0x0a, 0x59, 0x69, 0x1d, 0xfb, 0x6f, 0xf5, 0xa4, 0x09, 0xb7, 0x13,
0x26, 0x3e, 0x63, 0x71, 0xc2, 0xca, 0xa0, 0x5b, 0x43, 0xf5, 0x72, 0xeb, 0xd2, 0x98, 0xdb, 0x1d,
0x9e, 0x2d, 0x17, 0xb9, 0xee, 0x4c, 0xd2, 0xad, 0x4b, 0x75, 0xbb, 0x05, 0x34, 0x3c, 0x95, 0xb4,
0x6e, 0x65, 0x7a, 0xf9, 0x30, 0x6c, 0xc6, 0xcb, 0x2a, 0x37, 0x7b, 0xb1, 0x74, 0x3d, 0x75, 0x18,
0x75, 0xd4, 0x13, 0x44, 0xd2, 0x56, 0x4f, 0x1a, 0x1e, 0x0f, 0x3a, 0x6e, 0x4d, 0x3c, 0x6d, 0x77,
0xd8, 0x6a, 0x85, 0xd4, 0xe3, 0xfe, 0x0a, 0xf0, 0x30, 0x56, 0x47, 0xd5, 0x41, 0x5a, 0x89, 0xbd,
0x34, 0xcb, 0x06, 0x9b, 0x81, 0x30, 0x69, 0xa0, 0xe0, 0x61, 0x2c, 0x02, 0x13, 0x91, 0xdc, 0x1c,
0x5e, 0xe6, 0x83, 0x2e, 0x3b, 0x92, 0xea, 0x15, 0xc9, 0x2e, 0x0d, 0x0e, 0xd4, 0x9c, 0xa6, 0x36,
0xb5, 0x1d, 0x86, 0x1b, 0xae, 0x55, 0xe1, 0xed, 0xde, 0x3c, 0x78, 0xda, 0x2f, 0x29, 0xb9, 0xb2,
0xdc, 0xa5, 0x4c, 0x78, 0x2b, 0xc9, 0xbd, 0x0e, 0x0a, 0x1c, 0x4a, 0xaa, 0x61, 0xf4, 0x32, 0x4d,
0xe6, 0x4c, 0xa0, 0x0f, 0xaa, 0x5c, 0x20, 0xf8, 0xa0, 0x0a, 0x80, 0xa0, 0xeb, 0xd4, 0xdf, 0xcd,
0x69, 0xec, 0x38, 0xc1, 0xba, 0x4e, 0x2b, 0x3b, 0x54, 0xa8, 0xeb, 0x50, 0x1a, 0xcc, 0x06, 0xc6,
0xad, 0xfe, 0xcc, 0xc5, 0xc3, 0x90, 0x19, 0xf0, 0xad, 0x8b, 0xcd, 0x5e, 0x2c, 0x58, 0x51, 0xac,
0xc3, 0x74, 0x91, 0x0a, 0x6c, 0x45, 0x71, 0x6c, 0xd4, 0x48, 0x68, 0x45, 0x69, 0xa3, 0x54, 0xf5,
0xea, 0x1c, 0x61, 0x9c, 0x84, 0xab, 0xa7, 0x98, 0x7e, 0xd5, 0x33, 0x6c, 0xeb, 0xb9, 0x6a, 0x6e,
0x42, 0x46, 0x5c, 0xe8, 0xcd, 0x32, 0x12, 0xdb, 0xf2, 0xf5, 0x67, 0x08, 0x86, 0x66, 0x1d, 0x4a,
0x01, 0x3e, 0x2f, 0xa8, 0xb9, 0xe6, 0xd1, 0x6f, 0x51, 0xb0, 0xb8, 0x8c, 0xf3, 0x19, 0xba, 0x39,
0x95, 0x06, 0x5b, 0x64, 0x68, 0x73, 0x4a, 0x6a, 0x80, 0xa7, 0xf6, 0xfe, 0xfb, 0xc5, 0xc8, 0x50,
0x30, 0x2f, 0xf2, 0xfa, 0xaf, 0x17, 0x3f, 0xe8, 0x41, 0xc2, 0xa7, 0xf6, 0x0d, 0x60, 0xce, 0xdd,
0x95, 0xd3, 0x0f, 0x02, 0xa6, 0x7c, 0x34, 0xb4, 0x11, 0xa6, 0x55, 0x40, 0x50, 0x3b, 0x67, 0x8b,
0x9f, 0xb3, 0x15, 0x16, 0xd4, 0xee, 0x21, 0xe1, 0xe7, 0x6c, 0x15, 0x0a, 0xea, 0x36, 0x0a, 0xf2,
0x4c, 0x77, 0x1f, 0xb4, 0x1e, 0xd0, 0x77, 0xb7, 0x3e, 0x1b, 0x9d, 0x1c, 0x18, 0x39, 0xbb, 0xe9,
0x95, 0xf7, 0x98, 0x02, 0x29, 0xe8, 0x6e, 0x7a, 0x85, 0x3f, 0xa5, 0xd8, 0xec, 0xc5, 0xc2, 0x1b,
0x01, 0xb1, 0x60, 0xaf, 0x9b, 0x47, 0xf5, 0x48, 0x71, 0xa5, 0xbc, 0xf5, 0xac, 0xfe, 0x7e, 0x37,
0x68, 0xef, 0xdf, 0x1e, 0x97, 0x7c, 0xc6, 0xaa, 0x4a, 0x7f, 0x01, 0xd6, 0xbf, 0xe0, 0xa4, 0x65,
0x43, 0xf0, 0xfd, 0xd7, 0xbb, 0x61, 0xc8, 0xf9, 0x6c, 0xa3, 0x12, 0xd9, 0xaf, 0x49, 0xad, 0xa3,
0x9a, 0xed, 0x0f, 0x49, 0x6d, 0x74, 0x72, 0x76, 0x78, 0x69, 0xa9, 0xfb, 0xf9, 0xa8, 0xfb, 0xa8,
0x3a, 0xf6, 0xe5, 0xa8, 0x07, 0x3d, 0x48, 0xed, 0xea, 0xb3, 0xe8, 0xcd, 0x03, 0x3e, 0x9f, 0xb0,
0x3c, 0x19, 0x7c, 0xcf, 0xbf, 0xc1, 0xcb, 0xe7, 0xc3, 0xfa, 0xcf, 0xc6, 0xe8, 0x0d, 0x4a, 0x6c,
0xef, 0x20, 0xee, 0xb2, 0xb3, 0xe5, 0x7c, 0x22, 0x62, 0x01, 0xee, 0x20, 0xca, 0xbf, 0x0f, 0x6b,
0x01, 0x71, 0x07, 0xd1, 0x03, 0x80, 0xbd, 0x69, 0xc9, 0x18, 0x6a, 0xaf, 0x16, 0x04, 0xed, 0x69,
0xc0, 0x66, 0x11, 0xc6, 0x5e, 0x9d, 0xa8, 0xc3, 0x3b, 0x83, 0x56, 0x47, 0x4a, 0x89, 0x2c, 0xa2,
0x4d, 0xd9, 0xe0, 0x56, 0xd5, 0x97, 0x5f, 0xf3, 0x59, 0x2e, 0x16, 0x71, 0xb9, 0x02, 0xc1, 0xad,
0x6b, 0xe9, 0x00, 0x44, 0x70, 0xa3, 0xa0, 0x1d, 0xb5, 0x4d, 0x33, 0xcf, 0x2e, 0xf7, 0x79, 0xc9,
0x97, 0x22, 0xcd, 0x19, 0xfc, 0xa2, 0x8b, 0x69, 0x50, 0x97, 0x21, 0x46, 0x2d, 0xc5, 0xda, 0x2c,
0x57, 0x12, 0xea, 0x3a, 0xa3, 0xfc, 0x04, 0x7e, 0x25, 0x78, 0x09, 0x1f, 0x67, 0x2a, 0x2b, 0x10,
0x22, 0xb2, 0x5c, 0x12, 0x06, 0x7d, 0x7f, 0x9c, 0xe6, 0x73, 0xb4, 0xef, 0x8f, 0xdd, 0xaf, 0x2a,
0xdf, 0xa2, 0x01, 0x3b, 0xa0, 0x54, 0xa3, 0xa9, 0x01, 0xa0, 0x5f, 0x65, 0x46, 0x1b, 0xdd, 0x25,
0x88, 0x01, 0x85, 0x93, 0xc0, 0xd5, 0x17, 0x05, 0xcb, 0x59, 0xd2, 0x5c, 0xda, 0xc3, 0x5c, 0x79,
0x44, 0xd0, 0x15, 0x24, 0xed, 0x5c, 0x24, 0xe5, 0x27, 0xcb, 0xfc, 0xb8, 0xe4, 0xe7, 0x69, 0xc6,
0x4a, 0x30, 0x17, 0x29, 0x75, 0x47, 0x4e, 0xcc, 0x45, 0x18, 0x67, 0x6f, 0x7f, 0x48, 0xa9, 0xf7,
0x3b, 0x0e, 0xd3, 0x32, 0x9e, 0xc1, 0xdb, 0x1f, 0xca, 0x46, 0x1b, 0x23, 0x4e, 0x06, 0x03, 0xb8,
0x93, 0xe8, 0x28, 0xd7, 0xf9, 0x4a, 0xc6, 0x87, 0x7e, 0x95, 0x56, 0x7e, 0x6b, 0xb8, 0x02, 0x89,
0x8e, 0x36, 0x87, 0x91, 0x44, 0xa2, 0x13, 0xd6, 0xb0, 0x4b, 0x89, 0xe4, 0x8e, 0xf4, 0xad, 0x26,
0xb0, 0x94, 0x28, 0x1b, 0x8d, 0x90, 0x58, 0x4a, 0x5a, 0x10, 0x98, 0x90, 0x9a, 0x61, 0x30, 0x47,
0x27, 0x24, 0x23, 0x0d, 0x4e, 0x48, 0x2e, 0x65, 0x27, 0x8a, 0x71, 0x9e, 0x8a, 0x34, 0xce, 0x26,
0x4c, 0x1c, 0xc7, 0x65, 0xbc, 0x60, 0x82, 0x95, 0x70, 0xa2, 0xd0, 0xc8, 0xd0, 0x63, 0x88, 0x89,
0x82, 0x62, 0xb5, 0xc3, 0xdf, 0x8e, 0xde, 0xae, 0xd7, 0x7d, 0x96, 0xeb, 0x5f, 0xa0, 0x7a, 0x2e,
0x7f, 0xba, 0x6e, 0xf0, 0x8e, 0xb1, 0x31, 0x11, 0x25, 0x8b, 0x17, 0x8d, 0xed, 0xb7, 0xcc, 0xdf,
0x25, 0xf8, 0x78, 0xad, 0x8e, 0xe7, 0x23, 0x2e, 0xd2, 0xf3, 0x7a, 0x9b, 0xad, 0x5f, 0x60, 0x02,
0xf1, 0xec, 0x8a, 0x87, 0x81, 0x4f, 0xb1, 0x60, 0x9c, 0x9d, 0xa7, 0x5d, 0xe9, 0x09, 0x2b, 0x32,
0x38, 0x4f, 0x7b, 0xda, 0x12, 0x20, 0xe6, 0x69, 0x14, 0xb4, 0x83, 0xd3, 0x15, 0x4f, 0x59, 0xb8,
0x32, 0x53, 0xd6, 0xaf, 0x32, 0x53, 0xef, 0x9d, 0x90, 0x2c, 0x7a, 0xfb, 0x90, 0x2d, 0xce, 0x58,
0x59, 0x5d, 0xa4, 0x05, 0xf5, 0x3d, 0x64, 0x4b, 0x74, 0x7e, 0x0f, 0x99, 0x40, 0xed, 0x4a, 0x60,
0x81, 0x71, 0x75, 0x14, 0x2f, 0x98, 0xfc, 0xb0, 0x0c, 0x58, 0x09, 0x1c, 0x23, 0x0e, 0x44, 0xac,
0x04, 0x24, 0xec, 0xbc, 0x5e, 0x66, 0x99, 0x13, 0x36, 0xaf, 0x23, 0xac, 0x3c, 0x8e, 0x57, 0x0b,
0x96, 0x0b, 0x6d, 0x12, 0x9c, 0xc9, 0x3b, 0x26, 0x71, 0x9e, 0x38, 0x93, 0xef, 0xa3, 0xe7, 0x4c,
0x4d, 0x5e, 0xc3, 0x1f, 0xf3, 0x52, 0xa8, 0xdf, 0x97, 0x3b, 0x2d, 0x33, 0x30, 0x35, 0xf9, 0x8d,
0xea, 0x91, 0xc4, 0xd4, 0x14, 0xd6, 0x70, 0x7e, 0x4b, 0xc4, 0x2b, 0xc3, 0x0b, 0x56, 0x9a, 0x38,
0x79, 0xbe, 0x88, 0xd3, 0x4c, 0x47, 0xc3, 0xf7, 0x03, 0xb6, 0x09, 0x1d, 0xe2, 0xb7, 0x44, 0xfa,
0xea, 0x3a, 0xbf, 0xbe, 0x12, 0x2e, 0x21, 0x78, 0x44, 0xd0, 0x61, 0x9f, 0x78, 0x44, 0xd0, 0xad,
0x65, 0x77, 0xee, 0x96, 0x95, 0xdc, 0x4a, 0x12, 0x3b, 0x3c, 0x81, 0xe7, 0x85, 0x8e, 0x4d, 0x00,
0x12, 0x3b, 0xf7, 0xa0, 0x82, 0x4d, 0x0d, 0x2c, 0xb6, 0x97, 0xe6, 0x71, 0x96, 0xfe, 0x18, 0xa6,
0xf5, 0x8e, 0x9d, 0x86, 0x20, 0x52, 0x03, 0x9c, 0xc4, 0x5c, 0xed, 0x33, 0x31, 0x4d, 0xeb, 0xa9,
0xff, 0x7e, 0xa0, 0xdd, 0x24, 0xd1, 0xed, 0xca, 0x21, 0x9d, 0x6f, 0x1f, 0xc3, 0x66, 0x1d, 0x15,
0xc5, 0xa4, 0x5e, 0x55, 0x4f, 0xd8, 0x8c, 0xa5, 0x85, 0x18, 0x7c, 0x14, 0x6e, 0x2b, 0x80, 0x13,
0x17, 0x2d, 0x7a, 0xa8, 0x39, 0x8f, 0xef, 0xeb, 0xb9, 0x64, 0xa2, 0x7e, 0x78, 0xf5, 0xb4, 0x62,
0xa5, 0x4e, 0x34, 0xf6, 0x99, 0x00, 0xa3, 0xd3, 0xe1, 0x86, 0x0e, 0x58, 0x57, 0x94, 0x18, 0x9d,
0x61, 0x0d, 0x7b, 0xd8, 0xe7, 0x70, 0xfa, 0xdb, 0x01, 0xf2, 0xba, 0xe3, 0x23, 0xd2, 0x98, 0x43,
0x11, 0x87, 0x7d, 0x34, 0x6d, 0xb3, 0xb5, 0xb6, 0xdb, 0x51, 0xbe, 0x1a, 0xc3, 0x2b, 0x13, 0x88,
0x25, 0x89, 0x11, 0xd9, 0x5a, 0x00, 0x77, 0x0e, 0xc3, 0x4b, 0x1e, 0x27, 0xb3, 0xb8, 0x12, 0xc7,
0xf1, 0x2a, 0xe3, 0x71, 0x22, 0xd7, 0x75, 0x78, 0x18, 0xde, 0x30, 0x43, 0x17, 0xa2, 0x0e, 0xc3,
0x29, 0xd8, 0xcd, 0xce, 0xe4, 0xef, 0xc9, 0xea, 0xab, 0xa4, 0x30, 0x3b, 0x93, 0xe5, 0x85, 0xd7,
0x48, 0xef, 0x86, 0x21, 0xfb, 0x0a, 0x9c, 0x12, 0xc9, 0x34, 0xe4, 0x16, 0xa6, 0xe3, 0x25, 0x20,
0xef, 0x07, 0x08, 0xfb, 0x59, 0x16, 0xf5, 0xf7, 0xe6, 0x27, 0xc4, 0x84, 0xfe, 0x42, 0xfc, 0x23,
0x4c, 0xd7, 0x85, 0xbc, 0x1b, 0x6a, 0x5b, 0x3d, 0x69, 0x9b, 0x66, 0xee, 0x5c, 0xc4, 0x62, 0x94,
0x24, 0x87, 0xac, 0x42, 0xde, 0x67, 0xaf, 0x85, 0x43, 0x2b, 0x25, 0xd2, 0xcc, 0x36, 0x65, 0x03,
0xbd, 0x96, 0x3d, 0x4f, 0x52, 0xa1, 0x65, 0xcd, 0x05, 0xed, 0x47, 0x6d, 0x03, 0x6d, 0x8a, 0xa8,
0x15, 0x4d, 0xdb, 0xb9, 0xbc, 0x66, 0xa6, 0x7c, 0x3e, 0xcf, 0x98, 0x86, 0x4e, 0x58, 0xac, 0x3e,
0x90, 0xb9, 0xdd, 0xb6, 0x85, 0x82, 0xc4, 0x5c, 0x1e, 0x54, 0xb0, 0x69, 0x64, 0x8d, 0xa9, 0x47,
0x52, 0x4d, 0xc3, 0x6e, 0xb4, 0xcd, 0x78, 0x00, 0x91, 0x46, 0xa2, 0xa0, 0x7d, 0xed, 0xae, 0x16,
0xef, 0xb3, 0xa6, 0x25, 0xe0, 0x17, 0xb8, 0xa4, 0xb2, 0x23, 0x26, 0x5e, 0xbb, 0x43, 0x30, 0xbb,
0x4f, 0x00, 0x1e, 0x9e, 0xad, 0xc6, 0x09, 0xdc, 0x27, 0x40, 0x7d, 0xc9, 0x10, 0xfb, 0x04, 0x8a,
0xf5, 0xbb, 0xce, 0x9c, 0x7b, 0x1d, 0xc4, 0x95, 0xad, 0x1c, 0xd2, 0x75, 0x28, 0x18, 0xea, 0x3a,
0x4a, 0xc1, 0x6f, 0x52, 0xf7, 0x68, 0x0d, 0x69, 0x52, 0xec, 0x5c, 0x6d, 0xbd, 0x0b, 0xb3, 0xb9,
0x7f, 0x2d, 0x3c, 0x61, 0x71, 0x62, 0x2a, 0x86, 0xe8, 0xba, 0x72, 0x22, 0xf7, 0xc7, 0x38, 0xed,
0xe4, 0xf7, 0xa2, 0x81, 0xaa, 0x46, 0xe9, 0xba, 0xb9, 0x85, 0x15, 0xb1, 0x26, 0x88, 0x89, 0xca,
0x27, 0x9c, 0xc4, 0xcd, 0xeb, 0xa2, 0x29, 0xd7, 0x0e, 0xf4, 0x6b, 0xa1, 0x15, 0x48, 0xdc, 0xfc,
0x66, 0x6f, 0xd1, 0x44, 0xe2, 0xd6, 0xad, 0xe5, 0x7c, 0x8c, 0x08, 0x74, 0xd9, 0x5e, 0xc9, 0x17,
0xb0, 0x4c, 0x9f, 0x06, 0xbb, 0x07, 0xd1, 0x20, 0x3e, 0x46, 0xd4, 0x4f, 0xd3, 0xae, 0x41, 0xe6,
0xec, 0x40, 0x5e, 0x4f, 0xc3, 0x7f, 0x05, 0x45, 0x09, 0x89, 0x35, 0xa8, 0x05, 0x39, 0x3f, 0x9d,
0x3a, 0x7e, 0x59, 0xa6, 0x22, 0xcd, 0xe7, 0x53, 0xce, 0x33, 0x78, 0x64, 0x39, 0x1a, 0x0f, 0x5d,
0x29, 0xf5, 0xd3, 0xa9, 0x2d, 0xca, 0x2e, 0x71, 0xa3, 0xf1, 0x68, 0x29, 0xf8, 0x79, 0x9a, 0x65,
0x20, 0x72, 0x46, 0xe3, 0x61, 0x23, 0x21, 0x22, 0xc7, 0x27, 0x9c, 0x1f, 0xfc, 0x1c, 0xcb, 0xd3,
0x7f, 0x7d, 0x02, 0x7a, 0x07, 0xea, 0x38, 0x42, 0xea, 0x07, 0x3f, 0x21, 0xe4, 0xfc, 0x80, 0xe9,
0x18, 0xfb, 0x29, 0x97, 0x4d, 0xa8, 0x8e, 0x40, 0xd4, 0x0f, 0x98, 0x52, 0xb0, 0xf3, 0x4e, 0xf2,
0xf1, 0xb2, 0xba, 0xf0, 0x8f, 0x0c, 0xd4, 0xe6, 0x50, 0x7d, 0xb6, 0xf5, 0x29, 0xf8, 0x41, 0x21,
0x9f, 0x1d, 0x7a, 0x30, 0x71, 0x3d, 0xad, 0x53, 0x49, 0x15, 0xe6, 0xd9, 0xfb, 0xff, 0xf5, 0xd5,
0x8d, 0xb5, 0x9f, 0x7e, 0x75, 0x63, 0xed, 0x7f, 0xbe, 0xba, 0xb1, 0xf6, 0x93, 0xaf, 0x6f, 0xbc,
0xf1, 0xd3, 0xaf, 0x6f, 0xbc, 0xf1, 0xdf, 0x5f, 0xdf, 0x78, 0xe3, 0xcb, 0x37, 0x2b, 0x95, 0x9b,
0x9d, 0xfd, 0x5c, 0x51, 0x72, 0xc1, 0x9f, 0xfe, 0x7f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xc7, 0xe9,
0x8b, 0xe6, 0x86, 0x80, 0x00, 0x00,
0x52, 0xc0, 0xcf, 0x3c, 0xb0, 0x50, 0xc7, 0x2d, 0xd0, 0x0b, 0xcb, 0xdd, 0x72, 0x37, 0xdf, 0x63,
0xcf, 0x8c, 0xc7, 0xed, 0xd9, 0x99, 0xfd, 0xe2, 0x0e, 0x09, 0x7a, 0xec, 0xb1, 0xb7, 0x6f, 0x6d,
0xaf, 0x71, 0xb7, 0x67, 0xc4, 0x4a, 0x48, 0x94, 0xbb, 0xd2, 0xed, 0xc2, 0xd5, 0x95, 0x75, 0x55,
0xd9, 0x9e, 0xe9, 0x43, 0x20, 0x10, 0x27, 0x10, 0x08, 0xc4, 0x89, 0x2f, 0xc1, 0x13, 0x12, 0x7f,
0x01, 0x7f, 0x01, 0xcf, 0x3c, 0xde, 0x23, 0x8f, 0x68, 0xf7, 0x1f, 0x41, 0x95, 0x99, 0x95, 0x1f,
0x51, 0x11, 0x59, 0xe5, 0xe5, 0x69, 0x46, 0x8e, 0x5f, 0x44, 0xe4, 0x47, 0x64, 0x66, 0x64, 0x56,
0x56, 0x75, 0x74, 0xb3, 0x38, 0xdb, 0x2e, 0x4a, 0x2e, 0x78, 0xb5, 0x5d, 0xb1, 0xf2, 0x2a, 0x9d,
0xb1, 0xe6, 0xdf, 0xa1, 0xfc, 0xf3, 0xe0, 0xad, 0x38, 0x5f, 0x89, 0x55, 0xc1, 0xde, 0xfb, 0xb6,
0x25, 0x67, 0x7c, 0xb1, 0x88, 0xf3, 0xa4, 0x52, 0xc8, 0x7b, 0xef, 0x5a, 0x09, 0xbb, 0x62, 0xb9,
0xd0, 0x7f, 0x7f, 0xfa, 0x5f, 0x3f, 0xf9, 0xb9, 0xe8, 0xed, 0x9d, 0x2c, 0x65, 0xb9, 0xd8, 0xd1,
0x1a, 0x83, 0x2f, 0xa2, 0x6f, 0x8d, 0x8a, 0x62, 0x9f, 0x89, 0x97, 0xac, 0xac, 0x52, 0x9e, 0x0f,
0xee, 0x0e, 0xb5, 0x83, 0xe1, 0x49, 0x31, 0x1b, 0x8e, 0x8a, 0x62, 0x68, 0x85, 0xc3, 0x13, 0xf6,
0xa3, 0x25, 0xab, 0xc4, 0x7b, 0xf7, 0xc2, 0x50, 0x55, 0xf0, 0xbc, 0x62, 0x83, 0xf3, 0xe8, 0x57,
0x47, 0x45, 0x31, 0x61, 0x62, 0x97, 0xd5, 0x15, 0x98, 0x88, 0x58, 0xb0, 0xc1, 0x46, 0x4b, 0xd5,
0x07, 0x8c, 0x8f, 0x07, 0xdd, 0xa0, 0xf6, 0x33, 0x8d, 0xbe, 0x59, 0xfb, 0xb9, 0x58, 0x8a, 0x84,
0xbf, 0xce, 0x07, 0xb7, 0xdb, 0x8a, 0x5a, 0x64, 0x6c, 0xdf, 0x09, 0x21, 0xda, 0xea, 0xab, 0xe8,
0x97, 0x5e, 0xc5, 0x59, 0xc6, 0xc4, 0x4e, 0xc9, 0xea, 0x82, 0xfb, 0x3a, 0x4a, 0x34, 0x54, 0x32,
0x63, 0xf7, 0x6e, 0x90, 0xd1, 0x86, 0xbf, 0x88, 0xbe, 0xa5, 0x24, 0x27, 0x6c, 0xc6, 0xaf, 0x58,
0x39, 0x40, 0xb5, 0xb4, 0x90, 0x68, 0xf2, 0x16, 0x04, 0x6d, 0xef, 0xf0, 0xfc, 0x8a, 0x95, 0x02,
0xb7, 0xad, 0x85, 0x61, 0xdb, 0x16, 0xd2, 0xb6, 0xff, 0x7a, 0x2d, 0xfa, 0xee, 0x68, 0x36, 0xe3,
0xcb, 0x5c, 0x1c, 0xf0, 0x59, 0x9c, 0x1d, 0xa4, 0xf9, 0xe5, 0x11, 0x7b, 0xbd, 0x73, 0x51, 0xf3,
0xf9, 0x9c, 0x0d, 0x9e, 0xf9, 0xad, 0xaa, 0xd0, 0xa1, 0x61, 0x87, 0x2e, 0x6c, 0x7c, 0x7f, 0x70,
0x3d, 0x25, 0x5d, 0x96, 0xbf, 0x5f, 0x8b, 0x6e, 0xc0, 0xb2, 0x4c, 0x78, 0x76, 0xc5, 0x6c, 0x69,
0x3e, 0xec, 0x30, 0xec, 0xe3, 0xa6, 0x3c, 0x1f, 0x5d, 0x57, 0x4d, 0x97, 0xe8, 0xcf, 0xd6, 0xa2,
0xef, 0xc0, 0x12, 0xa9, 0x9e, 0x1f, 0x15, 0xc5, 0xe0, 0x49, 0x87, 0x55, 0x43, 0x9a, 0x72, 0xbc,
0x7f, 0x0d, 0x0d, 0x5d, 0x84, 0x3f, 0x89, 0xbe, 0x0d, 0x4b, 0x70, 0x90, 0x56, 0x62, 0x54, 0x14,
0xd5, 0x60, 0xbb, 0xc3, 0x5c, 0x03, 0x1a, 0xff, 0x4f, 0xfa, 0x2b, 0x04, 0x5a, 0xe0, 0x84, 0x5d,
0xf1, 0xcb, 0x5e, 0x2d, 0x60, 0xc8, 0xde, 0x2d, 0xe0, 0x6a, 0xe8, 0x22, 0x64, 0xd1, 0x3b, 0xee,
0x98, 0x9d, 0xb0, 0x4a, 0xce, 0x69, 0x0f, 0xe9, 0x61, 0xa9, 0x11, 0xe3, 0xf4, 0x51, 0x1f, 0x54,
0x7b, 0x4b, 0xa3, 0x81, 0xf6, 0x96, 0xf1, 0xca, 0x38, 0x7b, 0x80, 0x5a, 0x70, 0x08, 0xe3, 0xeb,
0x61, 0x0f, 0x52, 0xbb, 0xfa, 0xc3, 0xe8, 0x97, 0x5f, 0xf1, 0xf2, 0xb2, 0x2a, 0xe2, 0x19, 0xd3,
0xf3, 0xd1, 0x7d, 0x5f, 0xbb, 0x91, 0xc2, 0x29, 0x69, 0xbd, 0x0b, 0x73, 0x66, 0x8e, 0x46, 0xf8,
0x79, 0xc1, 0xe0, 0x42, 0x60, 0x15, 0x6b, 0x21, 0x35, 0x73, 0x40, 0x48, 0xdb, 0xbe, 0x8c, 0x06,
0xd6, 0xf6, 0xd9, 0x1f, 0xb1, 0x99, 0x18, 0x25, 0x09, 0xec, 0x15, 0xab, 0x2b, 0x89, 0xe1, 0x28,
0x49, 0xa8, 0x5e, 0xc1, 0x51, 0xed, 0xec, 0x75, 0xf4, 0x2e, 0x70, 0x26, 0x43, 0x35, 0x49, 0x06,
0x5b, 0x61, 0x2b, 0x1a, 0x33, 0x4e, 0x87, 0x7d, 0x71, 0x27, 0xfe, 0x11, 0xcf, 0x27, 0x6c, 0xc1,
0xaf, 0x18, 0x88, 0x7f, 0xd4, 0x9a, 0x22, 0x89, 0xf8, 0x0f, 0x6b, 0x20, 0x61, 0x32, 0x61, 0x19,
0x9b, 0x09, 0x32, 0x4c, 0x94, 0xb8, 0x33, 0x4c, 0x0c, 0xe6, 0x8c, 0xb0, 0x46, 0xb8, 0xcf, 0xc4,
0xce, 0xb2, 0x2c, 0x59, 0x2e, 0xc8, 0xbe, 0xb4, 0x48, 0x67, 0x5f, 0x7a, 0x28, 0x52, 0x9f, 0x7d,
0x26, 0x46, 0x59, 0x46, 0xd6, 0x47, 0x89, 0x3b, 0xeb, 0x63, 0x30, 0xed, 0x61, 0x16, 0xfd, 0x8a,
0xd3, 0x62, 0x62, 0x9c, 0x9f, 0xf3, 0x01, 0xdd, 0x16, 0x52, 0x6e, 0x7c, 0x6c, 0x74, 0x72, 0x48,
0x35, 0x5e, 0xbc, 0x29, 0x78, 0x49, 0x77, 0x8b, 0x12, 0x77, 0x56, 0xc3, 0x60, 0xda, 0xc3, 0x1f,
0x44, 0x6f, 0xeb, 0x09, 0xb2, 0x49, 0x2a, 0xee, 0xa1, 0xb3, 0x27, 0xcc, 0x2a, 0xee, 0x77, 0x50,
0x2d, 0xf3, 0x87, 0xe9, 0xbc, 0xac, 0x67, 0x1f, 0xdc, 0xbc, 0x96, 0x76, 0x98, 0xb7, 0x94, 0x36,
0xcf, 0xa3, 0x5f, 0xf3, 0xcd, 0xef, 0xc4, 0xf9, 0x8c, 0x65, 0x83, 0x47, 0x21, 0x75, 0xc5, 0x18,
0x57, 0x9b, 0xbd, 0x58, 0x3b, 0xd9, 0x69, 0x42, 0x4f, 0xa6, 0x77, 0x51, 0x6d, 0x30, 0x95, 0xde,
0x0b, 0x43, 0x2d, 0xdb, 0xbb, 0x2c, 0x63, 0xa4, 0x6d, 0x25, 0xec, 0xb0, 0x6d, 0x20, 0x6d, 0xbb,
0x8c, 0x7e, 0xdd, 0x74, 0x73, 0x9d, 0x9c, 0x49, 0x79, 0xbd, 0xe8, 0x6c, 0x12, 0xfd, 0xe8, 0x42,
0xc6, 0xd7, 0xe3, 0x7e, 0x70, 0xab, 0x3e, 0x7a, 0x46, 0xc1, 0xeb, 0x03, 0xe6, 0x93, 0x7b, 0x61,
0x48, 0xdb, 0xfe, 0x9b, 0xb5, 0xe8, 0x7b, 0x5a, 0xf6, 0x22, 0x8f, 0xcf, 0x32, 0x26, 0x57, 0xf7,
0x23, 0x26, 0x5e, 0xf3, 0xf2, 0x72, 0xb2, 0xca, 0x67, 0x44, 0x4e, 0x89, 0xc3, 0x1d, 0x39, 0x25,
0xa9, 0xa4, 0x0b, 0xf3, 0xc7, 0x26, 0x7d, 0xda, 0xb9, 0x88, 0xf3, 0x39, 0xfb, 0x61, 0xc5, 0xf3,
0x51, 0x91, 0x8e, 0x92, 0xa4, 0x1c, 0x0c, 0xf1, 0xae, 0x87, 0x9c, 0x29, 0xc1, 0x76, 0x6f, 0xde,
0xd9, 0xc3, 0xe8, 0x56, 0x16, 0xbc, 0x80, 0x7b, 0x98, 0xa6, 0xf9, 0x04, 0x2f, 0xa8, 0x3d, 0x8c,
0x8f, 0xb4, 0xac, 0x1e, 0xd6, 0x6b, 0x10, 0x6e, 0xf5, 0xd0, 0x5d, 0x74, 0xee, 0x84, 0x10, 0xbb,
0x06, 0x34, 0x0d, 0xc5, 0xf3, 0xf3, 0x74, 0x7e, 0x5a, 0x24, 0xf5, 0x18, 0x7a, 0x88, 0xd7, 0xd9,
0x41, 0x88, 0x35, 0x80, 0x40, 0xb5, 0xb7, 0xbf, 0xb3, 0xa9, 0xbe, 0x9e, 0x97, 0xf6, 0x4a, 0xbe,
0x38, 0x60, 0xf3, 0x78, 0xb6, 0xd2, 0x93, 0xe9, 0x07, 0xa1, 0x59, 0x0c, 0xd2, 0xa6, 0x10, 0x1f,
0x5e, 0x53, 0x4b, 0x97, 0xe7, 0xdf, 0xd7, 0xa2, 0x7b, 0x5e, 0x9c, 0xe8, 0x60, 0x52, 0xa5, 0x1f,
0xe5, 0xc9, 0x09, 0xab, 0x44, 0x5c, 0x8a, 0xc1, 0xf7, 0x03, 0x31, 0x40, 0xe8, 0x98, 0xb2, 0xfd,
0xe0, 0x6b, 0xe9, 0xda, 0x5e, 0x9f, 0xd4, 0xab, 0x84, 0x9e, 0x7f, 0xfc, 0x5e, 0x97, 0x12, 0x38,
0xfb, 0xdc, 0x09, 0x21, 0xb6, 0xd7, 0xa5, 0x60, 0x9c, 0x5f, 0xa5, 0x82, 0xed, 0xb3, 0x9c, 0x95,
0xed, 0x5e, 0x57, 0xaa, 0x3e, 0x42, 0xf4, 0x3a, 0x81, 0xda, 0xb3, 0x03, 0xc7, 0x9b, 0xaa, 0x38,
0x38, 0x3b, 0x70, 0x0d, 0x28, 0x80, 0x38, 0x3b, 0x40, 0x41, 0x3b, 0xa3, 0x7a, 0xb5, 0x32, 0x19,
0xcd, 0x66, 0xa0, 0xb0, 0xad, 0x9c, 0xe6, 0x71, 0x3f, 0x98, 0x68, 0x49, 0xb1, 0x5f, 0x1b, 0x09,
0xb6, 0xa4, 0x42, 0x7a, 0xb5, 0xa4, 0x41, 0xd1, 0x96, 0x54, 0x9b, 0xa6, 0x40, 0x4b, 0x2a, 0xa0,
0x47, 0x4b, 0x1a, 0xd0, 0x26, 0x39, 0x8e, 0x9f, 0x97, 0x29, 0x7b, 0x0d, 0x92, 0x1c, 0x57, 0xb9,
0x16, 0x13, 0x49, 0x0e, 0x82, 0x69, 0x0f, 0x47, 0xd1, 0x2f, 0x4a, 0xe1, 0x0f, 0x79, 0x9a, 0x0f,
0x6e, 0x22, 0x4a, 0xb5, 0xc0, 0x58, 0xbd, 0x45, 0x03, 0xa0, 0xc4, 0xf5, 0x5f, 0x75, 0xc6, 0x71,
0x9f, 0x50, 0x02, 0xc9, 0xc6, 0x7a, 0x17, 0x66, 0xb3, 0x4b, 0x29, 0xac, 0x67, 0xe5, 0xc9, 0x45,
0x5c, 0xa6, 0xf9, 0x7c, 0x80, 0xe9, 0x3a, 0x72, 0x22, 0xbb, 0xc4, 0x38, 0x10, 0x4e, 0x5a, 0x71,
0x54, 0x14, 0x65, 0x3d, 0xd9, 0x63, 0xe1, 0xe4, 0x23, 0xc1, 0x70, 0x6a, 0xa1, 0xb8, 0xb7, 0x5d,
0x36, 0xcb, 0xd2, 0x3c, 0xe8, 0x4d, 0x23, 0x7d, 0xbc, 0x59, 0x14, 0x04, 0xef, 0x01, 0x8b, 0xaf,
0x58, 0x53, 0x33, 0xac, 0x65, 0x5c, 0x20, 0x18, 0xbc, 0x00, 0xb4, 0x5b, 0x79, 0x29, 0x3e, 0x8c,
0x2f, 0x59, 0xdd, 0xc0, 0xac, 0x4e, 0x15, 0x06, 0x98, 0xbe, 0x47, 0x10, 0x5b, 0x79, 0x9c, 0xd4,
0xae, 0x96, 0xd1, 0xbb, 0x52, 0x7e, 0x1c, 0x97, 0x22, 0x9d, 0xa5, 0x45, 0x9c, 0x37, 0x5b, 0x44,
0x6c, 0x16, 0x69, 0x51, 0xc6, 0xe5, 0x56, 0x4f, 0x5a, 0xbb, 0xfd, 0x97, 0xb5, 0xe8, 0x36, 0xf4,
0x7b, 0xcc, 0xca, 0x45, 0x2a, 0x4f, 0x1a, 0x2a, 0x3d, 0xc3, 0x7e, 0x1c, 0x36, 0xda, 0x52, 0x30,
0xa5, 0xf9, 0xe4, 0xfa, 0x8a, 0x36, 0xbf, 0x9c, 0xe8, 0xdd, 0xd7, 0xe7, 0x65, 0xd2, 0x3a, 0x0e,
0x9d, 0x34, 0x5b, 0x2a, 0x29, 0x24, 0xf2, 0xcb, 0x16, 0x04, 0x46, 0xf8, 0x69, 0x5e, 0x35, 0xd6,
0xb1, 0x11, 0x6e, 0xc5, 0xc1, 0x11, 0xee, 0x61, 0x76, 0x84, 0x1f, 0x2f, 0xcf, 0xb2, 0xb4, 0xba,
0x48, 0xf3, 0xb9, 0xde, 0x4c, 0xf8, 0xba, 0x56, 0x0c, 0xf7, 0x13, 0x1b, 0x9d, 0x1c, 0xe6, 0x44,
0x07, 0x0b, 0xe9, 0x04, 0x84, 0xc9, 0x46, 0x27, 0x67, 0xf7, 0x78, 0x56, 0x7a, 0x90, 0x56, 0x02,
0xec, 0xf1, 0x1c, 0xd5, 0x5a, 0x4a, 0xec, 0xf1, 0xda, 0x94, 0xdd, 0xe3, 0xb9, 0x75, 0xa8, 0x78,
0x76, 0xc5, 0x4e, 0xcb, 0x14, 0xec, 0xf1, 0xbc, 0xf2, 0x35, 0x0c, 0xb1, 0xc7, 0xa3, 0x58, 0x3b,
0x51, 0x59, 0x62, 0x9f, 0x89, 0x89, 0x88, 0xc5, 0xb2, 0x02, 0x13, 0x95, 0x63, 0xc3, 0x20, 0xc4,
0x44, 0x45, 0xa0, 0xda, 0xdb, 0xef, 0x45, 0x91, 0x3a, 0x97, 0x91, 0x67, 0x67, 0xfe, 0xda, 0xa3,
0x0f, 0x6c, 0xbc, 0x83, 0xb3, 0xdb, 0x01, 0xc2, 0xa6, 0x71, 0xea, 0xef, 0xf2, 0x48, 0x70, 0x80,
0x6a, 0x48, 0x11, 0x91, 0xc6, 0x01, 0x04, 0x16, 0x74, 0x72, 0xc1, 0x5f, 0xe3, 0x05, 0xad, 0x25,
0xe1, 0x82, 0x6a, 0xc2, 0x3e, 0x29, 0xd1, 0x05, 0xc5, 0x9e, 0x94, 0x34, 0xc5, 0x08, 0x3d, 0x29,
0x81, 0x8c, 0x8d, 0x19, 0xd7, 0xf0, 0x73, 0xce, 0x2f, 0x17, 0x71, 0x79, 0x09, 0x62, 0xc6, 0x53,
0x6e, 0x18, 0x22, 0x66, 0x28, 0xd6, 0xc6, 0x8c, 0xeb, 0xb0, 0xde, 0x04, 0x9c, 0x96, 0x19, 0x88,
0x19, 0xcf, 0x86, 0x46, 0x88, 0x98, 0x21, 0x50, 0x3b, 0x3b, 0xb9, 0xde, 0x26, 0x0c, 0x1e, 0x0b,
0x79, 0xea, 0x13, 0x46, 0x1d, 0x0b, 0x21, 0x18, 0x0c, 0xa1, 0xfd, 0x32, 0x2e, 0x2e, 0xf0, 0x10,
0x92, 0xa2, 0x70, 0x08, 0x35, 0x08, 0xec, 0xef, 0x09, 0x8b, 0xcb, 0xd9, 0x05, 0xde, 0xdf, 0x4a,
0x16, 0xee, 0x6f, 0xc3, 0xc0, 0xfe, 0x56, 0x82, 0x57, 0xa9, 0xb8, 0x38, 0x64, 0x22, 0xc6, 0xfb,
0xdb, 0x67, 0xc2, 0xfd, 0xdd, 0x62, 0x6d, 0xf6, 0xef, 0x3a, 0x9c, 0x2c, 0xcf, 0xaa, 0x59, 0x99,
0x9e, 0xb1, 0x41, 0xc0, 0x8a, 0x81, 0x88, 0xec, 0x9f, 0x84, 0xb5, 0xcf, 0x9f, 0xae, 0x45, 0x37,
0x9b, 0x6e, 0xe7, 0x55, 0xa5, 0xd7, 0x3e, 0xdf, 0xfd, 0x87, 0x78, 0xff, 0x12, 0x38, 0xf1, 0xec,
0xaa, 0x87, 0x9a, 0x93, 0x1b, 0xe0, 0x45, 0x3a, 0xcd, 0x2b, 0x53, 0xa8, 0x8f, 0xfb, 0x58, 0x77,
0x14, 0x88, 0xdc, 0xa0, 0x97, 0xa2, 0x4d, 0xcb, 0x74, 0xff, 0x34, 0xb2, 0x71, 0x52, 0x81, 0xb4,
0xac, 0x69, 0x6f, 0x87, 0x20, 0xd2, 0x32, 0x9c, 0x84, 0xa1, 0xb0, 0x5f, 0xf2, 0x65, 0x51, 0x75,
0x84, 0x02, 0x80, 0xc2, 0xa1, 0xd0, 0x86, 0xb5, 0xcf, 0x37, 0xd1, 0x6f, 0xb8, 0xe1, 0xe7, 0x36,
0xf6, 0x16, 0x1d, 0x53, 0x58, 0x13, 0x0f, 0xfb, 0xe2, 0x36, 0xa3, 0x68, 0x3c, 0x8b, 0x5d, 0x26,
0xe2, 0x34, 0xab, 0x06, 0xeb, 0xb8, 0x8d, 0x46, 0x4e, 0x64, 0x14, 0x18, 0x07, 0xe7, 0xb7, 0xdd,
0x65, 0x91, 0xa5, 0xb3, 0xf6, 0x43, 0x2b, 0xad, 0x6b, 0xc4, 0xe1, 0xf9, 0xcd, 0xc5, 0xe0, 0x7c,
0x5d, 0xa7, 0x7e, 0xf2, 0x3f, 0xd3, 0x55, 0xc1, 0xf0, 0xf9, 0xda, 0x43, 0xc2, 0xf3, 0x35, 0x44,
0x61, 0x7d, 0x26, 0x4c, 0x1c, 0xc4, 0x2b, 0xbe, 0x24, 0xe6, 0x6b, 0x23, 0x0e, 0xd7, 0xc7, 0xc5,
0xec, 0xde, 0xc0, 0x78, 0x18, 0xe7, 0x82, 0x95, 0x79, 0x9c, 0xed, 0x65, 0xf1, 0xbc, 0x1a, 0x10,
0x73, 0x8c, 0x4f, 0x11, 0x7b, 0x03, 0x9a, 0x46, 0x9a, 0x71, 0x5c, 0xed, 0xc5, 0x57, 0xbc, 0x4c,
0x05, 0xdd, 0x8c, 0x16, 0xe9, 0x6c, 0x46, 0x0f, 0x45, 0xbd, 0x8d, 0xca, 0xd9, 0x45, 0x7a, 0xc5,
0x92, 0x80, 0xb7, 0x06, 0xe9, 0xe1, 0xcd, 0x41, 0x91, 0x4e, 0x9b, 0xf0, 0x65, 0x39, 0x63, 0x64,
0xa7, 0x29, 0x71, 0x67, 0xa7, 0x19, 0x4c, 0x7b, 0xf8, 0xc9, 0x5a, 0xf4, 0x9b, 0x4a, 0xea, 0x3e,
0x49, 0xda, 0x8d, 0xab, 0x8b, 0x33, 0x1e, 0x97, 0xc9, 0xe0, 0x7d, 0xcc, 0x0e, 0x8a, 0x1a, 0xd7,
0x4f, 0xaf, 0xa3, 0x02, 0x9b, 0xb5, 0xce, 0xbb, 0xed, 0x88, 0x43, 0x9b, 0xd5, 0x43, 0xc2, 0xcd,
0x0a, 0x51, 0x38, 0x81, 0x48, 0xb9, 0x3a, 0x68, 0x5c, 0x27, 0xf5, 0xfd, 0xd3, 0xc6, 0x8d, 0x4e,
0x0e, 0xce, 0x8f, 0xb5, 0xd0, 0x8f, 0x96, 0x2d, 0xca, 0x06, 0x1e, 0x31, 0xc3, 0xbe, 0x38, 0xe9,
0xd9, 0x8c, 0x8a, 0xb0, 0xe7, 0xd6, 0xc8, 0x18, 0xf6, 0xc5, 0x09, 0xcf, 0xce, 0xb4, 0x16, 0xf2,
0x8c, 0x4c, 0x6d, 0xc3, 0xbe, 0x38, 0xcc, 0xbe, 0x34, 0xd3, 0xac, 0x0b, 0x8f, 0x02, 0x76, 0xe0,
0xda, 0xb0, 0xd9, 0x8b, 0xd5, 0x0e, 0xff, 0x6a, 0x2d, 0xfa, 0xae, 0xf5, 0x78, 0xc8, 0x93, 0xf4,
0x7c, 0xa5, 0xa0, 0x97, 0x71, 0xb6, 0x64, 0xd5, 0xe0, 0x29, 0x65, 0xad, 0xcd, 0x9a, 0x12, 0x3c,
0xbb, 0x96, 0x0e, 0x1c, 0x3b, 0xa3, 0xa2, 0xc8, 0x56, 0x53, 0xb6, 0x28, 0x32, 0x72, 0xec, 0x78,
0x48, 0x78, 0xec, 0x40, 0x14, 0x66, 0xe5, 0x53, 0x5e, 0xe7, 0xfc, 0x68, 0x56, 0x2e, 0x45, 0xe1,
0xac, 0xbc, 0x41, 0x60, 0xae, 0x34, 0xe5, 0x3b, 0x3c, 0xcb, 0xd8, 0x4c, 0xb4, 0x6f, 0xa3, 0x18,
0x4d, 0x4b, 0x84, 0x73, 0x25, 0x40, 0xda, 0x53, 0xb9, 0x66, 0x0f, 0x19, 0x97, 0xec, 0xf9, 0xea,
0x20, 0xcd, 0x2f, 0x07, 0x78, 0x5a, 0x60, 0x01, 0xe2, 0x54, 0x0e, 0x05, 0xe1, 0x5e, 0xf5, 0x34,
0x4f, 0x38, 0xbe, 0x57, 0xad, 0x25, 0xe1, 0xbd, 0xaa, 0x26, 0xa0, 0xc9, 0x13, 0x46, 0x99, 0xac,
0x25, 0x61, 0x93, 0x9a, 0xc0, 0xa6, 0x42, 0xfd, 0x44, 0x8a, 0x9c, 0x0a, 0xc1, 0x33, 0xa8, 0x8d,
0x4e, 0x0e, 0xee, 0xb9, 0xb4, 0x03, 0x34, 0x22, 0x80, 0xf1, 0xbb, 0x41, 0x06, 0x86, 0x7e, 0xb3,
0x1b, 0xde, 0x63, 0x62, 0x76, 0x81, 0x87, 0xbe, 0x87, 0x84, 0x43, 0x1f, 0xa2, 0xb0, 0xad, 0xa6,
0xdc, 0xec, 0xe6, 0xd7, 0xf1, 0xc0, 0x6b, 0xed, 0xe4, 0x37, 0x3a, 0x39, 0xd8, 0x56, 0xe3, 0x05,
0xdd, 0x56, 0x4a, 0x16, 0x6e, 0x2b, 0xc3, 0xc0, 0xd2, 0x2b, 0x81, 0x3c, 0x24, 0x5b, 0xa7, 0x15,
0xbd, 0x63, 0xb2, 0x8d, 0x4e, 0x4e, 0x3b, 0xf9, 0x27, 0xb3, 0x3f, 0x54, 0xd2, 0x23, 0x5e, 0x0f,
0xbe, 0x97, 0x71, 0x96, 0x26, 0xb1, 0x60, 0x53, 0x7e, 0xc9, 0x72, 0x7c, 0x2b, 0xa6, 0x4b, 0xab,
0xf8, 0xa1, 0xa7, 0x10, 0xde, 0x8a, 0x85, 0x15, 0x61, 0x9c, 0x28, 0xfa, 0xb4, 0x62, 0x3b, 0x71,
0x45, 0x4c, 0x91, 0x1e, 0x12, 0x8e, 0x13, 0x88, 0xc2, 0x44, 0x58, 0xc9, 0x5f, 0xbc, 0x29, 0x58,
0x99, 0xb2, 0x7c, 0xc6, 0xf0, 0x44, 0x18, 0x52, 0xe1, 0x44, 0x18, 0xa1, 0xe1, 0x26, 0x70, 0x37,
0x16, 0xec, 0xf9, 0x6a, 0x9a, 0x2e, 0x58, 0x25, 0xe2, 0x45, 0x81, 0x6f, 0x02, 0x01, 0x14, 0xde,
0x04, 0xb6, 0xe1, 0xd6, 0x99, 0x93, 0x99, 0x69, 0xdb, 0xb7, 0xe3, 0x20, 0x11, 0xb8, 0x1d, 0x47,
0xa0, 0xb0, 0x61, 0x2d, 0x80, 0x3e, 0x7d, 0x68, 0x59, 0x09, 0x3e, 0x7d, 0xa0, 0xe9, 0xd6, 0x49,
0x9e, 0x61, 0x26, 0xf5, 0xd0, 0xec, 0x28, 0xfa, 0xc4, 0x1d, 0xa2, 0x9b, 0xbd, 0x58, 0xfc, 0xe8,
0xf0, 0x84, 0x65, 0xb1, 0x5c, 0x0f, 0x03, 0xe7, 0x73, 0x0d, 0xd3, 0xe7, 0xe8, 0xd0, 0x61, 0xb5,
0xc3, 0x3f, 0x5f, 0x8b, 0xde, 0xc3, 0x3c, 0x7e, 0x5e, 0x48, 0xbf, 0x4f, 0xba, 0x6d, 0x29, 0x92,
0xb8, 0xfe, 0x17, 0xd6, 0xb0, 0x37, 0x58, 0x1a, 0x91, 0xbd, 0x1d, 0xa8, 0x0b, 0xe0, 0x67, 0x83,
0xa6, 0xfc, 0x90, 0x23, 0x6e, 0xb0, 0x84, 0x78, 0xbb, 0xd1, 0xf2, 0xcb, 0x55, 0x81, 0x8d, 0x96,
0xb1, 0xa1, 0xc5, 0xc4, 0x46, 0x0b, 0xc1, 0xec, 0xe8, 0x74, 0xab, 0xf7, 0x2a, 0x15, 0x17, 0x32,
0x91, 0x03, 0xa3, 0xd3, 0x2b, 0xab, 0x81, 0x88, 0xd1, 0x49, 0xc2, 0x30, 0xd5, 0x69, 0xc0, 0x7a,
0x6c, 0x62, 0x73, 0xb9, 0x31, 0xe4, 0x8e, 0xcc, 0x07, 0xdd, 0x20, 0x8c, 0xd7, 0x46, 0xac, 0xf7,
0x54, 0x8f, 0x42, 0x16, 0xc0, 0xbe, 0x6a, 0xb3, 0x17, 0xab, 0x1d, 0xfe, 0x69, 0xf4, 0x9d, 0x56,
0xc5, 0xf6, 0x58, 0x2c, 0x96, 0x25, 0x4b, 0xc0, 0x6d, 0xf1, 0x76, 0xb9, 0x1b, 0x90, 0xb8, 0x2d,
0x1e, 0x54, 0x68, 0x25, 0xff, 0x0d, 0xa7, 0xc2, 0xca, 0x94, 0xe1, 0x69, 0xc8, 0xa4, 0xcf, 0x06,
0x93, 0x7f, 0x5a, 0xa7, 0xb5, 0x7f, 0x77, 0xa3, 0x6b, 0x74, 0x15, 0xa7, 0x99, 0x7c, 0x0a, 0xfc,
0x7e, 0xc8, 0xa8, 0x87, 0x06, 0xf7, 0xef, 0xa4, 0x4a, 0x6b, 0x66, 0x96, 0x63, 0xdc, 0xd9, 0xf7,
0x3d, 0xa6, 0x67, 0x02, 0x64, 0xdb, 0xb7, 0xd5, 0x93, 0xd6, 0x6e, 0x45, 0xb3, 0xe4, 0xd5, 0x7f,
0x76, 0x83, 0x1c, 0xf3, 0xaa, 0x55, 0x91, 0x48, 0xdf, 0xea, 0x49, 0xdb, 0x57, 0x15, 0xda, 0x5e,
0xf5, 0x42, 0xb4, 0xdd, 0x69, 0x0a, 0xac, 0x45, 0x4f, 0xfa, 0x2b, 0x68, 0xf7, 0xff, 0x6a, 0x0e,
0xbc, 0x95, 0xff, 0x19, 0x5f, 0x2c, 0x58, 0x9e, 0xb0, 0xa4, 0xd1, 0xa8, 0xea, 0x8d, 0xd9, 0x27,
0xb4, 0x5d, 0xa3, 0x30, 0x74, 0x35, 0x4c, 0x89, 0x7e, 0xeb, 0x6b, 0x68, 0xea, 0xa2, 0xfd, 0xe7,
0x5a, 0xf4, 0x10, 0x2d, 0x5a, 0x13, 0xb8, 0x5e, 0x11, 0x7f, 0xb7, 0x8f, 0x23, 0x4c, 0xd3, 0x14,
0x75, 0xf4, 0xff, 0xb0, 0xa0, 0x8b, 0xfc, 0x6f, 0x6b, 0xd1, 0x1d, 0xab, 0x58, 0x87, 0xf7, 0x0e,
0xcf, 0xcf, 0xb3, 0x74, 0x26, 0xe4, 0xa3, 0x5e, 0xad, 0x42, 0x37, 0x27, 0xa5, 0xd1, 0xdd, 0x9c,
0x01, 0x4d, 0x5d, 0xb6, 0x7f, 0x5c, 0x8b, 0x6e, 0xb9, 0xcd, 0x29, 0x9f, 0x13, 0xab, 0x63, 0xd7,
0x46, 0xb1, 0x1a, 0x7c, 0x44, 0xb7, 0x01, 0xc6, 0x9b, 0x72, 0x7d, 0x7c, 0x6d, 0x3d, 0xbb, 0x57,
0xff, 0x34, 0xad, 0x04, 0x2f, 0x57, 0x93, 0x0b, 0xfe, 0xba, 0x79, 0xf5, 0xce, 0x5f, 0x2d, 0x34,
0x30, 0x74, 0x08, 0x62, 0xaf, 0x8e, 0x93, 0x2d, 0x57, 0xf6, 0x15, 0xbd, 0x8a, 0x70, 0xe5, 0x10,
0x1d, 0xae, 0x7c, 0xd2, 0xae, 0x95, 0x4d, 0xad, 0xec, 0xfb, 0x84, 0x1b, 0x78, 0x51, 0xdb, 0xef,
0x14, 0x3e, 0xe8, 0x06, 0x6d, 0xc6, 0xac, 0xc5, 0xbb, 0xe9, 0xf9, 0xb9, 0xa9, 0x13, 0x5e, 0x52,
0x17, 0x21, 0x32, 0x66, 0x02, 0xb5, 0x9b, 0xbe, 0xbd, 0x34, 0x63, 0xf2, 0x51, 0xd5, 0xe7, 0xe7,
0xe7, 0x19, 0x8f, 0x13, 0xb0, 0xe9, 0xab, 0xc5, 0x43, 0x57, 0x4e, 0x6c, 0xfa, 0x30, 0xce, 0x5e,
0x82, 0xa9, 0xa5, 0xf5, 0x98, 0xcb, 0x67, 0x69, 0x06, 0x2f, 0x8d, 0x4b, 0x4d, 0x23, 0x24, 0x2e,
0xc1, 0xb4, 0x20, 0x9b, 0x98, 0xd5, 0xa2, 0x7a, 0xac, 0x34, 0xe5, 0xbf, 0xdf, 0x56, 0x74, 0xc4,
0x44, 0x62, 0x86, 0x60, 0xf6, 0x50, 0xa5, 0x16, 0x9e, 0x16, 0xd2, 0xf8, 0xad, 0xb6, 0x96, 0x92,
0x10, 0x87, 0x2a, 0x3e, 0x61, 0xf7, 0xf0, 0xf5, 0xdf, 0x77, 0xf9, 0xeb, 0x5c, 0x1a, 0xbd, 0xd3,
0x56, 0x69, 0x64, 0xc4, 0x1e, 0x1e, 0x32, 0xda, 0xf0, 0x67, 0xd1, 0x2f, 0x48, 0xc3, 0x25, 0x2f,
0x06, 0x37, 0x10, 0x85, 0xd2, 0xb9, 0x62, 0x7d, 0x93, 0x94, 0xdb, 0x3b, 0x33, 0x26, 0x36, 0x4e,
0xab, 0x78, 0x0e, 0xdf, 0x8b, 0xb0, 0x3d, 0x2e, 0xa5, 0xc4, 0x9d, 0x99, 0x36, 0xe5, 0x47, 0xc5,
0x11, 0x4f, 0xb4, 0x75, 0xa4, 0x86, 0x46, 0x18, 0x8a, 0x0a, 0x17, 0xb2, 0xc9, 0xf4, 0x51, 0x7c,
0x95, 0xce, 0x4d, 0xc2, 0xa3, 0xa6, 0xaf, 0x0a, 0x24, 0xd3, 0x96, 0x19, 0x3a, 0x10, 0x91, 0x4c,
0x93, 0xb0, 0x33, 0x19, 0x5b, 0x66, 0xbf, 0x39, 0x86, 0x1e, 0xe7, 0xe7, 0xbc, 0x4e, 0xbd, 0x0f,
0xd2, 0xfc, 0x12, 0x4e, 0xc6, 0x8e, 0x49, 0x9c, 0x27, 0x26, 0xe3, 0x3e, 0x7a, 0x76, 0xd7, 0xd4,
0x9c, 0xd1, 0xda, 0x8b, 0x1a, 0x4a, 0x03, 0xec, 0x9a, 0xcc, 0x51, 0x2e, 0xe4, 0x88, 0x5d, 0x53,
0x88, 0xb7, 0x5d, 0x6c, 0x9c, 0x67, 0x3c, 0x87, 0x5d, 0x6c, 0x2d, 0xd4, 0x42, 0xa2, 0x8b, 0x5b,
0x90, 0x9d, 0x8f, 0x1b, 0x91, 0x3a, 0xf5, 0x1b, 0x65, 0x19, 0x98, 0x8f, 0x8d, 0xaa, 0x01, 0x88,
0xf9, 0x18, 0x05, 0xb5, 0x9f, 0x93, 0xe8, 0x9b, 0x75, 0x93, 0x1e, 0x97, 0xec, 0x2a, 0x65, 0xf0,
0x4e, 0x91, 0x23, 0x21, 0xc6, 0xbf, 0x4f, 0xd8, 0x91, 0x75, 0x9a, 0x57, 0x45, 0x16, 0x57, 0x17,
0xfa, 0x96, 0x89, 0x5f, 0xe7, 0x46, 0x08, 0xef, 0x99, 0xdc, 0xef, 0xa0, 0xec, 0xa4, 0xde, 0xc8,
0xcc, 0x14, 0xb3, 0x8e, 0xab, 0xb6, 0xa6, 0x99, 0x8d, 0x4e, 0xce, 0x3e, 0xca, 0xd9, 0x8f, 0xb3,
0x8c, 0x95, 0xab, 0x46, 0x76, 0x18, 0xe7, 0xe9, 0x39, 0xab, 0x04, 0x78, 0x94, 0xa3, 0xa9, 0x21,
0xc4, 0x88, 0x47, 0x39, 0x01, 0xdc, 0xee, 0x26, 0x81, 0xe7, 0x71, 0x9e, 0xb0, 0x37, 0x60, 0x37,
0x09, 0xed, 0x48, 0x86, 0xd8, 0x4d, 0x52, 0xac, 0x7d, 0xa4, 0xf1, 0x3c, 0xe3, 0xb3, 0x4b, 0xbd,
0x04, 0xf8, 0x1d, 0x2c, 0x25, 0x70, 0x0d, 0xb8, 0x13, 0x42, 0xec, 0x22, 0x20, 0x05, 0x27, 0xac,
0xc8, 0xe2, 0x19, 0xbc, 0x58, 0xa6, 0x74, 0xb4, 0x8c, 0x58, 0x04, 0x20, 0x03, 0x8a, 0xab, 0x2f,
0xac, 0x61, 0xc5, 0x05, 0xf7, 0xd5, 0xee, 0x84, 0x10, 0xbb, 0x0c, 0x4a, 0xc1, 0xa4, 0xc8, 0x52,
0x01, 0x86, 0x81, 0xd2, 0x90, 0x12, 0x62, 0x18, 0xf8, 0x04, 0x30, 0x79, 0xc8, 0xca, 0x39, 0x43,
0x4d, 0x4a, 0x49, 0xd0, 0x64, 0x43, 0xd8, 0x5b, 0xf4, 0xaa, 0xee, 0xbc, 0x58, 0x81, 0x5b, 0xf4,
0xba, 0x5a, 0xbc, 0x58, 0x11, 0xb7, 0xe8, 0x3d, 0x00, 0x14, 0xf1, 0x38, 0xae, 0x04, 0x5e, 0x44,
0x29, 0x09, 0x16, 0xb1, 0x21, 0xec, 0x1a, 0xad, 0x8a, 0xb8, 0x14, 0x60, 0x8d, 0xd6, 0x05, 0x70,
0xae, 0x56, 0xdc, 0x24, 0xe5, 0x76, 0x26, 0x51, 0xbd, 0xc2, 0xc4, 0x5e, 0xca, 0xb2, 0xa4, 0x02,
0x33, 0x89, 0x6e, 0xf7, 0x46, 0x4a, 0xcc, 0x24, 0x6d, 0x0a, 0x84, 0x92, 0x7e, 0x2e, 0x83, 0xd5,
0x0e, 0x3c, 0x96, 0xb9, 0x13, 0x42, 0xec, 0xfc, 0xd4, 0x14, 0x7a, 0x27, 0x2e, 0xcb, 0xb4, 0x5e,
0xfc, 0xd7, 0xf1, 0x02, 0x35, 0x72, 0x62, 0x7e, 0xc2, 0x38, 0x30, 0xbc, 0x9a, 0x89, 0x1b, 0x2b,
0x18, 0x9c, 0xba, 0xef, 0x06, 0x19, 0x9b, 0x71, 0x4a, 0x89, 0x73, 0x37, 0x00, 0x6b, 0x4d, 0xe4,
0x6a, 0xc0, 0x7a, 0x17, 0xe6, 0xbc, 0x38, 0x68, 0x5c, 0x1c, 0xf2, 0x2b, 0x36, 0xe5, 0x2f, 0xde,
0xa4, 0x55, 0xbd, 0x09, 0xd4, 0x2b, 0xf7, 0x33, 0xc2, 0x12, 0x06, 0x13, 0x2f, 0x0e, 0x76, 0x2a,
0xd9, 0x04, 0x02, 0x94, 0xe5, 0x88, 0xbd, 0x46, 0x13, 0x08, 0x68, 0xd1, 0x70, 0x44, 0x02, 0x11,
0xe2, 0xed, 0x39, 0x9e, 0x71, 0xae, 0x3f, 0xd9, 0x31, 0xe5, 0x4d, 0x2e, 0x47, 0x59, 0x83, 0x20,
0x71, 0x94, 0x12, 0x54, 0xb0, 0xfb, 0x4b, 0xe3, 0xdf, 0x0e, 0xb1, 0x07, 0x84, 0x9d, 0xf6, 0x30,
0x7b, 0xd8, 0x83, 0x44, 0x5c, 0xd9, 0x0b, 0x2e, 0x94, 0xab, 0xf6, 0xfd, 0x96, 0x87, 0x3d, 0x48,
0xe7, 0x4c, 0xd0, 0xad, 0xd6, 0xf3, 0x78, 0x76, 0x39, 0x2f, 0xf9, 0x32, 0x4f, 0x76, 0x78, 0xc6,
0x4b, 0x70, 0x26, 0xe8, 0x95, 0x1a, 0xa0, 0xc4, 0x99, 0x60, 0x87, 0x8a, 0xcd, 0xe0, 0xdc, 0x52,
0x8c, 0xb2, 0x74, 0x0e, 0x77, 0xd4, 0x9e, 0x21, 0x09, 0x10, 0x19, 0x1c, 0x0a, 0x22, 0x41, 0xa4,
0x76, 0xdc, 0x22, 0x9d, 0xc5, 0x99, 0xf2, 0xb7, 0x4d, 0x9b, 0xf1, 0xc0, 0xce, 0x20, 0x42, 0x14,
0x90, 0x7a, 0x4e, 0x97, 0x65, 0x3e, 0xce, 0x05, 0x27, 0xeb, 0xd9, 0x00, 0x9d, 0xf5, 0x74, 0x40,
0x30, 0xad, 0x4e, 0xd9, 0x9b, 0xba, 0x34, 0xf5, 0x3f, 0xd8, 0xb4, 0x5a, 0xff, 0x7d, 0xa8, 0xe5,
0xa1, 0x69, 0x15, 0x70, 0xa0, 0x32, 0xda, 0x89, 0x0a, 0x98, 0x80, 0xb6, 0x1f, 0x26, 0x0f, 0xba,
0x41, 0xdc, 0xcf, 0x44, 0xac, 0x32, 0x16, 0xf2, 0x23, 0x81, 0x3e, 0x7e, 0x1a, 0xd0, 0x1e, 0xb7,
0x78, 0xf5, 0xb9, 0x60, 0xb3, 0xcb, 0xd6, 0x7d, 0x3d, 0xbf, 0xa0, 0x0a, 0x21, 0x8e, 0x5b, 0x08,
0x14, 0xef, 0xa2, 0xf1, 0x8c, 0xe7, 0xa1, 0x2e, 0xaa, 0xe5, 0x7d, 0xba, 0x48, 0x73, 0x76, 0xf3,
0x6b, 0xa4, 0x3a, 0x32, 0x55, 0x37, 0x6d, 0x12, 0x16, 0x5c, 0x88, 0xd8, 0xfc, 0x92, 0xb0, 0xcd,
0xc9, 0xa1, 0xcf, 0xc3, 0xf6, 0xcb, 0x0c, 0x2d, 0x2b, 0x87, 0xf4, 0xcb, 0x0c, 0x14, 0x4b, 0x57,
0x52, 0xc5, 0x48, 0x87, 0x15, 0x3f, 0x4e, 0x1e, 0xf7, 0x83, 0xed, 0x96, 0xc7, 0xf3, 0xb9, 0x93,
0xb1, 0xb8, 0x54, 0x5e, 0xb7, 0x02, 0x86, 0x2c, 0x46, 0x6c, 0x79, 0x02, 0x38, 0x98, 0xc2, 0x3c,
0xcf, 0x3b, 0x3c, 0x17, 0x2c, 0x17, 0xd8, 0x14, 0xe6, 0x1b, 0xd3, 0x60, 0x68, 0x0a, 0xa3, 0x14,
0x40, 0xdc, 0xca, 0xf3, 0x20, 0x26, 0x8e, 0xe2, 0x05, 0x9a, 0xb1, 0xa9, 0xb3, 0x1e, 0x25, 0x0f,
0xc5, 0x2d, 0xe0, 0x9c, 0x87, 0xcc, 0xae, 0x97, 0x69, 0x5c, 0xce, 0xcd, 0xe9, 0x46, 0x32, 0x78,
0x42, 0xdb, 0xf1, 0x49, 0xe2, 0x21, 0x73, 0x58, 0x03, 0x4c, 0x3b, 0xe3, 0x45, 0x3c, 0x37, 0x35,
0x45, 0x6a, 0x20, 0xe5, 0xad, 0xaa, 0x3e, 0xe8, 0x06, 0x81, 0x9f, 0x97, 0x69, 0xc2, 0x78, 0xc0,
0x8f, 0x94, 0xf7, 0xf1, 0x03, 0x41, 0x90, 0xbd, 0xd5, 0xf5, 0xd6, 0x1f, 0xd5, 0xca, 0x13, 0xbd,
0x8f, 0x1d, 0x12, 0xcd, 0x03, 0xb8, 0x50, 0xf6, 0x46, 0xf0, 0x60, 0x8c, 0x36, 0x07, 0xb4, 0xa1,
0x31, 0x6a, 0xce, 0x5f, 0xfb, 0x8c, 0x51, 0x0c, 0xd6, 0x3e, 0x7f, 0xac, 0xc7, 0xe8, 0x6e, 0x2c,
0xe2, 0x3a, 0x6f, 0x7f, 0x99, 0xb2, 0xd7, 0x7a, 0x23, 0x8c, 0xd4, 0xb7, 0xa1, 0x86, 0xf2, 0x5d,
0x6c, 0xb0, 0x2b, 0xde, 0xee, 0xcd, 0x07, 0x7c, 0xeb, 0x1d, 0x42, 0xa7, 0x6f, 0xb0, 0x55, 0xd8,
0xee, 0xcd, 0x07, 0x7c, 0xeb, 0x4f, 0x57, 0x74, 0xfa, 0x06, 0xdf, 0xaf, 0xd8, 0xee, 0xcd, 0x6b,
0xdf, 0x7f, 0xd1, 0x0c, 0x5c, 0xd7, 0x79, 0x9d, 0x87, 0xcd, 0x44, 0x7a, 0xc5, 0xb0, 0x74, 0xd2,
0xb7, 0x67, 0xd0, 0x50, 0x3a, 0x49, 0xab, 0x38, 0x5f, 0xf0, 0xc3, 0x4a, 0x71, 0xcc, 0xab, 0x54,
0x5e, 0x12, 0x79, 0xd6, 0xc3, 0x68, 0x03, 0x87, 0x36, 0x4d, 0x21, 0x25, 0xfb, 0xb8, 0xdb, 0x43,
0xed, 0xf5, 0xfc, 0xc7, 0x01, 0x7b, 0xed, 0x5b, 0xfa, 0x5b, 0x3d, 0x69, 0xfb, 0xe0, 0xd9, 0x63,
0x9a, 0x47, 0x86, 0x13, 0x86, 0xae, 0x12, 0xc6, 0x94, 0x79, 0x94, 0xec, 0x3e, 0x3b, 0x7d, 0xd2,
0x5f, 0xa1, 0xc3, 0xfd, 0x28, 0x49, 0xfa, 0xb9, 0x77, 0x9f, 0xb9, 0x3f, 0xe9, 0xaf, 0xa0, 0xdd,
0xff, 0x65, 0xb3, 0xad, 0x81, 0xfe, 0xf5, 0x18, 0x7c, 0xda, 0xc7, 0x22, 0x18, 0x87, 0xcf, 0xae,
0xa5, 0xa3, 0x0b, 0xf2, 0xb7, 0xcd, 0xfe, 0xbd, 0x41, 0xe5, 0x3b, 0x52, 0xf2, 0xdd, 0x6a, 0x3d,
0x24, 0x43, 0x51, 0x65, 0x61, 0x38, 0x30, 0x3f, 0xbc, 0xa6, 0x96, 0xf3, 0x39, 0x49, 0x0f, 0xd6,
0xef, 0xf2, 0x3a, 0xe5, 0x09, 0x59, 0x76, 0x68, 0x58, 0xa0, 0x8f, 0xae, 0xab, 0x46, 0x0d, 0x55,
0x07, 0x96, 0xdf, 0xf2, 0x79, 0xd6, 0xd3, 0xb0, 0xf7, 0x75, 0x9f, 0x0f, 0xae, 0xa7, 0xa4, 0xcb,
0xf2, 0x1f, 0x6b, 0xd1, 0x7d, 0x8f, 0xb5, 0x8f, 0x33, 0xc0, 0xa1, 0xcb, 0x0f, 0x02, 0xf6, 0x29,
0x25, 0x53, 0xb8, 0xdf, 0xfe, 0x7a, 0xca, 0xf6, 0xb3, 0x7f, 0x9e, 0xca, 0x5e, 0x9a, 0x09, 0x56,
0xb6, 0x3f, 0xfb, 0xe7, 0xdb, 0x55, 0xd4, 0x90, 0xfe, 0xec, 0x5f, 0x00, 0x77, 0x3e, 0xfb, 0x87,
0x78, 0x46, 0x3f, 0xfb, 0x87, 0x5a, 0x0b, 0x7e, 0xf6, 0x2f, 0xac, 0x41, 0xad, 0x2e, 0x4d, 0x11,
0xd4, 0xb1, 0x79, 0x2f, 0x8b, 0xfe, 0x29, 0xfa, 0xd3, 0xeb, 0xa8, 0x10, 0xeb, 0xab, 0xe2, 0xe4,
0x35, 0xcf, 0x1e, 0x6d, 0xea, 0x5d, 0xf5, 0xdc, 0xee, 0xcd, 0x6b, 0xdf, 0x3f, 0xd2, 0x9b, 0x2b,
0xb3, 0x9a, 0xf0, 0x52, 0x7e, 0xf2, 0x71, 0x33, 0xb4, 0x3a, 0xd4, 0x16, 0xdc, 0x9e, 0x7f, 0xdc,
0x0f, 0x26, 0xaa, 0x5b, 0x13, 0xba, 0xd3, 0x87, 0x5d, 0x86, 0x40, 0x97, 0x6f, 0xf7, 0xe6, 0x89,
0x65, 0x44, 0xf9, 0x56, 0xbd, 0xdd, 0xc3, 0x98, 0xdf, 0xd7, 0x4f, 0xfa, 0x2b, 0x68, 0xf7, 0x57,
0x3a, 0x6b, 0x75, 0xdd, 0xcb, 0x7e, 0xde, 0xea, 0x32, 0x35, 0xf1, 0xba, 0x79, 0xd8, 0x17, 0x0f,
0xe5, 0x2f, 0xee, 0x12, 0xda, 0x95, 0xbf, 0xa0, 0xcb, 0xe8, 0x07, 0xd7, 0x53, 0xd2, 0x65, 0xf9,
0x87, 0xb5, 0xe8, 0x26, 0x59, 0x16, 0x1d, 0x07, 0x1f, 0xf5, 0xb5, 0x0c, 0xe2, 0xe1, 0xe3, 0x6b,
0xeb, 0xe9, 0x42, 0xfd, 0xf3, 0x5a, 0x74, 0x2b, 0x50, 0x28, 0x15, 0x20, 0xd7, 0xb0, 0xee, 0x07,
0xca, 0x27, 0xd7, 0x57, 0xa4, 0x96, 0x7b, 0x17, 0x9f, 0xb4, 0x3f, 0xe1, 0x16, 0xb0, 0x3d, 0xa1,
0x3f, 0xe1, 0xd6, 0xad, 0x05, 0xcf, 0x98, 0xe2, 0xb3, 0x66, 0xcf, 0x87, 0x9e, 0x31, 0xc9, 0x0b,
0x9a, 0xc1, 0x8f, 0xb6, 0x60, 0x1c, 0xe6, 0xe4, 0xc5, 0x9b, 0x22, 0xce, 0x13, 0xda, 0x89, 0x92,
0x77, 0x3b, 0x31, 0x1c, 0x3c, 0x9b, 0xab, 0xa5, 0x27, 0xbc, 0xd9, 0xc7, 0x3d, 0xa4, 0xf4, 0x0d,
0x12, 0x3c, 0x9b, 0x6b, 0xa1, 0x84, 0x37, 0x9d, 0x35, 0x86, 0xbc, 0x81, 0x64, 0xf1, 0x51, 0x1f,
0x14, 0xec, 0x10, 0x8c, 0x37, 0x73, 0xe4, 0xff, 0x38, 0x64, 0xa5, 0x75, 0xec, 0xbf, 0xd5, 0x93,
0x26, 0xdc, 0x4e, 0x98, 0xf8, 0x94, 0xc5, 0x09, 0x2b, 0x83, 0x6e, 0x0d, 0xd5, 0xcb, 0xad, 0x4b,
0x63, 0x6e, 0x77, 0x78, 0xb6, 0x5c, 0xe4, 0xba, 0x33, 0x49, 0xb7, 0x2e, 0xd5, 0xed, 0x16, 0xd0,
0xf0, 0x54, 0xd2, 0xba, 0x95, 0xe9, 0xe5, 0xa3, 0xb0, 0x19, 0x2f, 0xab, 0xdc, 0xec, 0xc5, 0xd2,
0xf5, 0xd4, 0x61, 0xd4, 0x51, 0x4f, 0x10, 0x49, 0x5b, 0x3d, 0x69, 0x78, 0x3c, 0xe8, 0xb8, 0x35,
0xf1, 0xb4, 0xdd, 0x61, 0xab, 0x15, 0x52, 0x4f, 0xfa, 0x2b, 0xc0, 0xc3, 0x58, 0x1d, 0x55, 0x07,
0x69, 0x25, 0xf6, 0xd2, 0x2c, 0x1b, 0x6c, 0x06, 0xc2, 0xa4, 0x81, 0x82, 0x87, 0xb1, 0x08, 0x4c,
0x44, 0x72, 0x73, 0x78, 0x99, 0x0f, 0xba, 0xec, 0x48, 0xaa, 0x57, 0x24, 0xbb, 0x34, 0x38, 0x50,
0x73, 0x9a, 0xda, 0xd4, 0x76, 0x18, 0x6e, 0xb8, 0x56, 0x85, 0xb7, 0x7b, 0xf3, 0xe0, 0x69, 0xbf,
0xa4, 0xe4, 0xca, 0x72, 0x8f, 0x32, 0xe1, 0xad, 0x24, 0xf7, 0x3b, 0x28, 0x70, 0x28, 0xa9, 0x86,
0xd1, 0xab, 0x34, 0x99, 0x33, 0x81, 0x3e, 0xa8, 0x72, 0x81, 0xe0, 0x83, 0x2a, 0x00, 0x82, 0xae,
0x53, 0x7f, 0x37, 0xa7, 0xb1, 0xe3, 0x04, 0xeb, 0x3a, 0xad, 0xec, 0x50, 0xa1, 0xae, 0x43, 0x69,
0x30, 0x1b, 0x18, 0xb7, 0xfa, 0x33, 0x17, 0x8f, 0x42, 0x66, 0xc0, 0xb7, 0x2e, 0x36, 0x7b, 0xb1,
0x60, 0x45, 0xb1, 0x0e, 0xd3, 0x45, 0x2a, 0xb0, 0x15, 0xc5, 0xb1, 0x51, 0x23, 0xa1, 0x15, 0xa5,
0x8d, 0x52, 0xd5, 0xab, 0x73, 0x84, 0x71, 0x12, 0xae, 0x9e, 0x62, 0xfa, 0x55, 0xcf, 0xb0, 0xad,
0xe7, 0xaa, 0xb9, 0x09, 0x19, 0x71, 0xa1, 0x37, 0xcb, 0x48, 0x6c, 0x3b, 0xbf, 0xec, 0x60, 0xc1,
0xd0, 0xac, 0x43, 0x29, 0xc0, 0xe7, 0x05, 0xcd, 0x6f, 0x41, 0x4c, 0x98, 0x18, 0x15, 0x05, 0x8b,
0xcb, 0x38, 0x9f, 0xa1, 0x9b, 0x53, 0xf3, 0xdb, 0x0e, 0x1e, 0x19, 0xda, 0x9c, 0x92, 0x1a, 0xe0,
0xa9, 0xbd, 0xff, 0x7e, 0x31, 0x32, 0x14, 0xcc, 0x8b, 0xbc, 0xfe, 0xeb, 0xc5, 0x0f, 0x7b, 0x90,
0xf0, 0xa9, 0x7d, 0x03, 0x98, 0x73, 0x77, 0xe5, 0xf4, 0xfd, 0x80, 0x29, 0x1f, 0x0d, 0x6d, 0x84,
0x69, 0x15, 0x10, 0xd4, 0xce, 0xd9, 0xe2, 0x67, 0x6c, 0x85, 0x05, 0xb5, 0x7b, 0x48, 0xf8, 0x19,
0x5b, 0x85, 0x82, 0xba, 0x8d, 0x82, 0x3c, 0xd3, 0xdd, 0x07, 0xad, 0x07, 0xf4, 0xdd, 0xad, 0xcf,
0x46, 0x27, 0x07, 0x46, 0xce, 0x6e, 0x7a, 0xe5, 0x3d, 0xa6, 0x40, 0x0a, 0xba, 0x9b, 0x5e, 0xe1,
0x4f, 0x29, 0x36, 0x7b, 0xb1, 0xf0, 0x46, 0x40, 0x2c, 0xd8, 0x9b, 0xe6, 0x51, 0x3d, 0x52, 0x5c,
0x29, 0x6f, 0x3d, 0xab, 0x7f, 0xd0, 0x0d, 0xda, 0xfb, 0xb7, 0xc7, 0x25, 0x9f, 0xb1, 0xaa, 0xd2,
0x5f, 0x80, 0xf5, 0x2f, 0x38, 0x69, 0xd9, 0x10, 0x7c, 0xff, 0xf5, 0x5e, 0x18, 0x72, 0x3e, 0xdb,
0xa8, 0x44, 0xf6, 0x6b, 0x52, 0xeb, 0xa8, 0x66, 0xfb, 0x43, 0x52, 0x1b, 0x9d, 0x9c, 0x1d, 0x5e,
0x5a, 0xea, 0x7e, 0x3e, 0xea, 0x01, 0xaa, 0x8e, 0x7d, 0x39, 0xea, 0x61, 0x0f, 0x52, 0xbb, 0xfa,
0x34, 0x7a, 0xeb, 0x80, 0xcf, 0x27, 0x2c, 0x4f, 0x06, 0xdf, 0xf3, 0x6f, 0xf0, 0xf2, 0xf9, 0xb0,
0xfe, 0xb3, 0x31, 0x7a, 0x83, 0x12, 0xdb, 0x3b, 0x88, 0xbb, 0xec, 0x6c, 0x39, 0x9f, 0x88, 0x58,
0x80, 0x3b, 0x88, 0xf2, 0xef, 0xc3, 0x5a, 0x40, 0xdc, 0x41, 0xf4, 0x00, 0x60, 0x6f, 0x5a, 0x32,
0x86, 0xda, 0xab, 0x05, 0x41, 0x7b, 0x1a, 0xb0, 0x59, 0x84, 0xb1, 0x57, 0x27, 0xea, 0xf0, 0xce,
0xa0, 0xd5, 0x91, 0x52, 0x22, 0x8b, 0x68, 0x53, 0x36, 0xb8, 0x55, 0xf5, 0xe5, 0xd7, 0x7c, 0x96,
0x8b, 0x45, 0x5c, 0xae, 0x40, 0x70, 0xeb, 0x5a, 0x3a, 0x00, 0x11, 0xdc, 0x28, 0x68, 0x47, 0x6d,
0xd3, 0xcc, 0xb3, 0xcb, 0x7d, 0x5e, 0xf2, 0xa5, 0x48, 0x73, 0x06, 0xbf, 0xe8, 0x62, 0x1a, 0xd4,
0x65, 0x88, 0x51, 0x4b, 0xb1, 0x36, 0xcb, 0x95, 0x84, 0xba, 0xce, 0x28, 0x3f, 0xb5, 0x5f, 0x09,
0x5e, 0xc2, 0xc7, 0x99, 0xca, 0x0a, 0x84, 0x88, 0x2c, 0x97, 0x84, 0x41, 0xdf, 0x1f, 0xa7, 0xf9,
0x1c, 0xed, 0xfb, 0x63, 0xf7, 0xab, 0xca, 0xb7, 0x68, 0xc0, 0x0e, 0x28, 0xd5, 0x68, 0x6a, 0x00,
0xe8, 0x57, 0x99, 0xd1, 0x46, 0x77, 0x09, 0x62, 0x40, 0xe1, 0x24, 0x70, 0xf5, 0x79, 0xc1, 0x72,
0x96, 0x34, 0x97, 0xf6, 0x30, 0x57, 0x1e, 0x11, 0x74, 0x05, 0x49, 0x3b, 0x17, 0x49, 0xf9, 0xc9,
0x32, 0x3f, 0x2e, 0xf9, 0x79, 0x9a, 0xb1, 0x12, 0xcc, 0x45, 0x4a, 0xdd, 0x91, 0x13, 0x73, 0x11,
0xc6, 0xd9, 0xdb, 0x1f, 0x52, 0xea, 0xfd, 0x5e, 0xc4, 0xb4, 0x8c, 0x67, 0xf0, 0xf6, 0x87, 0xb2,
0xd1, 0xc6, 0x88, 0x93, 0xc1, 0x00, 0xee, 0x24, 0x3a, 0xca, 0x75, 0xbe, 0x92, 0xf1, 0xa1, 0x5f,
0xa5, 0x95, 0xdf, 0x1a, 0xae, 0x40, 0xa2, 0xa3, 0xcd, 0x61, 0x24, 0x91, 0xe8, 0x84, 0x35, 0xec,
0x52, 0x22, 0xb9, 0x23, 0x7d, 0xab, 0x09, 0x2c, 0x25, 0xca, 0x46, 0x23, 0x24, 0x96, 0x92, 0x16,
0x04, 0x26, 0xa4, 0x66, 0x18, 0xcc, 0xd1, 0x09, 0xc9, 0x48, 0x83, 0x13, 0x92, 0x4b, 0xd9, 0x89,
0x62, 0x9c, 0xa7, 0x22, 0x8d, 0xb3, 0x09, 0x13, 0xc7, 0x71, 0x19, 0x2f, 0x98, 0x60, 0x25, 0x9c,
0x28, 0x34, 0x32, 0xf4, 0x18, 0x62, 0xa2, 0xa0, 0x58, 0xed, 0xf0, 0x77, 0xa2, 0x77, 0xea, 0x75,
0x9f, 0xe5, 0xfa, 0x97, 0xae, 0x5e, 0xc8, 0xdf, 0x29, 0x1c, 0xbc, 0x6b, 0x6c, 0x4c, 0x44, 0xc9,
0xe2, 0x45, 0x63, 0xfb, 0x6d, 0xf3, 0x77, 0x09, 0x3e, 0x59, 0xab, 0xe3, 0xf9, 0x88, 0x8b, 0xf4,
0xbc, 0xde, 0x66, 0xeb, 0x17, 0x98, 0x40, 0x3c, 0xbb, 0xe2, 0x61, 0xe0, 0x53, 0x2c, 0x18, 0x67,
0xe7, 0x69, 0x57, 0x7a, 0xc2, 0x8a, 0x0c, 0xce, 0xd3, 0x9e, 0xb6, 0x04, 0x88, 0x79, 0x1a, 0x05,
0xed, 0xe0, 0x74, 0xc5, 0x53, 0x16, 0xae, 0xcc, 0x94, 0xf5, 0xab, 0xcc, 0xd4, 0x7b, 0x27, 0x24,
0x8b, 0xde, 0x39, 0x64, 0x8b, 0x33, 0x56, 0x56, 0x17, 0x69, 0x41, 0x7d, 0x0f, 0xd9, 0x12, 0x9d,
0xdf, 0x43, 0x26, 0x50, 0xbb, 0x12, 0x58, 0x60, 0x5c, 0x1d, 0xc5, 0x0b, 0x26, 0x3f, 0x2c, 0x03,
0x56, 0x02, 0xc7, 0x88, 0x03, 0x11, 0x2b, 0x01, 0x09, 0x3b, 0xaf, 0x97, 0x59, 0xe6, 0x84, 0xcd,
0xeb, 0x08, 0x2b, 0x8f, 0xe3, 0xd5, 0x82, 0xe5, 0x42, 0x9b, 0x04, 0x67, 0xf2, 0x8e, 0x49, 0x9c,
0x27, 0xce, 0xe4, 0xfb, 0xe8, 0x39, 0x53, 0x93, 0xd7, 0xf0, 0xc7, 0xbc, 0x14, 0xea, 0x27, 0xec,
0x4e, 0xcb, 0x0c, 0x4c, 0x4d, 0x7e, 0xa3, 0x7a, 0x24, 0x31, 0x35, 0x85, 0x35, 0x9c, 0xdf, 0x2c,
0xf1, 0xca, 0xf0, 0x92, 0x95, 0x26, 0x4e, 0x5e, 0x2c, 0xe2, 0x34, 0xd3, 0xd1, 0xf0, 0xfd, 0x80,
0x6d, 0x42, 0x87, 0xf8, 0xcd, 0x92, 0xbe, 0xba, 0xce, 0xaf, 0xbc, 0x84, 0x4b, 0x08, 0x1e, 0x11,
0x74, 0xd8, 0x27, 0x1e, 0x11, 0x74, 0x6b, 0xd9, 0x9d, 0xbb, 0x65, 0x25, 0xb7, 0x92, 0xc4, 0x0e,
0x4f, 0xe0, 0x79, 0xa1, 0x63, 0x13, 0x80, 0xc4, 0xce, 0x3d, 0xa8, 0x60, 0x53, 0x03, 0x8b, 0xed,
0xa5, 0x79, 0x9c, 0xa5, 0x3f, 0x86, 0x69, 0xbd, 0x63, 0xa7, 0x21, 0x88, 0xd4, 0x00, 0x27, 0x31,
0x57, 0xfb, 0x4c, 0x4c, 0xd3, 0x7a, 0xea, 0x7f, 0x10, 0x68, 0x37, 0x49, 0x74, 0xbb, 0x72, 0x48,
0xe7, 0xdb, 0xc7, 0xb0, 0x59, 0x47, 0x45, 0x31, 0xa9, 0x57, 0xd5, 0x13, 0x36, 0x63, 0x69, 0x21,
0x06, 0x1f, 0x86, 0xdb, 0x0a, 0xe0, 0xc4, 0x45, 0x8b, 0x1e, 0x6a, 0xce, 0xe3, 0xfb, 0x7a, 0x2e,
0x99, 0xa8, 0x5f, 0xd9, 0x3d, 0xad, 0x58, 0xa9, 0x13, 0x8d, 0x7d, 0x26, 0xc0, 0xe8, 0x74, 0xb8,
0xa1, 0x03, 0xd6, 0x15, 0x25, 0x46, 0x67, 0x58, 0xc3, 0x1e, 0xf6, 0x39, 0x9c, 0xfe, 0x76, 0x80,
0xbc, 0xee, 0xf8, 0x98, 0x34, 0xe6, 0x50, 0xc4, 0x61, 0x1f, 0x4d, 0xdb, 0x6c, 0xad, 0xed, 0x76,
0x94, 0xaf, 0xc6, 0xf0, 0xca, 0x04, 0x62, 0x49, 0x62, 0x44, 0xb6, 0x16, 0xc0, 0x9d, 0xc3, 0xf0,
0x92, 0xc7, 0xc9, 0x2c, 0xae, 0xc4, 0x71, 0xbc, 0xca, 0x78, 0x9c, 0xc8, 0x75, 0x1d, 0x1e, 0x86,
0x37, 0xcc, 0xd0, 0x85, 0xa8, 0xc3, 0x70, 0x0a, 0x76, 0xb3, 0x33, 0xf9, 0xe3, 0xc1, 0xfa, 0x2a,
0x29, 0xcc, 0xce, 0x64, 0x79, 0xe1, 0x35, 0xd2, 0x7b, 0x61, 0xc8, 0xbe, 0x02, 0xa7, 0x44, 0x32,
0x0d, 0xb9, 0x85, 0xe9, 0x78, 0x09, 0xc8, 0xed, 0x00, 0x61, 0x3f, 0xcb, 0xa2, 0xfe, 0xde, 0xfc,
0x54, 0x99, 0xd0, 0x5f, 0x88, 0x7f, 0x8c, 0xe9, 0xba, 0x90, 0x77, 0x43, 0x6d, 0xab, 0x27, 0x6d,
0xd3, 0xcc, 0x9d, 0x8b, 0x58, 0x8c, 0x92, 0xe4, 0x90, 0x55, 0xc8, 0xfb, 0xec, 0xb5, 0x70, 0x68,
0xa5, 0x44, 0x9a, 0xd9, 0xa6, 0x6c, 0xa0, 0xd7, 0xb2, 0x17, 0x49, 0x2a, 0xb4, 0xac, 0xb9, 0xa0,
0xfd, 0xb8, 0x6d, 0xa0, 0x4d, 0x11, 0xb5, 0xa2, 0x69, 0x3b, 0x97, 0xd7, 0xcc, 0x94, 0xcf, 0xe7,
0x19, 0xd3, 0xd0, 0x09, 0x8b, 0xd5, 0x07, 0x32, 0xb7, 0xdb, 0xb6, 0x50, 0x90, 0x98, 0xcb, 0x83,
0x0a, 0x36, 0x8d, 0xac, 0x31, 0xf5, 0x48, 0xaa, 0x69, 0xd8, 0x8d, 0xb6, 0x19, 0x0f, 0x20, 0xd2,
0x48, 0x14, 0xb4, 0xaf, 0xdd, 0xd5, 0xe2, 0x7d, 0xd6, 0xb4, 0x04, 0xfc, 0x02, 0x97, 0x54, 0x76,
0xc4, 0xc4, 0x6b, 0x77, 0x08, 0x66, 0xf7, 0x09, 0xc0, 0xc3, 0xf3, 0xd5, 0x38, 0x81, 0xfb, 0x04,
0xa8, 0x2f, 0x19, 0x62, 0x9f, 0x40, 0xb1, 0x7e, 0xd7, 0x99, 0x73, 0xaf, 0x83, 0xb8, 0xb2, 0x95,
0x43, 0xba, 0x0e, 0x05, 0x43, 0x5d, 0x47, 0x29, 0xf8, 0x4d, 0xea, 0x1e, 0xad, 0x21, 0x4d, 0x8a,
0x9d, 0xab, 0xad, 0x77, 0x61, 0x36, 0xf7, 0xaf, 0x85, 0x27, 0x2c, 0x4e, 0x4c, 0xc5, 0x10, 0x5d,
0x57, 0x4e, 0xe4, 0xfe, 0x18, 0xa7, 0x9d, 0xfc, 0x7e, 0x34, 0x50, 0xd5, 0x28, 0x5d, 0x37, 0xb7,
0xb0, 0x22, 0xd6, 0x04, 0x31, 0x51, 0xf9, 0x84, 0x93, 0xb8, 0x79, 0x5d, 0x34, 0xe5, 0xda, 0x81,
0x7e, 0x2d, 0xb4, 0x02, 0x89, 0x9b, 0xdf, 0xec, 0x2d, 0x9a, 0x48, 0xdc, 0xba, 0xb5, 0x9c, 0x8f,
0x11, 0x81, 0x2e, 0xdb, 0x2b, 0xf9, 0x02, 0x96, 0xe9, 0x93, 0x60, 0xf7, 0x20, 0x1a, 0xc4, 0xc7,
0x88, 0xfa, 0x69, 0xda, 0x35, 0xc8, 0x9c, 0x1d, 0xc8, 0xeb, 0x69, 0xf8, 0xaf, 0xa0, 0x28, 0x21,
0xb1, 0x06, 0xb5, 0x20, 0xfb, 0x1e, 0x72, 0x13, 0x47, 0xa3, 0x2c, 0x1b, 0xdc, 0xc6, 0x43, 0xc3,
0xfd, 0xc0, 0xc3, 0x9d, 0x10, 0xe2, 0xfc, 0xf0, 0xeb, 0xf8, 0x55, 0x99, 0x8a, 0x34, 0x9f, 0x4f,
0x39, 0xcf, 0xe0, 0x41, 0xe8, 0x68, 0x3c, 0x74, 0xa5, 0xd4, 0x0f, 0xbf, 0xb6, 0x28, 0xbb, 0x70,
0x8e, 0xc6, 0xa3, 0xa5, 0xe0, 0xe7, 0x69, 0x96, 0x81, 0x78, 0x1c, 0x8d, 0x87, 0x8d, 0x84, 0x88,
0x47, 0x9f, 0x70, 0x7e, 0xae, 0x74, 0x2c, 0x9f, 0x29, 0xe8, 0x73, 0xd5, 0xbb, 0x50, 0xc7, 0x11,
0x52, 0x3f, 0x57, 0x0a, 0x21, 0xe7, 0xe7, 0x57, 0xc7, 0xd8, 0x0f, 0xc4, 0x6c, 0x42, 0x75, 0x04,
0xa2, 0x7e, 0x7e, 0x95, 0x82, 0x9d, 0x37, 0x9d, 0x8f, 0x97, 0xd5, 0x85, 0x7f, 0x10, 0xa1, 0xb6,
0x9c, 0xea, 0x63, 0xb0, 0xcf, 0xc0, 0xcf, 0x14, 0xf9, 0xec, 0xd0, 0x83, 0x89, 0x4b, 0x6f, 0x9d,
0x4a, 0xaa, 0x30, 0xcf, 0x6f, 0xff, 0xf7, 0x97, 0x37, 0xd6, 0x7e, 0xf6, 0xe5, 0x8d, 0xb5, 0xff,
0xfd, 0xf2, 0xc6, 0xda, 0x4f, 0xbf, 0xba, 0xf1, 0x8d, 0x9f, 0x7d, 0x75, 0xe3, 0x1b, 0xff, 0xf3,
0xd5, 0x8d, 0x6f, 0x7c, 0xf1, 0x56, 0xa5, 0x32, 0xbe, 0xb3, 0x9f, 0x2f, 0x4a, 0x2e, 0xf8, 0xb3,
0xff, 0x0b, 0x00, 0x00, 0xff, 0xff, 0xf4, 0x6f, 0x9e, 0xb4, 0xc9, 0x82, 0x00, 0x00,
}
// This is a compile-time assertion to ensure that this generated file
@ -406,6 +411,9 @@ type ClientCommandsHandler interface {
WalletConvert(context.Context, *pb.RpcWalletConvertRequest) *pb.RpcWalletConvertResponse
AccountLocalLinkNewChallenge(context.Context, *pb.RpcAccountLocalLinkNewChallengeRequest) *pb.RpcAccountLocalLinkNewChallengeResponse
AccountLocalLinkSolveChallenge(context.Context, *pb.RpcAccountLocalLinkSolveChallengeRequest) *pb.RpcAccountLocalLinkSolveChallengeResponse
AccountLocalLinkCreateApp(context.Context, *pb.RpcAccountLocalLinkCreateAppRequest) *pb.RpcAccountLocalLinkCreateAppResponse
AccountLocalLinkListApps(context.Context, *pb.RpcAccountLocalLinkListAppsRequest) *pb.RpcAccountLocalLinkListAppsResponse
AccountLocalLinkRevokeApp(context.Context, *pb.RpcAccountLocalLinkRevokeAppRequest) *pb.RpcAccountLocalLinkRevokeAppResponse
WalletCreateSession(context.Context, *pb.RpcWalletCreateSessionRequest) *pb.RpcWalletCreateSessionResponse
WalletCloseSession(context.Context, *pb.RpcWalletCloseSessionRequest) *pb.RpcWalletCloseSessionResponse
// Workspace
@ -440,6 +448,7 @@ type ClientCommandsHandler interface {
// ***
SpaceDelete(context.Context, *pb.RpcSpaceDeleteRequest) *pb.RpcSpaceDeleteResponse
SpaceInviteGenerate(context.Context, *pb.RpcSpaceInviteGenerateRequest) *pb.RpcSpaceInviteGenerateResponse
SpaceInviteChange(context.Context, *pb.RpcSpaceInviteChangeRequest) *pb.RpcSpaceInviteChangeResponse
SpaceInviteGetCurrent(context.Context, *pb.RpcSpaceInviteGetCurrentRequest) *pb.RpcSpaceInviteGetCurrentResponse
SpaceInviteGetGuest(context.Context, *pb.RpcSpaceInviteGetGuestRequest) *pb.RpcSpaceInviteGetGuestResponse
SpaceInviteRevoke(context.Context, *pb.RpcSpaceInviteRevokeRequest) *pb.RpcSpaceInviteRevokeResponse
@ -748,6 +757,7 @@ type ClientCommandsHandler interface {
ChatSubscribeToMessagePreviews(context.Context, *pb.RpcChatSubscribeToMessagePreviewsRequest) *pb.RpcChatSubscribeToMessagePreviewsResponse
ChatUnsubscribeFromMessagePreviews(context.Context, *pb.RpcChatUnsubscribeFromMessagePreviewsRequest) *pb.RpcChatUnsubscribeFromMessagePreviewsResponse
ObjectChatAdd(context.Context, *pb.RpcObjectChatAddRequest) *pb.RpcObjectChatAddResponse
ChatReadAll(context.Context, *pb.RpcChatReadAllRequest) *pb.RpcChatReadAllResponse
// mock AI RPCs for compatibility between branches. Not implemented in main
AIWritingTools(context.Context, *pb.RpcAIWritingToolsRequest) *pb.RpcAIWritingToolsResponse
AIAutofill(context.Context, *pb.RpcAIAutofillRequest) *pb.RpcAIAutofillResponse
@ -921,6 +931,66 @@ func AccountLocalLinkSolveChallenge(b []byte) (resp []byte) {
return resp
}
func AccountLocalLinkCreateApp(b []byte) (resp []byte) {
defer func() {
if PanicHandler != nil {
if r := recover(); r != nil {
resp, _ = (&pb.RpcAccountLocalLinkCreateAppResponse{Error: &pb.RpcAccountLocalLinkCreateAppResponseError{Code: pb.RpcAccountLocalLinkCreateAppResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal()
PanicHandler(r)
}
}
}()
in := new(pb.RpcAccountLocalLinkCreateAppRequest)
if err := in.Unmarshal(b); err != nil {
resp, _ = (&pb.RpcAccountLocalLinkCreateAppResponse{Error: &pb.RpcAccountLocalLinkCreateAppResponseError{Code: pb.RpcAccountLocalLinkCreateAppResponseError_BAD_INPUT, Description: err.Error()}}).Marshal()
return resp
}
resp, _ = clientCommandsHandler.AccountLocalLinkCreateApp(context.Background(), in).Marshal()
return resp
}
func AccountLocalLinkListApps(b []byte) (resp []byte) {
defer func() {
if PanicHandler != nil {
if r := recover(); r != nil {
resp, _ = (&pb.RpcAccountLocalLinkListAppsResponse{Error: &pb.RpcAccountLocalLinkListAppsResponseError{Code: pb.RpcAccountLocalLinkListAppsResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal()
PanicHandler(r)
}
}
}()
in := new(pb.RpcAccountLocalLinkListAppsRequest)
if err := in.Unmarshal(b); err != nil {
resp, _ = (&pb.RpcAccountLocalLinkListAppsResponse{Error: &pb.RpcAccountLocalLinkListAppsResponseError{Code: pb.RpcAccountLocalLinkListAppsResponseError_BAD_INPUT, Description: err.Error()}}).Marshal()
return resp
}
resp, _ = clientCommandsHandler.AccountLocalLinkListApps(context.Background(), in).Marshal()
return resp
}
func AccountLocalLinkRevokeApp(b []byte) (resp []byte) {
defer func() {
if PanicHandler != nil {
if r := recover(); r != nil {
resp, _ = (&pb.RpcAccountLocalLinkRevokeAppResponse{Error: &pb.RpcAccountLocalLinkRevokeAppResponseError{Code: pb.RpcAccountLocalLinkRevokeAppResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal()
PanicHandler(r)
}
}
}()
in := new(pb.RpcAccountLocalLinkRevokeAppRequest)
if err := in.Unmarshal(b); err != nil {
resp, _ = (&pb.RpcAccountLocalLinkRevokeAppResponse{Error: &pb.RpcAccountLocalLinkRevokeAppResponseError{Code: pb.RpcAccountLocalLinkRevokeAppResponseError_BAD_INPUT, Description: err.Error()}}).Marshal()
return resp
}
resp, _ = clientCommandsHandler.AccountLocalLinkRevokeApp(context.Background(), in).Marshal()
return resp
}
func WalletCreateSession(b []byte) (resp []byte) {
defer func() {
if PanicHandler != nil {
@ -1481,6 +1551,26 @@ func SpaceInviteGenerate(b []byte) (resp []byte) {
return resp
}
func SpaceInviteChange(b []byte) (resp []byte) {
defer func() {
if PanicHandler != nil {
if r := recover(); r != nil {
resp, _ = (&pb.RpcSpaceInviteChangeResponse{Error: &pb.RpcSpaceInviteChangeResponseError{Code: pb.RpcSpaceInviteChangeResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal()
PanicHandler(r)
}
}
}()
in := new(pb.RpcSpaceInviteChangeRequest)
if err := in.Unmarshal(b); err != nil {
resp, _ = (&pb.RpcSpaceInviteChangeResponse{Error: &pb.RpcSpaceInviteChangeResponseError{Code: pb.RpcSpaceInviteChangeResponseError_BAD_INPUT, Description: err.Error()}}).Marshal()
return resp
}
resp, _ = clientCommandsHandler.SpaceInviteChange(context.Background(), in).Marshal()
return resp
}
func SpaceInviteGetCurrent(b []byte) (resp []byte) {
defer func() {
if PanicHandler != nil {
@ -6521,6 +6611,26 @@ func ObjectChatAdd(b []byte) (resp []byte) {
return resp
}
func ChatReadAll(b []byte) (resp []byte) {
defer func() {
if PanicHandler != nil {
if r := recover(); r != nil {
resp, _ = (&pb.RpcChatReadAllResponse{Error: &pb.RpcChatReadAllResponseError{Code: pb.RpcChatReadAllResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal()
PanicHandler(r)
}
}
}()
in := new(pb.RpcChatReadAllRequest)
if err := in.Unmarshal(b); err != nil {
resp, _ = (&pb.RpcChatReadAllResponse{Error: &pb.RpcChatReadAllResponseError{Code: pb.RpcChatReadAllResponseError_BAD_INPUT, Description: err.Error()}}).Marshal()
return resp
}
resp, _ = clientCommandsHandler.ChatReadAll(context.Background(), in).Marshal()
return resp
}
func AIWritingTools(b []byte) (resp []byte) {
defer func() {
if PanicHandler != nil {
@ -6643,6 +6753,12 @@ func CommandAsync(cmd string, data []byte, callback func(data []byte)) {
cd = AccountLocalLinkNewChallenge(data)
case "AccountLocalLinkSolveChallenge":
cd = AccountLocalLinkSolveChallenge(data)
case "AccountLocalLinkCreateApp":
cd = AccountLocalLinkCreateApp(data)
case "AccountLocalLinkListApps":
cd = AccountLocalLinkListApps(data)
case "AccountLocalLinkRevokeApp":
cd = AccountLocalLinkRevokeApp(data)
case "WalletCreateSession":
cd = WalletCreateSession(data)
case "WalletCloseSession":
@ -6699,6 +6815,8 @@ func CommandAsync(cmd string, data []byte, callback func(data []byte)) {
cd = SpaceDelete(data)
case "SpaceInviteGenerate":
cd = SpaceInviteGenerate(data)
case "SpaceInviteChange":
cd = SpaceInviteChange(data)
case "SpaceInviteGetCurrent":
cd = SpaceInviteGetCurrent(data)
case "SpaceInviteGetGuest":
@ -7203,6 +7321,8 @@ func CommandAsync(cmd string, data []byte, callback func(data []byte)) {
cd = ChatUnsubscribeFromMessagePreviews(data)
case "ObjectChatAdd":
cd = ObjectChatAdd(data)
case "ChatReadAll":
cd = ChatReadAll(data)
case "AIWritingTools":
cd = AIWritingTools(data)
case "AIAutofill":
@ -7347,6 +7467,48 @@ func (h *ClientCommandsHandlerProxy) AccountLocalLinkSolveChallenge(ctx context.
call, _ := actualCall(ctx, req)
return call.(*pb.RpcAccountLocalLinkSolveChallengeResponse)
}
func (h *ClientCommandsHandlerProxy) AccountLocalLinkCreateApp(ctx context.Context, req *pb.RpcAccountLocalLinkCreateAppRequest) *pb.RpcAccountLocalLinkCreateAppResponse {
actualCall := func(ctx context.Context, req any) (any, error) {
return h.client.AccountLocalLinkCreateApp(ctx, req.(*pb.RpcAccountLocalLinkCreateAppRequest)), nil
}
for _, interceptor := range h.interceptors {
toCall := actualCall
currentInterceptor := interceptor
actualCall = func(ctx context.Context, req any) (any, error) {
return currentInterceptor(ctx, req, "AccountLocalLinkCreateApp", toCall)
}
}
call, _ := actualCall(ctx, req)
return call.(*pb.RpcAccountLocalLinkCreateAppResponse)
}
func (h *ClientCommandsHandlerProxy) AccountLocalLinkListApps(ctx context.Context, req *pb.RpcAccountLocalLinkListAppsRequest) *pb.RpcAccountLocalLinkListAppsResponse {
actualCall := func(ctx context.Context, req any) (any, error) {
return h.client.AccountLocalLinkListApps(ctx, req.(*pb.RpcAccountLocalLinkListAppsRequest)), nil
}
for _, interceptor := range h.interceptors {
toCall := actualCall
currentInterceptor := interceptor
actualCall = func(ctx context.Context, req any) (any, error) {
return currentInterceptor(ctx, req, "AccountLocalLinkListApps", toCall)
}
}
call, _ := actualCall(ctx, req)
return call.(*pb.RpcAccountLocalLinkListAppsResponse)
}
func (h *ClientCommandsHandlerProxy) AccountLocalLinkRevokeApp(ctx context.Context, req *pb.RpcAccountLocalLinkRevokeAppRequest) *pb.RpcAccountLocalLinkRevokeAppResponse {
actualCall := func(ctx context.Context, req any) (any, error) {
return h.client.AccountLocalLinkRevokeApp(ctx, req.(*pb.RpcAccountLocalLinkRevokeAppRequest)), nil
}
for _, interceptor := range h.interceptors {
toCall := actualCall
currentInterceptor := interceptor
actualCall = func(ctx context.Context, req any) (any, error) {
return currentInterceptor(ctx, req, "AccountLocalLinkRevokeApp", toCall)
}
}
call, _ := actualCall(ctx, req)
return call.(*pb.RpcAccountLocalLinkRevokeAppResponse)
}
func (h *ClientCommandsHandlerProxy) WalletCreateSession(ctx context.Context, req *pb.RpcWalletCreateSessionRequest) *pb.RpcWalletCreateSessionResponse {
actualCall := func(ctx context.Context, req any) (any, error) {
return h.client.WalletCreateSession(ctx, req.(*pb.RpcWalletCreateSessionRequest)), nil
@ -7739,6 +7901,20 @@ func (h *ClientCommandsHandlerProxy) SpaceInviteGenerate(ctx context.Context, re
call, _ := actualCall(ctx, req)
return call.(*pb.RpcSpaceInviteGenerateResponse)
}
func (h *ClientCommandsHandlerProxy) SpaceInviteChange(ctx context.Context, req *pb.RpcSpaceInviteChangeRequest) *pb.RpcSpaceInviteChangeResponse {
actualCall := func(ctx context.Context, req any) (any, error) {
return h.client.SpaceInviteChange(ctx, req.(*pb.RpcSpaceInviteChangeRequest)), nil
}
for _, interceptor := range h.interceptors {
toCall := actualCall
currentInterceptor := interceptor
actualCall = func(ctx context.Context, req any) (any, error) {
return currentInterceptor(ctx, req, "SpaceInviteChange", toCall)
}
}
call, _ := actualCall(ctx, req)
return call.(*pb.RpcSpaceInviteChangeResponse)
}
func (h *ClientCommandsHandlerProxy) SpaceInviteGetCurrent(ctx context.Context, req *pb.RpcSpaceInviteGetCurrentRequest) *pb.RpcSpaceInviteGetCurrentResponse {
actualCall := func(ctx context.Context, req any) (any, error) {
return h.client.SpaceInviteGetCurrent(ctx, req.(*pb.RpcSpaceInviteGetCurrentRequest)), nil
@ -11267,6 +11443,20 @@ func (h *ClientCommandsHandlerProxy) ObjectChatAdd(ctx context.Context, req *pb.
call, _ := actualCall(ctx, req)
return call.(*pb.RpcObjectChatAddResponse)
}
func (h *ClientCommandsHandlerProxy) ChatReadAll(ctx context.Context, req *pb.RpcChatReadAllRequest) *pb.RpcChatReadAllResponse {
actualCall := func(ctx context.Context, req any) (any, error) {
return h.client.ChatReadAll(ctx, req.(*pb.RpcChatReadAllRequest)), nil
}
for _, interceptor := range h.interceptors {
toCall := actualCall
currentInterceptor := interceptor
actualCall = func(ctx context.Context, req any) (any, error) {
return currentInterceptor(ctx, req, "ChatReadAll", toCall)
}
}
call, _ := actualCall(ctx, req)
return call.(*pb.RpcChatReadAllResponse)
}
func (h *ClientCommandsHandlerProxy) AIWritingTools(ctx context.Context, req *pb.RpcAIWritingToolsRequest) *pb.RpcAIWritingToolsResponse {
actualCall := func(ctx context.Context, req any) (any, error) {
return h.client.AIWritingTools(ctx, req.(*pb.RpcAIWritingToolsRequest)), nil

View file

@ -38,6 +38,7 @@ import (
"github.com/anyproto/anytype-heart/pb/service"
"github.com/anyproto/anytype-heart/pkg/lib/logging"
"github.com/anyproto/anytype-heart/util/conc"
"github.com/anyproto/anytype-heart/util/grpcprocess"
"github.com/anyproto/anytype-heart/util/vcs"
)
@ -180,6 +181,9 @@ func main() {
}
unaryInterceptors = appendInterceptor(unaryInterceptors, mw)
unaryInterceptors = append(unaryInterceptors, grpcprocess.ProcessInfoInterceptor(
"/anytype.ClientCommands/AccountLocalLinkNewChallenge",
))
server := grpc.NewServer(grpc.MaxRecvMsgSize(20*1024*1024),
grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(unaryInterceptors...)),

View file

@ -4,13 +4,14 @@ import (
"context"
"github.com/anyproto/any-sync/net"
"google.golang.org/grpc/peer"
"github.com/anyproto/anytype-heart/core/anytype/config"
"github.com/anyproto/anytype-heart/core/application"
"github.com/anyproto/anytype-heart/core/session"
walletComp "github.com/anyproto/anytype-heart/core/wallet"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/space/spacecore/storage/migrator"
"github.com/anyproto/anytype-heart/util/grpcprocess"
)
func (mw *Middleware) AccountCreate(cctx context.Context, req *pb.RpcAccountCreateRequest) *pb.RpcAccountCreateResponse {
@ -248,7 +249,7 @@ func (mw *Middleware) AccountChangeJsonApiAddr(ctx context.Context, req *pb.RpcA
func (mw *Middleware) AccountLocalLinkNewChallenge(ctx context.Context, request *pb.RpcAccountLocalLinkNewChallengeRequest) *pb.RpcAccountLocalLinkNewChallengeResponse {
info := getClientInfo(ctx)
info.Name = request.AppName
challengeId, err := mw.applicationService.LinkLocalStartNewChallenge(request.Scope, &info)
code := mapErrorCode(err,
errToCode(session.ErrTooManyChallengeRequests, pb.RpcAccountLocalLinkNewChallengeResponseError_TOO_MANY_REQUESTS),
@ -282,16 +283,56 @@ func (mw *Middleware) AccountLocalLinkSolveChallenge(_ context.Context, req *pb.
}
}
func (mw *Middleware) AccountLocalLinkCreateApp(_ context.Context, req *pb.RpcAccountLocalLinkCreateAppRequest) *pb.RpcAccountLocalLinkCreateAppResponse {
appKey, err := mw.applicationService.LinkLocalCreateApp(req)
code := mapErrorCode(err,
errToCode(application.ErrApplicationIsNotRunning, pb.RpcAccountLocalLinkCreateAppResponseError_ACCOUNT_IS_NOT_RUNNING),
)
return &pb.RpcAccountLocalLinkCreateAppResponse{
AppKey: appKey,
Error: &pb.RpcAccountLocalLinkCreateAppResponseError{
Code: code,
Description: getErrorDescription(err),
},
}
}
func (mw *Middleware) AccountLocalLinkListApps(_ context.Context, req *pb.RpcAccountLocalLinkListAppsRequest) *pb.RpcAccountLocalLinkListAppsResponse {
apps, err := mw.applicationService.LinkLocalListApps()
code := mapErrorCode(err,
errToCode(application.ErrApplicationIsNotRunning, pb.RpcAccountLocalLinkListAppsResponseError_ACCOUNT_IS_NOT_RUNNING),
)
return &pb.RpcAccountLocalLinkListAppsResponse{
App: apps,
Error: &pb.RpcAccountLocalLinkListAppsResponseError{
Code: code,
Description: getErrorDescription(err),
},
}
}
func (mw *Middleware) AccountLocalLinkRevokeApp(_ context.Context, req *pb.RpcAccountLocalLinkRevokeAppRequest) *pb.RpcAccountLocalLinkRevokeAppResponse {
err := mw.applicationService.LinkLocalRevokeApp(req)
code := mapErrorCode(err,
errToCode(walletComp.ErrAppLinkNotFound, pb.RpcAccountLocalLinkRevokeAppResponseError_NOT_FOUND),
errToCode(application.ErrApplicationIsNotRunning, pb.RpcAccountLocalLinkRevokeAppResponseError_ACCOUNT_IS_NOT_RUNNING),
)
return &pb.RpcAccountLocalLinkRevokeAppResponse{
Error: &pb.RpcAccountLocalLinkRevokeAppResponseError{
Code: code,
Description: getErrorDescription(err),
},
}
}
func getClientInfo(ctx context.Context) pb.EventAccountLinkChallengeClientInfo {
p, ok := peer.FromContext(ctx)
info, ok := grpcprocess.FromContext(ctx)
if !ok {
return pb.EventAccountLinkChallengeClientInfo{}
}
// todo: get process info
return pb.EventAccountLinkChallengeClientInfo{
ProcessName: p.Addr.String(),
ProcessPath: "",
SignatureVerified: false,
ProcessName: info.Name,
ProcessPath: info.Path,
}
}

View file

@ -9,7 +9,9 @@ import (
"github.com/anyproto/any-sync/app"
"github.com/anyproto/any-sync/commonspace/acl/aclclient"
"github.com/anyproto/any-sync/commonspace/object/acl/aclrecordproto"
"github.com/anyproto/any-sync/commonspace/object/acl/list"
"github.com/anyproto/any-sync/commonspace/object/acl/recordverifier"
"github.com/anyproto/any-sync/coordinator/coordinatorclient"
"github.com/anyproto/any-sync/coordinator/coordinatorproto"
"github.com/anyproto/any-sync/identityrepo/identityrepoproto"
@ -45,7 +47,8 @@ type AccountPermissions struct {
type AclService interface {
app.Component
GenerateInvite(ctx context.Context, spaceId string) (domain.InviteInfo, error)
GenerateInvite(ctx context.Context, spaceId string, inviteType model.InviteType, permissions model.ParticipantPermissions) (domain.InviteInfo, error)
ChangeInvite(ctx context.Context, spaceId string, permissions model.ParticipantPermissions) error
RevokeInvite(ctx context.Context, spaceId string) error
GetCurrentInvite(ctx context.Context, spaceId string) (domain.InviteInfo, error)
GetGuestUserInvite(ctx context.Context, spaceId string) (domain.InviteInfo, error)
@ -82,6 +85,7 @@ type aclService struct {
accountService account.Service
coordClient coordinatorclient.CoordinatorClient
identityRepo identityRepoClient
recordVerifier recordverifier.AcceptorVerifier
}
func (a *aclService) Init(ap *app.App) (err error) {
@ -92,6 +96,7 @@ func (a *aclService) Init(ap *app.App) (err error) {
a.inviteService = app.MustComponent[inviteservice.InviteService](ap)
a.coordClient = app.MustComponent[coordinatorclient.CoordinatorClient](ap)
a.identityRepo = app.MustComponent[identityRepoClient](ap)
a.recordVerifier = recordverifier.New()
return nil
}
@ -398,47 +403,76 @@ func (a *aclService) Join(ctx context.Context, spaceId, networkId string, invite
if err != nil {
return convertedOrInternalError("get invite payload", err)
}
if invitePayload.InviteType == model.InvitePayload_JoinAsGuest {
switch invitePayload.InviteType {
case model.InviteType_Guest:
guestKey, err := crypto.UnmarshalEd25519PrivateKeyProto(invitePayload.GuestKey)
if err != nil {
return convertedOrInternalError("unmarshal invite key", err)
}
return a.joinAsGuest(ctx, invitePayload.SpaceId, guestKey)
}
inviteKey, err := crypto.UnmarshalEd25519PrivateKeyProto(invitePayload.AclKey)
if err != nil {
return convertedOrInternalError("unmarshal invite key", err)
}
aclHeadId, err := a.joiningClient.RequestJoin(ctx, spaceId, list.RequestJoinPayload{
InviteKey: inviteKey,
Metadata: a.spaceService.AccountMetadataPayload(),
})
if err != nil {
if errors.Is(err, coordinatorproto.ErrSpaceIsDeleted) {
return space.ErrSpaceDeleted
case model.InviteType_Member:
inviteKey, err := crypto.UnmarshalEd25519PrivateKeyProto(invitePayload.AclKey)
if err != nil {
return convertedOrInternalError("unmarshal invite key", err)
}
if errors.Is(err, list.ErrInsufficientPermissions) {
err = a.joiningClient.CancelRemoveSelf(ctx, spaceId)
if err != nil {
return convertedOrAclRequestError(err)
aclHeadId, err := a.joiningClient.RequestJoin(ctx, spaceId, list.RequestJoinPayload{
InviteKey: inviteKey,
Metadata: a.spaceService.AccountMetadataPayload(),
})
// nolint: nestif
if err != nil {
if errors.Is(err, coordinatorproto.ErrSpaceIsDeleted) {
return space.ErrSpaceDeleted
}
err = a.spaceService.CancelLeave(ctx, spaceId)
if err != nil {
return convertedOrInternalError("cancel leave", err)
if errors.Is(err, list.ErrInsufficientPermissions) {
err = a.joiningClient.CancelRemoveSelf(ctx, spaceId)
if err != nil {
return convertedOrAclRequestError(err)
}
err = a.spaceService.CancelLeave(ctx, spaceId)
if err != nil {
return convertedOrInternalError("cancel leave", err)
}
}
return convertedOrAclRequestError(err)
}
err = a.spaceService.Join(ctx, spaceId, aclHeadId)
if err != nil {
return convertedOrInternalError("join space", err)
}
err = a.spaceService.TechSpace().SpaceViewSetData(ctx, spaceId,
domain.NewDetails().
SetString(bundle.RelationKeyName, invitePayload.SpaceName).
SetString(bundle.RelationKeyIconImage, invitePayload.SpaceIconCid))
if err != nil {
return convertedOrInternalError("set space data", err)
}
case model.InviteType_WithoutApprove:
inviteKey, err := crypto.UnmarshalEd25519PrivateKeyProto(invitePayload.AclKey)
if err != nil {
return convertedOrInternalError("unmarshal invite key", err)
}
aclHeadId, err := a.joiningClient.InviteJoin(ctx, spaceId, list.InviteJoinPayload{
InviteKey: inviteKey,
Metadata: a.spaceService.AccountMetadataPayload(),
})
if err != nil {
if errors.Is(err, coordinatorproto.ErrSpaceIsDeleted) {
return space.ErrSpaceDeleted
}
return convertedOrAclRequestError(err)
}
err = a.spaceService.InviteJoin(ctx, spaceId, aclHeadId)
if err != nil {
return convertedOrInternalError("join space", err)
}
err = a.spaceService.TechSpace().SpaceViewSetData(ctx, spaceId,
domain.NewDetails().
SetString(bundle.RelationKeyName, invitePayload.SpaceName).
SetString(bundle.RelationKeyIconImage, invitePayload.SpaceIconCid))
if err != nil {
return convertedOrInternalError("set space data", err)
}
return convertedOrAclRequestError(err)
}
err = a.spaceService.Join(ctx, spaceId, aclHeadId)
if err != nil {
return convertedOrInternalError("join space", err)
}
err = a.spaceService.TechSpace().SpaceViewSetData(ctx, spaceId,
domain.NewDetails().
SetString(bundle.RelationKeyName, invitePayload.SpaceName).
SetString(bundle.RelationKeyIconImage, invitePayload.SpaceIconCid))
if err != nil {
return convertedOrInternalError("set space data", err)
}
return nil
}
@ -472,12 +506,12 @@ func (a *aclService) ViewInvite(ctx context.Context, inviteCid cid.Cid, inviteFi
if err != nil {
return domain.InviteView{}, convertedOrAclRequestError(err)
}
lst, err := list.BuildAclListWithIdentity(a.accountService.Keys(), store, list.NoOpAcceptorVerifier{})
lst, err := list.BuildAclListWithIdentity(a.accountService.Keys(), store, a.recordVerifier)
if err != nil {
return domain.InviteView{}, convertedOrAclRequestError(err)
}
for _, inv := range lst.AclState().Invites() {
if inviteKey.GetPublic().Equals(inv) {
if inviteKey.GetPublic().Equals(inv.Key) {
return res, nil
}
}
@ -533,26 +567,82 @@ func (a *aclService) GetCurrentInvite(ctx context.Context, spaceId string) (doma
return a.inviteService.GetCurrent(ctx, spaceId)
}
func (a *aclService) GenerateInvite(ctx context.Context, spaceId string) (result domain.InviteInfo, err error) {
func (a *aclService) ChangeInvite(ctx context.Context, spaceId string, permissions model.ParticipantPermissions) (err error) {
if spaceId == a.accountService.PersonalSpaceID() {
err = ErrPersonalSpace
return
}
current, err := a.inviteService.GetCurrent(ctx, spaceId)
if err == nil {
return current, nil
if current.InviteType != domain.InviteTypeAnyone {
return inviteservice.ErrInviteNotExists
}
}
acceptSpace, err := a.spaceService.Get(ctx, spaceId)
if err != nil {
return convertedOrSpaceErr(err)
}
aclClient := acceptSpace.CommonSpace().AclClient()
acl := acceptSpace.CommonSpace().Acl()
acl.RLock()
invites := acl.AclState().Invites(aclrecordproto.AclInviteType_AnyoneCanJoin)
if len(invites) == 0 {
acl.RUnlock()
return inviteservice.ErrInviteNotExists
}
acl.RUnlock()
var (
invite = invites[0]
invitePermissions = domain.ConvertParticipantPermissions(permissions)
)
if invite.Permissions == invitePermissions {
return ErrIncorrectPermissions
}
err = aclClient.ChangeInvite(ctx, invites[0].Id, invitePermissions)
if err != nil {
return convertedOrAclRequestError(err)
}
err = a.inviteService.Change(ctx, spaceId, invitePermissions)
if err != nil {
return convertedOrInternalError("change invite", err)
}
return nil
}
func (a *aclService) GenerateInvite(ctx context.Context, spaceId string, invType model.InviteType, permissions model.ParticipantPermissions) (result domain.InviteInfo, err error) {
if spaceId == a.accountService.PersonalSpaceID() {
err = ErrPersonalSpace
return
}
var (
inviteExists = false
inviteType = domain.InviteType(invType)
)
current, err := a.inviteService.GetCurrent(ctx, spaceId)
if err == nil {
inviteExists = true
if current.InviteType == inviteType {
return current, nil
}
}
acceptSpace, err := a.spaceService.Get(ctx, spaceId)
if err != nil {
return
}
aclClient := acceptSpace.CommonSpace().AclClient()
res, err := aclClient.GenerateInvite()
aclPermissions := domain.ConvertParticipantPermissions(permissions)
res, err := aclClient.GenerateInvite(inviteExists, inviteType == domain.InviteTypeDefault, aclPermissions)
if err != nil {
err = convertedOrInternalError("couldn't generate acl invite", err)
return
}
return a.inviteService.Generate(ctx, spaceId, res.InviteKey, func() error {
params := inviteservice.GenerateInviteParams{
SpaceId: spaceId,
Key: res.InviteKey,
InviteType: inviteType,
Permissions: aclPermissions,
}
return a.inviteService.Generate(ctx, params, func() error {
err := aclClient.AddRecord(ctx, res.InviteRec)
if err != nil {
return convertedOrAclRequestError(err)

View file

@ -11,12 +11,15 @@ import (
"github.com/anyproto/any-sync/commonspace/acl/aclclient/mock_aclclient"
"github.com/anyproto/any-sync/commonspace/mock_commonspace"
"github.com/anyproto/any-sync/commonspace/object/accountdata"
"github.com/anyproto/any-sync/commonspace/object/acl/aclrecordproto"
"github.com/anyproto/any-sync/commonspace/object/acl/list"
"github.com/anyproto/any-sync/commonspace/object/acl/list/mock_list"
"github.com/anyproto/any-sync/commonspace/object/acl/recordverifier"
"github.com/anyproto/any-sync/commonspace/object/acl/syncacl/headupdater"
"github.com/anyproto/any-sync/commonspace/spacesyncproto"
"github.com/anyproto/any-sync/commonspace/sync/syncdeps"
"github.com/anyproto/any-sync/commonspace/syncstatus"
"github.com/anyproto/any-sync/consensus/consensusproto"
"github.com/anyproto/any-sync/coordinator/coordinatorclient/mock_coordinatorclient"
"github.com/anyproto/any-sync/coordinator/coordinatorproto"
"github.com/anyproto/any-sync/net/peer"
@ -108,6 +111,7 @@ func newFixture(t *testing.T) *fixture {
Register(fx.mockConfig).
Register(fx.aclService)
require.NoError(t, fx.a.Start(ctx))
fx.aclService.recordVerifier = recordverifier.NewValidateFull()
return fx
}
@ -365,7 +369,7 @@ func TestService_ViewInvite(t *testing.T) {
InviteRevokes: invRecIds,
})
require.NoError(t, err)
err = aclList.AddRawRecord(list.WrapAclRecord(removeInv))
err = aclList.AddRawRecord(list.WrapAclRecord(removeInv.Rec))
require.NoError(t, err)
recs, err := aclList.RecordsAfter(ctx, "")
require.NoError(t, err)
@ -387,6 +391,192 @@ func TestService_ViewInvite(t *testing.T) {
})
}
func TestService_ChangeInvite(t *testing.T) {
t.Run("change invite", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
spaceId := "spaceId"
fx.mockAccountService.EXPECT().PersonalSpaceID().Return("personal")
mockSpace := mock_clientspace.NewMockSpace(t)
mockCommonSpace := mock_commonspace.NewMockSpace(fx.ctrl)
fx.mockInviteService.EXPECT().GetCurrent(ctx, spaceId).Return(domain.InviteInfo{
InviteType: domain.InviteTypeAnyone,
InviteFileCid: "testCid",
}, nil)
fx.mockSpaceService.EXPECT().Get(ctx, spaceId).Return(mockSpace, nil)
mockSpace.EXPECT().CommonSpace().Return(mockCommonSpace)
exec := list.NewAclExecutor(spaceId)
type cmdErr struct {
cmd string
err error
}
cmds := []cmdErr{
{"a.init::a", nil},
{"a.invite_anyone::invId,r", nil},
}
for _, cmd := range cmds {
err := exec.Execute(cmd.cmd)
require.Equal(t, cmd.err, err, cmd)
}
acl := mockSyncAcl{exec.ActualAccounts()["a"].Acl}
invId := acl.AclState().Invites(aclrecordproto.AclInviteType_AnyoneCanJoin)[0].Id
mockCommonSpace.EXPECT().Acl().Return(acl)
aclClient := mock_aclclient.NewMockAclSpaceClient(fx.ctrl)
mockCommonSpace.EXPECT().AclClient().Return(aclClient)
aclClient.EXPECT().ChangeInvite(ctx, invId, list.AclPermissionsWriter).Return(nil)
fx.mockInviteService.EXPECT().Change(ctx, spaceId, list.AclPermissionsWriter).Return(nil)
err := fx.ChangeInvite(ctx, spaceId, model.ParticipantPermissions_Writer)
require.NoError(t, err)
})
t.Run("different invite type", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
spaceId := "spaceId"
fx.mockAccountService.EXPECT().PersonalSpaceID().Return("personal")
fx.mockInviteService.EXPECT().GetCurrent(ctx, spaceId).Return(domain.InviteInfo{
InviteType: domain.InviteTypeDefault,
InviteFileCid: "testCid",
}, nil)
err := fx.ChangeInvite(ctx, spaceId, model.ParticipantPermissions_Writer)
require.Equal(t, inviteservice.ErrInviteNotExists, err)
})
}
func TestService_GenerateInvite(t *testing.T) {
t.Run("new default invite", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
spaceId := "spaceId"
keys, err := accountdata.NewRandom()
require.NoError(t, err)
fx.mockAccountService.EXPECT().PersonalSpaceID().Return("personal")
fx.mockInviteService.EXPECT().GetCurrent(ctx, spaceId).Return(domain.InviteInfo{}, inviteservice.ErrInviteNotExists)
mockSpace := mock_clientspace.NewMockSpace(t)
mockCommonSpace := mock_commonspace.NewMockSpace(fx.ctrl)
mockAclClient := mock_aclclient.NewMockAclSpaceClient(fx.ctrl)
mockSpace.EXPECT().CommonSpace().Return(mockCommonSpace)
mockCommonSpace.EXPECT().AclClient().Return(mockAclClient)
fx.mockSpaceService.EXPECT().Get(ctx, spaceId).Return(mockSpace, nil)
rec := &consensusproto.RawRecord{
Payload: []byte("test"),
}
mockAclClient.EXPECT().GenerateInvite(false, true, list.AclPermissionsReader).
Return(list.InviteResult{
InviteRec: rec,
InviteKey: keys.SignKey,
}, nil)
params := inviteservice.GenerateInviteParams{
SpaceId: spaceId,
InviteType: domain.InviteTypeDefault,
Key: keys.SignKey,
Permissions: list.AclPermissionsReader,
}
mockAclClient.EXPECT().AddRecord(ctx, rec).Return(nil)
fx.mockInviteService.EXPECT().Generate(ctx, params, mock.Anything).
RunAndReturn(func(ctx2 context.Context, params inviteservice.GenerateInviteParams, f func() error) (domain.InviteInfo, error) {
return domain.InviteInfo{
InviteFileCid: "testCid",
}, f()
})
info, err := fx.GenerateInvite(ctx, spaceId, model.InviteType_Member, model.ParticipantPermissions_Reader)
require.NoError(t, err)
require.Equal(t, "testCid", info.InviteFileCid)
})
t.Run("new anyone can join invite", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
spaceId := "spaceId"
keys, err := accountdata.NewRandom()
require.NoError(t, err)
fx.mockAccountService.EXPECT().PersonalSpaceID().Return("personal")
fx.mockInviteService.EXPECT().GetCurrent(ctx, spaceId).Return(domain.InviteInfo{}, inviteservice.ErrInviteNotExists)
mockSpace := mock_clientspace.NewMockSpace(t)
mockCommonSpace := mock_commonspace.NewMockSpace(fx.ctrl)
mockAclClient := mock_aclclient.NewMockAclSpaceClient(fx.ctrl)
mockSpace.EXPECT().CommonSpace().Return(mockCommonSpace)
mockCommonSpace.EXPECT().AclClient().Return(mockAclClient)
fx.mockSpaceService.EXPECT().Get(ctx, spaceId).Return(mockSpace, nil)
rec := &consensusproto.RawRecord{
Payload: []byte("test"),
}
mockAclClient.EXPECT().GenerateInvite(false, false, list.AclPermissionsReader).
Return(list.InviteResult{
InviteRec: rec,
InviteKey: keys.SignKey,
}, nil)
params := inviteservice.GenerateInviteParams{
SpaceId: spaceId,
InviteType: domain.InviteTypeAnyone,
Key: keys.SignKey,
Permissions: list.AclPermissionsReader,
}
mockAclClient.EXPECT().AddRecord(ctx, rec).Return(nil)
fx.mockInviteService.EXPECT().Generate(ctx, params, mock.Anything).
RunAndReturn(func(ctx2 context.Context, params inviteservice.GenerateInviteParams, f func() error) (domain.InviteInfo, error) {
return domain.InviteInfo{
InviteFileCid: "testCid",
}, f()
})
info, err := fx.GenerateInvite(ctx, spaceId, model.InviteType_WithoutApprove, model.ParticipantPermissions_Reader)
require.NoError(t, err)
require.Equal(t, "testCid", info.InviteFileCid)
})
t.Run("anyone can join invite after default invite", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
spaceId := "spaceId"
keys, err := accountdata.NewRandom()
require.NoError(t, err)
fx.mockAccountService.EXPECT().PersonalSpaceID().Return("personal")
fx.mockInviteService.EXPECT().GetCurrent(ctx, spaceId).Return(domain.InviteInfo{
InviteType: domain.InviteTypeDefault,
}, nil)
mockSpace := mock_clientspace.NewMockSpace(t)
mockCommonSpace := mock_commonspace.NewMockSpace(fx.ctrl)
mockAclClient := mock_aclclient.NewMockAclSpaceClient(fx.ctrl)
mockSpace.EXPECT().CommonSpace().Return(mockCommonSpace)
mockCommonSpace.EXPECT().AclClient().Return(mockAclClient)
fx.mockSpaceService.EXPECT().Get(ctx, spaceId).Return(mockSpace, nil)
rec := &consensusproto.RawRecord{
Payload: []byte("test"),
}
mockAclClient.EXPECT().GenerateInvite(true, false, list.AclPermissionsReader).
Return(list.InviteResult{
InviteRec: rec,
InviteKey: keys.SignKey,
}, nil)
params := inviteservice.GenerateInviteParams{
SpaceId: spaceId,
InviteType: domain.InviteTypeAnyone,
Key: keys.SignKey,
Permissions: list.AclPermissionsReader,
}
mockAclClient.EXPECT().AddRecord(ctx, rec).Return(nil)
fx.mockInviteService.EXPECT().Generate(ctx, params, mock.Anything).
RunAndReturn(func(ctx2 context.Context, params inviteservice.GenerateInviteParams, f func() error) (domain.InviteInfo, error) {
return domain.InviteInfo{
InviteFileCid: "testCid",
}, f()
})
info, err := fx.GenerateInvite(ctx, spaceId, model.InviteType_WithoutApprove, model.ParticipantPermissions_Reader)
require.NoError(t, err)
require.Equal(t, "testCid", info.InviteFileCid)
})
t.Run("invite already exists", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
spaceId := "spaceId"
fx.mockAccountService.EXPECT().PersonalSpaceID().Return("personal")
fx.mockInviteService.EXPECT().GetCurrent(ctx, spaceId).Return(domain.InviteInfo{
InviteType: domain.InviteTypeAnyone,
InviteFileCid: "testCid",
}, nil)
info, err := fx.GenerateInvite(ctx, spaceId, model.InviteType_WithoutApprove, model.ParticipantPermissions_Reader)
require.NoError(t, err)
require.Equal(t, "testCid", info.InviteFileCid)
})
}
func TestService_Join(t *testing.T) {
t.Run("join success", func(t *testing.T) {
fx := newFixture(t)
@ -468,6 +658,35 @@ func TestService_Join(t *testing.T) {
err = fx.Join(ctx, "spaceId", "", realCid, key)
require.NoError(t, err)
})
t.Run("join success without approve", func(t *testing.T) {
fx := newFixture(t)
defer fx.finish(t)
cidString, err := cidutil.NewCidFromBytes([]byte("spaceId"))
require.NoError(t, err)
realCid, err := cid.Decode(cidString)
require.NoError(t, err)
key, err := crypto.NewRandomAES()
require.NoError(t, err)
inviteKey, _, err := crypto.GenerateRandomEd25519KeyPair()
require.NoError(t, err)
protoKey, err := inviteKey.Marshall()
require.NoError(t, err)
fx.mockInviteService.EXPECT().GetPayload(ctx, realCid, key).Return(&model.InvitePayload{
AclKey: protoKey,
InviteType: model.InviteType_WithoutApprove,
}, nil)
metadataPayload := []byte("metadata")
fx.mockSpaceService.EXPECT().AccountMetadataPayload().Return(metadataPayload)
fx.mockJoiningClient.EXPECT().InviteJoin(ctx, "spaceId", list.InviteJoinPayload{
InviteKey: inviteKey,
Metadata: metadataPayload,
}).Return("aclHeadId", nil)
fx.mockSpaceService.EXPECT().InviteJoin(ctx, "spaceId", "aclHeadId").Return(nil)
fx.mockSpaceService.EXPECT().TechSpace().Return(&clientspace.TechSpace{TechSpace: fx.mockTechSpace})
fx.mockTechSpace.EXPECT().SpaceViewSetData(ctx, "spaceId", mock.Anything).Return(nil)
err = fx.Join(ctx, "spaceId", "", realCid, key)
require.NoError(t, err)
})
t.Run("join fail, different network", func(t *testing.T) {
// given
fx := newFixture(t)

View file

@ -41,6 +41,8 @@ import (
"github.com/anyproto/anytype-heart/core/block/bookmark"
decorator "github.com/anyproto/anytype-heart/core/block/bookmark/bookmarkimporter"
"github.com/anyproto/anytype-heart/core/block/chats"
"github.com/anyproto/anytype-heart/core/block/chats/chatrepository"
"github.com/anyproto/anytype-heart/core/block/chats/chatsubscription"
"github.com/anyproto/anytype-heart/core/block/collection"
"github.com/anyproto/anytype-heart/core/block/dataviewservice"
"github.com/anyproto/anytype-heart/core/block/detailservice"
@ -264,6 +266,8 @@ func Bootstrap(a *app.App, components ...app.Component) {
Register(files.New()).
Register(fileoffloader.New()).
Register(fileacl.New()).
Register(chatrepository.New()).
Register(chatsubscription.New()).
Register(chats.New()).
Register(sourceimpl.New()).
Register(spacefactory.New()).

View file

@ -490,7 +490,7 @@ func (c *Config) GetNetworkMode() pb.RpcAccountNetworkMode {
func (c *Config) GetPublishServer() publishclient.Config {
publishPeerId := "12D3KooWEQPgbxGPvkny8kikS3zqfziM7JsQBnJHXHL9ByCcATs7"
publishAddr := "load-balancer.anytype.io:4940"
publishAddr := "anytype-publish-server.anytype.io:4940"
if peerId := os.Getenv("ANYTYPE_PUBLISH_PEERID"); peerId != "" {
if addr := os.Getenv("ANYTYPE_PUBLISH_ADDRESS"); addr != "" {
@ -510,8 +510,8 @@ func (c *Config) GetPublishServer() publishclient.Config {
}
func (c *Config) GetPushConfig() PushConfig {
pushPeerId := "12D3KooWR8Ci1XidFCCXoZppGrUmiy4D1Mjoux9xK6QoZrpbQC3J"
pushAddr := "stage1-anytype-push-server1.toolpad.org:4940"
pushPeerId := "12D3KooWMATrdteJNq2YvYhtq3RDeWxq6RVXDAr36MsGd5RJzXDn"
pushAddr := "anytype-push-server.anytype.io:4941"
if peerId := os.Getenv("ANYTYPE_PUSH_PEERID"); peerId != "" {
if addr := os.Getenv("ANYTYPE_PUSH_ADDRESS"); addr != "" {

View file

@ -11,8 +11,8 @@ type AccountService interface {
GetInfo(ctx context.Context) (*model.AccountInfo, error)
}
type ExportService interface {
ExportSingleInMemory(ctx context.Context, spaceId string, objectId string, format model.ExportFormat) (res string, err error)
type EventService interface {
Broadcast(event *pb.Event)
}
type ClientCommands interface {
@ -39,7 +39,7 @@ type ClientCommands interface {
ObjectSearchUnsubscribe(context.Context, *pb.RpcObjectSearchUnsubscribeRequest) *pb.RpcObjectSearchUnsubscribeResponse
ObjectSetDetails(context.Context, *pb.RpcObjectSetDetailsRequest) *pb.RpcObjectSetDetailsResponse
ObjectSetIsArchived(context.Context, *pb.RpcObjectSetIsArchivedRequest) *pb.RpcObjectSetIsArchivedResponse
ObjectListExport(context.Context, *pb.RpcObjectListExportRequest) *pb.RpcObjectListExportResponse
ObjectExport(context.Context, *pb.RpcObjectExportRequest) *pb.RpcObjectExportResponse
// Type
ObjectCreateObjectType(context.Context, *pb.RpcObjectCreateObjectTypeRequest) *pb.RpcObjectCreateObjectTypeResponse

View file

@ -561,51 +561,51 @@ func (_c *MockClientCommands_ObjectCreateRelationOption_Call) RunAndReturn(run f
return _c
}
// ObjectListExport provides a mock function with given fields: _a0, _a1
func (_m *MockClientCommands) ObjectListExport(_a0 context.Context, _a1 *pb.RpcObjectListExportRequest) *pb.RpcObjectListExportResponse {
// ObjectExport provides a mock function with given fields: _a0, _a1
func (_m *MockClientCommands) ObjectExport(_a0 context.Context, _a1 *pb.RpcObjectExportRequest) *pb.RpcObjectExportResponse {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for ObjectListExport")
panic("no return value specified for ObjectExport")
}
var r0 *pb.RpcObjectListExportResponse
if rf, ok := ret.Get(0).(func(context.Context, *pb.RpcObjectListExportRequest) *pb.RpcObjectListExportResponse); ok {
var r0 *pb.RpcObjectExportResponse
if rf, ok := ret.Get(0).(func(context.Context, *pb.RpcObjectExportRequest) *pb.RpcObjectExportResponse); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*pb.RpcObjectListExportResponse)
r0 = ret.Get(0).(*pb.RpcObjectExportResponse)
}
}
return r0
}
// MockClientCommands_ObjectListExport_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ObjectListExport'
type MockClientCommands_ObjectListExport_Call struct {
// MockClientCommands_ObjectExport_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ObjectExport'
type MockClientCommands_ObjectExport_Call struct {
*mock.Call
}
// ObjectListExport is a helper method to define mock.On call
// ObjectExport is a helper method to define mock.On call
// - _a0 context.Context
// - _a1 *pb.RpcObjectListExportRequest
func (_e *MockClientCommands_Expecter) ObjectListExport(_a0 interface{}, _a1 interface{}) *MockClientCommands_ObjectListExport_Call {
return &MockClientCommands_ObjectListExport_Call{Call: _e.mock.On("ObjectListExport", _a0, _a1)}
// - _a1 *pb.RpcObjectExportRequest
func (_e *MockClientCommands_Expecter) ObjectExport(_a0 interface{}, _a1 interface{}) *MockClientCommands_ObjectExport_Call {
return &MockClientCommands_ObjectExport_Call{Call: _e.mock.On("ObjectExport", _a0, _a1)}
}
func (_c *MockClientCommands_ObjectListExport_Call) Run(run func(_a0 context.Context, _a1 *pb.RpcObjectListExportRequest)) *MockClientCommands_ObjectListExport_Call {
func (_c *MockClientCommands_ObjectExport_Call) Run(run func(_a0 context.Context, _a1 *pb.RpcObjectExportRequest)) *MockClientCommands_ObjectExport_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(*pb.RpcObjectListExportRequest))
run(args[0].(context.Context), args[1].(*pb.RpcObjectExportRequest))
})
return _c
}
func (_c *MockClientCommands_ObjectListExport_Call) Return(_a0 *pb.RpcObjectListExportResponse) *MockClientCommands_ObjectListExport_Call {
func (_c *MockClientCommands_ObjectExport_Call) Return(_a0 *pb.RpcObjectExportResponse) *MockClientCommands_ObjectExport_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockClientCommands_ObjectListExport_Call) RunAndReturn(run func(context.Context, *pb.RpcObjectListExportRequest) *pb.RpcObjectListExportResponse) *MockClientCommands_ObjectListExport_Call {
func (_c *MockClientCommands_ObjectExport_Call) RunAndReturn(run func(context.Context, *pb.RpcObjectExportRequest) *pb.RpcObjectExportResponse) *MockClientCommands_ObjectExport_Call {
_c.Call.Return(run)
return _c
}

View file

@ -0,0 +1,68 @@
// Code generated by mockery. DO NOT EDIT.
package mock_apicore
import (
pb "github.com/anyproto/anytype-heart/pb"
mock "github.com/stretchr/testify/mock"
)
// MockEventService is an autogenerated mock type for the EventService type
type MockEventService struct {
mock.Mock
}
type MockEventService_Expecter struct {
mock *mock.Mock
}
func (_m *MockEventService) EXPECT() *MockEventService_Expecter {
return &MockEventService_Expecter{mock: &_m.Mock}
}
// Broadcast provides a mock function with given fields: event
func (_m *MockEventService) Broadcast(event *pb.Event) {
_m.Called(event)
}
// MockEventService_Broadcast_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Broadcast'
type MockEventService_Broadcast_Call struct {
*mock.Call
}
// Broadcast is a helper method to define mock.On call
// - event *pb.Event
func (_e *MockEventService_Expecter) Broadcast(event interface{}) *MockEventService_Broadcast_Call {
return &MockEventService_Broadcast_Call{Call: _e.mock.On("Broadcast", event)}
}
func (_c *MockEventService_Broadcast_Call) Run(run func(event *pb.Event)) *MockEventService_Broadcast_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(*pb.Event))
})
return _c
}
func (_c *MockEventService_Broadcast_Call) Return() *MockEventService_Broadcast_Call {
_c.Call.Return()
return _c
}
func (_c *MockEventService_Broadcast_Call) RunAndReturn(run func(*pb.Event)) *MockEventService_Broadcast_Call {
_c.Call.Return(run)
return _c
}
// NewMockEventService creates a new instance of MockEventService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockEventService(t interface {
mock.TestingT
Cleanup(func())
}) *MockEventService {
mock := &MockEventService{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View file

@ -1,96 +0,0 @@
// Code generated by mockery. DO NOT EDIT.
package mock_apicore
import (
context "context"
model "github.com/anyproto/anytype-heart/pkg/lib/pb/model"
mock "github.com/stretchr/testify/mock"
)
// MockExportService is an autogenerated mock type for the ExportService type
type MockExportService struct {
mock.Mock
}
type MockExportService_Expecter struct {
mock *mock.Mock
}
func (_m *MockExportService) EXPECT() *MockExportService_Expecter {
return &MockExportService_Expecter{mock: &_m.Mock}
}
// ExportSingleInMemory provides a mock function with given fields: ctx, spaceId, objectId, format
func (_m *MockExportService) ExportSingleInMemory(ctx context.Context, spaceId string, objectId string, format model.ExportFormat) (string, error) {
ret := _m.Called(ctx, spaceId, objectId, format)
if len(ret) == 0 {
panic("no return value specified for ExportSingleInMemory")
}
var r0 string
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, model.ExportFormat) (string, error)); ok {
return rf(ctx, spaceId, objectId, format)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, model.ExportFormat) string); ok {
r0 = rf(ctx, spaceId, objectId, format)
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func(context.Context, string, string, model.ExportFormat) error); ok {
r1 = rf(ctx, spaceId, objectId, format)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockExportService_ExportSingleInMemory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExportSingleInMemory'
type MockExportService_ExportSingleInMemory_Call struct {
*mock.Call
}
// ExportSingleInMemory is a helper method to define mock.On call
// - ctx context.Context
// - spaceId string
// - objectId string
// - format model.ExportFormat
func (_e *MockExportService_Expecter) ExportSingleInMemory(ctx interface{}, spaceId interface{}, objectId interface{}, format interface{}) *MockExportService_ExportSingleInMemory_Call {
return &MockExportService_ExportSingleInMemory_Call{Call: _e.mock.On("ExportSingleInMemory", ctx, spaceId, objectId, format)}
}
func (_c *MockExportService_ExportSingleInMemory_Call) Run(run func(ctx context.Context, spaceId string, objectId string, format model.ExportFormat)) *MockExportService_ExportSingleInMemory_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(model.ExportFormat))
})
return _c
}
func (_c *MockExportService_ExportSingleInMemory_Call) Return(res string, err error) *MockExportService_ExportSingleInMemory_Call {
_c.Call.Return(res, err)
return _c
}
func (_c *MockExportService_ExportSingleInMemory_Call) RunAndReturn(run func(context.Context, string, string, model.ExportFormat) (string, error)) *MockExportService_ExportSingleInMemory_Call {
_c.Call.Return(run)
return _c
}
// NewMockExportService creates a new instance of MockExportService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockExportService(t interface {
mock.TestingT
Cleanup(func())
}) *MockExportService {
mock := &MockExportService{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

File diff suppressed because one or more lines are too long

6072
core/api/docs/openapi.json Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -10,28 +10,31 @@ import (
"github.com/anyproto/anytype-heart/core/api/util"
)
/*
TO BE DEPRECATED
// DisplayCodeHandler starts a new challenge and returns the challenge ID
//
// @Summary Start new challenge
// @Description Generates a one-time authentication challenge for granting API access to the user's vault. Upon providing a valid `app_name`, the server issues a unique `challenge_id` and displays a short code within the Anytype Desktop On success, the service returns a unique challenge ID. This challenge ID must then be used with the token endpoint (see below) to solve the challenge and retrieve an authentication token. This mechanism ensures that only trusted applications and authorized users gain access.
// @ID createAuthChallenge
// @Tags Auth
// @Accept json
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param app_name query string true "The name of the app requesting API access"
// @Success 200 {object} apimodel.DisplayCodeResponse "The challenge ID associated with the started challenge"
// @Failure 400 {object} util.ValidationError "Invalid input"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /auth/display_code [post]
// @Summary Start challenge
// @Description Generates a one-time authentication challenge for granting API access to the user's vault. Upon providing a valid `app_name`, the server issues a unique `challenge_id` and displays a short code within the Anytype Desktop. The `challenge_id` must then be used with the token endpoint (see below) to solve the challenge and retrieve an authentication token. This mechanism ensures that only trusted applications and authorized users gain access.
// @ID create_auth_challenge
// @Tags Auth
// @Accept json
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param app_name query string true "The name of the app requesting API access"
// @Success 200 {object} apimodel.DisplayCodeResponse "The challenge ID associated with the started challenge"
// @Failure 400 {object} util.ValidationError "Bad request"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /v1/auth/display_code [post]
*/
func DisplayCodeHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
appName := c.Query("app_name")
challengeId, err := s.NewChallenge(c.Request.Context(), appName)
challengeId, err := s.CreateChallenge(c.Request.Context(), appName)
code := util.MapErrorCode(err,
util.ErrToCode(service.ErrMissingAppName, http.StatusBadRequest),
util.ErrToCode(service.ErrFailedGenerateChallenge, http.StatusInternalServerError))
util.ErrToCode(service.ErrFailedCreateNewChallenge, http.StatusInternalServerError))
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
@ -39,25 +42,28 @@ func DisplayCodeHandler(s *service.Service) gin.HandlerFunc {
return
}
c.JSON(http.StatusOK, apimodel.DisplayCodeResponse{ChallengeId: challengeId})
c.JSON(299, apimodel.DisplayCodeResponse{ChallengeId: challengeId})
}
}
/*
TO BE DEPRECATED
// TokenHandler retrieves an authentication token using a code and challenge ID
//
// @Summary Solve challenge
// @Description After receiving a challenge ID from the display_code endpoint, the client calls this endpoint to provide the corresponding 4-digit code (also via a query parameter) along with the challenge ID. The endpoint verifies that the challenge solution is correct and, if it is, returns a permanent app key. This endpoint is central to the authentication process, as it validates the user's identity and issues a token that can be used for further interactions with the API.
// @ID solveAuthChallenge
// @Description After receiving a `challenge_id` from the `display_code` endpoint, the client calls this endpoint to provide the corresponding 4-digit code along with the challenge ID. The endpoint verifies that the challenge solution is correct and, if it is, returns a permanent `app_key`. This endpoint is central to the authentication process, as it validates the user's identity and issues a token that can be used for further interactions with the API.
// @ID solve_auth_challenge
// @Tags Auth
// @Accept json
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param challenge_id query string true "The ID of the challenge to solve"
// @Param code query string true "4-digit code retrieved from Anytype Desktop app"
// @Success 200 {object} apimodel.TokenResponse "The app key that can be used in the Authorization header for subsequent requests"
// @Failure 400 {object} util.ValidationError "Invalid input"
// @Failure 400 {object} util.ValidationError "Bad request"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /auth/token [post]
// @Router /v1/auth/token [post]
*/
func TokenHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
challengeId := c.Query("challenge_id")
@ -65,7 +71,7 @@ func TokenHandler(s *service.Service) gin.HandlerFunc {
appKey, err := s.SolveChallenge(c.Request.Context(), challengeId, code)
errCode := util.MapErrorCode(err,
util.ErrToCode(service.ErrInvalidInput, http.StatusBadRequest),
util.ErrToCode(util.ErrBad, http.StatusBadRequest),
util.ErrToCode(service.ErrFailedAuthenticate, http.StatusInternalServerError),
)
@ -75,6 +81,84 @@ func TokenHandler(s *service.Service) gin.HandlerFunc {
return
}
c.JSON(http.StatusOK, apimodel.TokenResponse{AppKey: appKey})
c.JSON(299, apimodel.TokenResponse{AppKey: appKey})
}
}
// CreateChallengeHandler creates a new challenge for API key generation
//
// @Summary Create Challenge
// @Description Generates a one-time authentication challenge for granting API access to the user's vault. Upon providing a valid `app_name`, the server issues a unique `challenge_id` and displays a 4-digit code within the Anytype Desktop. The `challenge_id` must then be used with the `/v1/auth/api_keys` endpoint to solve the challenge and retrieve an authentication token. This mechanism ensures that only trusted applications and authorized users gain access.
// @ID create_auth_challenge
// @Tags Auth
// @Accept json
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param app_name query string true "The name of the app requesting API access"
// @Success 201 {object} apimodel.CreateChallengeResponse "The challenge ID associated with the started challenge"
// @Failure 400 {object} util.ValidationError "Bad request"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /v1/auth/challenges [post]
func CreateChallengeHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
request := apimodel.CreateChallengeRequest{}
if err := c.ShouldBindJSON(&request); err != nil {
apiErr := util.CodeToAPIError(http.StatusBadRequest, err.Error())
c.JSON(http.StatusBadRequest, apiErr)
return
}
challengeId, err := s.CreateChallenge(c.Request.Context(), request.AppName)
code := util.MapErrorCode(err,
util.ErrToCode(service.ErrMissingAppName, http.StatusBadRequest),
util.ErrToCode(service.ErrFailedCreateNewChallenge, http.StatusInternalServerError))
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
c.JSON(code, apiErr)
return
}
c.JSON(http.StatusCreated, apimodel.CreateChallengeResponse{ChallengeId: challengeId})
}
}
// CreateApiKeyHandler creates a new api key using a code and challenge ID
//
// @Summary Create API Key
// @Description After receiving a `challenge_id` from the `/v1/auth/challenges` endpoint, the client calls this endpoint to provide the corresponding 4-digit code along with the challenge ID. The endpoint verifies that the challenge solution is correct and, if it is, returns an `api_key`. This endpoint is central to the authentication process, as it validates the user's identity and issues a key that can be used for further interactions with the API.
// @ID create_api_key
// @Tags Auth
// @Accept json
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param challenge_id query string true "The ID of the challenge to solve"
// @Param code query string true "The 4-digit code retrieved from Anytype Desktop app"
// @Success 201 {object} apimodel.CreateApiKeyResponse "The API key that can be used in the Authorization header for subsequent requests"
// @Failure 400 {object} util.ValidationError "Bad request"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /v1/auth/api_keys [post]
func CreateApiKeyHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
request := apimodel.CreateApiKeyRequest{}
if err := c.ShouldBindJSON(&request); err != nil {
apiErr := util.CodeToAPIError(http.StatusBadRequest, err.Error())
c.JSON(http.StatusBadRequest, apiErr)
return
}
apiKey, err := s.SolveChallenge(c.Request.Context(), request.ChallengeId, request.Code)
errCode := util.MapErrorCode(err,
util.ErrToCode(util.ErrBad, http.StatusBadRequest),
util.ErrToCode(service.ErrFailedAuthenticate, http.StatusInternalServerError),
)
if errCode != http.StatusOK {
apiErr := util.CodeToAPIError(errCode, err.Error())
c.JSON(errCode, apiErr)
return
}
c.JSON(http.StatusCreated, apimodel.CreateApiKeyResponse{ApiKey: apiKey})
}
}

View file

@ -5,6 +5,7 @@ import (
"github.com/gin-gonic/gin"
apimodel "github.com/anyproto/anytype-heart/core/api/model"
"github.com/anyproto/anytype-heart/core/api/pagination"
"github.com/anyproto/anytype-heart/core/api/service"
"github.com/anyproto/anytype-heart/core/api/util"
@ -14,12 +15,12 @@ import (
//
// @Summary Get list views
// @Description Returns a paginated list of views defined for a specific list (query or collection) within a space. Each view includes details such as layout, applied filters, and sorting options, enabling clients to render the list according to user preferences and context. This endpoint is essential for applications that need to display lists in various formats (e.g., grid, table) or with different sorting/filtering criteria.
// @Id getListViews
// @Id get_list_views
// @Tags Lists
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space to which the list belongs"
// @Param list_id path string true "The ID of the list to retrieve views for"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space to which the list belongs; must be retrieved from ListSpaces endpoint"
// @Param list_id path string true "The ID of the list to retrieve views for; must be retrieved from SearchSpace endpoint with types: ['collection', 'set']"
// @Param offset query int false "The number of items to skip before starting to collect the result set" default(0)
// @Param limit query int false "The number of items to return"
// @Success 200 {object} pagination.PaginatedResponse[apimodel.View] "The list of views associated with the specified list"
@ -27,7 +28,7 @@ import (
// @Failure 404 {object} util.NotFoundError "Not found"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/lists/{list_id}/views [get]
// @Router /v1/spaces/{space_id}/lists/{list_id}/views [get]
func GetListViewsHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -55,13 +56,13 @@ func GetListViewsHandler(s *service.Service) gin.HandlerFunc {
//
// @Summary Get objects in list
// @Description Returns a paginated list of objects associated with a specific list (query or collection) within a space. When a view ID is provided, the objects are filtered and sorted according to the view's configuration. If no view ID is specified, all list objects are returned without filtering and sorting. This endpoint helps clients to manage grouped objects (for example, tasks within a list) by returning information for each item of the list.
// @Id getListObjects
// @Id get_list_objects
// @Tags Lists
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space to which the list belongs"
// @Param list_id path string true "The ID of the list to retrieve objects for"
// @Param view_id path string true "The ID of the view to retrieve objects for"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space to which the list belongs; must be retrieved from ListSpaces endpoint"
// @Param list_id path string true "The ID of the list to retrieve objects for; must be retrieved from SearchSpace endpoint with types: ['collection', 'set']"
// @Param view_id path string true "The ID of the view to retrieve objects for; must be retrieved from ListViews endpoint or omitted if you want to get all objects in the list"
// @Param offset query int false "The number of items to skip before starting to collect the result set" default(0)
// @Param limit query int false "The number of items to return"
// @Success 200 {object} pagination.PaginatedResponse[apimodel.Object] "The list of objects associated with the specified list"
@ -69,7 +70,7 @@ func GetListViewsHandler(s *service.Service) gin.HandlerFunc {
// @Failure 404 {object} util.NotFoundError "Not found"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/lists/{list_id}/{view_id}/objects [get]
// @Router /v1/spaces/{space_id}/lists/{list_id}/views/{view_id}/objects [get]
func GetObjectsInListHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -102,35 +103,35 @@ func GetObjectsInListHandler(s *service.Service) gin.HandlerFunc {
//
// @Summary Add objects to list
// @Description Adds one or more objects to a specific list (collection only) by submitting a JSON array of object IDs. Upon success, the endpoint returns a confirmation message. This endpoint is vital for building user interfaces that allow draganddrop or multiselect additions to collections, enabling users to dynamically manage their collections without needing to modify the underlying object data.
// @Id addListObjects
// @Id add_list_objects
// @Tags Lists
// @Accept json
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space to which the list belongs"
// @Param list_id path string true "The ID of the list to which objects will be added"
// @Param objects body []string true "The list of object IDs to add to the list"
// @Success 200 {object} string "Objects added successfully"
// @Failure 400 {object} util.ValidationError "Bad request"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 404 {object} util.NotFoundError "Not found"
// @Failure 429 {object} util.RateLimitError "Rate limit exceeded"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space to which the list belongs; must be retrieved from ListSpaces endpoint"
// @Param list_id path string true "The ID of the list to which objects will be added; must be retrieved from SearchSpace endpoint with types: ['collection', 'set']"
// @Param objects body apimodel.AddObjectsToListRequest true "The list of object IDs to add to the list; must be retrieved from SearchSpace or GlobalSearch endpoints or obtained from response context"
// @Success 200 {object} string "Objects added successfully"
// @Failure 400 {object} util.ValidationError "Bad request"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 404 {object} util.NotFoundError "Not found"
// @Failure 429 {object} util.RateLimitError "Rate limit exceeded"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/lists/{list_id}/objects [post]
// @Router /v1/spaces/{space_id}/lists/{list_id}/objects [post]
func AddObjectsToListHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
listId := c.Param("list_id")
objects := []string{}
if err := c.ShouldBindJSON(&objects); err != nil {
request := apimodel.AddObjectsToListRequest{}
if err := c.ShouldBindJSON(&request); err != nil {
apiErr := util.CodeToAPIError(http.StatusBadRequest, err.Error())
c.JSON(http.StatusBadRequest, apiErr)
return
}
err := s.AddObjectsToList(c, spaceId, listId, objects)
err := s.AddObjectsToList(c, spaceId, listId, request)
code := util.MapErrorCode(err,
util.ErrToCode(service.ErrFailedAddObjectsToList, http.StatusInternalServerError),
)
@ -149,13 +150,13 @@ func AddObjectsToListHandler(s *service.Service) gin.HandlerFunc {
//
// @Summary Remove object from list
// @Description Removes a given object from the specified list (collection only) in a space. The endpoint takes the space, list, and object identifiers as path parameters and is subject to rate limiting. It is used for dynamically managing collections without affecting the underlying object data.
// @Id removeListObject
// @Id remove_list_object
// @Tags Lists
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space to which the list belongs"
// @Param list_id path string true "The ID of the list from which the object will be removed"
// @Param object_id path string true "The ID of the object to remove from the list"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space to which the list belongs; must be retrieved from ListSpaces endpoint"
// @Param list_id path string true "The ID of the list from which the object will be removed; must be retrieved from SearchSpace endpoint with types: ['collection', 'set']"
// @Param object_id path string true "The ID of the object to remove from the list; must be retrieved from SearchSpace or GlobalSearch endpoints or obtained from response context"
// @Success 200 {object} string "Objects removed successfully"
// @Failure 400 {object} util.ValidationError "Bad request"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
@ -163,7 +164,7 @@ func AddObjectsToListHandler(s *service.Service) gin.HandlerFunc {
// @Failure 429 {object} util.RateLimitError "Rate limit exceeded"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/lists/{list_id}/objects/{object_id} [delete]
// @Router /v1/spaces/{space_id}/lists/{list_id}/objects/{object_id} [delete]
func RemoveObjectFromListHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")

View file

@ -15,18 +15,18 @@ import (
//
// @Summary List members
// @Description Returns a paginated list of members belonging to the specified space. Each member record includes the members profile ID, name, icon (which may be derived from an emoji or image), network identity, global name, status (e.g. joining, active) and role (e.g. Viewer, Editor, Owner). This endpoint supports collaborative features by allowing clients to show who is in a space and manage access rights.
// @Id listMembers
// @Id list_members
// @Tags Members
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space to list members for"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space to list members for; must be retrieved from ListSpaces endpoint"
// @Param offset query int false "The number of items to skip before starting to collect the result set" default(0)
// @Param limit query int false "The number of items to return" default(100) maximum(1000)
// @Success 200 {object} pagination.PaginatedResponse[apimodel.Member] "The list of members in the space"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/members [get]
// @Router /v1/spaces/{space_id}/members [get]
func ListMembersHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -52,18 +52,18 @@ func ListMembersHandler(s *service.Service) gin.HandlerFunc {
//
// @Summary Get member
// @Description Fetches detailed information about a single member within a space. The endpoint returns the members identifier, name, icon, identity, global name, status and role. The member_id path parameter can be provided as either the member's ID (starting with `_participant`) or the member's identity. This is useful for user profile pages, permission management, and displaying member-specific information in collaborative environments.
// @Id getMember
// @Id get_member
// @Tags Members
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space to get the member from"
// @Param member_id path string true "Member ID or Identity"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space to get the member from; must be retrieved from ListSpaces endpoint"
// @Param member_id path string true "Member ID or Identity; must be retrieved from ListMembers endpoint or obtained from response context"
// @Success 200 {object} apimodel.MemberResponse "The member details"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 404 {object} util.NotFoundError "Member not found"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/members/{member_id} [get]
// @Router /v1/spaces/{space_id}/members/{member_id} [get]
func GetMemberHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -90,13 +90,13 @@ func GetMemberHandler(s *service.Service) gin.HandlerFunc {
//
// @Summary Update member
// @Description Modifies a member's status and role in a space. Use this endpoint to approve a joining member by setting the status to `active` and specifying a role (`reader` or `writer`), reject a joining member by setting the status to `declined`, remove a member by setting the status to `removed`, or update an active member's role. This endpoint enables fine-grained control over member access and permissions.
// @Id updateMember
// @Id update_member
// @Tags Members
// @Accept json
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space to update the member in"
// @Param member_id path string true "The ID of the member to update"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space to update the member in; must be retrieved from ListSpaces endpoint"
// @Param member_id path string true "The ID or Identity of the member to update; must be retrieved from ListMembers endpoint or obtained from response context"
// @Param body body apimodel.UpdateMemberRequest true "The request body containing the member's new status and role"
// @Success 200 {object} apimodel.MemberResponse "Member updated successfully"
// @Failure 400 {object} util.ValidationError "Bad request"
@ -105,7 +105,7 @@ func GetMemberHandler(s *service.Service) gin.HandlerFunc {
// @Failure 429 {object} util.RateLimitError "Rate limit exceeded"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/members/{member_id} [patch]
// @Router /v1/spaces/{space_id}/members/{member_id} [patch]
func UpdateMemberHandler(s *SpaceService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")

View file

@ -15,18 +15,18 @@ import (
//
// @Summary List objects
// @Description Retrieves a paginated list of objects in the given space. The endpoint takes query parameters for pagination (offset and limit) and returns detailed data about each object including its ID, name, icon, type information, a snippet of the content (if applicable), layout, space ID, blocks and details. It is intended for building views where users can see all objects in a space at a glance.
// @Id listObjects
// @Id list_objects
// @Tags Objects
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space in which to list objects"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space in which to list objects; must be retrieved from ListSpaces endpoint"
// @Param offset query int false "The number of items to skip before starting to collect the result set" default(0)
// @Param limit query int false "The number of items to return" default(100) maximum(1000)
// @Success 200 {object} pagination.PaginatedResponse[apimodel.Object] "The list of objects in the specified space"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/objects [get]
// @Router /v1/spaces/{space_id}/objects [get]
func ListObjectsHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -54,12 +54,12 @@ func ListObjectsHandler(s *service.Service) gin.HandlerFunc {
//
// @Summary Get object
// @Description Fetches the full details of a single object identified by the object ID within the specified space. The response includes not only basic metadata (ID, name, icon, type) but also the complete set of blocks (which may include text, files, properties and dataviews) and extra details (such as timestamps and linked member information). This endpoint is essential when a client needs to render or edit the full object view.
// @Id getObject
// @Id get_object
// @Tags Objects
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space in which the object exists"
// @Param object_id path string true "The ID of the object to retrieve"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space in which the object exists; must be retrieved from ListSpaces endpoint"
// @Param object_id path string true "The ID of the object to retrieve; must be retrieved from ListObjects, SearchSpace or GlobalSearch endpoints or obtained from response context"
// @Param format query apimodel.BodyFormat false "The format to return the object body in" default("md")
// @Success 200 {object} apimodel.ObjectResponse "The retrieved object"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
@ -67,7 +67,7 @@ func ListObjectsHandler(s *service.Service) gin.HandlerFunc {
// @Failure 410 {object} util.GoneError "Resource deleted"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/objects/{object_id} [get]
// @Router /v1/spaces/{space_id}/objects/{object_id} [get]
func GetObjectHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -96,20 +96,20 @@ func GetObjectHandler(s *service.Service) gin.HandlerFunc {
//
// @Summary Create object
// @Description Creates a new object in the specified space using a JSON payload. The creation process is subject to rate limiting. The payload must include key details such as the object name, icon, description, body content (which may support Markdown), source URL (required for bookmark objects), template identifier, and the type_key (which is the non-unique identifier of the type of object to create). Post-creation, additional operations (like setting featured properties or fetching bookmark metadata) may occur. The endpoint then returns the full object data, ready for further interactions.
// @Id createObject
// @Id create_object
// @Tags Objects
// @Accept json
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space in which to create the object"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space in which to create the object; must be retrieved from ListSpaces endpoint"
// @Param object body apimodel.CreateObjectRequest true "The object to create"
// @Success 200 {object} apimodel.ObjectResponse "The created object"
// @Success 201 {object} apimodel.ObjectResponse "The created object"
// @Failure 400 {object} util.ValidationError "Bad request"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 429 {object} util.RateLimitError "Rate limit exceeded"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/objects [post]
// @Router /v1/spaces/{space_id}/objects [post]
func CreateObjectHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -139,7 +139,7 @@ func CreateObjectHandler(s *service.Service) gin.HandlerFunc {
return
}
c.JSON(http.StatusOK, apimodel.ObjectResponse{Object: object})
c.JSON(http.StatusCreated, apimodel.ObjectResponse{Object: object})
}
}
@ -147,13 +147,13 @@ func CreateObjectHandler(s *service.Service) gin.HandlerFunc {
//
// @Summary Update object
// @Description This endpoint updates an existing object in the specified space using a JSON payload. The update process is subject to rate limiting. The payload must include the details to be updated. The endpoint then returns the full object data, ready for further interactions.
// @Id updateObject
// @Id update_object
// @Tags Objects
// @Accept json
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space in which the object exists"
// @Param object_id path string true "The ID of the object to update"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space in which the object exists; must be retrieved from ListSpaces endpoint"
// @Param object_id path string true "The ID of the object to update; must be retrieved from ListObjects, SearchSpace or GlobalSearch endpoints or obtained from response context"
// @Param object body apimodel.UpdateObjectRequest true "The details of the object to update"
// @Success 200 {object} apimodel.ObjectResponse "The updated object"
// @Failure 400 {object} util.ValidationError "Bad request"
@ -163,7 +163,7 @@ func CreateObjectHandler(s *service.Service) gin.HandlerFunc {
// @Failure 429 {object} util.RateLimitError "Rate limit exceeded"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/objects/{object_id} [patch]
// @Router /v1/spaces/{space_id}/objects/{object_id} [patch]
func UpdateObjectHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -199,12 +199,12 @@ func UpdateObjectHandler(s *service.Service) gin.HandlerFunc {
//
// @Summary Delete object
// @Description This endpoint “deletes” an object by marking it as archived. The deletion process is performed safely and is subject to rate limiting. It returns the objects details after it has been archived. Proper error handling is in place for situations such as when the object isnt found or the deletion cannot be performed because of permission issues.
// @Id deleteObject
// @Id delete_object
// @Tags Objects
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space in which the object exists"
// @Param object_id path string true "The ID of the object to delete"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space in which the object exists; must be retrieved from ListSpaces endpoint"
// @Param object_id path string true "The ID of the object to delete; must be retrieved from ListObjects, SearchSpace or GlobalSearch endpoints or obtained from response context"
// @Success 200 {object} apimodel.ObjectResponse "The deleted object"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 403 {object} util.ForbiddenError "Forbidden"
@ -213,7 +213,7 @@ func UpdateObjectHandler(s *service.Service) gin.HandlerFunc {
// @Failure 429 {object} util.RateLimitError "Rate limit exceeded"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/objects/{object_id} [delete]
// @Router /v1/spaces/{space_id}/objects/{object_id} [delete]
func DeleteObjectHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")

View file

@ -15,18 +15,18 @@ import (
//
// @Summary List properties
// @Description ⚠ Warning: Properties are experimental and may change in the next update. ⚠ Retrieves a paginated list of properties available within a specific space. Each property record includes its unique identifier, name and format. This information is essential for clients to understand the available properties for filtering or creating objects.
// @Id listProperties
// @Id list_properties
// @Tags Properties
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space to list properties for"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space to list properties for; must be retrieved from ListSpaces endpoint"
// @Param offset query int false "The number of items to skip before starting to collect the result set" default(0)
// @Param limit query int false "The number of items to return" default(100) maximum(1000)
// @Success 200 {object} pagination.PaginatedResponse[apimodel.Property] "The list of properties in the specified space"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/properties [get]
// @Router /v1/spaces/{space_id}/properties [get]
func ListPropertiesHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -52,19 +52,19 @@ func ListPropertiesHandler(s *service.Service) gin.HandlerFunc {
//
// @Summary Get property
// @Description ⚠ Warning: Properties are experimental and may change in the next update. ⚠ Fetches detailed information about one specific property by its ID. This includes the propertys unique identifier, name and format. This detailed view assists clients in showing property options to users and in guiding the user interface (such as displaying appropriate input fields or selection options).
// @Id getProperty
// @Id get_property
// @Tags Properties
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space to which the property belongs"
// @Param property_id path string true "The ID of the property to retrieve"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space to which the property belongs; must be retrieved from ListSpaces endpoint"
// @Param property_id path string true "The ID of the property to retrieve; must be retrieved from ListProperties endpoint or obtained from response context"
// @Success 200 {object} apimodel.PropertyResponse "The requested property"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 404 {object} util.NotFoundError "Resource not found"
// @Failure 410 {object} util.GoneError "Resource deleted"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/properties/{property_id} [get]
// @Router /v1/spaces/{space_id}/properties/{property_id} [get]
func GetPropertyHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -91,20 +91,20 @@ func GetPropertyHandler(s *service.Service) gin.HandlerFunc {
//
// @Summary Create property
// @Description ⚠ Warning: Properties are experimental and may change in the next update. ⚠ Creates a new property in the specified space using a JSON payload. The creation process is subject to rate limiting. The payload must include property details such as the name and format. The endpoint then returns the full property data, ready for further interactions.
// @Id createProperty
// @Id create_property
// @Tags Properties
// @Accept json
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space to create the property in"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space to create the property in; must be retrieved from ListSpaces endpoint"
// @Param property body apimodel.CreatePropertyRequest true "The property to create"
// @Success 200 {object} apimodel.PropertyResponse "The created property"
// @Success 201 {object} apimodel.PropertyResponse "The created property"
// @Failure 400 {object} util.ValidationError "Bad request"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 429 {object} util.RateLimitError "Rate limit exceeded"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/properties [post]
// @Router /v1/spaces/{space_id}/properties [post]
func CreatePropertyHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -129,7 +129,7 @@ func CreatePropertyHandler(s *service.Service) gin.HandlerFunc {
return
}
c.JSON(http.StatusOK, apimodel.PropertyResponse{Property: property})
c.JSON(http.StatusCreated, apimodel.PropertyResponse{Property: property})
}
}
@ -137,13 +137,13 @@ func CreatePropertyHandler(s *service.Service) gin.HandlerFunc {
//
// @Summary Update property
// @Description ⚠ Warning: Properties are experimental and may change in the next update. ⚠ This endpoint updates an existing property in the specified space using a JSON payload. The update process is subject to rate limiting. The payload must include the name to be updated. The endpoint then returns the full property data, ready for further interactions.
// @Id updateProperty
// @Id update_property
// @Tags Properties
// @Accept json
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space to which the property belongs"
// @Param property_id path string true "The ID of the property to update"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space to which the property belongs; must be retrieved from ListSpaces endpoint"
// @Param property_id path string true "The ID of the property to update; must be retrieved from ListProperties endpoint or obtained from response context"
// @Param property body apimodel.UpdatePropertyRequest true "The property to update"
// @Success 200 {object} apimodel.PropertyResponse "The updated property"
// @Failure 400 {object} util.ValidationError "Bad request"
@ -154,7 +154,7 @@ func CreatePropertyHandler(s *service.Service) gin.HandlerFunc {
// @Failure 429 {object} util.RateLimitError "Rate limit exceeded"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/properties/{property_id} [patch]
// @Router /v1/spaces/{space_id}/properties/{property_id} [patch]
func UpdatePropertyHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -191,12 +191,12 @@ func UpdatePropertyHandler(s *service.Service) gin.HandlerFunc {
//
// @Summary Delete property
// @Description ⚠ Warning: Properties are experimental and may change in the next update. ⚠ This endpoint “deletes” a property by marking it as archived. The deletion process is performed safely and is subject to rate limiting. It returns the propertys details after it has been archived. Proper error handling is in place for situations such as when the property isnt found or the deletion cannot be performed because of permission issues.
// @Id deleteProperty
// @Id delete_property
// @Tags Properties
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space to which the property belongs"
// @Param property_id path string true "The ID of the property to delete"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space to which the property belongs; must be retrieved from ListSpaces endpoint"
// @Param property_id path string true "The ID of the property to delete; must be retrieved from ListProperties endpoint or obtained from response context"
// @Success 200 {object} apimodel.PropertyResponse "The deleted property"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 403 {object} util.ForbiddenError "Forbidden"
@ -205,7 +205,7 @@ func UpdatePropertyHandler(s *service.Service) gin.HandlerFunc {
// @Failure 429 {object} util.RateLimitError "Rate limit exceeded"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/properties/{property_id} [delete]
// @Router /v1/spaces/{space_id}/properties/{property_id} [delete]
func DeletePropertyHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")

View file

@ -14,12 +14,12 @@ import (
// GlobalSearchHandler searches and retrieves objects across all spaces
//
// @Summary Search objects across all spaces
// @Description Executes a global search over every space accessible by the authenticated user. The request body must specify the `query` text, optional filters on object types (e.g., "page", "task"), and sort directives (default: descending by last updated timestamp). Pagination is controlled via `offset` and `limit` query parameters to facilitate lazy loading in client UIs. The response returns a unified list of matched objects with their metadata and properties.
// @Id searchGlobal
// @Description Executes a global search over all spaces accessible to the authenticated user. The request body must specify the `query` text (currently matching only name and snippet of an object), optional filters on types (e.g., "page", "task"), and sort directives (default: descending by last modified date). Pagination is controlled via `offset` and `limit` query parameters to facilitate lazy loading in client UIs. The response returns a unified list of matched objects with their metadata and properties.
// @Id search_global
// @Tags Search
// @Accept json
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param offset query int false "The number of items to skip before starting to collect the result set" default(0)
// @Param limit query int false "The number of items to return" default(100) maximum(1000)
// @Param request body apimodel.SearchRequest true "The search parameters used to filter and sort the results"
@ -27,7 +27,7 @@ import (
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /search [post]
// @Router /v1/search [post]
func GlobalSearchHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
offset := c.GetInt("offset")
@ -58,13 +58,13 @@ func GlobalSearchHandler(s *service.Service) gin.HandlerFunc {
// SearchHandler searches and retrieves objects within a space
//
// @Summary Search objects within a space
// @Description Performs a focused search within a single space (specified by the space_id path parameter). Like the global search, it accepts pagination parameters and a JSON payload containing the search query, object types, and sorting preferences. The search is limited to the provided space and returns a list of objects that match the query. This allows clients to implement spacespecific filtering without having to process extraneous results.
// @Id searchSpace
// @Description Performs a search within a single space (specified by the `space_id` path parameter). Like the global search, it accepts pagination parameters and a JSON payload containing the search `query`, `types`, and sorting preferences. The search is limited to the provided space and returns a list of objects that match the query. This allows clients to implement spacespecific filtering without having to process extraneous results.
// @Id search_space
// @Tags Search
// @Accept json
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space to search in"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space to search in; must be retrieved from ListSpaces endpoint"
// @Param offset query int false "The number of items to skip before starting to collect the result set" default(0)
// @Param limit query int false "The number of items to return" default(100) maximum(1000)
// @Param request body apimodel.SearchRequest true "The search parameters used to filter and sort the results"
@ -72,7 +72,7 @@ func GlobalSearchHandler(s *service.Service) gin.HandlerFunc {
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/search [post]
// @Router /v1/spaces/{space_id}/search [post]
func SearchHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceID := c.Param("space_id")

View file

@ -15,17 +15,17 @@ import (
//
// @Summary List spaces
// @Description Retrieves a paginated list of all spaces that are accessible by the authenticated user. Each space record contains detailed information such as the space ID, name, icon (derived either from an emoji or image URL), and additional metadata. This endpoint is key to displaying a users workspaces.
// @Id listSpaces
// @Id list_spaces
// @Tags Spaces
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param offset query int false "The number of items to skip before starting to collect the result set" default(0)
// @Param limit query int false "The number of items to return" default(100) maximum(1000)
// @Success 200 {object} pagination.PaginatedResponse[apimodel.Space] "The list of spaces accessible by the authenticated user"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces [get]
// @Router /v1/spaces [get]
func ListSpacesHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
offset := c.GetInt("offset")
@ -52,17 +52,17 @@ func ListSpacesHandler(s *service.Service) gin.HandlerFunc {
//
// @Summary Get space
// @Description Fetches full details about a single space identified by its space ID. The response includes metadata such as the space name, icon, and various workspace IDs (home, archive, profile, etc.). This detailed view supports use cases such as displaying space-specific settings.
// @Id getSpace
// @Id get_space
// @Tags Spaces
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space to retrieve"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space to retrieve; must be retrieved from ListSpaces endpoint"
// @Success 200 {object} apimodel.SpaceResponse "The space details"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 404 {object} util.NotFoundError "Space not found"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id} [get]
// @Router /v1/spaces/{space_id} [get]
func GetSpaceHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -88,19 +88,19 @@ func GetSpaceHandler(s *service.Service) gin.HandlerFunc {
//
// @Summary Create space
// @Description Creates a new space based on a supplied name and description in the JSON request body. The endpoint is subject to rate limiting and automatically applies default configurations such as generating a random icon and initializing the workspace with default settings (for example, a default dashboard or home page). On success, the new spaces full metadata is returned, enabling the client to immediately switch context to the new internal.
// @Id createSpace
// @Id create_space
// @Tags Spaces
// @Accept json
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param name body apimodel.CreateSpaceRequest true "The space to create"
// @Success 200 {object} apimodel.SpaceResponse "The created space"
// @Success 201 {object} apimodel.SpaceResponse "The created space"
// @Failure 400 {object} util.ValidationError "Bad request"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 429 {object} util.RateLimitError "Rate limit exceeded"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces [post]
// @Router /v1/spaces [post]
func CreateSpaceHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
var req apimodel.CreateSpaceRequest
@ -124,7 +124,7 @@ func CreateSpaceHandler(s *service.Service) gin.HandlerFunc {
return
}
c.JSON(http.StatusOK, apimodel.SpaceResponse{Space: space})
c.JSON(http.StatusCreated, apimodel.SpaceResponse{Space: space})
}
}
@ -132,12 +132,12 @@ func CreateSpaceHandler(s *service.Service) gin.HandlerFunc {
//
// @Summary Update space
// @Description Updates the name or description of an existing space. The request body should contain the new name and/or description in JSON format. This endpoint is useful for renaming or rebranding a workspace without needing to recreate it. The updated spaces metadata is returned in the response.
// @Id updateSpace
// @Id update_space
// @Tags Spaces
// @Accept json
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space to update"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space to update; must be retrieved from ListSpaces endpoint"
// @Param name body apimodel.UpdateSpaceRequest true "The space details to update"
// @Success 200 {object} apimodel.SpaceResponse "The updated space"
// @Failure 400 {object} util.ValidationError "Bad request"
@ -147,7 +147,7 @@ func CreateSpaceHandler(s *service.Service) gin.HandlerFunc {
// @Failure 429 {object} util.RateLimitError "Rate limit exceeded"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id} [patch]
// @Router /v1/spaces/{space_id} [patch]
func UpdateSpaceHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")

View file

@ -15,18 +15,18 @@ import (
//
// @Summary List tags
// @Description This endpoint retrieves a paginated list of tags available for a specific property within a space. Each tag record includes its unique identifier, name, and color. This information is essential for clients to display select or multi-select options to users when they are creating or editing objects. The endpoint also supports pagination through offset and limit parameters.
// @Id listTags
// @Id list_tags
// @Tags Tags
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space to list tags for"
// @Param property_id path string true "The ID of the property to list tags for"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space to list tags for; must be retrieved from ListSpaces endpoint"
// @Param property_id path string true "The ID of the property to list tags for; must be retrieved from ListProperties endpoint or obtained from response context"
// @Success 200 {object} pagination.PaginatedResponse[apimodel.Tag] "The list of tags"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 404 {object} util.NotFoundError "Property not found"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/properties/{property_id}/tags [get]
// @Router /v1/spaces/{space_id}/properties/{property_id}/tags [get]
func ListTagsHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -54,20 +54,20 @@ func ListTagsHandler(s *service.Service) gin.HandlerFunc {
//
// @Summary Get tag
// @Description This endpoint retrieves a tag for a given property id. The tag is identified by its unique identifier within the specified space. The response includes the tag's details such as its ID, name, and color. This is useful for clients to display or when editing a specific tag option.
// @Id getTag
// @Id get_tag
// @Tags Tags
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space to retrieve the tag from"
// @Param property_id path string true "The ID of the property to retrieve the tag for"
// @Param tag_id path string true "The ID of the tag to retrieve"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space to retrieve the tag from; must be retrieved from ListSpaces endpoint"
// @Param property_id path string true "The ID of the property to retrieve the tag for; must be retrieved from ListProperties endpoint or obtained from response context"
// @Param tag_id path string true "The ID of the tag to retrieve; must be retrieved from ListTags endpoint or obtained from response context"
// @Success 200 {object} apimodel.TagResponse "The retrieved tag"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 404 {object} util.NotFoundError "Resource not found"
// @Failure 410 {object} util.GoneError "Resource deleted"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/properties/{property_id}/tags/{tag_id} [get]
// @Router /v1/spaces/{space_id}/properties/{property_id}/tags/{tag_id} [get]
func GetTagHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -95,21 +95,21 @@ func GetTagHandler(s *service.Service) gin.HandlerFunc {
//
// @Summary Create tag
// @Description This endpoint creates a new tag for a given property id in a space. The creation process is subject to rate limiting. The tag is identified by its unique identifier within the specified space. The request must include the tag's name and color. The response includes the tag's details such as its ID, name, and color. This is useful for clients when users want to add new tag options to a property.
// @Id createTag
// @Id create_tag
// @Tags Tags
// @Accept json
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space to create the tag in"
// @Param property_id path string true "The ID of the property to create the tag for"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space to create the tag in; must be retrieved from ListSpaces endpoint"
// @Param property_id path string true "The ID of the property to create the tag for; must be retrieved from ListProperties endpoint or obtained from response context"
// @Param tag body apimodel.CreateTagRequest true "The tag to create"
// @Success 200 {object} apimodel.TagResponse "The created tag"
// @Success 201 {object} apimodel.TagResponse "The created tag"
// @Failure 400 {object} util.ValidationError "Bad request"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 429 {object} util.RateLimitError "Rate limit exceeded"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/properties/{property_id}/tags [post]
// @Router /v1/spaces/{space_id}/properties/{property_id}/tags [post]
func CreateTagHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -135,7 +135,7 @@ func CreateTagHandler(s *service.Service) gin.HandlerFunc {
return
}
c.JSON(http.StatusOK, apimodel.TagResponse{Tag: option})
c.JSON(http.StatusCreated, apimodel.TagResponse{Tag: option})
}
}
@ -143,14 +143,14 @@ func CreateTagHandler(s *service.Service) gin.HandlerFunc {
//
// @Summary Update tag
// @Description This endpoint updates a tag for a given property id in a space. The update process is subject to rate limiting. The tag is identified by its unique identifier within the specified space. The request must include the tag's name and color. The response includes the tag's details such as its ID, name, and color. This is useful for clients when users want to edit existing tags for a property.
// @Id updateTag
// @Id update_tag
// @Tags Tags
// @Accept json
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space to update the tag in"
// @Param property_id path string true "The ID of the property to update the tag for"
// @Param tag_id path string true "The ID of the tag to update"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space to update the tag in; must be retrieved from ListSpaces endpoint"
// @Param property_id path string true "The ID of the property to update the tag for; must be retrieved from ListProperties endpoint or obtained from response context"
// @Param tag_id path string true "The ID of the tag to update; must be retrieved from ListTags endpoint or obtained from response context"
// @Param tag body apimodel.UpdateTagRequest true "The tag to update"
// @Success 200 {object} apimodel.TagResponse "The updated tag"
// @Failure 400 {object} util.ValidationError "Bad request"
@ -161,7 +161,7 @@ func CreateTagHandler(s *service.Service) gin.HandlerFunc {
// @Failure 429 {object} util.RateLimitError "Rate limit exceeded"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/properties/{property_id}/tags/{tag_id} [patch]
// @Router /v1/spaces/{space_id}/properties/{property_id}/tags/{tag_id} [patch]
func UpdateTagHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -198,13 +198,13 @@ func UpdateTagHandler(s *service.Service) gin.HandlerFunc {
//
// @Summary Delete tag
// @Description This endpoint “deletes” a tag by marking it as archived. The deletion process is performed safely and is subject to rate limiting. It returns the tags details after it has been archived. Proper error handling is in place for situations such as when the tag isnt found or the deletion cannot be performed because of permission issues.
// @Id deleteTag
// @Id delete_tag
// @Tags Tags
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space to delete the tag from"
// @Param property_id path string true "The ID of the property to delete the tag for"
// @Param tag_id path string true "The ID of the tag to delete"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space to delete the tag from; must be retrieved from ListSpaces endpoint"
// @Param property_id path string true "The ID of the property to delete the tag for; must be retrieved from ListProperties endpoint or obtained from response context"
// @Param tag_id path string true "The ID of the tag to delete; must be retrieved from ListTags endpoint or obtained from response context"
// @Success 200 {object} apimodel.TagResponse "The deleted tag"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 403 {object} util.ForbiddenError "Forbidden"
@ -213,7 +213,7 @@ func UpdateTagHandler(s *service.Service) gin.HandlerFunc {
// @Failure 429 {object} util.RateLimitError "Rate limit exceeded"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/properties/{property_id}/tags/{tag_id} [delete]
// @Router /v1/spaces/{space_id}/properties/{property_id}/tags/{tag_id} [delete]
func DeleteTagHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")

View file

@ -14,20 +14,20 @@ import (
// ListTemplatesHandler retrieves a list of templates for a type in a space
//
// @Summary List templates
// @Description This endpoint returns a paginated list of templates that are associated with a specific object type within a space. Templates provide preconfigured structures for creating new objects. Each template record contains its identifier, name, and icon, so that clients can offer users a selection of templates when creating objects.
// @Id listTemplates
// @Description This endpoint returns a paginated list of templates that are associated with a specific type within a space. Templates provide preconfigured structures for creating new objects. Each template record contains its identifier, name, and icon, so that clients can offer users a selection of templates when creating objects.
// @Id list_templates
// @Tags Templates
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space to which the type belongs"
// @Param type_id path string true "The ID of the object type to retrieve templates for"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space to which the type belongs; must be retrieved from ListSpaces endpoint"
// @Param type_id path string true "The ID of the type to retrieve templates for; must be retrieved from ListTypes endpoint or obtained from response context"
// @Param offset query int false "The number of items to skip before starting to collect the result set" default(0)
// @Param limit query int false "The number of items to return" default(100) maximum(1000)
// @Success 200 {object} pagination.PaginatedResponse[apimodel.Object] "List of templates"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/types/{type_id}/templates [get]
// @Router /v1/spaces/{space_id}/types/{type_id}/templates [get]
func ListTemplatesHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -56,21 +56,21 @@ func ListTemplatesHandler(s *service.Service) gin.HandlerFunc {
// GetTemplateHandler retrieves a template for a type in a space
//
// @Summary Get template
// @Description Fetches full details for one template associated with a particular object type in a space. The response provides the templates identifier, name, icon, and any other relevant metadata. This endpoint is useful when a client needs to preview or apply a template to prefill object creation fields.
// @Id getTemplate
// @Description Fetches full details for one template associated with a particular type in a space. The response provides the templates identifier, name, icon, and any other relevant metadata. This endpoint is useful when a client needs to preview or apply a template to prefill object creation fields.
// @Id get_template
// @Tags Templates
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space to which the template belongs"
// @Param type_id path string true "The ID of the object type to which the template belongs"
// @Param template_id path string true "The ID of the template to retrieve"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space to which the template belongs; must be retrieved from ListSpaces endpoint"
// @Param type_id path string true "The ID of the type to which the template belongs; must be retrieved from ListTypes endpoint or obtained from response context"
// @Param template_id path string true "The ID of the template to retrieve; must be retrieved from ListTemplates endpoint or obtained from response context"
// @Success 200 {object} apimodel.TemplateResponse "The requested template"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 404 {object} util.NotFoundError "Resource not found"
// @Failure 410 {object} util.GoneError "Resource deleted"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/types/{type_id}/templates/{template_id} [get]
// @Router /v1/spaces/{space_id}/types/{type_id}/templates/{template_id} [get]
func GetTemplateHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")

View file

@ -14,19 +14,19 @@ import (
// ListTypesHandler retrieves a list of types in a space
//
// @Summary List types
// @Description This endpoint retrieves a paginated list of object types (e.g. 'Page', 'Note', 'Task') available within the specified space. Each types record includes its unique identifier, type key, display name, icon, and layout. While a type's id is truly unique, a type's key can be the same across spaces for known types, e.g. 'page' for 'Page'. Clients use this information when offering choices for object creation or for filtering objects by type through search.
// @Id listTypes
// @Description This endpoint retrieves a paginated list of types (e.g. 'Page', 'Note', 'Task') available within the specified space. Each types record includes its unique identifier, type key, display name, icon, and layout. While a type's id is truly unique, a type's key can be the same across spaces for known types, e.g. 'page' for 'Page'. Clients use this information when offering choices for object creation or for filtering objects by type through search.
// @Id list_types
// @Tags Types
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space to retrieve types from"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space to retrieve types from; must be retrieved from ListSpaces endpoint"
// @Param offset query int false "The number of items to skip before starting to collect the result set" default(0)
// @Param limit query int false "The number of items to return" default(100) maximum(1000)
// @Success 200 {object} pagination.PaginatedResponse[apimodel.Type] "The list of types"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/types [get]
// @Router /v1/spaces/{space_id}/types [get]
func ListTypesHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -51,20 +51,20 @@ func ListTypesHandler(s *service.Service) gin.HandlerFunc {
// GetTypeHandler retrieves a type in a space
//
// @Summary Get type
// @Description Fetches detailed information about one specific object type by its ID. This includes the types unique key, name, icon, and layout. This detailed view assists clients in understanding the expected structure and style for objects of that type and in guiding the user interface (such as displaying appropriate icons or layout hints).
// @Id getType
// @Description Fetches detailed information about one specific type by its ID. This includes the types unique key, name, icon, and layout. This detailed view assists clients in understanding the expected structure and style for objects of that type and in guiding the user interface (such as displaying appropriate icons or layout hints).
// @Id get_type
// @Tags Types
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space from which to retrieve the type"
// @Param type_id path string true "The ID of the type to retrieve"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space from which to retrieve the type; must be retrieved from ListSpaces endpoint"
// @Param type_id path string true "The ID of the type to retrieve; must be retrieved from ListTypes endpoint or obtained from response context"
// @Success 200 {object} apimodel.TypeResponse "The requested type"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 404 {object} util.NotFoundError "Resource not found"
// @Failure 410 {object} util.GoneError "Resource deleted"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/types/{type_id} [get]
// @Router /v1/spaces/{space_id}/types/{type_id} [get]
func GetTypeHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -90,21 +90,21 @@ func GetTypeHandler(s *service.Service) gin.HandlerFunc {
// CreateTypeHandler creates a new type in a space
//
// @Summary Create type
// @Description Creates a new object type in the specified space using a JSON payload. The creation process is subject to rate limiting. The payload must include type details such as the name, icon, and layout. The endpoint then returns the full type data, ready to be used for creating objects.
// @Id createType
// @Description Creates a new type in the specified space using a JSON payload. The creation process is subject to rate limiting. The payload must include type details such as the name, icon, and layout. The endpoint then returns the full type data, ready to be used for creating objects.
// @Id create_type
// @Tags Types
// @Accept json
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space in which to create the type"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space in which to create the type; must be retrieved from ListSpaces endpoint"
// @Param type body apimodel.CreateTypeRequest true "The type to create"
// @Success 200 {object} apimodel.TypeResponse "The created type"
// @Success 201 {object} apimodel.TypeResponse "The created type"
// @Failure 400 {object} util.ValidationError "Bad request"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 429 {object} util.RateLimitError "Rate limit exceeded"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/types [post]
// @Router /v1/spaces/{space_id}/types [post]
func CreateTypeHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -129,21 +129,21 @@ func CreateTypeHandler(s *service.Service) gin.HandlerFunc {
return
}
c.JSON(http.StatusOK, apimodel.TypeResponse{Type: object})
c.JSON(http.StatusCreated, apimodel.TypeResponse{Type: object})
}
}
// UpdateTypeHandler updates a type in a space
//
// @Summary Update type
// @Description This endpoint updates an existing object type in the specified space using a JSON payload. The update process is subject to rate limiting. The payload must include the name and properties to be updated. The endpoint then returns the full type data, ready for further interactions.
// @Id updateType
// @Description This endpoint updates an existing type in the specified space using a JSON payload. The update process is subject to rate limiting. The payload must include the name and properties to be updated. The endpoint then returns the full type data, ready for further interactions.
// @Id update_type
// @Tags Types
// @Accept json
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space in which the type exists"
// @Param type_id path string true "The ID of the type to update"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space in which the type exists; must be retrieved from ListSpaces endpoint"
// @Param type_id path string true "The ID of the type to update; must be retrieved from ListTypes endpoint or obtained from response context"
// @Param type body apimodel.UpdateTypeRequest true "The type details to update"
// @Success 200 {object} apimodel.TypeResponse "The updated type"
// @Failure 400 {object} util.ValidationError "Bad request"
@ -153,7 +153,7 @@ func CreateTypeHandler(s *service.Service) gin.HandlerFunc {
// @Failure 429 {object} util.RateLimitError "Rate limit exceeded"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/types/{type_id} [patch]
// @Router /v1/spaces/{space_id}/types/{type_id} [patch]
func UpdateTypeHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
@ -188,13 +188,13 @@ func UpdateTypeHandler(s *service.Service) gin.HandlerFunc {
// DeleteTypeHandler deletes a type in a space
//
// @Summary Delete type
// @Description This endpoint “deletes” an object type by marking it as archived. The deletion process is performed safely and is subject to rate limiting. It returns the types details after it has been archived. Proper error handling is in place for situations such as when the type isnt found or the deletion cannot be performed because of permission issues.
// @Id deleteType
// @Description This endpoint “deletes” an type by marking it as archived. The deletion process is performed safely and is subject to rate limiting. It returns the types details after it has been archived. Proper error handling is in place for situations such as when the type isnt found or the deletion cannot be performed because of permission issues.
// @Id delete_type
// @Tags Types
// @Produce json
// @Param Anytype-Version header string true "The version of the API to use" default(2025-04-22)
// @Param space_id path string true "The ID of the space from which to delete the type"
// @Param type_id path string true "The ID of the type to delete"
// @Param Anytype-Version header string true "The version of the API to use" default(2025-05-20)
// @Param space_id path string true "The ID of the space from which to delete the type; must be retrieved from ListSpaces endpoint"
// @Param type_id path string true "The ID of the type to delete; must be retrieved from ListTypes endpoint or obtained from response context"
// @Success 200 {object} apimodel.TypeResponse "The deleted type"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 403 {object} util.ForbiddenError "Forbidden"
@ -203,7 +203,7 @@ func UpdateTypeHandler(s *service.Service) gin.HandlerFunc {
// @Failure 429 {object} util.RateLimitError "Rate limit exceeded"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Security bearerauth
// @Router /spaces/{space_id}/types/{type_id} [delete]
// @Router /v1/spaces/{space_id}/types/{type_id} [delete]
func DeleteTypeHandler(s *service.Service) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")

View file

@ -1,9 +1,28 @@
package apimodel
// TO BE DEPRECATED
type DisplayCodeResponse struct {
ChallengeId string `json:"challenge_id" example:"67647f5ecda913e9a2e11b26"` // The challenge id associated with the displayed code and needed to solve the challenge for token
}
// TO BE DEPRECATED
type TokenResponse struct {
AppKey string `json:"app_key" example:"zhSG/zQRmgADyilWPtgdnfo1qD60oK02/SVgi1GaFt6="` // The app key used to authenticate requests
}
type CreateChallengeRequest struct {
AppName string `json:"app_name" example:"anytype_mcp"` // The name of the app that is requesting the challenge
}
type CreateChallengeResponse struct {
ChallengeId string `json:"challenge_id" example:"67647f5ecda913e9a2e11b26"` // The challenge id associated with the displayed code and needed to solve the challenge for api_key
}
type CreateApiKeyRequest struct {
ChallengeId string `json:"challenge_id" example:"67647f5ecda913e9a2e11b26"` // The challenge id associated with the previously displayed code
Code string `json:"code" example:"1234"` // The 4-digit code retrieved from Anytype Desktop app
}
type CreateApiKeyResponse struct {
ApiKey string `json:"api_key" example:"zhSG/zQRmgADyilWPtgdnfo1qD60oK02/SVgi1GaFt6="` // The api key used to authenticate requests
}

View file

@ -3,7 +3,6 @@ package apimodel
import (
"encoding/json"
"fmt"
"unicode"
"github.com/anyproto/anytype-heart/core/api/util"
)
@ -59,7 +58,814 @@ func (c *Color) UnmarshalJSON(data []byte) error {
}
}
var iconOptionToColor = map[float64]Color{
type IconName string
const (
IconNameAccessibility IconName = "accessibility"
IconNameAddCircle IconName = "add-circle"
IconNameAirplane IconName = "airplane"
IconNameAlarm IconName = "alarm"
IconNameAlbums IconName = "albums"
IconNameAlertCircle IconName = "alert-circle"
IconNameAmericanFootball IconName = "american-football"
IconNameAnalytics IconName = "analytics"
IconNameAperture IconName = "aperture"
IconNameApps IconName = "apps"
IconNameArchive IconName = "archive"
IconNameArrowBackCircle IconName = "arrow-back-circle"
IconNameArrowDownCircle IconName = "arrow-down-circle"
IconNameArrowForwardCircle IconName = "arrow-forward-circle"
IconNameArrowRedoCircle IconName = "arrow-redo-circle"
IconNameArrowRedo IconName = "arrow-redo"
IconNameArrowUndoCircle IconName = "arrow-undo-circle"
IconNameArrowUndo IconName = "arrow-undo"
IconNameArrowUpCircle IconName = "arrow-up-circle"
IconNameAtCircle IconName = "at-circle"
IconNameAttach IconName = "attach"
IconNameBackspace IconName = "backspace"
IconNameBagAdd IconName = "bag-add"
IconNameBagCheck IconName = "bag-check"
IconNameBagHandle IconName = "bag-handle"
IconNameBagRemove IconName = "bag-remove"
IconNameBag IconName = "bag"
IconNameBalloon IconName = "balloon"
IconNameBan IconName = "ban"
IconNameBandage IconName = "bandage"
IconNameBarChart IconName = "bar-chart"
IconNameBarbell IconName = "barbell"
IconNameBarcode IconName = "barcode"
IconNameBaseball IconName = "baseball"
IconNameBasket IconName = "basket"
IconNameBasketball IconName = "basketball"
IconNameBatteryCharging IconName = "battery-charging"
IconNameBatteryDead IconName = "battery-dead"
IconNameBatteryFull IconName = "battery-full"
IconNameBatteryHalf IconName = "battery-half"
IconNameBeaker IconName = "beaker"
IconNameBed IconName = "bed"
IconNameBeer IconName = "beer"
IconNameBicycle IconName = "bicycle"
IconNameBinoculars IconName = "binoculars"
IconNameBluetooth IconName = "bluetooth"
IconNameBoat IconName = "boat"
IconNameBody IconName = "body"
IconNameBonfire IconName = "bonfire"
IconNameBook IconName = "book"
IconNameBookmark IconName = "bookmark"
IconNameBookmarks IconName = "bookmarks"
IconNameBowlingBall IconName = "bowling-ball"
IconNameBriefcase IconName = "briefcase"
IconNameBrowsers IconName = "browsers"
IconNameBrush IconName = "brush"
IconNameBug IconName = "bug"
IconNameBuild IconName = "build"
IconNameBulb IconName = "bulb"
IconNameBus IconName = "bus"
IconNameBusiness IconName = "business"
IconNameCafe IconName = "cafe"
IconNameCalculator IconName = "calculator"
IconNameCalendarClear IconName = "calendar-clear"
IconNameCalendarNumber IconName = "calendar-number"
IconNameCalendar IconName = "calendar"
IconNameCall IconName = "call"
IconNameCameraReverse IconName = "camera-reverse"
IconNameCamera IconName = "camera"
IconNameCarSport IconName = "car-sport"
IconNameCar IconName = "car"
IconNameCard IconName = "card"
IconNameCaretBackCircle IconName = "caret-back-circle"
IconNameCaretBack IconName = "caret-back"
IconNameCaretDownCircle IconName = "caret-down-circle"
IconNameCaretDown IconName = "caret-down"
IconNameCaretForwardCircle IconName = "caret-forward-circle"
IconNameCaretForward IconName = "caret-forward"
IconNameCaretUpCircle IconName = "caret-up-circle"
IconNameCaretUp IconName = "caret-up"
IconNameCart IconName = "cart"
IconNameCash IconName = "cash"
IconNameCellular IconName = "cellular"
IconNameChatboxEllipses IconName = "chatbox-ellipses"
IconNameChatbox IconName = "chatbox"
IconNameChatbubbleEllipses IconName = "chatbubble-ellipses"
IconNameChatbubble IconName = "chatbubble"
IconNameChatbubbles IconName = "chatbubbles"
IconNameCheckbox IconName = "checkbox"
IconNameCheckmarkCircle IconName = "checkmark-circle"
IconNameCheckmarkDoneCircle IconName = "checkmark-done-circle"
IconNameChevronBackCircle IconName = "chevron-back-circle"
IconNameChevronDownCircle IconName = "chevron-down-circle"
IconNameChevronForwardCircle IconName = "chevron-forward-circle"
IconNameChevronUpCircle IconName = "chevron-up-circle"
IconNameClipboard IconName = "clipboard"
IconNameCloseCircle IconName = "close-circle"
IconNameCloudCircle IconName = "cloud-circle"
IconNameCloudDone IconName = "cloud-done"
IconNameCloudDownload IconName = "cloud-download"
IconNameCloudOffline IconName = "cloud-offline"
IconNameCloudUpload IconName = "cloud-upload"
IconNameCloud IconName = "cloud"
IconNameCloudyNight IconName = "cloudy-night"
IconNameCloudy IconName = "cloudy"
IconNameCodeSlash IconName = "code-slash"
IconNameCode IconName = "code"
IconNameCog IconName = "cog"
IconNameColorFill IconName = "color-fill"
IconNameColorFilter IconName = "color-filter"
IconNameColorPalette IconName = "color-palette"
IconNameColorWand IconName = "color-wand"
IconNameCompass IconName = "compass"
IconNameConstruct IconName = "construct"
IconNameContact IconName = "contact"
IconNameContract IconName = "contract"
IconNameContrast IconName = "contrast"
IconNameCopy IconName = "copy"
IconNameCreate IconName = "create"
IconNameCrop IconName = "crop"
IconNameCube IconName = "cube"
IconNameCut IconName = "cut"
IconNameDesktop IconName = "desktop"
IconNameDiamond IconName = "diamond"
IconNameDice IconName = "dice"
IconNameDisc IconName = "disc"
IconNameDocumentAttach IconName = "document-attach"
IconNameDocumentLock IconName = "document-lock"
IconNameDocumentText IconName = "document-text"
IconNameDocument IconName = "document"
IconNameDocuments IconName = "documents"
IconNameDownload IconName = "download"
IconNameDuplicate IconName = "duplicate"
IconNameEar IconName = "ear"
IconNameEarth IconName = "earth"
IconNameEasel IconName = "easel"
IconNameEgg IconName = "egg"
IconNameEllipse IconName = "ellipse"
IconNameEllipsisHorizontalCircle IconName = "ellipsis-horizontal-circle"
IconNameEllipsisVerticalCircle IconName = "ellipsis-vertical-circle"
IconNameEnter IconName = "enter"
IconNameExit IconName = "exit"
IconNameExpand IconName = "expand"
IconNameExtensionPuzzle IconName = "extension-puzzle"
IconNameEyeOff IconName = "eye-off"
IconNameEye IconName = "eye"
IconNameEyedrop IconName = "eyedrop"
IconNameFastFood IconName = "fast-food"
IconNameFemale IconName = "female"
IconNameFileTrayFull IconName = "file-tray-full"
IconNameFileTrayStacked IconName = "file-tray-stacked"
IconNameFileTray IconName = "file-tray"
IconNameFilm IconName = "film"
IconNameFilterCircle IconName = "filter-circle"
IconNameFingerPrint IconName = "finger-print"
IconNameFish IconName = "fish"
IconNameFitness IconName = "fitness"
IconNameFlag IconName = "flag"
IconNameFlame IconName = "flame"
IconNameFlashOff IconName = "flash-off"
IconNameFlash IconName = "flash"
IconNameFlashlight IconName = "flashlight"
IconNameFlask IconName = "flask"
IconNameFlower IconName = "flower"
IconNameFolderOpen IconName = "folder-open"
IconNameFolder IconName = "folder"
IconNameFootball IconName = "football"
IconNameFootsteps IconName = "footsteps"
IconNameFunnel IconName = "funnel"
IconNameGameController IconName = "game-controller"
IconNameGift IconName = "gift"
IconNameGitBranch IconName = "git-branch"
IconNameGitCommit IconName = "git-commit"
IconNameGitCompare IconName = "git-compare"
IconNameGitMerge IconName = "git-merge"
IconNameGitNetwork IconName = "git-network"
IconNameGitPullRequest IconName = "git-pull-request"
IconNameGlasses IconName = "glasses"
IconNameGlobe IconName = "globe"
IconNameGolf IconName = "golf"
IconNameGrid IconName = "grid"
IconNameHammer IconName = "hammer"
IconNameHandLeft IconName = "hand-left"
IconNameHandRight IconName = "hand-right"
IconNameHappy IconName = "happy"
IconNameHardwareChip IconName = "hardware-chip"
IconNameHeadset IconName = "headset"
IconNameHeartCircle IconName = "heart-circle"
IconNameHeartDislikeCircle IconName = "heart-dislike-circle"
IconNameHeartDislike IconName = "heart-dislike"
IconNameHeartHalf IconName = "heart-half"
IconNameHeart IconName = "heart"
IconNameHelpBuoy IconName = "help-buoy"
IconNameHelpCircle IconName = "help-circle"
IconNameHome IconName = "home"
IconNameHourglass IconName = "hourglass"
IconNameIceCream IconName = "ice-cream"
IconNameIdCard IconName = "id-card"
IconNameImage IconName = "image"
IconNameImages IconName = "images"
IconNameInfinite IconName = "infinite"
IconNameInformationCircle IconName = "information-circle"
IconNameInvertMode IconName = "invert-mode"
IconNameJournal IconName = "journal"
IconNameKey IconName = "key"
IconNameKeypad IconName = "keypad"
IconNameLanguage IconName = "language"
IconNameLaptop IconName = "laptop"
IconNameLayers IconName = "layers"
IconNameLeaf IconName = "leaf"
IconNameLibrary IconName = "library"
IconNameLink IconName = "link"
IconNameListCircle IconName = "list-circle"
IconNameList IconName = "list"
IconNameLocate IconName = "locate"
IconNameLocation IconName = "location"
IconNameLockClosed IconName = "lock-closed"
IconNameLockOpen IconName = "lock-open"
IconNameLogIn IconName = "log-in"
IconNameLogOut IconName = "log-out"
IconNameLogoAlipay IconName = "logo-alipay"
IconNameLogoAmazon IconName = "logo-amazon"
IconNameLogoAmplify IconName = "logo-amplify"
IconNameLogoAndroid IconName = "logo-android"
IconNameMagnet IconName = "magnet"
IconNameMailOpen IconName = "mail-open"
IconNameMailUnread IconName = "mail-unread"
IconNameMail IconName = "mail"
IconNameMaleFemale IconName = "male-female"
IconNameMale IconName = "male"
IconNameMan IconName = "man"
IconNameMap IconName = "map"
IconNameMedal IconName = "medal"
IconNameMedical IconName = "medical"
IconNameMedkit IconName = "medkit"
IconNameMegaphone IconName = "megaphone"
IconNameMenu IconName = "menu"
IconNameMicCircle IconName = "mic-circle"
IconNameMicOffCircle IconName = "mic-off-circle"
IconNameMicOff IconName = "mic-off"
IconNameMic IconName = "mic"
IconNameMoon IconName = "moon"
IconNameMove IconName = "move"
IconNameMusicalNote IconName = "musical-note"
IconNameMusicalNotes IconName = "musical-notes"
IconNameNavigateCircle IconName = "navigate-circle"
IconNameNavigate IconName = "navigate"
IconNameNewspaper IconName = "newspaper"
IconNameNotificationsCircle IconName = "notifications-circle"
IconNameNotificationsOffCircle IconName = "notifications-off-circle"
IconNameNotificationsOff IconName = "notifications-off"
IconNameNotifications IconName = "notifications"
IconNameNuclear IconName = "nuclear"
IconNameNutrition IconName = "nutrition"
IconNameOptions IconName = "options"
IconNamePaperPlane IconName = "paper-plane"
IconNamePartlySunny IconName = "partly-sunny"
IconNamePauseCircle IconName = "pause-circle"
IconNamePause IconName = "pause"
IconNamePaw IconName = "paw"
IconNamePencil IconName = "pencil"
IconNamePeopleCircle IconName = "people-circle"
IconNamePeople IconName = "people"
IconNamePersonAdd IconName = "person-add"
IconNamePersonCircle IconName = "person-circle"
IconNamePersonRemove IconName = "person-remove"
IconNamePerson IconName = "person"
IconNamePhoneLandscape IconName = "phone-landscape"
IconNamePhonePortrait IconName = "phone-portrait"
IconNamePieChart IconName = "pie-chart"
IconNamePin IconName = "pin"
IconNamePint IconName = "pint"
IconNamePizza IconName = "pizza"
IconNamePlanet IconName = "planet"
IconNamePlayBackCircle IconName = "play-back-circle"
IconNamePlayBack IconName = "play-back"
IconNamePlayCircle IconName = "play-circle"
IconNamePlayForwardCircle IconName = "play-forward-circle"
IconNamePlayForward IconName = "play-forward"
IconNamePlaySkipBackCircle IconName = "play-skip-back-circle"
IconNamePlaySkipBack IconName = "play-skip-back"
IconNamePlaySkipForwardCircle IconName = "play-skip-forward-circle"
IconNamePlaySkipForward IconName = "play-skip-forward"
IconNamePlay IconName = "play"
IconNamePodium IconName = "podium"
IconNamePower IconName = "power"
IconNamePricetag IconName = "pricetag"
IconNamePricetags IconName = "pricetags"
IconNamePrint IconName = "print"
IconNamePrism IconName = "prism"
IconNamePulse IconName = "pulse"
IconNamePush IconName = "push"
IconNameQrCode IconName = "qr-code"
IconNameRadioButtonOff IconName = "radio-button-off"
IconNameRadioButtonOn IconName = "radio-button-on"
IconNameRadio IconName = "radio"
IconNameRainy IconName = "rainy"
IconNameReader IconName = "reader"
IconNameReceipt IconName = "receipt"
IconNameRecording IconName = "recording"
IconNameRefreshCircle IconName = "refresh-circle"
IconNameRefresh IconName = "refresh"
IconNameReloadCircle IconName = "reload-circle"
IconNameReload IconName = "reload"
IconNameRemoveCircle IconName = "remove-circle"
IconNameRepeat IconName = "repeat"
IconNameResize IconName = "resize"
IconNameRestaurant IconName = "restaurant"
IconNameRibbon IconName = "ribbon"
IconNameRocket IconName = "rocket"
IconNameRose IconName = "rose"
IconNameSad IconName = "sad"
IconNameSave IconName = "save"
IconNameScale IconName = "scale"
IconNameScanCircle IconName = "scan-circle"
IconNameScan IconName = "scan"
IconNameSchool IconName = "school"
IconNameSearchCircle IconName = "search-circle"
IconNameSearch IconName = "search"
IconNameSend IconName = "send"
IconNameServer IconName = "server"
IconNameSettings IconName = "settings"
IconNameShapes IconName = "shapes"
IconNameShareSocial IconName = "share-social"
IconNameShare IconName = "share"
IconNameShieldCheckmark IconName = "shield-checkmark"
IconNameShieldHalf IconName = "shield-half"
IconNameShield IconName = "shield"
IconNameShirt IconName = "shirt"
IconNameShuffle IconName = "shuffle"
IconNameSkull IconName = "skull"
IconNameSnow IconName = "snow"
IconNameSparkles IconName = "sparkles"
IconNameSpeedometer IconName = "speedometer"
IconNameSquare IconName = "square"
IconNameStarHalf IconName = "star-half"
IconNameStar IconName = "star"
IconNameStatsChart IconName = "stats-chart"
IconNameStopCircle IconName = "stop-circle"
IconNameStop IconName = "stop"
IconNameStopwatch IconName = "stopwatch"
IconNameStorefront IconName = "storefront"
IconNameSubway IconName = "subway"
IconNameSunny IconName = "sunny"
IconNameSwapHorizontal IconName = "swap-horizontal"
IconNameSwapVertical IconName = "swap-vertical"
IconNameSyncCircle IconName = "sync-circle"
IconNameSync IconName = "sync"
IconNameTabletLandscape IconName = "tablet-landscape"
IconNameTabletPortrait IconName = "tablet-portrait"
IconNameTelescope IconName = "telescope"
IconNameTennisball IconName = "tennisball"
IconNameTerminal IconName = "terminal"
IconNameText IconName = "text"
IconNameThermometer IconName = "thermometer"
IconNameThumbsDown IconName = "thumbs-down"
IconNameThumbsUp IconName = "thumbs-up"
IconNameThunderstorm IconName = "thunderstorm"
IconNameTicket IconName = "ticket"
IconNameTime IconName = "time"
IconNameTimer IconName = "timer"
IconNameToday IconName = "today"
IconNameToggle IconName = "toggle"
IconNameTrailSign IconName = "trail-sign"
IconNameTrain IconName = "train"
IconNameTransgender IconName = "transgender"
IconNameTrashBin IconName = "trash-bin"
IconNameTrash IconName = "trash"
IconNameTrendingDown IconName = "trending-down"
IconNameTrendingUp IconName = "trending-up"
IconNameTriangle IconName = "triangle"
IconNameTrophy IconName = "trophy"
IconNameTv IconName = "tv"
IconNameUmbrella IconName = "umbrella"
IconNameUnlink IconName = "unlink"
IconNameVideocamOff IconName = "videocam-off"
IconNameVideocam IconName = "videocam"
IconNameVolumeHigh IconName = "volume-high"
IconNameVolumeLow IconName = "volume-low"
IconNameVolumeMedium IconName = "volume-medium"
IconNameVolumeMute IconName = "volume-mute"
IconNameVolumeOff IconName = "volume-off"
IconNameWalk IconName = "walk"
IconNameWallet IconName = "wallet"
IconNameWarning IconName = "warning"
IconNameWatch IconName = "watch"
IconNameWater IconName = "water"
IconNameWifi IconName = "wifi"
IconNameWine IconName = "wine"
IconNameWoman IconName = "woman"
)
var validIconNames = func() map[IconName]struct{} {
m := make(map[IconName]struct{}, 390)
for _, v := range []IconName{
IconNameAccessibility,
IconNameAddCircle,
IconNameAirplane,
IconNameAlarm,
IconNameAlbums,
IconNameAlertCircle,
IconNameAmericanFootball,
IconNameAnalytics,
IconNameAperture,
IconNameApps,
IconNameArchive,
IconNameArrowBackCircle,
IconNameArrowDownCircle,
IconNameArrowForwardCircle,
IconNameArrowRedoCircle,
IconNameArrowRedo,
IconNameArrowUndoCircle,
IconNameArrowUndo,
IconNameArrowUpCircle,
IconNameAtCircle,
IconNameAttach,
IconNameBackspace,
IconNameBagAdd,
IconNameBagCheck,
IconNameBagHandle,
IconNameBagRemove,
IconNameBag,
IconNameBalloon,
IconNameBan,
IconNameBandage,
IconNameBarChart,
IconNameBarbell,
IconNameBarcode,
IconNameBaseball,
IconNameBasket,
IconNameBasketball,
IconNameBatteryCharging,
IconNameBatteryDead,
IconNameBatteryFull,
IconNameBatteryHalf,
IconNameBeaker,
IconNameBed,
IconNameBeer,
IconNameBicycle,
IconNameBinoculars,
IconNameBluetooth,
IconNameBoat,
IconNameBody,
IconNameBonfire,
IconNameBook,
IconNameBookmark,
IconNameBookmarks,
IconNameBowlingBall,
IconNameBriefcase,
IconNameBrowsers,
IconNameBrush,
IconNameBug,
IconNameBuild,
IconNameBulb,
IconNameBus,
IconNameBusiness,
IconNameCafe,
IconNameCalculator,
IconNameCalendarClear,
IconNameCalendarNumber,
IconNameCalendar,
IconNameCall,
IconNameCameraReverse,
IconNameCamera,
IconNameCarSport,
IconNameCar,
IconNameCard,
IconNameCaretBackCircle,
IconNameCaretBack,
IconNameCaretDownCircle,
IconNameCaretDown,
IconNameCaretForwardCircle,
IconNameCaretForward,
IconNameCaretUpCircle,
IconNameCaretUp,
IconNameCart,
IconNameCash,
IconNameCellular,
IconNameChatboxEllipses,
IconNameChatbox,
IconNameChatbubbleEllipses,
IconNameChatbubble,
IconNameChatbubbles,
IconNameCheckbox,
IconNameCheckmarkCircle,
IconNameCheckmarkDoneCircle,
IconNameChevronBackCircle,
IconNameChevronDownCircle,
IconNameChevronForwardCircle,
IconNameChevronUpCircle,
IconNameClipboard,
IconNameCloseCircle,
IconNameCloudCircle,
IconNameCloudDone,
IconNameCloudDownload,
IconNameCloudOffline,
IconNameCloudUpload,
IconNameCloud,
IconNameCloudyNight,
IconNameCloudy,
IconNameCodeSlash,
IconNameCode,
IconNameCog,
IconNameColorFill,
IconNameColorFilter,
IconNameColorPalette,
IconNameColorWand,
IconNameCompass,
IconNameConstruct,
IconNameContact,
IconNameContract,
IconNameContrast,
IconNameCopy,
IconNameCreate,
IconNameCrop,
IconNameCube,
IconNameCut,
IconNameDesktop,
IconNameDiamond,
IconNameDice,
IconNameDisc,
IconNameDocumentAttach,
IconNameDocumentLock,
IconNameDocumentText,
IconNameDocument,
IconNameDocuments,
IconNameDownload,
IconNameDuplicate,
IconNameEar,
IconNameEarth,
IconNameEasel,
IconNameEgg,
IconNameEllipse,
IconNameEllipsisHorizontalCircle,
IconNameEllipsisVerticalCircle,
IconNameEnter,
IconNameExit,
IconNameExpand,
IconNameExtensionPuzzle,
IconNameEyeOff,
IconNameEye,
IconNameEyedrop,
IconNameFastFood,
IconNameFemale,
IconNameFileTrayFull,
IconNameFileTrayStacked,
IconNameFileTray,
IconNameFilm,
IconNameFilterCircle,
IconNameFingerPrint,
IconNameFish,
IconNameFitness,
IconNameFlag,
IconNameFlame,
IconNameFlashOff,
IconNameFlash,
IconNameFlashlight,
IconNameFlask,
IconNameFlower,
IconNameFolderOpen,
IconNameFolder,
IconNameFootball,
IconNameFootsteps,
IconNameFunnel,
IconNameGameController,
IconNameGift,
IconNameGitBranch,
IconNameGitCommit,
IconNameGitCompare,
IconNameGitMerge,
IconNameGitNetwork,
IconNameGitPullRequest,
IconNameGlasses,
IconNameGlobe,
IconNameGolf,
IconNameGrid,
IconNameHammer,
IconNameHandLeft,
IconNameHandRight,
IconNameHappy,
IconNameHardwareChip,
IconNameHeadset,
IconNameHeartCircle,
IconNameHeartDislikeCircle,
IconNameHeartDislike,
IconNameHeartHalf,
IconNameHeart,
IconNameHelpBuoy,
IconNameHelpCircle,
IconNameHome,
IconNameHourglass,
IconNameIceCream,
IconNameIdCard,
IconNameImage,
IconNameImages,
IconNameInfinite,
IconNameInformationCircle,
IconNameInvertMode,
IconNameJournal,
IconNameKey,
IconNameKeypad,
IconNameLanguage,
IconNameLaptop,
IconNameLayers,
IconNameLeaf,
IconNameLibrary,
IconNameLink,
IconNameListCircle,
IconNameList,
IconNameLocate,
IconNameLocation,
IconNameLockClosed,
IconNameLockOpen,
IconNameLogIn,
IconNameLogOut,
IconNameLogoAlipay,
IconNameLogoAmazon,
IconNameLogoAmplify,
IconNameLogoAndroid,
IconNameMagnet,
IconNameMailOpen,
IconNameMailUnread,
IconNameMail,
IconNameMaleFemale,
IconNameMale,
IconNameMan,
IconNameMap,
IconNameMedal,
IconNameMedical,
IconNameMedkit,
IconNameMegaphone,
IconNameMenu,
IconNameMicCircle,
IconNameMicOffCircle,
IconNameMicOff,
IconNameMic,
IconNameMoon,
IconNameMove,
IconNameMusicalNote,
IconNameMusicalNotes,
IconNameNavigateCircle,
IconNameNavigate,
IconNameNewspaper,
IconNameNotificationsCircle,
IconNameNotificationsOffCircle,
IconNameNotificationsOff,
IconNameNotifications,
IconNameNuclear,
IconNameNutrition,
IconNameOptions,
IconNamePaperPlane,
IconNamePartlySunny,
IconNamePauseCircle,
IconNamePause,
IconNamePaw,
IconNamePencil,
IconNamePeopleCircle,
IconNamePeople,
IconNamePersonAdd,
IconNamePersonCircle,
IconNamePersonRemove,
IconNamePerson,
IconNamePhoneLandscape,
IconNamePhonePortrait,
IconNamePieChart,
IconNamePin,
IconNamePint,
IconNamePizza,
IconNamePlanet,
IconNamePlayBackCircle,
IconNamePlayBack,
IconNamePlayCircle,
IconNamePlayForwardCircle,
IconNamePlayForward,
IconNamePlaySkipBackCircle,
IconNamePlaySkipBack,
IconNamePlaySkipForwardCircle,
IconNamePlaySkipForward,
IconNamePlay,
IconNamePodium,
IconNamePower,
IconNamePricetag,
IconNamePricetags,
IconNamePrint,
IconNamePrism,
IconNamePulse,
IconNamePush,
IconNameQrCode,
IconNameRadioButtonOff,
IconNameRadioButtonOn,
IconNameRadio,
IconNameRainy,
IconNameReader,
IconNameReceipt,
IconNameRecording,
IconNameRefreshCircle,
IconNameRefresh,
IconNameReloadCircle,
IconNameReload,
IconNameRemoveCircle,
IconNameRepeat,
IconNameResize,
IconNameRestaurant,
IconNameRibbon,
IconNameRocket,
IconNameRose,
IconNameSad,
IconNameSave,
IconNameScale,
IconNameScanCircle,
IconNameScan,
IconNameSchool,
IconNameSearchCircle,
IconNameSearch,
IconNameSend,
IconNameServer,
IconNameSettings,
IconNameShapes,
IconNameShareSocial,
IconNameShare,
IconNameShieldCheckmark,
IconNameShieldHalf,
IconNameShield,
IconNameShirt,
IconNameShuffle,
IconNameSkull,
IconNameSnow,
IconNameSparkles,
IconNameSpeedometer,
IconNameSquare,
IconNameStarHalf,
IconNameStar,
IconNameStatsChart,
IconNameStopCircle,
IconNameStop,
IconNameStopwatch,
IconNameStorefront,
IconNameSubway,
IconNameSunny,
IconNameSwapHorizontal,
IconNameSwapVertical,
IconNameSyncCircle,
IconNameSync,
IconNameTabletLandscape,
IconNameTabletPortrait,
IconNameTelescope,
IconNameTennisball,
IconNameTerminal,
IconNameText,
IconNameThermometer,
IconNameThumbsDown,
IconNameThumbsUp,
IconNameThunderstorm,
IconNameTicket,
IconNameTime,
IconNameTimer,
IconNameToday,
IconNameToggle,
IconNameTrailSign,
IconNameTrain,
IconNameTransgender,
IconNameTrashBin,
IconNameTrash,
IconNameTrendingDown,
IconNameTrendingUp,
IconNameTriangle,
IconNameTrophy,
IconNameTv,
IconNameUmbrella,
IconNameUnlink,
IconNameVideocamOff,
IconNameVideocam,
IconNameVolumeHigh,
IconNameVolumeLow,
IconNameVolumeMedium,
IconNameVolumeMute,
IconNameVolumeOff,
IconNameWalk,
IconNameWallet,
IconNameWarning,
IconNameWatch,
IconNameWater,
IconNameWifi,
IconNameWine,
IconNameWoman,
} {
m[v] = struct{}{}
}
return m
}()
func (n *IconName) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
name := IconName(s)
if _, ok := validIconNames[name]; !ok {
return util.ErrBadInput(fmt.Sprintf("invalid icon name: %q", s))
}
*n = name
return nil
}
var IconOptionToColor = map[float64]Color{
1: ColorGrey,
2: ColorYellow,
3: ColorOrange,
@ -72,6 +878,19 @@ var iconOptionToColor = map[float64]Color{
10: ColorLime,
}
var ColorToIconOption = map[Color]int64{
ColorGrey: 1,
ColorYellow: 2,
ColorOrange: 3,
ColorRed: 4,
ColorPink: 5,
ColorPurple: 6,
ColorBlue: 7,
ColorIce: 8,
ColorTeal: 9,
ColorLime: 10,
}
var ColorOptionToColor = map[string]Color{
"grey": ColorGrey,
"yellow": ColorYellow,
@ -102,10 +921,6 @@ func StringPtr(s string) *string {
return &s
}
func ColorPtr(c Color) *Color {
return &c
}
type Icon struct {
WrappedIcon `swaggerignore:"true"`
}
@ -162,49 +977,11 @@ type FileIcon struct {
func (FileIcon) isIcon() {}
// TODO: the enum gen for IconFormat through swaggo is bugged; only the last enum (before: "icon") is used
type NamedIcon struct {
Format IconFormat `json:"format" enums:"icon"` // The format of the icon
Name string `json:"name" example:"document"` // The name of the icon
Color *Color `json:"color,omitempty" example:"yellow" enums:"grey,yellow,orange,red,pink,purple,blue,ice,teal,lime"` // The color of the icon
Format IconFormat `json:"format" enums:"emoji,file,icon"` // The format of the icon
Name IconName `json:"name" example:"document"` // The name of the icon
Color Color `json:"color" example:"yellow" enums:"grey,yellow,orange,red,pink,purple,blue,ice,teal,lime"` // The color of the icon
}
func (NamedIcon) isIcon() {}
func IsEmoji(s string) bool {
if s == "" {
return false
}
for _, r := range s {
if unicode.Is(unicode.Cf, r) || unicode.Is(unicode.So, r) || unicode.Is(unicode.Sk, r) {
continue
} else {
return false
}
}
return true
}
// GetIcon returns the appropriate Icon implementation.
func GetIcon(gatewayUrl string, iconEmoji string, iconImage string, iconName string, iconOption float64) Icon {
if iconName != "" {
return Icon{NamedIcon{
Format: IconFormatIcon,
Name: iconName,
Color: ColorPtr(iconOptionToColor[iconOption]),
}}
}
if iconEmoji != "" {
return Icon{EmojiIcon{
Format: IconFormatEmoji,
Emoji: iconEmoji,
}}
}
if iconImage != "" {
return Icon{FileIcon{
Format: IconFormatFile,
File: fmt.Sprintf("%s/image/%s", gatewayUrl, iconImage),
}}
}
return Icon{NamedIcon{Format: ""}}
}

View file

@ -1,5 +1,9 @@
package apimodel
type AddObjectsToListRequest struct {
Objects []string `json:"objects" example:"[\"bafyreie6n5l5nkbjal37su54cha4coy7qzuhrnajluzv5qd5jvtsrxkequ\"]"` // The list of object IDs to add to the list
}
type View struct {
Id string `json:"id" example:"67bf3f21cda9134102e2422c"` // The id of the view
Name string `json:"name" example:"All"` // The name of the view

View file

@ -47,13 +47,13 @@ type CreateObjectRequest struct {
Body string `json:"body" example:"This is the body of the object. Markdown syntax is supported here."` // The body of the object
TemplateId string `json:"template_id" example:"bafyreictrp3obmnf6dwejy5o4p7bderaaia4bdg2psxbfzf44yya5uutge"` // The id of the template to use
TypeKey string `json:"type_key" binding:"required" example:"page"` // The key of the type of object to create
Properties []PropertyLinkWithValue `json:"properties" oneOf:"TextPropertyLinkValue,NumberPropertyLinkValue,SelectPropertyLinkValue,MultiSelectPropertyLinkValue,DatePropertyLinkValue,FilesPropertyLinkValue,CheckboxPropertyLinkValue,URLPropertyLinkValue,EmailPropertyLinkValue,PhonePropertyLinkValue,ObjectsPropertyLinkValue"` // ⚠ Warning: Properties are experimental and may change in the next update. ⚠ The properties to set on the object
Properties []PropertyLinkWithValue `json:"properties" oneOf:"TextPropertyLinkValue,NumberPropertyLinkValue,SelectPropertyLinkValue,MultiSelectPropertyLinkValue,DatePropertyLinkValue,FilesPropertyLinkValue,CheckboxPropertyLinkValue,URLPropertyLinkValue,EmailPropertyLinkValue,PhonePropertyLinkValue,ObjectsPropertyLinkValue"` // ⚠ Warning: Properties are experimental and may change in the next update. ⚠ The properties to set on the object; see ListTypes or GetType endpoints for linked properties
}
type UpdateObjectRequest struct {
Name *string `json:"name,omitempty" example:"My object"` // The name of the object
Icon *Icon `json:"icon,omitempty" oneOf:"EmojiIcon,FileIcon,NamedIcon"` // The icon to set for the object
Properties *[]PropertyLinkWithValue `json:"properties,omitempty" oneOf:"TextPropertyLinkValue,NumberPropertyLinkValue,SelectPropertyLinkValue,MultiSelectPropertyLinkValue,DatePropertyLinkValue,FilesPropertyLinkValue,CheckboxPropertyLinkValue,URLPropertyLinkValue,EmailPropertyLinkValue,PhonePropertyLinkValue,ObjectsPropertyLinkValue"` // ⚠ Warning: Properties are experimental and may change in the next update. ⚠ The properties to set for the object
Properties *[]PropertyLinkWithValue `json:"properties,omitempty" oneOf:"TextPropertyLinkValue,NumberPropertyLinkValue,SelectPropertyLinkValue,MultiSelectPropertyLinkValue,DatePropertyLinkValue,FilesPropertyLinkValue,CheckboxPropertyLinkValue,URLPropertyLinkValue,EmailPropertyLinkValue,PhonePropertyLinkValue,ObjectsPropertyLinkValue"` // ⚠ Warning: Properties are experimental and may change in the next update. ⚠ The properties to set for the object; see ListTypes or GetType endpoints for linked properties
}
type ObjectResponse struct {

View file

@ -42,11 +42,13 @@ type PropertyResponse struct {
}
type CreatePropertyRequest struct {
Key string `json:"key" example:"some_user_defined_property_key"` // The key of the property; should always be snake_case, otherwise it will be converted to snake_case
Name string `json:"name" binding:"required" example:"Last modified date"` // The name of the property
Format PropertyFormat `json:"format" binding:"required" enums:"text,number,select,multi_select,date,files,checkbox,url,email,phone,objects"` // The format of the property
}
type UpdatePropertyRequest struct {
Key *string `json:"key,omitempty" example:"some_user_defined_property_key"` // The key to set for the property; ; should always be snake_case, otherwise it will be converted to snake_case
Name *string `json:"name,omitempty" binding:"required" example:"Last modified date"` // The name to set for the property
}
@ -56,6 +58,8 @@ type Property struct {
Key string `json:"key" example:"last_modified_date"` // The key of the property
Name string `json:"name" example:"Last modified date"` // The name of the property
Format PropertyFormat `json:"format" enums:"text,number,select,multi_select,date,files,checkbox,url,email,phone,objects"` // The format of the property
// Rk is internal-only to simplify lookup on entry, won't be serialized to property responses
RelationKey string `json:"-" swaggerignore:"true"`
}
type PropertyLink struct {
@ -212,10 +216,10 @@ func (DatePropertyValue) isPropertyWithValue() {}
type FilesPropertyValue struct {
PropertyBase
Key string `json:"key" example:"files"` // The key of the property
Name string `json:"name" example:"Files"` // The name of the property
Format PropertyFormat `json:"format" enums:"files"` // The format of the property
Files []string `json:"files" example:"['fileId']"` // The file values of the property
Key string `json:"key" example:"files"` // The key of the property
Name string `json:"name" example:"Files"` // The name of the property
Format PropertyFormat `json:"format" enums:"files"` // The format of the property
Files []string `json:"files" example:"['file_id']"` // The file values of the property
}
func (FilesPropertyValue) isPropertyWithValue() {}
@ -262,10 +266,10 @@ func (PhonePropertyValue) isPropertyWithValue() {}
type ObjectsPropertyValue struct {
PropertyBase
Key string `json:"key" example:"creator"` // The key of the property
Name string `json:"name" example:"Created by"` // The name of the property
Format PropertyFormat `json:"format" enums:"objects"` // The format of the property
Objects []string `json:"objects" example:"['objectId']"` // The object values of the property
Key string `json:"key" example:"creator"` // The key of the property
Name string `json:"name" example:"Created by"` // The name of the property
Format PropertyFormat `json:"format" enums:"objects"` // The format of the property
Objects []string `json:"objects" example:"['object_id']"` // The object values of the property
}
func (ObjectsPropertyValue) isPropertyWithValue() {}
@ -279,173 +283,158 @@ func (p PropertyLinkWithValue) MarshalJSON() ([]byte, error) {
}
func (p *PropertyLinkWithValue) UnmarshalJSON(data []byte) error {
var raw struct {
Format PropertyFormat `json:"format"`
}
if err := json.Unmarshal(data, &raw); err != nil {
var aux map[string]json.RawMessage
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
switch raw.Format {
case PropertyFormatText:
switch {
case aux["text"] != nil:
var v TextPropertyLinkValue
if err := json.Unmarshal(data, &v); err != nil {
return err
}
p.WrappedPropertyLinkWithValue = v
case PropertyFormatNumber:
case aux["number"] != nil:
var v NumberPropertyLinkValue
if err := json.Unmarshal(data, &v); err != nil {
return err
}
p.WrappedPropertyLinkWithValue = v
case PropertyFormatSelect:
case aux["select"] != nil:
var v SelectPropertyLinkValue
if err := json.Unmarshal(data, &v); err != nil {
return err
}
p.WrappedPropertyLinkWithValue = v
case PropertyFormatMultiSelect:
case aux["multi_select"] != nil:
var v MultiSelectPropertyLinkValue
if err := json.Unmarshal(data, &v); err != nil {
return err
}
p.WrappedPropertyLinkWithValue = v
case PropertyFormatDate:
case aux["date"] != nil:
var v DatePropertyLinkValue
if err := json.Unmarshal(data, &v); err != nil {
return err
}
p.WrappedPropertyLinkWithValue = v
case PropertyFormatFiles:
case aux["files"] != nil:
var v FilesPropertyLinkValue
if err := json.Unmarshal(data, &v); err != nil {
return err
}
p.WrappedPropertyLinkWithValue = v
case PropertyFormatCheckbox:
case aux["checkbox"] != nil:
var v CheckboxPropertyLinkValue
if err := json.Unmarshal(data, &v); err != nil {
return err
}
p.WrappedPropertyLinkWithValue = v
case PropertyFormatUrl:
case aux["url"] != nil:
var v URLPropertyLinkValue
if err := json.Unmarshal(data, &v); err != nil {
return err
}
p.WrappedPropertyLinkWithValue = v
case PropertyFormatEmail:
case aux["email"] != nil:
var v EmailPropertyLinkValue
if err := json.Unmarshal(data, &v); err != nil {
return err
}
p.WrappedPropertyLinkWithValue = v
case PropertyFormatPhone:
case aux["phone"] != nil:
var v PhonePropertyLinkValue
if err := json.Unmarshal(data, &v); err != nil {
return err
}
p.WrappedPropertyLinkWithValue = v
case PropertyFormatObjects:
case aux["objects"] != nil:
var v ObjectsPropertyLinkValue
if err := json.Unmarshal(data, &v); err != nil {
return err
}
p.WrappedPropertyLinkWithValue = v
default:
return util.ErrBadInput(fmt.Sprintf("invalid property link value format: %q", raw.Format))
return util.ErrBadInput("could not determine property link value type")
}
return nil
}
type WrappedPropertyLinkWithValue interface{ isPropertyLinkWithValue() }
type TextPropertyLinkValue struct {
Key string `json:"key" example:"description"`
Format PropertyFormat `json:"format" enums:"text"`
Text string `json:"text" example:"Some text..."` // The text value of the property
Key string `json:"key" example:"description"`
Text string `json:"text" example:"Some text..."` // The text value of the property
}
func (TextPropertyLinkValue) isPropertyLinkWithValue() {}
type NumberPropertyLinkValue struct {
Key string `json:"key" example:"height"`
Format PropertyFormat `json:"format" enums:"number"`
Number *float64 `json:"number" example:"42"` // The number value of the property
Key string `json:"key" example:"height"`
Number *float64 `json:"number" example:"42"` // The number value of the property
}
func (NumberPropertyLinkValue) isPropertyLinkWithValue() {}
type SelectPropertyLinkValue struct {
Key string `json:"key" example:"status"`
Format PropertyFormat `json:"format" enums:"select"`
Select *string `json:"select,omitempty"` // The selected tag value of the property
Key string `json:"key" example:"status"`
Select *string `json:"select,omitempty" example:"tag_id"` // The selected tag id of the property; see ListTags endpoint for valid values
}
func (SelectPropertyLinkValue) isPropertyLinkWithValue() {}
type MultiSelectPropertyLinkValue struct {
Key string `json:"key" example:"tag"`
Format PropertyFormat `json:"format" enums:"multi_select"`
MultiSelect []string `json:"multi_select,omitempty"` // The selected tag values of the property
Key string `json:"key" example:"tag"`
MultiSelect []string `json:"multi_select,omitempty" example:"['tag_id']"` // The selected tag ids of the property; see ListTags endpoint for valid values
}
func (MultiSelectPropertyLinkValue) isPropertyLinkWithValue() {}
type DatePropertyLinkValue struct {
Key string `json:"key" example:"last_modified_date"`
Format PropertyFormat `json:"format" enums:"date"`
Date *string `json:"date" example:"2025-02-14T12:34:56Z"` // The date value of the property
Key string `json:"key" example:"last_modified_date"`
Date *string `json:"date" example:"2025-02-14T12:34:56Z"` // The date value of the property
}
func (DatePropertyLinkValue) isPropertyLinkWithValue() {}
type FilesPropertyLinkValue struct {
Key string `json:"key" example:"files"`
Format PropertyFormat `json:"format" enums:"files"`
Files []string `json:"files" example:"['fileId']"` // The file values of the property
Key string `json:"key" example:"files"`
Files []string `json:"files" example:"['file_id']"` // The file ids of the property
}
func (FilesPropertyLinkValue) isPropertyLinkWithValue() {}
type CheckboxPropertyLinkValue struct {
Key string `json:"key" example:"done"`
Format PropertyFormat `json:"format" enums:"checkbox"`
Checkbox bool `json:"checkbox" example:"true"` // The checkbox value of the property
Key string `json:"key" example:"done"`
Checkbox bool `json:"checkbox" example:"true"` // The checkbox value of the property
}
func (CheckboxPropertyLinkValue) isPropertyLinkWithValue() {}
type URLPropertyLinkValue struct {
Key string `json:"key" example:"source"`
Format PropertyFormat `json:"format" enums:"url"`
Url string `json:"url" example:"https://example.com"` // The URL value of the property
Key string `json:"key" example:"source"`
Url string `json:"url" example:"https://example.com"` // The URL value of the property
}
func (URLPropertyLinkValue) isPropertyLinkWithValue() {}
type EmailPropertyLinkValue struct {
Key string `json:"key" example:"email"`
Format PropertyFormat `json:"format" enums:"email"`
Email string `json:"email" example:"example@example.com"` // The email value of the property
Key string `json:"key" example:"email"`
Email string `json:"email" example:"example@example.com"` // The email value of the property
}
func (EmailPropertyLinkValue) isPropertyLinkWithValue() {}
type PhonePropertyLinkValue struct {
Key string `json:"key" example:"phone"`
Format PropertyFormat `json:"format" enums:"phone"`
Phone string `json:"phone" example:"+1234567890"` // The phone value of the property
Key string `json:"key" example:"phone"`
Phone string `json:"phone" example:"+1234567890"` // The phone value of the property
}
func (PhonePropertyLinkValue) isPropertyLinkWithValue() {}
type ObjectsPropertyLinkValue struct {
Key string `json:"key" example:"creator"`
Format PropertyFormat `json:"format" enums:"objects"`
Objects []string `json:"objects" example:"['objectId']"` // The object values of the property
Key string `json:"key" example:"creator"`
Objects []string `json:"objects" example:"['object_id']"` // The object ids of the property
}
func (ObjectsPropertyLinkValue) isPropertyLinkWithValue() {}

View file

@ -1,5 +1,12 @@
package apimodel
import (
"encoding/json"
"fmt"
"github.com/anyproto/anytype-heart/core/api/util"
)
type SortDirection string
const (
@ -7,6 +14,20 @@ const (
Desc SortDirection = "desc"
)
func (sd *SortDirection) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
switch SortDirection(s) {
case Asc, Desc:
*sd = SortDirection(s)
return nil
default:
return util.ErrBadInput(fmt.Sprintf("invalid sort direction: %q", s))
}
}
type SortProperty string
const (
@ -16,13 +37,27 @@ const (
Name SortProperty = "name"
)
func (sp *SortProperty) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
switch SortProperty(s) {
case CreatedDate, LastModifiedDate, LastOpenedDate, Name:
*sp = SortProperty(s)
return nil
default:
return util.ErrBadInput(fmt.Sprintf("invalid sort property: %q", s))
}
}
type SearchRequest struct {
Query string `json:"query" example:"test"` // The search term to look for in object names and snippets
Types []string `json:"types" example:"page,678043f0cda9133be777049f,bafyreightzrdts2ymxyaeyzspwdfo2juspyam76ewq6qq7ixnw3523gs7q"` // The types of objects to search for, specified by key or ID
Sort SortOptions `json:"sort"` // The sorting criteria and direction for the search results
Query string `json:"query" example:"test"` // The text to search within object names and content; use types field for type filtering
Types []string `json:"types" example:"page,task,bookmark"` // The types of objects to include in results (e.g., "page", "task", "bookmark"); see ListTypes endpoint for valid values
Sort SortOptions `json:"sort"` // The sorting options for the search results
}
type SortOptions struct {
PropertyKey SortProperty `json:"property_key" enums:"created_date,last_modified_date,last_opened_date,name" default:"last_modified_date"` // The property to sort the search results by
Direction SortDirection `json:"direction" enums:"asc,desc" default:"desc"` // The direction to sort the search results
PropertyKey SortProperty `json:"property_key" enums:"created_date,last_modified_date,last_opened_date,name" default:"last_modified_date"` // The key of the property to sort the search results by
Direction SortDirection `json:"direction" enums:"asc,desc" default:"desc"` // The direction to sort the search results by
}

View file

@ -35,29 +35,33 @@ type TypeResponse struct {
}
type CreateTypeRequest struct {
Name string `json:"name" binding:"required" example:"Page"` // The name of the type
PluralName string `json:"plural_name" example:"Pages"` // The plural name of the type
Icon Icon `json:"icon" oneOf:"EmojiIcon,FileIcon,NamedIcon"` // The icon of the type
Layout TypeLayout `json:"layout" binding:"required" example:"basic"` // The layout of the type
Properties []PropertyLink `json:"properties"` // ⚠ Warning: Properties are experimental and may change in the next update. ⚠ The properties linked to the type
Key string `json:"key" example:"some_user_defined_type_key"` // The key of the type; should always be snake_case, otherwise it will be converted to snake_case
Name string `json:"name" binding:"required" example:"Page"` // The name of the type
PluralName string `json:"plural_name" binding:"required" example:"Pages"` // The plural name of the type
Icon Icon `json:"icon" oneOf:"EmojiIcon,FileIcon,NamedIcon"` // The icon of the type
Layout TypeLayout `json:"layout" binding:"required" enums:"basic,profile,action,note"` // The layout of the type
Properties []PropertyLink `json:"properties"` // ⚠ Warning: Properties are experimental and may change in the next update. ⚠ The properties linked to the type
}
type UpdateTypeRequest struct {
Key *string `json:"key,omitempty" example:"some_user_defined_type_key"` // The key to set for the type; should always be snake_case, otherwise it will be converted to snake_case
Name *string `json:"name,omitempty" example:"Page"` // The name to set for the type
PluralName *string `json:"plural_name,omitempty" example:"Pages"` // The plural name to set for the type
Icon *Icon `json:"icon,omitempty" oneOf:"EmojiIcon,FileIcon,NamedIcon"` // The icon to set for the type
Layout *TypeLayout `json:"layout,omitempty" example:"basic"` // The layout to set for the type
Layout *TypeLayout `json:"layout,omitempty" enums:"basic,profile,action,note"` // The layout to set for the type
Properties *[]PropertyLink `json:"properties,omitempty"` // ⚠ Warning: Properties are experimental and may change in the next update. ⚠ The properties to set for the type
}
type Type struct {
Object string `json:"object" example:"type"` // The data model of the object
Id string `json:"id" example:"bafyreigyb6l5szohs32ts26ku2j42yd65e6hqy2u3gtzgdwqv6hzftsetu"` // The id of the type (which is unique across spaces)
Key string `json:"key" example:"page"` // The key of the type (can be the same across spaces for known types)
Name string `json:"name" example:"Page"` // The name of the type
PluralName string `json:"plural_name" example:"Pages"` // The plural name of the type
Icon Icon `json:"icon" oneOf:"EmojiIcon,FileIcon,NamedIcon"` // The icon of the type
Archived bool `json:"archived" example:"false"` // Whether the type is archived
Layout ObjectLayout `json:"layout" example:"basic"` // The layout of the type
Properties []Property `json:"properties"` // The properties linked to the type
Object string `json:"object" example:"type"` // The data model of the object
Id string `json:"id" example:"bafyreigyb6l5szohs32ts26ku2j42yd65e6hqy2u3gtzgdwqv6hzftsetu"` // The id of the type (which is unique across spaces)
Key string `json:"key" example:"page"` // The key of the type (can be the same across spaces for known types)
Name string `json:"name" example:"Page"` // The name of the type
PluralName string `json:"plural_name" example:"Pages"` // The plural name of the type
Icon Icon `json:"icon" oneOf:"EmojiIcon,FileIcon,NamedIcon"` // The icon of the type
Archived bool `json:"archived" example:"false"` // Whether the type is archived
Layout ObjectLayout `json:"layout" enums:"basic,profile,action,note,bookmark,set,set,collection,participant"` // The layout of the type
Properties []Property `json:"properties"` // The properties linked to the type
// Uk is internal-only to simplify lookup on entry, won't be serialized to type responses
UniqueKey string `json:"-" swaggerignore:"true"`
}

View file

@ -12,10 +12,14 @@ import (
apicore "github.com/anyproto/anytype-heart/core/api/core"
"github.com/anyproto/anytype-heart/core/api/util"
"github.com/anyproto/anytype-heart/core/event"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/logging"
)
const ApiVersion = "2025-04-22"
const ApiVersion = "2025-05-20"
var log = logging.Logger("api-server")
var (
ErrMissingAuthorizationHeader = errors.New("missing authorization header")
@ -23,17 +27,21 @@ var (
ErrInvalidToken = errors.New("invalid token")
)
// rateLimit is a middleware that limits the number of requests per second.
func (s *Server) rateLimit(max float64) gin.HandlerFunc {
lmt := tollbooth.NewLimiter(max, nil)
// ensureRateLimit creates a shared write-rate limiter middleware.
func ensureRateLimit(rate float64, burst int, isRateLimitDisabled bool) gin.HandlerFunc {
lmt := tollbooth.NewLimiter(rate, nil)
lmt.SetBurst(burst)
lmt.SetIPLookup(limiter.IPLookup{
Name: "RemoteAddr",
IndexFromRight: 0,
})
return func(c *gin.Context) {
httpError := tollbooth.LimitByRequest(lmt, c.Writer, c.Request)
if httpError != nil {
if isRateLimitDisabled {
c.Next()
return
}
if httpError := tollbooth.LimitByRequest(lmt, c.Writer, c.Request); httpError != nil {
apiErr := util.CodeToAPIError(httpError.StatusCode, httpError.Message)
c.AbortWithStatusJSON(httpError.StatusCode, apiErr)
return
@ -62,7 +70,7 @@ func (s *Server) ensureAuthenticated(mw apicore.ClientCommands) gin.HandlerFunc
// Validate the key - if the key exists in the KeyToToken map, it is considered valid.
// Otherwise, attempt to create a new session using the key and add it to the map upon successful validation.
s.mu.Lock()
token, exists := s.KeyToToken[key]
apiSession, exists := s.KeyToToken[key]
s.mu.Unlock()
if !exists {
@ -72,19 +80,44 @@ func (s *Server) ensureAuthenticated(mw apicore.ClientCommands) gin.HandlerFunc
c.AbortWithStatusJSON(http.StatusUnauthorized, apiErr)
return
}
token = response.Token
apiSession = ApiSessionEntry{
Token: response.Token,
// TODO: enable once app name is returned
// AppName: response.AppName,
}
s.mu.Lock()
s.KeyToToken[key] = token
s.KeyToToken[key] = apiSession
s.mu.Unlock()
}
// Add token to request context for downstream services (subscriptions, events, etc.)
c.Set("token", token)
c.Set("token", apiSession.Token)
c.Set("apiAppName", apiSession.AppName)
c.Next()
}
}
// ensureAnalyticsEvent is a middleware that ensures broadcasting an analytics event after a successful request.
func (s *Server) ensureAnalyticsEvent(code string, eventService apicore.EventService) gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
status := c.Writer.Status()
payload, err := util.NewAnalyticsEventForApi(c.Request.Context(), code, status)
if err != nil {
log.Errorf("failed to create api analytics event: %v", err)
return
}
eventService.Broadcast(event.NewEventSingleMessage("", &pb.EventMessageValueOfPayloadBroadcast{
PayloadBroadcast: &pb.EventPayloadBroadcast{
Payload: payload,
},
}))
}
}
// ensureMetadataHeader is a middleware that ensures the metadata header is set.
func (s *Server) ensureMetadataHeader() gin.HandlerFunc {
return func(c *gin.Context) {

View file

@ -34,7 +34,7 @@ func TestEnsureAuthenticated(t *testing.T) {
t.Run("missing auth header", func(t *testing.T) {
// given
fx := newFixture(t)
fx.KeyToToken = make(map[string]string)
fx.KeyToToken = make(map[string]ApiSessionEntry)
middleware := fx.ensureAuthenticated(fx.mwMock)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
@ -54,7 +54,7 @@ func TestEnsureAuthenticated(t *testing.T) {
t.Run("invalid auth header format", func(t *testing.T) {
// given
fx := newFixture(t)
fx.KeyToToken = make(map[string]string)
fx.KeyToToken = make(map[string]ApiSessionEntry)
middleware := fx.ensureAuthenticated(fx.mwMock)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
@ -75,7 +75,7 @@ func TestEnsureAuthenticated(t *testing.T) {
t.Run("valid token creation", func(t *testing.T) {
// given
fx := newFixture(t)
fx.KeyToToken = make(map[string]string)
fx.KeyToToken = make(map[string]ApiSessionEntry)
tokenExpected := "valid-token"
fx.mwMock.
@ -108,7 +108,7 @@ func TestEnsureAuthenticated(t *testing.T) {
t.Run("invalid token", func(t *testing.T) {
// given
fx := newFixture(t)
fx.KeyToToken = make(map[string]string)
fx.KeyToToken = make(map[string]ApiSessionEntry)
middleware := fx.ensureAuthenticated(fx.mwMock)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
@ -139,9 +139,8 @@ func TestEnsureAuthenticated(t *testing.T) {
}
func TestRateLimit(t *testing.T) {
fx := newFixture(t)
router := gin.New()
router.GET("/", fx.rateLimit(1), func(c *gin.Context) {
router.GET("/", ensureRateLimit(1, 1, false), func(c *gin.Context) {
c.String(http.StatusOK, "OK")
})
@ -170,4 +169,51 @@ func TestRateLimit(t *testing.T) {
// then
require.Equal(t, http.StatusTooManyRequests, w.Code)
})
t.Run("burst of size 2 allows two requests", func(t *testing.T) {
burstRouter := gin.New()
burstRouter.GET("/", ensureRateLimit(1, 2, false), func(c *gin.Context) {
c.String(http.StatusOK, "OK")
})
// first request (within burst)
w1 := httptest.NewRecorder()
req1 := httptest.NewRequest("GET", "/", nil)
req1.RemoteAddr = "1.2.3.4:5678"
burstRouter.ServeHTTP(w1, req1)
require.Equal(t, http.StatusOK, w1.Code)
// second request (within burst)
w2 := httptest.NewRecorder()
req2 := httptest.NewRequest("GET", "/", nil)
req2.RemoteAddr = "1.2.3.4:5678"
burstRouter.ServeHTTP(w2, req2)
require.Equal(t, http.StatusOK, w2.Code)
// third request should be rate-limited
w3 := httptest.NewRecorder()
req3 := httptest.NewRequest("GET", "/", nil)
req3.RemoteAddr = "1.2.3.4:5678"
burstRouter.ServeHTTP(w3, req3)
require.Equal(t, http.StatusTooManyRequests, w3.Code)
})
t.Run("disabled rate limit allows all requests", func(t *testing.T) {
// given
disabledRouter := gin.New()
disabledRouter.GET("/", ensureRateLimit(1, 1, true), func(c *gin.Context) {
c.String(http.StatusOK, "OK")
})
// when
for i := 0; i < 5; i++ {
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/", nil)
req.RemoteAddr = "1.2.3.4:5678"
disabledRouter.ServeHTTP(w, req)
// then
require.Equal(t, http.StatusOK, w.Code)
}
})
}

View file

@ -9,7 +9,6 @@ import (
apicore "github.com/anyproto/anytype-heart/core/api/core"
_ "github.com/anyproto/anytype-heart/core/api/docs"
"github.com/anyproto/anytype-heart/core/api/handler"
"github.com/anyproto/anytype-heart/core/api/pagination"
)
@ -18,13 +17,15 @@ const (
defaultPageSize = 100
minPageSize = 1
maxPageSize = 1000
maxWriteRequestsPerSecond = 1
maxWriteRequestsPerSecond = 1 // allow sustained 1 request per second
maxBurstRequests = 60 // allow all requests in the first second
)
// NewRouter builds and returns a *gin.Engine with all routes configured.
func (s *Server) NewRouter(mw apicore.ClientCommands) *gin.Engine {
debug := os.Getenv("ANYTYPE_API_DEBUG") == "1"
if !debug {
func (s *Server) NewRouter(mw apicore.ClientCommands, eventService apicore.EventService, openapiYAML []byte, openapiJSON []byte) *gin.Engine {
isDebug := os.Getenv("ANYTYPE_API_DEBUG") == "1"
if !isDebug {
gin.SetMode(gin.ReleaseMode)
}
@ -32,9 +33,10 @@ func (s *Server) NewRouter(mw apicore.ClientCommands) *gin.Engine {
router.Use(gin.Recovery())
router.Use(s.ensureMetadataHeader())
if debug {
if isDebug {
router.Use(gin.Logger())
}
paginator := pagination.New(pagination.Config{
DefaultPage: defaultPage,
DefaultPageSize: defaultPageSize,
@ -42,17 +44,34 @@ func (s *Server) NewRouter(mw apicore.ClientCommands) *gin.Engine {
MaxPageSize: maxPageSize,
})
// Shared ratelimiter with option to disable it through env var
isRateLimitDisabled := os.Getenv("ANYTYPE_API_DISABLE_RATE_LIMIT") == "1"
writeRateLimitMW := ensureRateLimit(maxWriteRequestsPerSecond, maxBurstRequests, isRateLimitDisabled)
// Swagger route
router.GET("/swagger/*any", func(c *gin.Context) {
target := "https://developers.anytype.io/docs/reference"
c.Redirect(http.StatusMovedPermanently, target)
})
router.GET("/docs/openapi.yaml", func(c *gin.Context) {
c.Data(http.StatusOK, "application/x-yaml", openapiYAML)
})
router.GET("/docs/openapi.json", func(c *gin.Context) {
c.Data(http.StatusOK, "application/json", openapiJSON)
})
// Auth routes (no authentication required)
authGroup := router.Group("/v1/auth")
authGroup := router.Group("/v1")
{
authGroup.POST("/display_code", handler.DisplayCodeHandler(s.service))
authGroup.POST("/token", handler.TokenHandler(s.service))
// TO BE DEPRECATED
authGroup.POST("/auth/display_code", handler.DisplayCodeHandler(s.service))
// TO BE DEPRECATED
authGroup.POST("/auth/token", handler.TokenHandler(s.service))
authGroup.POST("/auth/challenges", handler.CreateChallengeHandler(s.service))
authGroup.POST("/auth/api_keys", handler.CreateApiKeyHandler(s.service))
}
// API routes
@ -62,63 +81,63 @@ func (s *Server) NewRouter(mw apicore.ClientCommands) *gin.Engine {
{
// Block
// TODO: implement create, update and delete block endpoints
// v1.POST("/spaces/:space_id/objects/:object_id/blocks", s.rateLimit(maxWriteRequestsPerSecond), object.CreateBlockHandler(s.service))
// v1.PATCH("/spaces/:space_id/objects/:object_id/blocks/:block_id", s.rateLimit(maxWriteRequestsPerSecond), object.UpdateBlockHandler(s.service))
// v1.DELETE("/spaces/:space_id/objects/:object_id/blocks/:block_id", s.rateLimit(maxWriteRequestsPerSecond), object.DeleteBlockHandler(s.service))
// v1.POST("/spaces/:space_id/objects/:object_id/blocks", writeRateLimitMW, object.CreateBlockHandler(s.service))
// v1.PATCH("/spaces/:space_id/objects/:object_id/blocks/:block_id", writeRateLimitMW, object.UpdateBlockHandler(s.service))
// v1.DELETE("/spaces/:space_id/objects/:object_id/blocks/:block_id", writeRateLimitMW, object.DeleteBlockHandler(s.service))
// List
v1.GET("/spaces/:space_id/lists/:list_id/views", handler.GetListViewsHandler(s.service))
v1.GET("/spaces/:space_id/lists/:list_id/:view_id/objects", handler.GetObjectsInListHandler(s.service))
v1.POST("/spaces/:space_id/lists/:list_id/objects", s.rateLimit(maxWriteRequestsPerSecond), handler.AddObjectsToListHandler(s.service))
v1.DELETE("/spaces/:space_id/lists/:list_id/objects/:object_id", s.rateLimit(maxWriteRequestsPerSecond), handler.RemoveObjectFromListHandler(s.service))
v1.GET("/spaces/:space_id/lists/:list_id/views", s.ensureAnalyticsEvent("ListGetViews", eventService), handler.GetListViewsHandler(s.service))
v1.GET("/spaces/:space_id/lists/:list_id/views/:view_id/objects", s.ensureAnalyticsEvent("ListGetObjects", eventService), handler.GetObjectsInListHandler(s.service))
v1.POST("/spaces/:space_id/lists/:list_id/objects", writeRateLimitMW, s.ensureAnalyticsEvent("ListAddObject", eventService), handler.AddObjectsToListHandler(s.service))
v1.DELETE("/spaces/:space_id/lists/:list_id/objects/:object_id", writeRateLimitMW, s.ensureAnalyticsEvent("ListRemoveObject", eventService), handler.RemoveObjectFromListHandler(s.service))
// Member
v1.GET("/spaces/:space_id/members", handler.ListMembersHandler(s.service))
v1.GET("/spaces/:space_id/members/:member_id", handler.GetMemberHandler(s.service))
v1.GET("/spaces/:space_id/members", s.ensureAnalyticsEvent("MemberList", eventService), handler.ListMembersHandler(s.service))
v1.GET("/spaces/:space_id/members/:member_id", s.ensureAnalyticsEvent("MemberOpen", eventService), handler.GetMemberHandler(s.service))
// TODO: renable when granular permissions are implementeds
// v1.PATCH("/spaces/:space_id/members/:member_id", s.rateLimit(maxWriteRequestsPerSecond), space.UpdateMemberHandler(s.service))
// v1.PATCH("/spaces/:space_id/members/:member_id", writeRateLimitMW, space.UpdateMemberHandler(s.service))
// Object
v1.GET("/spaces/:space_id/objects", handler.ListObjectsHandler(s.service))
v1.GET("/spaces/:space_id/objects/:object_id", handler.GetObjectHandler(s.service))
v1.POST("/spaces/:space_id/objects", s.rateLimit(maxWriteRequestsPerSecond), handler.CreateObjectHandler(s.service))
v1.PATCH("/spaces/:space_id/objects/:object_id", s.rateLimit(maxWriteRequestsPerSecond), handler.UpdateObjectHandler(s.service))
v1.DELETE("/spaces/:space_id/objects/:object_id", s.rateLimit(maxWriteRequestsPerSecond), handler.DeleteObjectHandler(s.service))
v1.GET("/spaces/:space_id/objects", s.ensureAnalyticsEvent("ObjectList", eventService), handler.ListObjectsHandler(s.service))
v1.GET("/spaces/:space_id/objects/:object_id", s.ensureAnalyticsEvent("ObjectOpen", eventService), handler.GetObjectHandler(s.service))
v1.POST("/spaces/:space_id/objects", writeRateLimitMW, s.ensureAnalyticsEvent("ObjectCreate", eventService), handler.CreateObjectHandler(s.service))
v1.PATCH("/spaces/:space_id/objects/:object_id", writeRateLimitMW, s.ensureAnalyticsEvent("ObjectUpdate", eventService), handler.UpdateObjectHandler(s.service))
v1.DELETE("/spaces/:space_id/objects/:object_id", writeRateLimitMW, s.ensureAnalyticsEvent("ObjectDelete", eventService), handler.DeleteObjectHandler(s.service))
// Property
v1.GET("/spaces/:space_id/properties", handler.ListPropertiesHandler(s.service))
v1.GET("/spaces/:space_id/properties/:property_id", handler.GetPropertyHandler(s.service))
v1.POST("/spaces/:space_id/properties", s.rateLimit(maxWriteRequestsPerSecond), handler.CreatePropertyHandler(s.service))
v1.PATCH("/spaces/:space_id/properties/:property_id", s.rateLimit(maxWriteRequestsPerSecond), handler.UpdatePropertyHandler(s.service))
v1.DELETE("/spaces/:space_id/properties/:property_id", s.rateLimit(maxWriteRequestsPerSecond), handler.DeletePropertyHandler(s.service))
v1.GET("/spaces/:space_id/properties", s.ensureAnalyticsEvent("PropertyList", eventService), handler.ListPropertiesHandler(s.service))
v1.GET("/spaces/:space_id/properties/:property_id", s.ensureAnalyticsEvent("PropertyOpen", eventService), handler.GetPropertyHandler(s.service))
v1.POST("/spaces/:space_id/properties", writeRateLimitMW, s.ensureAnalyticsEvent("PropertyCreate", eventService), handler.CreatePropertyHandler(s.service))
v1.PATCH("/spaces/:space_id/properties/:property_id", writeRateLimitMW, s.ensureAnalyticsEvent("PropertyUpdate", eventService), handler.UpdatePropertyHandler(s.service))
v1.DELETE("/spaces/:space_id/properties/:property_id", writeRateLimitMW, s.ensureAnalyticsEvent("PropertyDelete", eventService), handler.DeletePropertyHandler(s.service))
// Search
v1.POST("/search", handler.GlobalSearchHandler(s.service))
v1.POST("/spaces/:space_id/search", handler.SearchHandler(s.service))
v1.POST("/search", s.ensureAnalyticsEvent("SearchGlobal", eventService), handler.GlobalSearchHandler(s.service))
v1.POST("/spaces/:space_id/search", s.ensureAnalyticsEvent("SearchSpace", eventService), handler.SearchHandler(s.service))
// Space
v1.GET("/spaces", handler.ListSpacesHandler(s.service))
v1.GET("/spaces/:space_id", handler.GetSpaceHandler(s.service))
v1.POST("/spaces", s.rateLimit(maxWriteRequestsPerSecond), handler.CreateSpaceHandler(s.service))
v1.PATCH("/spaces/:space_id", s.rateLimit(maxWriteRequestsPerSecond), handler.UpdateSpaceHandler(s.service))
v1.GET("/spaces", s.ensureAnalyticsEvent("SpaceList", eventService), handler.ListSpacesHandler(s.service))
v1.GET("/spaces/:space_id", s.ensureAnalyticsEvent("SpaceOpen", eventService), handler.GetSpaceHandler(s.service))
v1.POST("/spaces", writeRateLimitMW, s.ensureAnalyticsEvent("SpaceCreate", eventService), handler.CreateSpaceHandler(s.service))
v1.PATCH("/spaces/:space_id", writeRateLimitMW, s.ensureAnalyticsEvent("SpaceUpdate", eventService), handler.UpdateSpaceHandler(s.service))
// Tag
v1.GET("/spaces/:space_id/properties/:property_id/tags", handler.ListTagsHandler(s.service))
v1.GET("/spaces/:space_id/properties/:property_id/tags/:tag_id", handler.GetTagHandler(s.service))
v1.POST("/spaces/:space_id/properties/:property_id/tags", s.rateLimit(maxWriteRequestsPerSecond), handler.CreateTagHandler(s.service))
v1.PATCH("/spaces/:space_id/properties/:property_id/tags/:tag_id", s.rateLimit(maxWriteRequestsPerSecond), handler.UpdateTagHandler(s.service))
v1.DELETE("/spaces/:space_id/properties/:property_id/tags/:tag_id", s.rateLimit(maxWriteRequestsPerSecond), handler.DeleteTagHandler(s.service))
v1.GET("/spaces/:space_id/properties/:property_id/tags", s.ensureAnalyticsEvent("TagList", eventService), handler.ListTagsHandler(s.service))
v1.GET("/spaces/:space_id/properties/:property_id/tags/:tag_id", s.ensureAnalyticsEvent("TagOpen", eventService), handler.GetTagHandler(s.service))
v1.POST("/spaces/:space_id/properties/:property_id/tags", writeRateLimitMW, s.ensureAnalyticsEvent("TagCreate", eventService), handler.CreateTagHandler(s.service))
v1.PATCH("/spaces/:space_id/properties/:property_id/tags/:tag_id", writeRateLimitMW, s.ensureAnalyticsEvent("TagUpdate", eventService), handler.UpdateTagHandler(s.service))
v1.DELETE("/spaces/:space_id/properties/:property_id/tags/:tag_id", writeRateLimitMW, s.ensureAnalyticsEvent("TagDelete", eventService), handler.DeleteTagHandler(s.service))
// Template
v1.GET("/spaces/:space_id/types/:type_id/templates", handler.ListTemplatesHandler(s.service))
v1.GET("/spaces/:space_id/types/:type_id/templates/:template_id", handler.GetTemplateHandler(s.service))
v1.GET("/spaces/:space_id/types/:type_id/templates", s.ensureAnalyticsEvent("TemplateList", eventService), handler.ListTemplatesHandler(s.service))
v1.GET("/spaces/:space_id/types/:type_id/templates/:template_id", s.ensureAnalyticsEvent("TemplateOpen", eventService), handler.GetTemplateHandler(s.service))
// Type
v1.GET("/spaces/:space_id/types", handler.ListTypesHandler(s.service))
v1.GET("/spaces/:space_id/types/:type_id", handler.GetTypeHandler(s.service))
v1.POST("/spaces/:space_id/types", s.rateLimit(maxWriteRequestsPerSecond), handler.CreateTypeHandler(s.service))
v1.PATCH("/spaces/:space_id/types/:type_id", s.rateLimit(maxWriteRequestsPerSecond), handler.UpdateTypeHandler(s.service))
v1.DELETE("/spaces/:space_id/types/:type_id", s.rateLimit(maxWriteRequestsPerSecond), handler.DeleteTypeHandler(s.service))
v1.GET("/spaces/:space_id/types", s.ensureAnalyticsEvent("TypeList", eventService), handler.ListTypesHandler(s.service))
v1.GET("/spaces/:space_id/types/:type_id", s.ensureAnalyticsEvent("TypeOpen", eventService), handler.GetTypeHandler(s.service))
v1.POST("/spaces/:space_id/types", writeRateLimitMW, s.ensureAnalyticsEvent("TypeCreate", eventService), handler.CreateTypeHandler(s.service))
v1.PATCH("/spaces/:space_id/types/:type_id", writeRateLimitMW, s.ensureAnalyticsEvent("TypeUpdate", eventService), handler.UpdateTypeHandler(s.service))
v1.DELETE("/spaces/:space_id/types/:type_id", writeRateLimitMW, s.ensureAnalyticsEvent("TypeDelete", eventService), handler.DeleteTypeHandler(s.service))
}
return router

View file

@ -16,7 +16,7 @@ func TestRouter_Unauthenticated(t *testing.T) {
t.Run("GET /v1/spaces without auth returns 401", func(t *testing.T) {
// given
fx := newFixture(t)
engine := fx.NewRouter(fx.mwMock)
engine := fx.NewRouter(fx.mwMock, &fx.eventService, []byte{}, []byte{})
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/v1/spaces", nil)
@ -32,7 +32,7 @@ func TestRouter_AuthRoute(t *testing.T) {
t.Run("POST /v1/auth/token is accessible without auth", func(t *testing.T) {
// given
fx := newFixture(t)
engine := fx.NewRouter(fx.mwMock)
engine := fx.NewRouter(fx.mwMock, &fx.eventService, []byte{}, []byte{})
w := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/v1/auth/token", nil)
@ -48,13 +48,14 @@ func TestRouter_MetadataHeader(t *testing.T) {
t.Run("Response includes Anytype-Version header", func(t *testing.T) {
// given
fx := newFixture(t)
engine := fx.NewRouter(fx.mwMock)
fx.KeyToToken = map[string]string{"validKey": "dummyToken"}
engine := fx.NewRouter(fx.mwMock, &fx.eventService, []byte{}, []byte{})
fx.KeyToToken = map[string]ApiSessionEntry{"validKey": {Token: "dummyToken", AppName: "dummyApp"}}
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).
Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}, nil).Once()
fx.eventService.On("Broadcast", mock.Anything).Return(nil).Maybe()
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/v1/spaces", nil)
@ -64,6 +65,6 @@ func TestRouter_MetadataHeader(t *testing.T) {
engine.ServeHTTP(w, req)
// then
require.Equal(t, "2025-04-22", w.Header().Get("Anytype-Version"))
require.Equal(t, "2025-05-20", w.Header().Get("Anytype-Version"))
})
}

View file

@ -10,25 +10,30 @@ import (
"github.com/anyproto/anytype-heart/core/api/service"
)
type ApiSessionEntry struct {
Token string `json:"token"`
AppName string `json:"appName"`
}
// Server wraps the HTTP server and service logic.
type Server struct {
engine *gin.Engine
service *service.Service
mu sync.Mutex
KeyToToken map[string]string // appKey -> token
KeyToToken map[string]ApiSessionEntry // appKey -> token
}
// NewServer constructs a new Server with default config and sets up the routes.
func NewServer(mw apicore.ClientCommands, accountService apicore.AccountService, exportService apicore.ExportService) *Server {
// NewServer constructs a new Server with the default config and sets up the routes.
func NewServer(mw apicore.ClientCommands, accountService apicore.AccountService, eventService apicore.EventService, openapiYAML []byte, openapiJSON []byte) *Server {
gatewayUrl, techSpaceId, err := getAccountInfo(accountService)
if err != nil {
panic(err)
}
s := &Server{service: service.NewService(mw, exportService, gatewayUrl, techSpaceId)}
s.engine = s.NewRouter(mw)
s.KeyToToken = make(map[string]string)
s := &Server{service: service.NewService(mw, gatewayUrl, techSpaceId)}
s.engine = s.NewRouter(mw, eventService, openapiYAML, openapiJSON)
s.KeyToToken = make(map[string]ApiSessionEntry)
return s
}

View file

@ -18,18 +18,19 @@ const (
type fixture struct {
*Server
mwMock *mock_apicore.MockClientCommands
eventService mock_apicore.MockEventService
mwMock *mock_apicore.MockClientCommands
}
func newFixture(t *testing.T) *fixture {
mwMock := mock_apicore.NewMockClientCommands(t)
accountService := mock_apicore.NewMockAccountService(t)
exportService := mock_apicore.NewMockExportService(t)
eventService := mock_apicore.NewMockEventService(t)
accountService.On("GetInfo", mock.Anything).Return(&model.AccountInfo{
GatewayUrl: mockedGatewayUrl,
TechSpaceId: mockedTechSpaceId,
}, nil).Once()
server := NewServer(mwMock, accountService, exportService)
server := NewServer(mwMock, accountService, eventService, []byte{}, []byte{})
return &fixture{
Server: server,

View file

@ -2,6 +2,7 @@ package api
import (
"context"
_ "embed"
"errors"
"fmt"
"net/http"
@ -14,7 +15,7 @@ import (
"github.com/anyproto/anytype-heart/core/anytype/config"
apicore "github.com/anyproto/anytype-heart/core/api/core"
"github.com/anyproto/anytype-heart/core/api/server"
"github.com/anyproto/anytype-heart/core/block/export"
"github.com/anyproto/anytype-heart/core/event"
)
const (
@ -24,6 +25,12 @@ const (
var (
mwSrv apicore.ClientCommands
//go:embed docs/openapi.yaml
openapiYAML []byte
//go:embed docs/openapi.json
openapiJSON []byte
)
type Service interface {
@ -36,7 +43,7 @@ type apiService struct {
httpSrv *http.Server
mw apicore.ClientCommands
accountService apicore.AccountService
exportService apicore.ExportService
eventService apicore.EventService
listenAddr string
lock sync.Mutex
}
@ -52,8 +59,8 @@ func (s *apiService) Name() (name string) {
// Init initializes the API service.
//
// @title Anytype API
// @version 2025-04-22
// @description This API empowers seamless interaction with Anytype's resources—spaces, objects, properties, types, templates, and beyond.
// @version 2025-05-20
// @description This API enables seamless interaction with Anytype's resources - spaces, objects, properties, types, templates, and beyond.
// @termsOfService https://anytype.io/terms_of_use
// @contact.name Anytype Support
// @contact.url https://anytype.io/contact
@ -61,14 +68,13 @@ func (s *apiService) Name() (name string) {
// @license.name Any Source Available License 1.0
// @license.url https://github.com/anyproto/anytype-api/blob/main/LICENSE.md
// @host http://localhost:31009
// @BasePath /v1
// @securitydefinitions.bearerauth BearerAuth
// @externalDocs.description OpenAPI
// @externalDocs.url https://swagger.io/resources/open-api/
func (s *apiService) Init(a *app.App) (err error) {
s.listenAddr = a.MustComponent(config.CName).(*config.Config).JsonApiListenAddr
s.accountService = a.MustComponent(account.CName).(account.Service)
s.exportService = a.MustComponent(export.CName).(apicore.ExportService)
s.eventService = a.MustComponent(event.CName).(apicore.EventService)
return nil
}
@ -89,7 +95,8 @@ func (s *apiService) runServer() {
return
}
s.srv = server.NewServer(s.mw, s.accountService, s.exportService)
s.srv = server.NewServer(s.mw, s.accountService, s.eventService, openapiYAML, openapiJSON)
s.httpSrv = &http.Server{
Addr: s.listenAddr,
Handler: s.srv.Engine(),

View file

@ -4,19 +4,19 @@ import (
"context"
"errors"
"github.com/anyproto/anytype-heart/core/api/util"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
var (
ErrMissingAppName = errors.New("missing app name")
ErrFailedGenerateChallenge = errors.New("failed to generate a new challenge")
ErrInvalidInput = errors.New("invalid input")
ErrFailedAuthenticate = errors.New("failed to authenticate user")
ErrMissingAppName = errors.New("missing app name")
ErrFailedCreateNewChallenge = errors.New("failed to create a new challenge")
ErrFailedAuthenticate = errors.New("failed to authenticate user")
)
// NewChallenge calls AccountLocalLinkNewChallenge and returns the challenge ID, or an error if it fails.
func (s *Service) NewChallenge(ctx context.Context, appName string) (string, error) {
// CreateChallenge calls AccountLocalLinkNewChallenge and returns the challenge ID
func (s *Service) CreateChallenge(ctx context.Context, appName string) (string, error) {
if appName == "" {
return "", ErrMissingAppName
}
@ -27,19 +27,18 @@ func (s *Service) NewChallenge(ctx context.Context, appName string) (string, err
})
if resp.Error != nil && resp.Error.Code != pb.RpcAccountLocalLinkNewChallengeResponseError_NULL {
return "", ErrFailedGenerateChallenge
return "", ErrFailedCreateNewChallenge
}
return resp.ChallengeId, nil
}
// SolveChallenge calls AccountLocalLinkSolveChallenge and returns the session token + app key, or an error if it fails.
// SolveChallenge calls AccountLocalLinkSolveChallenge and returns the session token + app key
func (s *Service) SolveChallenge(ctx context.Context, challengeId string, code string) (appKey string, err error) {
if challengeId == "" || code == "" {
return "", ErrInvalidInput
return "", util.ErrBadInput("challenge_id or code is empty")
}
// Call AccountLocalLinkSolveChallenge to retrieve session token and app key
resp := s.mw.AccountLocalLinkSolveChallenge(ctx, &pb.RpcAccountLocalLinkSolveChallengeRequest{
ChallengeId: challengeId,
Answer: code,

View file

@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/core/api/util"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
@ -34,7 +35,7 @@ func TestAuthService_GenerateNewChallenge(t *testing.T) {
}).Once()
// when
challengeId, err := fx.service.NewChallenge(ctx, mockedAppName)
challengeId, err := fx.service.CreateChallenge(ctx, mockedAppName)
// then
require.NoError(t, err)
@ -47,7 +48,7 @@ func TestAuthService_GenerateNewChallenge(t *testing.T) {
fx := newFixture(t)
// when
challengeId, err := fx.service.NewChallenge(ctx, "")
challengeId, err := fx.service.CreateChallenge(ctx, "")
// then
require.Error(t, err)
@ -69,11 +70,11 @@ func TestAuthService_GenerateNewChallenge(t *testing.T) {
}).Once()
// when
challengeId, err := fx.service.NewChallenge(ctx, mockedAppName)
challengeId, err := fx.service.CreateChallenge(ctx, mockedAppName)
// then
require.Error(t, err)
require.Equal(t, ErrFailedGenerateChallenge, err)
require.Equal(t, ErrFailedCreateNewChallenge, err)
require.Empty(t, challengeId)
})
}
@ -112,7 +113,7 @@ func TestAuthService_SolveChallengeForToken(t *testing.T) {
// then
require.Error(t, err)
require.Equal(t, ErrInvalidInput, err)
require.ErrorIs(t, err, util.ErrBad)
require.Empty(t, appKey)
})

47
core/api/service/icon.go Normal file
View file

@ -0,0 +1,47 @@
package service
import (
"fmt"
"unicode"
apimodel "github.com/anyproto/anytype-heart/core/api/model"
)
func IsEmoji(s string) bool {
if s == "" {
return false
}
for _, r := range s {
if unicode.Is(unicode.Cf, r) || unicode.Is(unicode.Mn, r) || unicode.Is(unicode.So, r) || unicode.Is(unicode.Sk, r) {
continue
} else {
return false
}
}
return true
}
// GetIcon returns the appropriate Icon implementation.
func GetIcon(gatewayUrl string, iconEmoji string, iconImage string, iconName string, iconOption float64) apimodel.Icon {
if iconName != "" {
return apimodel.Icon{WrappedIcon: apimodel.NamedIcon{
Format: apimodel.IconFormatIcon,
Name: apimodel.IconName(iconName),
Color: apimodel.IconOptionToColor[iconOption],
}}
}
if iconEmoji != "" {
return apimodel.Icon{WrappedIcon: apimodel.EmojiIcon{
Format: apimodel.IconFormatEmoji,
Emoji: iconEmoji,
}}
}
if iconImage != "" {
return apimodel.Icon{WrappedIcon: apimodel.FileIcon{
Format: apimodel.IconFormatFile,
File: fmt.Sprintf("%s/image/%s", gatewayUrl, iconImage),
}}
}
return apimodel.Icon{}
}

View file

@ -0,0 +1,47 @@
package service
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestIsEmoji(t *testing.T) {
tests := []struct {
name string
input string
want bool
}{
// valid single-code-point emoji
{"GrinningFace", "😀", true},
// yin-yang with variation selector
{"YinYangWithVS", "☯️", true},
// emoji + skin tone modifier
{"ThumbsUpMediumSkinTone", "👍🏽", true},
// ZWJ sequence (couple kissing)
{"CoupleKissingZWJ", "👩‍❤️‍💋‍👨", true},
// string of emojis
{"MultipleEmojis", "😀😃😄", true},
// invalid: letters only
{"Letters", "abc", false},
// invalid: mixed emoji + letter
{"EmojiPlusLetter", "😀a", false},
// invalid: digits
{"Digit", "1", false},
// invalid: punctuation
{"Punctuation", "!", false},
// invalid: whitespace
{"Whitespace", " ", false},
// invalid: empty string
{"EmptyString", "", false},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
got := IsEmoji(tc.input)
require.Equal(t, tc.want, got, "IsEmoji(%q)", tc.input)
})
}
}

View file

@ -152,7 +152,7 @@ func (s *Service) GetObjectsInList(ctx context.Context, spaceId string, listId s
var collectionId string
var source []string
switch model.ObjectTypeLayout(typeDetail.Fields[bundle.RelationKeyRecommendedLayout.String()].GetNumberValue()) {
case model.ObjectType_set:
case model.ObjectType_set, model.ObjectType_objectType:
// for queries, we search within the space for objects of the setOf type
setOfValues := resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeySetOf.String()].GetListValue().Values
for _, value := range setOfValues {
@ -197,31 +197,31 @@ func (s *Service) GetObjectsInList(ctx context.Context, spaceId string, listId s
total := int(searchResp.Counters.Total)
hasMore := searchResp.Counters.Total > int64(offset+limit)
propertyMap, err := s.GetPropertyMapFromStore(spaceId)
propertyMap, err := s.getPropertyMapFromStore(ctx, spaceId, true)
if err != nil {
return nil, 0, false, err
}
typeMap, err := s.GetTypeMapFromStore(spaceId, propertyMap)
typeMap, err := s.getTypeMapFromStore(ctx, spaceId, propertyMap, false)
if err != nil {
return nil, 0, false, err
}
tagMap, err := s.GetTagMapFromStore(spaceId)
tagMap, err := s.getTagMapFromStore(ctx, spaceId)
if err != nil {
return nil, 0, false, err
}
objects := make([]apimodel.Object, 0, len(searchResp.Records))
for _, record := range searchResp.Records {
objects = append(objects, s.GetObjectFromStruct(record, propertyMap, typeMap, tagMap))
objects = append(objects, s.getObjectFromStruct(record, propertyMap, typeMap, tagMap))
}
return objects, total, hasMore, nil
}
// AddObjectsToList adds objects to a list
func (s *Service) AddObjectsToList(ctx context.Context, spaceId string, listId string, objectIds []string) error {
func (s *Service) AddObjectsToList(ctx context.Context, _ string, listId string, request apimodel.AddObjectsToListRequest) error {
resp := s.mw.ObjectCollectionAdd(ctx, &pb.RpcObjectCollectionAddRequest{
ContextId: listId,
ObjectIds: objectIds,
ObjectIds: request.Objects,
})
if resp.Error != nil && resp.Error.Code != pb.RpcObjectCollectionAddResponseError_NULL {

View file

@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
apimodel "github.com/anyproto/anytype-heart/core/api/model"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
@ -408,7 +409,7 @@ func TestListService_GetObjectsInList(t *testing.T) {
}).
Return(&pb.RpcObjectSearchUnsubscribeResponse{}, nil).Once()
// Mock GetPropertyMapsFromStore
// Mock getPropertyMapsFromStore
fx.mwMock.
On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
@ -427,6 +428,7 @@ func TestListService_GetObjectsInList(t *testing.T) {
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyRelationKey.String(),
bundle.RelationKeyApiObjectKey.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyRelationFormat.String(),
},
@ -443,7 +445,7 @@ func TestListService_GetObjectsInList(t *testing.T) {
},
}, nil).Once()
// Mock GetTypeMapFromStore
// Mock getTypeMapFromStore
fx.mwMock.
On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
@ -460,6 +462,7 @@ func TestListService_GetObjectsInList(t *testing.T) {
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyUniqueKey.String(),
bundle.RelationKeyApiObjectKey.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyPluralName.String(),
bundle.RelationKeyIconEmoji.String(),
@ -489,7 +492,7 @@ func TestListService_GetObjectsInList(t *testing.T) {
},
}, nil).Once()
// Mock GetTagMapFromStore
// Mock getTagMapFromStore
fx.mwMock.
On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
@ -653,7 +656,7 @@ func TestListService_GetObjectsInList(t *testing.T) {
}).
Return(&pb.RpcObjectSearchUnsubscribeResponse{}, nil).Once()
// Mock GetPropertyMapsFromStore
// Mock getPropertyMapsFromStore
fx.mwMock.
On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
@ -672,6 +675,7 @@ func TestListService_GetObjectsInList(t *testing.T) {
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyRelationKey.String(),
bundle.RelationKeyApiObjectKey.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyRelationFormat.String(),
},
@ -688,7 +692,7 @@ func TestListService_GetObjectsInList(t *testing.T) {
},
}, nil).Once()
// Mock GetTypeMapFromStore
// Mock getTypeMapFromStore
fx.mwMock.
On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
@ -705,6 +709,7 @@ func TestListService_GetObjectsInList(t *testing.T) {
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyUniqueKey.String(),
bundle.RelationKeyApiObjectKey.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyPluralName.String(),
bundle.RelationKeyIconEmoji.String(),
@ -734,7 +739,7 @@ func TestListService_GetObjectsInList(t *testing.T) {
},
}, nil).Once()
// Mock GetTagMapFromStore
// Mock getTagMapFromStore
fx.mwMock.
On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
@ -1108,7 +1113,7 @@ func TestListService_GetObjectsInList(t *testing.T) {
}).
Return(&pb.RpcObjectSearchUnsubscribeResponse{}, nil).Once()
// Mock GetPropertyMapsFromStore to return an error.
// Mock getPropertyMapsFromStore to return an error.
fx.mwMock.
On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
@ -1127,6 +1132,7 @@ func TestListService_GetObjectsInList(t *testing.T) {
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyRelationKey.String(),
bundle.RelationKeyApiObjectKey.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyRelationFormat.String(),
},
@ -1149,19 +1155,19 @@ func TestListService_AddObjectsToList(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
objectIds := []string{"obj-1", "obj-2"}
request := apimodel.AddObjectsToListRequest{Objects: []string{"obj-1", "obj-2"}}
fx.mwMock.
On("ObjectCollectionAdd", mock.Anything, &pb.RpcObjectCollectionAddRequest{
ContextId: mockedListId,
ObjectIds: objectIds,
ObjectIds: request.Objects,
}).
Return(&pb.RpcObjectCollectionAddResponse{
Error: &pb.RpcObjectCollectionAddResponseError{Code: pb.RpcObjectCollectionAddResponseError_NULL},
}, nil).Once()
// when
err := fx.service.AddObjectsToList(ctx, mockedSpaceId, mockedListId, objectIds)
err := fx.service.AddObjectsToList(ctx, mockedSpaceId, mockedListId, request)
// then
require.NoError(t, err)
@ -1171,19 +1177,19 @@ func TestListService_AddObjectsToList(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
objectIds := []string{"obj-1"}
request := apimodel.AddObjectsToListRequest{Objects: []string{"obj-1"}}
fx.mwMock.
On("ObjectCollectionAdd", mock.Anything, &pb.RpcObjectCollectionAddRequest{
ContextId: mockedListId,
ObjectIds: objectIds,
ObjectIds: request.Objects,
}).
Return(&pb.RpcObjectCollectionAddResponse{
Error: &pb.RpcObjectCollectionAddResponseError{Code: pb.RpcObjectCollectionAddResponseError_UNKNOWN_ERROR},
}, nil).Once()
// when
err := fx.service.AddObjectsToList(ctx, mockedSpaceId, mockedListId, objectIds)
err := fx.service.AddObjectsToList(ctx, mockedSpaceId, mockedListId, request)
// then
require.ErrorIs(t, err, ErrFailedAddObjectsToList)

View file

@ -80,7 +80,7 @@ func (s *Service) ListMembers(ctx context.Context, spaceId string, offset int, l
members = make([]apimodel.Member, 0, len(paginatedMembers))
for _, record := range paginatedMembers {
icon := apimodel.GetIcon(s.gatewayUrl, record.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(), record.Fields[bundle.RelationKeyIconImage.String()].GetStringValue(), "", 0)
icon := GetIcon(s.gatewayUrl, record.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(), record.Fields[bundle.RelationKeyIconImage.String()].GetStringValue(), "", 0)
member := apimodel.Member{
Object: "member",
@ -107,7 +107,7 @@ func (s *Service) GetMember(ctx context.Context, spaceId string, memberId string
relationKey = bundle.RelationKeyIdentity
}
resp := s.mw.ObjectSearch(context.Background(), &pb.RpcObjectSearchRequest{
resp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
SpaceId: spaceId,
Filters: []*model.BlockContentDataviewFilter{
{
@ -127,7 +127,7 @@ func (s *Service) GetMember(ctx context.Context, spaceId string, memberId string
return apimodel.Member{}, ErrMemberNotFound
}
icon := apimodel.GetIcon(s.gatewayUrl, "", resp.Records[0].Fields[bundle.RelationKeyIconImage.String()].GetStringValue(), "", 0)
icon := GetIcon(s.gatewayUrl, "", resp.Records[0].Fields[bundle.RelationKeyIconImage.String()].GetStringValue(), "", 0)
return apimodel.Member{
Object: "member",

View file

@ -71,21 +71,21 @@ func (s *Service) ListObjects(ctx context.Context, spaceId string, offset int, l
objects = make([]apimodel.Object, 0, len(paginatedObjects))
// pre-fetch properties, types and tags to fill the objects
propertyMap, err := s.GetPropertyMapFromStore(spaceId)
propertyMap, err := s.getPropertyMapFromStore(ctx, spaceId, true)
if err != nil {
return nil, 0, false, err
}
typeMap, err := s.GetTypeMapFromStore(spaceId, propertyMap)
typeMap, err := s.getTypeMapFromStore(ctx, spaceId, propertyMap, false)
if err != nil {
return nil, 0, false, err
}
tagMap, err := s.GetTagMapFromStore(spaceId)
tagMap, err := s.getTagMapFromStore(ctx, spaceId)
if err != nil {
return nil, 0, false, err
}
for _, record := range paginatedObjects {
objects = append(objects, s.GetObjectFromStruct(record, propertyMap, typeMap, tagMap))
objects = append(objects, s.getObjectFromStruct(record, propertyMap, typeMap, tagMap))
}
return objects, total, hasMore, nil
}
@ -111,15 +111,15 @@ func (s *Service) GetObject(ctx context.Context, spaceId string, objectId string
}
}
propertyMap, err := s.GetPropertyMapFromStore(spaceId)
propertyMap, err := s.getPropertyMapFromStore(ctx, spaceId, true)
if err != nil {
return apimodel.ObjectWithBody{}, err
}
typeMap, err := s.GetTypeMapFromStore(spaceId, propertyMap)
typeMap, err := s.getTypeMapFromStore(ctx, spaceId, propertyMap, false)
if err != nil {
return apimodel.ObjectWithBody{}, err
}
tagMap, err := s.GetTagMapFromStore(spaceId)
tagMap, err := s.getTagMapFromStore(ctx, spaceId)
if err != nil {
return apimodel.ObjectWithBody{}, err
}
@ -129,17 +129,26 @@ func (s *Service) GetObject(ctx context.Context, spaceId string, objectId string
return apimodel.ObjectWithBody{}, err
}
return s.GetObjectWithBlocksFromStruct(resp.ObjectView.Details[0].Details, markdown, propertyMap, typeMap, tagMap), nil
return s.getObjectWithBlocksFromStruct(resp.ObjectView.Details[0].Details, markdown, propertyMap, typeMap, tagMap), nil
}
// CreateObject creates a new object in a specific space.
func (s *Service) CreateObject(ctx context.Context, spaceId string, request apimodel.CreateObjectRequest) (apimodel.ObjectWithBody, error) {
request.TypeKey = util.FromTypeApiKey(request.TypeKey)
details, err := s.buildObjectDetails(ctx, spaceId, request)
if err != nil {
return apimodel.ObjectWithBody{}, err
}
propertyMap, err := s.getPropertyMapFromStore(ctx, spaceId, true)
if err != nil {
return apimodel.ObjectWithBody{}, err
}
typeMap, err := s.getTypeMapFromStore(ctx, spaceId, propertyMap, true)
if err != nil {
return apimodel.ObjectWithBody{}, err
}
request.TypeKey = s.ResolveTypeApiKey(typeMap, request.TypeKey)
var objectId string
if request.TypeKey == "ot-bookmark" {
resp := s.mw.ObjectCreateBookmark(ctx, &pb.RpcObjectCreateBookmarkRequest{
@ -273,7 +282,7 @@ func (s *Service) buildObjectDetails(ctx context.Context, spaceId string, reques
bundle.RelationKeyOrigin.String(): pbtypes.Int64(int64(model.ObjectOrigin_api)),
}
iconFields, err := s.processIconFields(ctx, spaceId, request.Icon)
iconFields, err := s.processIconFields(spaceId, request.Icon, false)
if err != nil {
return nil, err
}
@ -300,7 +309,7 @@ func (s *Service) buildUpdatedObjectDetails(ctx context.Context, spaceId string,
}
if request.Icon != nil {
iconFields, err := s.processIconFields(ctx, spaceId, *request.Icon)
iconFields, err := s.processIconFields(spaceId, *request.Icon, false)
if err != nil {
return nil, err
}
@ -323,13 +332,18 @@ func (s *Service) buildUpdatedObjectDetails(ctx context.Context, spaceId string,
}
// processIconFields returns the detail fields corresponding to the given icon.
func (s *Service) processIconFields(ctx context.Context, spaceId string, icon apimodel.Icon) (map[string]*types.Value, error) {
func (s *Service) processIconFields(spaceId string, icon apimodel.Icon, isType bool) (map[string]*types.Value, error) {
iconFields := make(map[string]*types.Value)
switch e := icon.WrappedIcon.(type) {
case apimodel.NamedIcon:
return nil, util.ErrBadInput("icon name and color are not supported for object")
if isType {
iconFields[bundle.RelationKeyIconName.String()] = pbtypes.String(string(e.Name))
iconFields[bundle.RelationKeyIconOption.String()] = pbtypes.Int64(apimodel.ColorToIconOption[e.Color])
} else {
return nil, util.ErrBadInput("icon name and color are not supported for object")
}
case apimodel.EmojiIcon:
if len(e.Emoji) > 0 && !apimodel.IsEmoji(e.Emoji) {
if len(e.Emoji) > 0 && !IsEmoji(e.Emoji) {
return nil, util.ErrBadInput("icon emoji is not valid")
}
iconFields[bundle.RelationKeyIconEmoji.String()] = pbtypes.String(e.Emoji)
@ -361,7 +375,7 @@ func (s *Service) processIconFields(ctx context.Context, spaceId string, icon ap
// Style: model.BlockContentTextStyle_name[int32(content.Text.Style)],
// Checked: content.Text.Checked,
// Color: content.Text.Color,
// Icon: apimodel.GetIcon(s.gatewayUrl, content.Text.IconEmoji, content.Text.IconImage, "", 0),
// Icon: GetIcon(s.gatewayUrl, content.Text.IconEmoji, content.Text.IconImage, "", 0),
// }
// case *model.BlockContentOfFile:
// file = &apimodel.File{
@ -401,13 +415,13 @@ func (s *Service) processIconFields(ctx context.Context, spaceId string, icon ap
// return b
// }
// GetObjectFromStruct creates an Object without blocks from the details.
func (s *Service) GetObjectFromStruct(details *types.Struct, propertyMap map[string]apimodel.Property, typeMap map[string]apimodel.Type, tagMap map[string]apimodel.Tag) apimodel.Object {
// getObjectFromStruct creates an Object without blocks from the details.
func (s *Service) getObjectFromStruct(details *types.Struct, propertyMap map[string]*apimodel.Property, typeMap map[string]*apimodel.Type, tagMap map[string]apimodel.Tag) apimodel.Object {
return apimodel.Object{
Object: "object",
Id: details.Fields[bundle.RelationKeyId.String()].GetStringValue(),
Name: details.Fields[bundle.RelationKeyName.String()].GetStringValue(),
Icon: apimodel.GetIcon(s.gatewayUrl, details.GetFields()[bundle.RelationKeyIconEmoji.String()].GetStringValue(), details.GetFields()[bundle.RelationKeyIconImage.String()].GetStringValue(), details.GetFields()[bundle.RelationKeyIconName.String()].GetStringValue(), details.GetFields()[bundle.RelationKeyIconOption.String()].GetNumberValue()),
Icon: GetIcon(s.gatewayUrl, details.GetFields()[bundle.RelationKeyIconEmoji.String()].GetStringValue(), details.GetFields()[bundle.RelationKeyIconImage.String()].GetStringValue(), details.GetFields()[bundle.RelationKeyIconName.String()].GetStringValue(), details.GetFields()[bundle.RelationKeyIconOption.String()].GetNumberValue()),
Archived: details.Fields[bundle.RelationKeyIsArchived.String()].GetBoolValue(),
SpaceId: details.Fields[bundle.RelationKeySpaceId.String()].GetStringValue(),
Snippet: details.Fields[bundle.RelationKeySnippet.String()].GetStringValue(),
@ -417,13 +431,13 @@ func (s *Service) GetObjectFromStruct(details *types.Struct, propertyMap map[str
}
}
// GetObjectWithBlocksFromStruct creates an ObjectWithBody from the details.
func (s *Service) GetObjectWithBlocksFromStruct(details *types.Struct, markdown string, propertyMap map[string]apimodel.Property, typeMap map[string]apimodel.Type, tagMap map[string]apimodel.Tag) apimodel.ObjectWithBody {
// getObjectWithBlocksFromStruct creates an ObjectWithBody from the details.
func (s *Service) getObjectWithBlocksFromStruct(details *types.Struct, markdown string, propertyMap map[string]*apimodel.Property, typeMap map[string]*apimodel.Type, tagMap map[string]apimodel.Tag) apimodel.ObjectWithBody {
return apimodel.ObjectWithBody{
Object: "object",
Id: details.Fields[bundle.RelationKeyId.String()].GetStringValue(),
Name: details.Fields[bundle.RelationKeyName.String()].GetStringValue(),
Icon: apimodel.GetIcon(s.gatewayUrl, details.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(), details.Fields[bundle.RelationKeyIconImage.String()].GetStringValue(), details.Fields[bundle.RelationKeyIconName.String()].GetStringValue(), details.Fields[bundle.RelationKeyIconOption.String()].GetNumberValue()),
Icon: GetIcon(s.gatewayUrl, details.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(), details.Fields[bundle.RelationKeyIconImage.String()].GetStringValue(), details.Fields[bundle.RelationKeyIconName.String()].GetStringValue(), details.Fields[bundle.RelationKeyIconOption.String()].GetNumberValue()),
Archived: details.Fields[bundle.RelationKeyIsArchived.String()].GetBoolValue(),
SpaceId: details.Fields[bundle.RelationKeySpaceId.String()].GetStringValue(),
Snippet: details.Fields[bundle.RelationKeySnippet.String()].GetStringValue(),
@ -437,11 +451,17 @@ func (s *Service) GetObjectWithBlocksFromStruct(details *types.Struct, markdown
// getMarkdownExport retrieves the Markdown export of an object.
func (s *Service) getMarkdownExport(ctx context.Context, spaceId string, objectId string, layout model.ObjectTypeLayout) (string, error) {
if util.IsObjectLayout(layout) {
md, err := s.exportService.ExportSingleInMemory(ctx, spaceId, objectId, model.Export_Markdown)
if err != nil {
resp := s.mw.ObjectExport(ctx, &pb.RpcObjectExportRequest{
SpaceId: spaceId,
ObjectId: objectId,
Format: model.Export_Markdown,
})
if resp.Error != nil && resp.Error.Code != pb.RpcObjectExportResponseError_NULL {
return "", ErrFailedExportMarkdown
}
return md, nil
return resp.Result, nil
}
return "", nil
}

View file

@ -71,7 +71,7 @@ func TestObjectService_ListObjects(t *testing.T) {
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// Mock GetPropertyMapFromStore
// Mock getPropertyMapFromStore
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
@ -89,6 +89,7 @@ func TestObjectService_ListObjects(t *testing.T) {
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyRelationKey.String(),
bundle.RelationKeyApiObjectKey.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyRelationFormat.String(),
},
@ -138,7 +139,7 @@ func TestObjectService_ListObjects(t *testing.T) {
},
}, nil).Once()
// Mock GetTypeMapFromStore
// Mock getTypeMapFromStore
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
@ -154,6 +155,7 @@ func TestObjectService_ListObjects(t *testing.T) {
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyUniqueKey.String(),
bundle.RelationKeyApiObjectKey.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyPluralName.String(),
bundle.RelationKeyIconEmoji.String(),
@ -182,7 +184,7 @@ func TestObjectService_ListObjects(t *testing.T) {
},
}, nil).Once()
// Mock GetTagMapFromStore
// Mock getTagMapFromStore
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
@ -300,7 +302,7 @@ func TestObjectService_ListObjects(t *testing.T) {
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// Mock GetPropertyMapFromStore, GetTypeMapFromStore and GetTagMapFromStore
// Mock getPropertyMapFromStore, getTypeMapFromStore and getTagMapFromStore
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
@ -363,7 +365,7 @@ func TestObjectService_GetObject(t *testing.T) {
},
}, nil).Once()
// Mock GetPropertyMapsFromStore
// Mock getPropertyMapsFromStore
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
@ -381,6 +383,7 @@ func TestObjectService_GetObject(t *testing.T) {
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyRelationKey.String(),
bundle.RelationKeyApiObjectKey.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyRelationFormat.String(),
},
@ -430,7 +433,7 @@ func TestObjectService_GetObject(t *testing.T) {
},
}, nil).Once()
// Mock GetTypeMapFromStore
// Mock getTypeMapFromStore
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
@ -446,6 +449,7 @@ func TestObjectService_GetObject(t *testing.T) {
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyUniqueKey.String(),
bundle.RelationKeyApiObjectKey.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyPluralName.String(),
bundle.RelationKeyIconEmoji.String(),
@ -474,7 +478,7 @@ func TestObjectService_GetObject(t *testing.T) {
},
}, nil).Once()
// Mock GetTagMapFromStore
// Mock getTagMapFromStore
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
@ -501,9 +505,14 @@ func TestObjectService_GetObject(t *testing.T) {
}, nil).Once()
// Mock ExportMarkdown
fx.exportService.
On("ExportSingleInMemory", mock.Anything, mockedSpaceId, mockedObjectId, model.Export_Markdown).
Return("dummy markdown", nil).Once()
fx.mwMock.On("ObjectExport", mock.Anything, &pb.RpcObjectExportRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedObjectId,
Format: model.Export_Markdown,
}).Return(&pb.RpcObjectExportResponse{
Result: "dummy markdown",
Error: &pb.RpcObjectExportResponseError{Code: pb.RpcObjectExportResponseError_NULL},
}, nil).Once()
// when
object, err := fx.service.GetObject(ctx, mockedSpaceId, mockedObjectId)
@ -644,16 +653,59 @@ func TestObjectService_CreateObject(t *testing.T) {
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
}).Once()
// Mock GetPropertyMapFromStore, GetTypeMapFromStore and GetTagMapFromStore
// Mock getTypeMapFromStore to properly resolve typeKey for creation
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeyResolvedLayout.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Int64(int64(model.ObjectType_objectType)),
},
{
RelationKey: bundle.RelationKeyIsDeleted.String(),
},
},
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyUniqueKey.String(),
bundle.RelationKeyApiObjectKey.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyPluralName.String(),
bundle.RelationKeyIconEmoji.String(),
bundle.RelationKeyIconName.String(),
bundle.RelationKeyIconOption.String(),
bundle.RelationKeyRecommendedLayout.String(),
bundle.RelationKeyIsArchived.String(),
bundle.RelationKeyRecommendedFeaturedRelations.String(),
bundle.RelationKeyRecommendedRelations.String(),
},
}).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyUniqueKey.String(): pbtypes.String("ot-" + mockedTypeKey),
},
},
},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Times(1)
// Mock getPropertyMapFromStore, getTypeMapFromStore and getTagMapFromStore
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Times(3)
}).Times(4)
// Mock ExportMarkdown
fx.exportService.
On("ExportSingleInMemory", mock.Anything, mockedSpaceId, mockedNewObjectId, model.Export_Markdown).
Return("dummy markdown", nil).Once()
fx.mwMock.On("ObjectExport", mock.Anything, &pb.RpcObjectExportRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedNewObjectId,
Format: model.Export_Markdown,
}).Return(&pb.RpcObjectExportResponse{
Result: "dummy markdown",
Error: &pb.RpcObjectExportResponseError{Code: pb.RpcObjectExportResponseError_NULL},
}, nil).Once()
// when
object, err := fx.service.CreateObject(ctx, mockedSpaceId, apimodel.CreateObjectRequest{
@ -677,6 +729,12 @@ func TestObjectService_CreateObject(t *testing.T) {
ctx := context.Background()
fx := newFixture(t)
// Mock getPropertyMapFromStore, getTypeMapFromStore
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Times(2)
fx.mwMock.On("ObjectCreate", mock.Anything, mock.Anything).
Return(&pb.RpcObjectCreateResponse{
Error: &pb.RpcObjectCreateResponseError{Code: pb.RpcObjectCreateResponseError_UNKNOWN_ERROR},

View file

@ -9,6 +9,7 @@ import (
"time"
"github.com/gogo/protobuf/types"
"github.com/iancoleman/strcase"
apimodel "github.com/anyproto/anytype-heart/core/api/model"
"github.com/anyproto/anytype-heart/core/api/pagination"
@ -106,7 +107,7 @@ var RelationFormatToPropertyFormat = map[model.RelationFormat]apimodel.PropertyF
// ListProperties returns a list of properties for a specific space.
func (s *Service) ListProperties(ctx context.Context, spaceId string, offset int, limit int) (properties []apimodel.Property, total int, hasMore bool, err error) {
resp := s.mw.ObjectSearch(context.Background(), &pb.RpcObjectSearchRequest{
resp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
SpaceId: spaceId,
Filters: []*model.BlockContentDataviewFilter{
{
@ -129,6 +130,7 @@ func (s *Service) ListProperties(ctx context.Context, spaceId string, offset int
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyRelationKey.String(),
bundle.RelationKeyApiObjectKey.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyRelationFormat.String(),
},
@ -140,7 +142,7 @@ func (s *Service) ListProperties(ctx context.Context, spaceId string, offset int
filteredRecords := make([]*types.Struct, 0, len(resp.Records))
for _, record := range resp.Records {
rk, _ := s.mapPropertyFromRecord(record)
rk, _, _ := s.getPropertyFromStruct(record)
if _, isExcluded := excludedSystemProperties[rk]; isExcluded {
continue
}
@ -152,7 +154,7 @@ func (s *Service) ListProperties(ctx context.Context, spaceId string, offset int
properties = make([]apimodel.Property, 0, len(paginatedProperties))
for _, record := range paginatedProperties {
_, property := s.mapPropertyFromRecord(record)
_, _, property := s.getPropertyFromStruct(record)
properties = append(properties, property)
}
@ -161,7 +163,7 @@ func (s *Service) ListProperties(ctx context.Context, spaceId string, offset int
// GetProperty retrieves a single property by its ID in a specific space.
func (s *Service) GetProperty(ctx context.Context, spaceId string, propertyId string) (apimodel.Property, error) {
resp := s.mw.ObjectShow(context.Background(), &pb.RpcObjectShowRequest{
resp := s.mw.ObjectShow(ctx, &pb.RpcObjectShowRequest{
SpaceId: spaceId,
ObjectId: propertyId,
})
@ -180,7 +182,7 @@ func (s *Service) GetProperty(ctx context.Context, spaceId string, propertyId st
}
}
rk, property := s.mapPropertyFromRecord(resp.ObjectView.Details[0].Details)
rk, _, property := s.getPropertyFromStruct(resp.ObjectView.Details[0].Details)
if _, isExcluded := excludedSystemProperties[rk]; isExcluded {
return apimodel.Property{}, ErrPropertyNotFound
}
@ -191,11 +193,24 @@ func (s *Service) GetProperty(ctx context.Context, spaceId string, propertyId st
func (s *Service) CreateProperty(ctx context.Context, spaceId string, request apimodel.CreatePropertyRequest) (apimodel.Property, error) {
details := &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(request.Name),
bundle.RelationKeyName.String(): pbtypes.String(s.sanitizedString(request.Name)),
bundle.RelationKeyRelationFormat.String(): pbtypes.Int64(int64(PropertyFormatToRelationFormat[request.Format])),
bundle.RelationKeyOrigin.String(): pbtypes.Int64(int64(model.ObjectOrigin_api)),
},
}
if request.Key != "" {
apiKey := strcase.ToSnake(s.sanitizedString(request.Key))
propMap, err := s.getPropertyMapFromStore(ctx, spaceId, false)
if err != nil {
return apimodel.Property{}, err
}
if _, exists := propMap[apiKey]; exists {
return apimodel.Property{}, util.ErrBadInput(fmt.Sprintf("property key %q already exists", apiKey))
}
details.Fields[bundle.RelationKeyApiObjectKey.String()] = pbtypes.String(apiKey)
}
resp := s.mw.ObjectCreateRelation(ctx, &pb.RpcObjectCreateRelationRequest{
SpaceId: spaceId,
Details: details,
@ -215,21 +230,41 @@ func (s *Service) UpdateProperty(ctx context.Context, spaceId string, propertyId
return apimodel.Property{}, err
}
if bundle.HasRelation(domain.RelationKey(util.FromPropertyApiKey(prop.Key))) {
rel, err := bundle.PickRelation(domain.RelationKey(prop.RelationKey))
if err == nil && rel.ReadOnly {
return apimodel.Property{}, ErrPropertyCannotBeUpdated
}
var detailsToUpdate []*model.Detail
if request.Name != nil {
detail := model.Detail{
detailsToUpdate = append(detailsToUpdate, &model.Detail{
Key: bundle.RelationKeyName.String(),
Value: pbtypes.String(s.sanitizedString(*request.Name)),
})
}
if request.Key != nil {
newKey := strcase.ToSnake(s.sanitizedString(*request.Key))
propMap, err := s.getPropertyMapFromStore(ctx, spaceId, false)
if err != nil {
return apimodel.Property{}, err
}
if existing, exists := propMap[newKey]; exists && existing.Id != propertyId {
return apimodel.Property{}, util.ErrBadInput(fmt.Sprintf("property key %q already exists", newKey))
}
if bundle.HasRelation(domain.RelationKey(prop.RelationKey)) {
return apimodel.Property{}, util.ErrBadInput("property key of bundled properties cannot be changed")
}
detailsToUpdate = append(detailsToUpdate, &model.Detail{
Key: bundle.RelationKeyApiObjectKey.String(),
Value: pbtypes.String(newKey),
})
}
if len(detailsToUpdate) > 0 {
resp := s.mw.ObjectSetDetails(ctx, &pb.RpcObjectSetDetailsRequest{
ContextId: propertyId,
Details: []*model.Detail{&detail},
Details: detailsToUpdate,
})
if resp.Error != nil && resp.Error.Code != pb.RpcObjectSetDetailsResponseError_NULL {
return apimodel.Property{}, ErrFailedUpdateProperty
}
@ -267,7 +302,7 @@ func (s *Service) processProperties(ctx context.Context, spaceId string, entries
if len(entries) == 0 {
return fields, nil
}
propertyMap, err := s.GetPropertyMapFromStore(spaceId)
propertyMap, err := s.getPropertyMapFromStore(ctx, spaceId, false)
if err != nil {
return nil, err
}
@ -282,14 +317,14 @@ func (s *Service) processProperties(ctx context.Context, spaceId string, entries
case apimodel.NumberPropertyLinkValue:
key = e.Key
if e.Number == nil {
fields[util.FromPropertyApiKey(key)] = pbtypes.ToValue(nil)
fields[s.ResolvePropertyApiKey(propertyMap, key)] = pbtypes.ToValue(nil)
continue
}
raw = *e.Number
case apimodel.SelectPropertyLinkValue:
key = e.Key
if e.Select == nil {
fields[util.FromPropertyApiKey(key)] = pbtypes.ToValue(nil)
fields[s.ResolvePropertyApiKey(propertyMap, key)] = pbtypes.ToValue(nil)
continue
}
raw = *e.Select
@ -303,7 +338,7 @@ func (s *Service) processProperties(ctx context.Context, spaceId string, entries
case apimodel.DatePropertyLinkValue:
key = e.Key
if e.Date == nil {
fields[util.FromPropertyApiKey(key)] = pbtypes.ToValue(nil)
fields[s.ResolvePropertyApiKey(propertyMap, key)] = pbtypes.ToValue(nil)
continue
}
raw = *e.Date
@ -337,7 +372,7 @@ func (s *Service) processProperties(ctx context.Context, spaceId string, entries
return nil, util.ErrBadInput("unsupported property link value type " + fmt.Sprintf("%T", e))
}
rk := util.FromPropertyApiKey(key)
rk := s.ResolvePropertyApiKey(propertyMap, key)
if _, excluded := excludedSystemProperties[rk]; excluded {
continue
}
@ -346,10 +381,10 @@ func (s *Service) processProperties(ctx context.Context, spaceId string, entries
}
prop, ok := propertyMap[rk]
if !ok {
return nil, errors.New("unknown property '" + key + "'")
return nil, util.ErrBadInput(fmt.Sprintf("unknown property key: %q", rk))
}
sanitized, err := s.sanitizeAndValidatePropertyValue(ctx, spaceId, key, prop.Format, raw, prop)
sanitized, err := s.sanitizeAndValidatePropertyValue(spaceId, key, prop.Format, raw, prop, propertyMap)
if err != nil {
return nil, err
}
@ -359,7 +394,7 @@ func (s *Service) processProperties(ctx context.Context, spaceId string, entries
}
// sanitizeAndValidatePropertyValue checks the value for a property according to its format and ensures referenced IDs exist and are valid.
func (s *Service) sanitizeAndValidatePropertyValue(ctx context.Context, spaceId string, key string, format apimodel.PropertyFormat, value interface{}, property apimodel.Property) (interface{}, error) {
func (s *Service) sanitizeAndValidatePropertyValue(spaceId string, key string, format apimodel.PropertyFormat, value interface{}, property *apimodel.Property, propertyMap map[string]*apimodel.Property) (interface{}, error) {
switch format {
case apimodel.PropertyFormatText, apimodel.PropertyFormatUrl, apimodel.PropertyFormatEmail, apimodel.PropertyFormatPhone:
str, ok := value.(string)
@ -379,7 +414,7 @@ func (s *Service) sanitizeAndValidatePropertyValue(ctx context.Context, spaceId
if !ok {
return nil, util.ErrBadInput("property '" + key + "' must be a string (tag id)")
}
if !s.isValidSelectOption(spaceId, property, id) {
if !s.isValidSelectOption(spaceId, property, id, propertyMap) {
return nil, util.ErrBadInput("invalid select option for '" + key + "': " + id)
}
return id, nil
@ -395,7 +430,7 @@ func (s *Service) sanitizeAndValidatePropertyValue(ctx context.Context, spaceId
return nil, util.ErrBadInput("property '" + key + "' must be an array of strings (tag ids)")
}
id = s.sanitizedString(id)
if !s.isValidSelectOption(spaceId, property, id) {
if !s.isValidSelectOption(spaceId, property, id, propertyMap) {
return nil, util.ErrBadInput("invalid multi_select option for '" + key + "': " + id)
}
validIds = append(validIds, id)
@ -444,14 +479,14 @@ func (s *Service) sanitizeAndValidatePropertyValue(ctx context.Context, spaceId
}
// isValidSelectOption checks if the option id is valid for the given property.
func (s *Service) isValidSelectOption(spaceId string, property apimodel.Property, tagId string) bool {
func (s *Service) isValidSelectOption(spaceId string, property *apimodel.Property, tagId string, propertyMap map[string]*apimodel.Property) bool {
fields, err := util.GetFieldsByID(s.mw, spaceId, tagId, []string{bundle.RelationKeyResolvedLayout.String(), bundle.RelationKeyRelationKey.String()})
if err != nil {
return false
}
layout := model.ObjectTypeLayout(fields[bundle.RelationKeyResolvedLayout.String()].GetNumberValue())
rk := domain.RelationKey(fields[bundle.RelationKeyRelationKey.String()].GetStringValue())
return util.IsTagLayout(layout) && rk == domain.RelationKey(util.FromPropertyApiKey(property.Key))
rk := fields[bundle.RelationKeyRelationKey.String()].GetStringValue()
return util.IsTagLayout(layout) && rk == s.ResolvePropertyApiKey(propertyMap, property.Key)
}
func (s *Service) isValidObjectReference(spaceId string, objectId string) bool {
@ -473,7 +508,7 @@ func (s *Service) isValidFileReference(spaceId string, fileId string) bool {
}
// getRecommendedPropertiesFromLists combines featured and regular properties into a list of Properties.
func (s *Service) getRecommendedPropertiesFromLists(featured, regular *types.ListValue, propertyMap map[string]apimodel.Property) []apimodel.Property {
func (s *Service) getRecommendedPropertiesFromLists(featured, regular *types.ListValue, propertyMap map[string]*apimodel.Property) []apimodel.Property {
var props []apimodel.Property
lists := []*types.ListValue{featured, regular}
for _, lst := range lists {
@ -489,22 +524,22 @@ func (s *Service) getRecommendedPropertiesFromLists(featured, regular *types.Lis
if !ok {
continue
}
rk := util.FromPropertyApiKey(p.Key)
if _, excluded := excludedSystemProperties[rk]; excluded {
if _, excluded := excludedSystemProperties[p.RelationKey]; excluded {
continue
}
props = append(props, p)
props = append(props, *p)
}
}
return props
}
// GetPropertyMapsFromStore retrieves all properties for all spaces.
func (s *Service) GetPropertyMapsFromStore(spaceIds []string) (map[string]map[string]apimodel.Property, error) {
spacesToProperties := make(map[string]map[string]apimodel.Property, len(spaceIds))
// getPropertyMapsFromStore retrieves all properties for all spaces.
// Property entries can also be keyed by property id. Required for filling types with properties, as recommended properties are referenced by id and not key.
func (s *Service) getPropertyMapsFromStore(ctx context.Context, spaceIds []string, keyByPropertyId bool) (map[string]map[string]*apimodel.Property, error) {
spacesToProperties := make(map[string]map[string]*apimodel.Property, len(spaceIds))
for _, spaceId := range spaceIds {
propertyMap, err := s.GetPropertyMapFromStore(spaceId)
propertyMap, err := s.getPropertyMapFromStore(ctx, spaceId, keyByPropertyId)
if err != nil {
return nil, err
}
@ -514,10 +549,10 @@ func (s *Service) GetPropertyMapsFromStore(spaceIds []string) (map[string]map[st
return spacesToProperties, nil
}
// GetPropertyMapFromStore retrieves all properties for a specific space
// Property entries are also keyed by property id. Required for filling types with properties, as recommended properties are referenced by id and not key.
func (s *Service) GetPropertyMapFromStore(spaceId string) (map[string]apimodel.Property, error) {
resp := s.mw.ObjectSearch(context.Background(), &pb.RpcObjectSearchRequest{
// getPropertyMapFromStore retrieves all properties for a specific space
// Property entries can also be keyed by property id. Required for filling types with properties, as recommended properties are referenced by id and not key.
func (s *Service) getPropertyMapFromStore(ctx context.Context, spaceId string, keyByPropertyId bool) (map[string]*apimodel.Property, error) {
resp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
SpaceId: spaceId,
Filters: []*model.BlockContentDataviewFilter{
{
@ -534,6 +569,7 @@ func (s *Service) GetPropertyMapFromStore(spaceId string) (map[string]apimodel.P
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyRelationKey.String(),
bundle.RelationKeyApiObjectKey.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyRelationFormat.String(),
},
@ -543,30 +579,45 @@ func (s *Service) GetPropertyMapFromStore(spaceId string) (map[string]apimodel.P
return nil, ErrFailedRetrievePropertyMap
}
propertyMap := make(map[string]apimodel.Property, len(resp.Records))
propertyMap := make(map[string]*apimodel.Property, len(resp.Records))
for _, record := range resp.Records {
rk, p := s.mapPropertyFromRecord(record)
propertyMap[rk] = p
propertyMap[p.Id] = p // add property under id as key to map as well
rk, key, p := s.getPropertyFromStruct(record)
prop := p
propertyMap[rk] = &prop
propertyMap[key] = &prop // TODO: add under api key as well, double check
if keyByPropertyId {
propertyMap[p.Id] = &prop // add property under id as key to map as well
}
}
return propertyMap, nil
}
// mapPropertyFromRecord maps a single property record into a Property and returns its trimmed relation key.
func (s *Service) mapPropertyFromRecord(record *types.Struct) (string, apimodel.Property) {
rk := record.Fields[bundle.RelationKeyRelationKey.String()].GetStringValue()
return rk, apimodel.Property{
Object: "property",
Id: record.Fields[bundle.RelationKeyId.String()].GetStringValue(),
Key: util.ToPropertyApiKey(rk),
Name: record.Fields[bundle.RelationKeyName.String()].GetStringValue(),
Format: RelationFormatToPropertyFormat[model.RelationFormat(record.Fields[bundle.RelationKeyRelationFormat.String()].GetNumberValue())],
// getPropertyFromStruct maps a property's details into an apimodel.Property.
// `rk` is what we use internally, `key` is the key being referenced in the API.
func (s *Service) getPropertyFromStruct(details *types.Struct) (string, string, apimodel.Property) {
rk := details.Fields[bundle.RelationKeyRelationKey.String()].GetStringValue()
key := util.ToPropertyApiKey(rk)
// apiId as key takes precedence over relation key
if apiIDField, exists := details.Fields[bundle.RelationKeyApiObjectKey.String()]; exists {
if apiId := apiIDField.GetStringValue(); apiId != "" {
key = apiId
}
}
return rk, key, apimodel.Property{
Object: "property",
Id: details.Fields[bundle.RelationKeyId.String()].GetStringValue(),
Key: key,
Name: details.Fields[bundle.RelationKeyName.String()].GetStringValue(),
Format: RelationFormatToPropertyFormat[model.RelationFormat(details.Fields[bundle.RelationKeyRelationFormat.String()].GetNumberValue())],
RelationKey: rk, // internal-only for simplified lookup
}
}
// getPropertiesFromStruct retrieves the properties from the details.
func (s *Service) getPropertiesFromStruct(details *types.Struct, propertyMap map[string]apimodel.Property, tagMap map[string]apimodel.Tag) []apimodel.PropertyWithValue {
func (s *Service) getPropertiesFromStruct(details *types.Struct, propertyMap map[string]*apimodel.Property, tagMap map[string]apimodel.Tag) []apimodel.PropertyWithValue {
properties := make([]apimodel.PropertyWithValue, 0)
for rk, value := range details.GetFields() {
if _, isExcluded := excludedSystemProperties[rk]; isExcluded {
@ -585,7 +636,9 @@ func (s *Service) getPropertiesFromStruct(details *types.Struct, propertyMap map
id := prop.Id
name := prop.Name
properties = append(properties, s.buildPropertyWithValue(id, key, name, format, convertedVal))
if pwv := s.buildPropertyWithValue(id, key, name, format, convertedVal); pwv != nil {
properties = append(properties, *pwv)
}
}
return properties
@ -657,7 +710,7 @@ func (s *Service) convertPropertyValue(key string, value *types.Value, format ap
}
// buildPropertyWithValue creates a Property based on the format and converted value.
func (s *Service) buildPropertyWithValue(id string, key string, name string, format apimodel.PropertyFormat, val interface{}) apimodel.PropertyWithValue {
func (s *Service) buildPropertyWithValue(id string, key string, name string, format apimodel.PropertyFormat, val interface{}) *apimodel.PropertyWithValue {
base := apimodel.PropertyBase{
Object: "property",
Id: id,
@ -666,52 +719,37 @@ func (s *Service) buildPropertyWithValue(id string, key string, name string, for
switch format {
case apimodel.PropertyFormatText:
if str, ok := val.(string); ok {
return apimodel.PropertyWithValue{WrappedPropertyWithValue: apimodel.TextPropertyValue{
PropertyBase: base,
Key: key,
Name: name,
Format: format,
Text: str,
return &apimodel.PropertyWithValue{WrappedPropertyWithValue: apimodel.TextPropertyValue{
PropertyBase: base, Key: key, Name: name, Format: format,
Text: str,
}}
}
case apimodel.PropertyFormatNumber:
if num, ok := val.(float64); ok {
return apimodel.PropertyWithValue{WrappedPropertyWithValue: apimodel.NumberPropertyValue{
PropertyBase: base,
Key: key,
Name: name,
Format: format,
Number: num,
return &apimodel.PropertyWithValue{WrappedPropertyWithValue: apimodel.NumberPropertyValue{
PropertyBase: base, Key: key, Name: name, Format: format,
Number: num,
}}
}
case apimodel.PropertyFormatSelect:
if sel, ok := val.(apimodel.Tag); ok {
return apimodel.PropertyWithValue{WrappedPropertyWithValue: apimodel.SelectPropertyValue{
PropertyBase: base,
Key: key,
Name: name,
Format: format,
Select: &sel,
return &apimodel.PropertyWithValue{WrappedPropertyWithValue: apimodel.SelectPropertyValue{
PropertyBase: base, Key: key, Name: name, Format: format,
Select: &sel,
}}
}
case apimodel.PropertyFormatMultiSelect:
if ms, ok := val.([]apimodel.Tag); ok {
return apimodel.PropertyWithValue{WrappedPropertyWithValue: apimodel.MultiSelectPropertyValue{
PropertyBase: base,
Key: key,
Name: name,
Format: format,
MultiSelect: ms,
return &apimodel.PropertyWithValue{WrappedPropertyWithValue: apimodel.MultiSelectPropertyValue{
PropertyBase: base, Key: key, Name: name, Format: format,
MultiSelect: ms,
}}
}
case apimodel.PropertyFormatDate:
if dateStr, ok := val.(string); ok {
return apimodel.PropertyWithValue{WrappedPropertyWithValue: apimodel.DatePropertyValue{
PropertyBase: base,
Key: key,
Name: name,
Format: format,
Date: dateStr,
return &apimodel.PropertyWithValue{WrappedPropertyWithValue: apimodel.DatePropertyValue{
PropertyBase: base, Key: key, Name: name, Format: format,
Date: dateStr,
}}
}
case apimodel.PropertyFormatFiles:
@ -722,52 +760,37 @@ func (s *Service) buildPropertyWithValue(id string, key string, name string, for
files = append(files, str)
}
}
return apimodel.PropertyWithValue{WrappedPropertyWithValue: apimodel.FilesPropertyValue{
PropertyBase: base,
Key: key,
Name: name,
Format: format,
Files: files,
return &apimodel.PropertyWithValue{WrappedPropertyWithValue: apimodel.FilesPropertyValue{
PropertyBase: base, Key: key, Name: name, Format: format,
Files: files,
}}
}
case apimodel.PropertyFormatCheckbox:
if cb, ok := val.(bool); ok {
return apimodel.PropertyWithValue{WrappedPropertyWithValue: apimodel.CheckboxPropertyValue{
PropertyBase: base,
Key: key,
Name: name,
Format: format,
Checkbox: cb,
return &apimodel.PropertyWithValue{WrappedPropertyWithValue: apimodel.CheckboxPropertyValue{
PropertyBase: base, Key: key, Name: name, Format: format,
Checkbox: cb,
}}
}
case apimodel.PropertyFormatUrl:
if urlStr, ok := val.(string); ok {
return apimodel.PropertyWithValue{WrappedPropertyWithValue: apimodel.URLPropertyValue{
PropertyBase: base,
Key: key,
Name: name,
Format: format,
Url: urlStr,
return &apimodel.PropertyWithValue{WrappedPropertyWithValue: apimodel.URLPropertyValue{
PropertyBase: base, Key: key, Name: name, Format: format,
Url: urlStr,
}}
}
case apimodel.PropertyFormatEmail:
if email, ok := val.(string); ok {
return apimodel.PropertyWithValue{WrappedPropertyWithValue: apimodel.EmailPropertyValue{
PropertyBase: base,
Key: key,
Name: name,
Format: format,
Email: email,
return &apimodel.PropertyWithValue{WrappedPropertyWithValue: apimodel.EmailPropertyValue{
PropertyBase: base, Key: key, Name: name, Format: format,
Email: email,
}}
}
case apimodel.PropertyFormatPhone:
if phone, ok := val.(string); ok {
return apimodel.PropertyWithValue{WrappedPropertyWithValue: apimodel.PhonePropertyValue{
PropertyBase: base,
Key: key,
Name: name,
Format: format,
Phone: phone,
return &apimodel.PropertyWithValue{WrappedPropertyWithValue: apimodel.PhonePropertyValue{
PropertyBase: base, Key: key, Name: name, Format: format,
Phone: phone,
}}
}
case apimodel.PropertyFormatObjects:
@ -782,7 +805,7 @@ func (s *Service) buildPropertyWithValue(id string, key string, name string, for
}
}
}
return apimodel.PropertyWithValue{WrappedPropertyWithValue: apimodel.ObjectsPropertyValue{
return &apimodel.PropertyWithValue{WrappedPropertyWithValue: apimodel.ObjectsPropertyValue{
PropertyBase: base,
Key: key,
Name: name,
@ -791,20 +814,16 @@ func (s *Service) buildPropertyWithValue(id string, key string, name string, for
}}
}
if str, ok := val.(string); ok {
return apimodel.PropertyWithValue{WrappedPropertyWithValue: apimodel.TextPropertyValue{
PropertyBase: base,
Key: key,
Name: name,
Format: format,
Text: str,
}}
}
return apimodel.PropertyWithValue{WrappedPropertyWithValue: apimodel.TextPropertyValue{
PropertyBase: base,
Key: key,
Name: name,
Format: format,
Text: fmt.Sprintf("%v", val),
}}
return nil
}
// ResolvePropertyApiKey returns the internal relationKey for a clientKey by looking it up in the propertyMap
// TODO: If not found, this detail shouldn't be set by clients, and strict validation errors
func (s *Service) ResolvePropertyApiKey(propertyMap map[string]*apimodel.Property, clientKey string) string {
if p, ok := propertyMap[clientKey]; ok {
return p.RelationKey
}
return ""
// TODO: enable later for strict validation
// return "", false
}

View file

@ -22,7 +22,7 @@ var (
// GlobalSearch retrieves a paginated list of objects from all spaces that match the search parameters.
func (s *Service) GlobalSearch(ctx context.Context, request apimodel.SearchRequest, offset int, limit int) (objects []apimodel.Object, total int, hasMore bool, err error) {
spaceIds, err := s.GetAllSpaceIds()
spaceIds, err := s.GetAllSpaceIds(ctx)
if err != nil {
return nil, 0, false, err
}
@ -31,11 +31,29 @@ func (s *Service) GlobalSearch(ctx context.Context, request apimodel.SearchReque
queryFilters := s.prepareQueryFilter(request.Query)
sorts, criterionToSortAfter := s.prepareSorts(request.Sort)
// pre-fetch properties, types and tags to fill the objects
propertyMaps, err := s.getPropertyMapsFromStore(ctx, spaceIds, true)
if err != nil {
return nil, 0, false, err
}
typeMaps, err := s.getTypeMapsFromStore(ctx, spaceIds, propertyMaps, true)
if err != nil {
return nil, 0, false, err
}
tagMap, err := s.getTagMapsFromStore(ctx, spaceIds)
if err != nil {
return nil, 0, false, err
}
var combinedRecords []*types.Struct
for _, spaceId := range spaceIds {
// Resolve template type and object type IDs per spaceId, as they are unique per spaceId
// Resolve template and type IDs per spaceId, as they are unique per spaceId
templateFilter := s.prepareTemplateFilter()
typeFilters := s.prepareObjectTypeFilters(spaceId, request.Types)
typeFilters := s.prepareTypeFilters(request.Types, typeMaps[spaceId])
if len(request.Types) > 0 && len(typeFilters) == 0 {
// Skip spaces that dont have any of the requested types
continue
}
filters := s.combineFilters(model.BlockContentDataviewFilter_And, baseFilters, templateFilter, queryFilters, typeFilters)
objResp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
@ -76,23 +94,9 @@ func (s *Service) GlobalSearch(ctx context.Context, request apimodel.SearchReque
total = len(combinedRecords)
paginatedRecords, hasMore := pagination.Paginate(combinedRecords, offset, limit)
// pre-fetch properties, types and tags to fill the objects
propertyMaps, err := s.GetPropertyMapsFromStore(spaceIds)
if err != nil {
return nil, 0, false, err
}
typeMaps, err := s.GetTypeMapsFromStore(spaceIds, propertyMaps)
if err != nil {
return nil, 0, false, err
}
tagMap, err := s.GetTagMapsFromStore(spaceIds)
if err != nil {
return nil, 0, false, err
}
results := make([]apimodel.Object, 0, len(paginatedRecords))
for _, record := range paginatedRecords {
results = append(results, s.GetObjectFromStruct(record, propertyMaps[record.Fields[bundle.RelationKeySpaceId.String()].GetStringValue()], typeMaps[record.Fields[bundle.RelationKeySpaceId.String()].GetStringValue()], tagMap[record.Fields[bundle.RelationKeySpaceId.String()].GetStringValue()]))
results = append(results, s.getObjectFromStruct(record, propertyMaps[record.Fields[bundle.RelationKeySpaceId.String()].GetStringValue()], typeMaps[record.Fields[bundle.RelationKeySpaceId.String()].GetStringValue()], tagMap[record.Fields[bundle.RelationKeySpaceId.String()].GetStringValue()]))
}
return results, total, hasMore, nil
@ -103,7 +107,26 @@ func (s *Service) Search(ctx context.Context, spaceId string, request apimodel.S
baseFilters := s.prepareBaseFilters()
templateFilter := s.prepareTemplateFilter()
queryFilters := s.prepareQueryFilter(request.Query)
typeFilters := s.prepareObjectTypeFilters(spaceId, request.Types)
// pre-fetch properties and types to fill the objects
propertyMap, err := s.getPropertyMapFromStore(ctx, spaceId, true)
if err != nil {
return nil, 0, false, err
}
typeMap, err := s.getTypeMapFromStore(ctx, spaceId, propertyMap, true)
if err != nil {
return nil, 0, false, err
}
tagMap, err := s.getTagMapFromStore(ctx, spaceId)
if err != nil {
return nil, 0, false, err
}
typeFilters := s.prepareTypeFilters(request.Types, typeMap)
if len(request.Types) > 0 && len(typeFilters) == 0 {
// No matching types in this space; return empty result
return nil, 0, false, nil
}
filters := s.combineFilters(model.BlockContentDataviewFilter_And, baseFilters, templateFilter, queryFilters, typeFilters)
sorts, _ := s.prepareSorts(request.Sort)
@ -120,23 +143,9 @@ func (s *Service) Search(ctx context.Context, spaceId string, request apimodel.S
total = len(resp.Records)
paginatedRecords, hasMore := pagination.Paginate(resp.Records, offset, limit)
// pre-fetch properties and types to fill the objects
propertyMap, err := s.GetPropertyMapFromStore(spaceId)
if err != nil {
return nil, 0, false, err
}
typeMap, err := s.GetTypeMapFromStore(spaceId, propertyMap)
if err != nil {
return nil, 0, false, err
}
tagMap, err := s.GetTagMapFromStore(spaceId)
if err != nil {
return nil, 0, false, err
}
results := make([]apimodel.Object, 0, len(paginatedRecords))
for _, record := range paginatedRecords {
results = append(results, s.GetObjectFromStruct(record, propertyMap, typeMap, tagMap))
results = append(results, s.getObjectFromStruct(record, propertyMap, typeMap, tagMap))
}
return results, total, hasMore, nil
@ -215,26 +224,29 @@ func (s *Service) prepareQueryFilter(searchQuery string) []*model.BlockContentDa
}
}
// prepareObjectTypeFilters combines object type filters with an OR condition.
func (s *Service) prepareObjectTypeFilters(spaceId string, objectTypes []string) []*model.BlockContentDataviewFilter {
if len(objectTypes) == 0 || objectTypes[0] == "" {
// prepareTypeFilters combines type filters with an OR condition.
func (s *Service) prepareTypeFilters(types []string, typeMap map[string]*apimodel.Type) []*model.BlockContentDataviewFilter {
if len(types) == 0 {
return nil
}
// Prepare nested filters for each object type
nestedFilters := make([]*model.BlockContentDataviewFilter, 0, len(objectTypes))
for _, objectType := range objectTypes {
ukOrId := util.FromTypeApiKey(objectType)
typeId, err := util.ResolveUniqueKeyToTypeId(s.mw, spaceId, ukOrId)
if err != nil {
// client passed type id instead of type key
typeId = objectType
// Prepare nested filters for each type
nestedFilters := make([]*model.BlockContentDataviewFilter, 0, len(types))
for _, key := range types {
if key == "" {
continue
}
uk := s.ResolveTypeApiKey(typeMap, key)
typeDef, ok := typeMap[uk]
if !ok {
continue
}
nestedFilters = append(nestedFilters, &model.BlockContentDataviewFilter{
RelationKey: bundle.RelationKeyType.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(typeId),
Value: pbtypes.String(typeDef.Id),
})
}

View file

@ -129,7 +129,7 @@ func TestSearchService_GlobalSearch(t *testing.T) {
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// Mock GetPropertyMapsFromStore
// Mock getPropertyMapsFromStore
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
@ -147,6 +147,7 @@ func TestSearchService_GlobalSearch(t *testing.T) {
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyRelationKey.String(),
bundle.RelationKeyApiObjectKey.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyRelationFormat.String(),
},
@ -192,7 +193,7 @@ func TestSearchService_GlobalSearch(t *testing.T) {
},
}, nil).Once()
// Mock GetTypeMapsFromStore
// Mock getTypeMapsFromStore
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
@ -208,6 +209,7 @@ func TestSearchService_GlobalSearch(t *testing.T) {
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyUniqueKey.String(),
bundle.RelationKeyApiObjectKey.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyPluralName.String(),
bundle.RelationKeyIconEmoji.String(),
@ -229,7 +231,7 @@ func TestSearchService_GlobalSearch(t *testing.T) {
},
}, nil).Once()
// Mock GetTagMapFromStore
// Mock getTagMapFromStore
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
@ -438,7 +440,7 @@ func TestSearchService_Search(t *testing.T) {
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// Mock GetPropertyMapsFromStore
// Mock getPropertyMapsFromStore
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
@ -456,6 +458,7 @@ func TestSearchService_Search(t *testing.T) {
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyRelationKey.String(),
bundle.RelationKeyApiObjectKey.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyRelationFormat.String(),
},
@ -501,7 +504,7 @@ func TestSearchService_Search(t *testing.T) {
},
}, nil).Once()
// Mock GetTypeMapFromStore
// Mock getTypeMapFromStore
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
@ -517,6 +520,7 @@ func TestSearchService_Search(t *testing.T) {
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyUniqueKey.String(),
bundle.RelationKeyApiObjectKey.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyPluralName.String(),
bundle.RelationKeyIconEmoji.String(),
@ -538,7 +542,7 @@ func TestSearchService_Search(t *testing.T) {
},
}, nil).Once()
// Mock GetTagMapFromStore
// Mock getTagMapFromStore
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{

View file

@ -5,12 +5,11 @@ import (
)
type Service struct {
mw apicore.ClientCommands
exportService apicore.ExportService
gatewayUrl string
techSpaceId string
mw apicore.ClientCommands
gatewayUrl string
techSpaceId string
}
func NewService(mw apicore.ClientCommands, exportService apicore.ExportService, gatewayUrl string, techspaceId string) *Service {
return &Service{mw: mw, exportService: exportService, gatewayUrl: gatewayUrl, techSpaceId: techspaceId}
func NewService(mw apicore.ClientCommands, gatewayUrl string, techspaceId string) *Service {
return &Service{mw: mw, gatewayUrl: gatewayUrl, techSpaceId: techspaceId}
}

View file

@ -28,19 +28,16 @@ const (
)
type fixture struct {
service *Service
mwMock *mock_apicore.MockClientCommands
exportService *mock_apicore.MockExportService
service *Service
mwMock *mock_apicore.MockClientCommands
}
func newFixture(t *testing.T) *fixture {
mwMock := mock_apicore.NewMockClientCommands(t)
exportMock := mock_apicore.NewMockExportService(t)
service := NewService(mwMock, exportMock, gatewayUrl, techSpaceId)
service := NewService(mwMock, gatewayUrl, techSpaceId)
return &fixture{
service: service,
mwMock: mwMock,
exportService: exportMock,
service: service,
mwMock: mwMock,
}
}

View file

@ -73,7 +73,7 @@ func (s *Service) ListSpaces(ctx context.Context, offset int, limit int) (spaces
spaces = make([]apimodel.Space, 0, len(paginatedRecords))
for _, record := range paginatedRecords {
workspace, err := s.getSpaceInfo(record.Fields[bundle.RelationKeyTargetSpaceId.String()].GetStringValue())
workspace, err := s.getSpaceInfo(ctx, record.Fields[bundle.RelationKeyTargetSpaceId.String()].GetStringValue())
if err != nil {
return nil, 0, false, err
}
@ -117,7 +117,7 @@ func (s *Service) GetSpace(ctx context.Context, spaceId string) (apimodel.Space,
return apimodel.Space{}, ErrWorkspaceNotFound
}
return s.getSpaceInfo(spaceId)
return s.getSpaceInfo(ctx, spaceId)
}
// CreateSpace creates a new space with the given name and returns the space info.
@ -158,7 +158,7 @@ func (s *Service) CreateSpace(ctx context.Context, request apimodel.CreateSpaceR
}
}
return s.getSpaceInfo(resp.SpaceId)
return s.getSpaceInfo(ctx, resp.SpaceId)
}
// UpdateSpace updates the space with the given ID using the provided request.
@ -188,7 +188,7 @@ func (s *Service) UpdateSpace(ctx context.Context, spaceId string, request apimo
}
}
space, err := s.getSpaceInfo(spaceId)
space, err := s.getSpaceInfo(ctx, spaceId)
if err != nil {
return apimodel.Space{}, err
}
@ -197,8 +197,8 @@ func (s *Service) UpdateSpace(ctx context.Context, spaceId string, request apimo
}
// getSpaceInfo returns the workspace info for the space with the given ID.
func (s *Service) getSpaceInfo(spaceId string) (space apimodel.Space, err error) {
workspaceResponse := s.mw.WorkspaceOpen(context.Background(), &pb.RpcWorkspaceOpenRequest{
func (s *Service) getSpaceInfo(ctx context.Context, spaceId string) (space apimodel.Space, err error) {
workspaceResponse := s.mw.WorkspaceOpen(ctx, &pb.RpcWorkspaceOpenRequest{
SpaceId: spaceId,
})
@ -206,7 +206,7 @@ func (s *Service) getSpaceInfo(spaceId string) (space apimodel.Space, err error)
return apimodel.Space{}, ErrFailedOpenWorkspace
}
spaceResp := s.mw.ObjectShow(context.Background(), &pb.RpcObjectShowRequest{
spaceResp := s.mw.ObjectShow(ctx, &pb.RpcObjectShowRequest{
SpaceId: spaceId,
ObjectId: workspaceResponse.Info.WorkspaceObjectId,
})
@ -216,7 +216,7 @@ func (s *Service) getSpaceInfo(spaceId string) (space apimodel.Space, err error)
}
name := spaceResp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyName.String()].GetStringValue()
icon := apimodel.GetIcon(s.gatewayUrl, spaceResp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(), spaceResp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyIconImage.String()].GetStringValue(), "", 0)
icon := GetIcon(s.gatewayUrl, spaceResp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(), spaceResp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyIconImage.String()].GetStringValue(), "", 0)
description := spaceResp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyDescription.String()].GetStringValue()
return apimodel.Space{
@ -231,8 +231,8 @@ func (s *Service) getSpaceInfo(spaceId string) (space apimodel.Space, err error)
}
// GetAllSpaceIds effectively retrieves all space IDs from the tech space.
func (s *Service) GetAllSpaceIds() ([]string, error) {
resp := s.mw.ObjectSearch(context.Background(), &pb.RpcObjectSearchRequest{
func (s *Service) GetAllSpaceIds(ctx context.Context) ([]string, error) {
resp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
SpaceId: s.techSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{

View file

@ -68,7 +68,7 @@ func (s *Service) ListTags(ctx context.Context, spaceId string, propertyId strin
tags = make([]apimodel.Tag, 0, len(paginatedTags))
for _, record := range resp.Records {
tags = append(tags, s.mapTagFromRecord(record))
tags = append(tags, s.getTagFromStruct(record))
}
return tags, total, hasMore, nil
@ -95,7 +95,7 @@ func (s *Service) GetTag(ctx context.Context, spaceId string, propertyId string,
}
}
return s.mapTagFromRecord(resp.ObjectView.Details[0].Details), nil
return s.getTagFromStruct(resp.ObjectView.Details[0].Details), nil
}
// CreateTag creates a new tag option for a given property ID in a space.
@ -110,6 +110,7 @@ func (s *Service) CreateTag(ctx context.Context, spaceId string, propertyId stri
bundle.RelationKeyRelationKey.String(): pbtypes.String(rk),
bundle.RelationKeyName.String(): pbtypes.String(s.sanitizedString(request.Name)),
bundle.RelationKeyRelationOptionColor.String(): pbtypes.String(apimodel.ColorToColorOption[request.Color]),
bundle.RelationKeyOrigin.String(): pbtypes.Int64(int64(model.ObjectOrigin_api)),
},
}
@ -179,11 +180,11 @@ func (s *Service) DeleteTag(ctx context.Context, spaceId string, propertyId stri
return tag, nil
}
// GetTagMapsFromStore retrieves all tags for all spaces.
func (s *Service) GetTagMapsFromStore(spaceIds []string) (map[string]map[string]apimodel.Tag, error) {
// getTagMapsFromStore retrieves all tags for all spaces.
func (s *Service) getTagMapsFromStore(ctx context.Context, spaceIds []string) (map[string]map[string]apimodel.Tag, error) {
spacesToTags := make(map[string]map[string]apimodel.Tag)
for _, spaceId := range spaceIds {
tagMap, err := s.GetTagMapFromStore(spaceId)
tagMap, err := s.getTagMapFromStore(ctx, spaceId)
if err != nil {
return nil, err
}
@ -192,9 +193,9 @@ func (s *Service) GetTagMapsFromStore(spaceIds []string) (map[string]map[string]
return spacesToTags, nil
}
// GetTagMapFromStore retrieves all tags for a specific space.
func (s *Service) GetTagMapFromStore(spaceId string) (map[string]apimodel.Tag, error) {
resp := s.mw.ObjectSearch(context.Background(), &pb.RpcObjectSearchRequest{
// getTagMapFromStore retrieves all tags for a specific space.
func (s *Service) getTagMapFromStore(ctx context.Context, spaceId string) (map[string]apimodel.Tag, error) {
resp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
SpaceId: spaceId,
Filters: []*model.BlockContentDataviewFilter{
{
@ -222,20 +223,21 @@ func (s *Service) GetTagMapFromStore(spaceId string) (map[string]apimodel.Tag, e
tags := make(map[string]apimodel.Tag)
for _, record := range resp.Records {
tag := s.mapTagFromRecord(record)
tag := s.getTagFromStruct(record)
tags[tag.Id] = tag
}
return tags, nil
}
func (s *Service) mapTagFromRecord(record *types.Struct) apimodel.Tag {
// getTagFromStruct converts a tag's details from a struct to an apimodel.Tag.
func (s *Service) getTagFromStruct(details *types.Struct) apimodel.Tag {
return apimodel.Tag{
Object: "tag",
Id: record.Fields[bundle.RelationKeyId.String()].GetStringValue(),
Key: util.ToTagApiKey(record.Fields[bundle.RelationKeyUniqueKey.String()].GetStringValue()),
Name: record.Fields[bundle.RelationKeyName.String()].GetStringValue(),
Color: apimodel.ColorOptionToColor[record.Fields[bundle.RelationKeyRelationOptionColor.String()].GetStringValue()],
Id: details.Fields[bundle.RelationKeyId.String()].GetStringValue(),
Key: util.ToTagApiKey(details.Fields[bundle.RelationKeyUniqueKey.String()].GetStringValue()),
Name: details.Fields[bundle.RelationKeyName.String()].GetStringValue(),
Color: apimodel.ColorOptionToColor[details.Fields[bundle.RelationKeyRelationOptionColor.String()].GetStringValue()],
}
}

View file

@ -42,7 +42,7 @@ func (s *Service) ListTemplates(ctx context.Context, spaceId string, typeId stri
return nil, 0, false, ErrTemplateTypeNotFound
}
// Then, search all objects of the template type and filter by the target object type
// Then, search all objects of the template type and filter by the target type
templateTypeId := templateTypeIdResp.Records[0].Fields[bundle.RelationKeyId.String()].GetStringValue()
templateObjectsResp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
SpaceId: spaceId,
@ -68,21 +68,21 @@ func (s *Service) ListTemplates(ctx context.Context, spaceId string, typeId stri
paginatedTemplates, hasMore := pagination.Paginate(templateObjectsResp.Records, offset, limit)
templates = make([]apimodel.Object, 0, len(paginatedTemplates))
propertyMap, err := s.GetPropertyMapFromStore(spaceId)
propertyMap, err := s.getPropertyMapFromStore(ctx, spaceId, true)
if err != nil {
return nil, 0, false, err
}
typeMap, err := s.GetTypeMapFromStore(spaceId, propertyMap)
typeMap, err := s.getTypeMapFromStore(ctx, spaceId, propertyMap, false)
if err != nil {
return nil, 0, false, err
}
tagMap, err := s.GetTagMapFromStore(spaceId)
tagMap, err := s.getTagMapFromStore(ctx, spaceId)
if err != nil {
return nil, 0, false, err
}
for _, record := range paginatedTemplates {
templates = append(templates, s.GetObjectFromStruct(record, propertyMap, typeMap, tagMap))
templates = append(templates, s.getObjectFromStruct(record, propertyMap, typeMap, tagMap))
}
return templates, total, hasMore, nil
@ -109,15 +109,15 @@ func (s *Service) GetTemplate(ctx context.Context, spaceId string, _ string, tem
}
}
propertyMap, err := s.GetPropertyMapFromStore(spaceId)
propertyMap, err := s.getPropertyMapFromStore(ctx, spaceId, true)
if err != nil {
return apimodel.ObjectWithBody{}, err
}
typeMap, err := s.GetTypeMapFromStore(spaceId, propertyMap)
typeMap, err := s.getTypeMapFromStore(ctx, spaceId, propertyMap, false)
if err != nil {
return apimodel.ObjectWithBody{}, err
}
tagMap, err := s.GetTagMapFromStore(spaceId)
tagMap, err := s.getTagMapFromStore(ctx, spaceId)
if err != nil {
return apimodel.ObjectWithBody{}, err
}
@ -127,5 +127,5 @@ func (s *Service) GetTemplate(ctx context.Context, spaceId string, _ string, tem
return apimodel.ObjectWithBody{}, err
}
return s.GetObjectWithBlocksFromStruct(resp.ObjectView.Details[0].Details, markdown, propertyMap, typeMap, tagMap), nil
return s.getObjectWithBlocksFromStruct(resp.ObjectView.Details[0].Details, markdown, propertyMap, typeMap, tagMap), nil
}

View file

@ -49,7 +49,7 @@ func TestObjectService_ListTemplates(t *testing.T) {
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// Mock GetPropertyMapFromStore, GetTypeMapFromStore and GetTagMapFromStore
// Mock getPropertyMapFromStore, getTypeMapFromStore and getTagMapFromStore
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
@ -121,16 +121,21 @@ func TestObjectService_GetTemplate(t *testing.T) {
},
}).Once()
// Mock GetPropertyMapFromStore, GetTypeMapFromStore and GetTagMapFromStore
// Mock getPropertyMapFromStore, getTypeMapFromStore and getTagMapFromStore
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Times(3)
// Mock ExportMarkdown
fx.exportService.
On("ExportSingleInMemory", mock.Anything, mockedSpaceId, mockedTemplateId, model.Export_Markdown).
Return("dummy markdown", nil).Once()
fx.mwMock.On("ObjectExport", mock.Anything, &pb.RpcObjectExportRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedTemplateId,
Format: model.Export_Markdown,
}).Return(&pb.RpcObjectExportResponse{
Result: "dummy markdown",
Error: &pb.RpcObjectExportResponseError{Code: pb.RpcObjectExportResponseError_NULL},
}, nil).Once()
// when
template, err := fx.service.GetTemplate(ctx, mockedSpaceId, mockedTypeId, mockedTemplateId)

View file

@ -3,8 +3,10 @@ package service
import (
"context"
"errors"
"fmt"
"github.com/gogo/protobuf/types"
"github.com/iancoleman/strcase"
apimodel "github.com/anyproto/anytype-heart/core/api/model"
"github.com/anyproto/anytype-heart/core/api/pagination"
@ -53,6 +55,7 @@ func (s *Service) ListTypes(ctx context.Context, spaceId string, offset int, lim
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyUniqueKey.String(),
bundle.RelationKeyApiObjectKey.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyIconEmoji.String(),
bundle.RelationKeyIconName.String(),
@ -72,13 +75,14 @@ func (s *Service) ListTypes(ctx context.Context, spaceId string, offset int, lim
paginatedTypes, hasMore := pagination.Paginate(resp.Records, offset, limit)
types = make([]apimodel.Type, 0, len(paginatedTypes))
propertyMap, err := s.GetPropertyMapFromStore(spaceId)
propertyMap, err := s.getPropertyMapFromStore(ctx, spaceId, true)
if err != nil {
return nil, 0, false, err
}
for _, record := range paginatedTypes {
types = append(types, s.getTypeFromStruct(record, propertyMap))
_, _, t := s.getTypeFromStruct(record, propertyMap)
types = append(types, t)
}
return types, total, hasMore, nil
}
@ -105,16 +109,32 @@ func (s *Service) GetType(ctx context.Context, spaceId string, typeId string) (a
}
// pre-fetch properties to fill the type
propertyMap, err := s.GetPropertyMapFromStore(spaceId)
propertyMap, err := s.getPropertyMapFromStore(ctx, spaceId, true)
if err != nil {
return apimodel.Type{}, err
}
return s.getTypeFromStruct(resp.ObjectView.Details[0].Details, propertyMap), nil
_, _, t := s.getTypeFromStruct(resp.ObjectView.Details[0].Details, propertyMap)
return t, nil
}
// CreateType creates a new type in a specific space.
func (s *Service) CreateType(ctx context.Context, spaceId string, request apimodel.CreateTypeRequest) (apimodel.Type, error) {
if request.Key != "" {
newKey := strcase.ToSnake(s.sanitizedString(request.Key))
propertyMap, err := s.getPropertyMapFromStore(ctx, spaceId, true)
if err != nil {
return apimodel.Type{}, err
}
typeMap, err := s.getTypeMapFromStore(ctx, spaceId, propertyMap, true)
if err != nil {
return apimodel.Type{}, err
}
if _, exists := typeMap[newKey]; exists {
return apimodel.Type{}, util.ErrBadInput(fmt.Sprintf("type key %q already exists", newKey))
}
}
details, err := s.buildTypeDetails(ctx, spaceId, request)
if err != nil {
return apimodel.Type{}, err
@ -134,12 +154,12 @@ func (s *Service) CreateType(ctx context.Context, spaceId string, request apimod
// UpdateType updates an existing type in a specific space.
func (s *Service) UpdateType(ctx context.Context, spaceId string, typeId string, request apimodel.UpdateTypeRequest) (apimodel.Type, error) {
_, err := s.GetType(ctx, spaceId, typeId)
t, err := s.GetType(ctx, spaceId, typeId)
if err != nil {
return apimodel.Type{}, err
}
details, err := s.buildUpdatedTypeDetails(ctx, spaceId, typeId, request)
details, err := s.buildUpdatedTypeDetails(ctx, spaceId, t, request)
if err != nil {
return apimodel.Type{}, err
}
@ -175,12 +195,13 @@ func (s *Service) DeleteType(ctx context.Context, spaceId string, typeId string)
return t, nil
}
// GetTypeMapsFromStore retrieves all types from all spaces.
func (s *Service) GetTypeMapsFromStore(spaceIds []string, propertyMap map[string]map[string]apimodel.Property) (map[string]map[string]apimodel.Type, error) {
spacesToTypes := make(map[string]map[string]apimodel.Type, len(spaceIds))
// getTypeMapsFromStore retrieves all types from all spaces.
// Type entries can also be keyed by uniqueKey. Required for resolving type keys to IDs for search filters.
func (s *Service) getTypeMapsFromStore(ctx context.Context, spaceIds []string, propertyMap map[string]map[string]*apimodel.Property, keyByUniqueKey bool) (map[string]map[string]*apimodel.Type, error) {
spacesToTypes := make(map[string]map[string]*apimodel.Type, len(spaceIds))
for _, spaceId := range spaceIds {
typeMap, err := s.GetTypeMapFromStore(spaceId, propertyMap[spaceId])
typeMap, err := s.getTypeMapFromStore(ctx, spaceId, propertyMap[spaceId], keyByUniqueKey)
if err != nil {
return nil, err
}
@ -190,9 +211,10 @@ func (s *Service) GetTypeMapsFromStore(spaceIds []string, propertyMap map[string
return spacesToTypes, nil
}
// GetTypeMapFromStore retrieves all types for a specific space.
func (s *Service) GetTypeMapFromStore(spaceId string, propertyMap map[string]apimodel.Property) (map[string]apimodel.Type, error) {
resp := s.mw.ObjectSearch(context.Background(), &pb.RpcObjectSearchRequest{
// getTypeMapFromStore retrieves all types for a specific space.
// Type entries can also be keyed by uniqueKey. Required for resolving type keys to IDs for search filters.
func (s *Service) getTypeMapFromStore(ctx context.Context, spaceId string, propertyMap map[string]*apimodel.Property, keyByUniqueKey bool) (map[string]*apimodel.Type, error) {
resp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
SpaceId: spaceId,
Filters: []*model.BlockContentDataviewFilter{
{
@ -208,6 +230,7 @@ func (s *Service) GetTypeMapFromStore(spaceId string, propertyMap map[string]api
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyUniqueKey.String(),
bundle.RelationKeyApiObjectKey.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyPluralName.String(),
bundle.RelationKeyIconEmoji.String(),
@ -224,43 +247,68 @@ func (s *Service) GetTypeMapFromStore(spaceId string, propertyMap map[string]api
return nil, ErrFailedRetrieveTypes
}
typeMap := make(map[string]apimodel.Type, len(resp.Records))
typeMap := make(map[string]*apimodel.Type, len(resp.Records))
for _, record := range resp.Records {
t := s.getTypeFromStruct(record, propertyMap)
typeMap[t.Id] = t
uk, key, t := s.getTypeFromStruct(record, propertyMap)
ot := t
typeMap[t.Id] = &ot
if keyByUniqueKey {
typeMap[key] = &ot
typeMap[uk] = &ot
}
}
return typeMap, nil
}
// getTypeFromStruct builds an apimodel.Type from the provided fields map and propertyMap.
func (s *Service) getTypeFromStruct(details *types.Struct, propertyMap map[string]apimodel.Property) apimodel.Type {
return apimodel.Type{
// getTypeFromStruct maps a type's details into an apimodel.Type.
// `uk` is what we use internally, `key` is the key being referenced in the API.
func (s *Service) getTypeFromStruct(details *types.Struct, propertyMap map[string]*apimodel.Property) (string, string, apimodel.Type) {
uk := details.Fields[bundle.RelationKeyUniqueKey.String()].GetStringValue()
key := util.ToTypeApiKey(uk)
// apiId as key takes precedence over unique key
if apiIDField, exists := details.Fields[bundle.RelationKeyApiObjectKey.String()]; exists {
if apiId := apiIDField.GetStringValue(); apiId != "" {
key = apiId
}
}
return uk, key, apimodel.Type{
Object: "type",
Id: details.Fields[bundle.RelationKeyId.String()].GetStringValue(),
Key: util.ToTypeApiKey(details.Fields[bundle.RelationKeyUniqueKey.String()].GetStringValue()),
Key: key,
Name: details.Fields[bundle.RelationKeyName.String()].GetStringValue(),
PluralName: details.Fields[bundle.RelationKeyPluralName.String()].GetStringValue(),
Icon: apimodel.GetIcon(s.gatewayUrl, details.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(), "", details.Fields[bundle.RelationKeyIconName.String()].GetStringValue(), details.Fields[bundle.RelationKeyIconOption.String()].GetNumberValue()),
Icon: GetIcon(s.gatewayUrl, details.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(), "", details.Fields[bundle.RelationKeyIconName.String()].GetStringValue(), details.Fields[bundle.RelationKeyIconOption.String()].GetNumberValue()),
Archived: details.Fields[bundle.RelationKeyIsArchived.String()].GetBoolValue(),
Layout: s.otLayoutToObjectLayout(model.ObjectTypeLayout(details.Fields[bundle.RelationKeyRecommendedLayout.String()].GetNumberValue())),
Properties: s.getRecommendedPropertiesFromLists(details.Fields[bundle.RelationKeyRecommendedFeaturedRelations.String()].GetListValue(), details.Fields[bundle.RelationKeyRecommendedRelations.String()].GetListValue(), propertyMap),
UniqueKey: uk, // internal only for simplified lookup
}
}
// getTypeFromMap retrieves the type from the details.
func (s *Service) getTypeFromMap(details *types.Struct, typeMap map[string]apimodel.Type) apimodel.Type {
return typeMap[details.Fields[bundle.RelationKeyType.String()].GetStringValue()]
func (s *Service) getTypeFromMap(details *types.Struct, typeMap map[string]*apimodel.Type) apimodel.Type {
if t, ok := typeMap[details.Fields[bundle.RelationKeyType.String()].GetStringValue()]; ok {
return *t
}
return apimodel.Type{}
}
// buildTypeDetails builds the type details from the CreateTypeRequest.
func (s *Service) buildTypeDetails(ctx context.Context, spaceId string, request apimodel.CreateTypeRequest) (*types.Struct, error) {
fields := make(map[string]*types.Value)
fields := map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(s.sanitizedString(request.Name)),
bundle.RelationKeyPluralName.String(): pbtypes.String(s.sanitizedString(request.PluralName)),
bundle.RelationKeyRecommendedLayout.String(): pbtypes.Int64(int64(s.typeLayoutToObjectTypeLayout(request.Layout))),
bundle.RelationKeyOrigin.String(): pbtypes.Int64(int64(model.ObjectOrigin_api)),
}
fields[bundle.RelationKeyName.String()] = pbtypes.String(s.sanitizedString(request.Name))
fields[bundle.RelationKeyPluralName.String()] = pbtypes.String(s.sanitizedString(request.PluralName))
fields[bundle.RelationKeyRecommendedLayout.String()] = pbtypes.Int64(int64(s.typeLayoutToObjectTypeLayout(request.Layout)))
if request.Key != "" {
fields[bundle.RelationKeyApiObjectKey.String()] = pbtypes.String(strcase.ToSnake(s.sanitizedString(request.Key)))
}
iconFields, err := s.processIconFields(ctx, spaceId, request.Icon)
iconFields, err := s.processIconFields(spaceId, request.Icon, true)
if err != nil {
return nil, err
}
@ -268,7 +316,7 @@ func (s *Service) buildTypeDetails(ctx context.Context, spaceId string, request
fields[k] = v
}
propertyMap, err := s.GetPropertyMapFromStore(spaceId)
propertyMap, err := s.getPropertyMapFromStore(ctx, spaceId, true)
if err != nil {
return nil, err
}
@ -309,7 +357,7 @@ func (s *Service) buildTypeDetails(ctx context.Context, spaceId string, request
}
// buildUpdatedTypeDetails builds a partial details struct for UpdateTypeRequest.
func (s *Service) buildUpdatedTypeDetails(ctx context.Context, spaceId string, typeId string, request apimodel.UpdateTypeRequest) (*types.Struct, error) {
func (s *Service) buildUpdatedTypeDetails(ctx context.Context, spaceId string, t apimodel.Type, request apimodel.UpdateTypeRequest) (*types.Struct, error) {
fields := make(map[string]*types.Value)
if request.Name != nil {
fields[bundle.RelationKeyName.String()] = pbtypes.String(s.sanitizedString(*request.Name))
@ -320,9 +368,27 @@ func (s *Service) buildUpdatedTypeDetails(ctx context.Context, spaceId string, t
if request.Layout != nil {
fields[bundle.RelationKeyRecommendedLayout.String()] = pbtypes.Int64(int64(s.typeLayoutToObjectTypeLayout(*request.Layout)))
}
if request.Key != nil {
newKey := strcase.ToSnake(s.sanitizedString(*request.Key))
propertyMap, err := s.getPropertyMapFromStore(ctx, spaceId, true)
if err != nil {
return nil, err
}
typeMap, err := s.getTypeMapFromStore(ctx, spaceId, propertyMap, true)
if err != nil {
return nil, err
}
if existing, exists := typeMap[newKey]; exists && existing.Id != t.Id {
return nil, util.ErrBadInput(fmt.Sprintf("type key %q already exists", newKey))
}
if bundle.HasObjectTypeByKey(domain.TypeKey(util.ToTypeApiKey(t.UniqueKey))) {
return nil, util.ErrBadInput("type key of bundled types cannot be changed")
}
fields[bundle.RelationKeyApiObjectKey.String()] = pbtypes.String(newKey)
}
if request.Icon != nil {
iconFields, err := s.processIconFields(ctx, spaceId, *request.Icon)
iconFields, err := s.processIconFields(spaceId, *request.Icon, true)
if err != nil {
return nil, err
}
@ -335,12 +401,12 @@ func (s *Service) buildUpdatedTypeDetails(ctx context.Context, spaceId string, t
return &types.Struct{Fields: fields}, nil
}
propertyMap, err := s.GetPropertyMapFromStore(spaceId)
propertyMap, err := s.getPropertyMapFromStore(ctx, spaceId, true)
if err != nil {
return nil, err
}
currentFields, err := util.GetFieldsByID(s.mw, spaceId, typeId, []string{bundle.RelationKeyRecommendedFeaturedRelations.String()})
currentFields, err := util.GetFieldsByID(s.mw, spaceId, t.Id, []string{bundle.RelationKeyRecommendedFeaturedRelations.String()})
if err != nil {
return nil, err
}
@ -378,10 +444,10 @@ func (s *Service) buildUpdatedTypeDetails(ctx context.Context, spaceId string, t
}
// buildRelationIds constructs relation IDs for property links, creating new properties if necessary.
func (s *Service) buildRelationIds(ctx context.Context, spaceId string, props []apimodel.PropertyLink, propertyMap map[string]apimodel.Property) ([]string, error) {
func (s *Service) buildRelationIds(ctx context.Context, spaceId string, props []apimodel.PropertyLink, propertyMap map[string]*apimodel.Property) ([]string, error) {
relationIds := make([]string, 0, len(props))
for _, propLink := range props {
rk := util.FromPropertyApiKey(propLink.Key)
rk := s.ResolvePropertyApiKey(propertyMap, propLink.Key)
if propDef, exists := propertyMap[rk]; exists {
relationIds = append(relationIds, propDef.Id)
continue
@ -398,27 +464,15 @@ func (s *Service) buildRelationIds(ctx context.Context, spaceId string, props []
return relationIds, nil
}
func (s *Service) objectLayoutToObjectTypeLayout(objectLayout apimodel.ObjectLayout) model.ObjectTypeLayout {
switch objectLayout {
case apimodel.ObjectLayoutBasic:
return model.ObjectType_basic
case apimodel.ObjectLayoutProfile:
return model.ObjectType_profile
case apimodel.ObjectLayoutAction:
return model.ObjectType_todo
case apimodel.ObjectLayoutNote:
return model.ObjectType_note
case apimodel.ObjectLayoutBookmark:
return model.ObjectType_bookmark
case apimodel.ObjectLayoutSet:
return model.ObjectType_set
case apimodel.ObjectLayoutCollection:
return model.ObjectType_collection
case apimodel.ObjectLayoutParticipant:
return model.ObjectType_participant
default:
return model.ObjectType_basic
// ResolveTypeApiKey returns the internal uniqueKey for a clientKey by looking it up in the typeMap
// TODO: If not found, this detail shouldn't be set by clients, and strict validation errors
func (s *Service) ResolveTypeApiKey(typeMap map[string]*apimodel.Type, clientKey string) string {
if p, ok := typeMap[clientKey]; ok {
return p.UniqueKey
}
return ""
// TODO: enable later for strict validation
// return "", false
}
func (s *Service) otLayoutToObjectLayout(objectTypeLayout model.ObjectTypeLayout) apimodel.ObjectLayout {
@ -458,18 +512,3 @@ func (s *Service) typeLayoutToObjectTypeLayout(typeLayout apimodel.TypeLayout) m
return model.ObjectType_basic
}
}
func (s *Service) otLayoutToTypeLayout(objectTypeLayout model.ObjectTypeLayout) apimodel.TypeLayout {
switch objectTypeLayout {
case model.ObjectType_basic:
return apimodel.TypeLayoutBasic
case model.ObjectType_profile:
return apimodel.TypeLayoutProfile
case model.ObjectType_todo:
return apimodel.TypeLayoutAction
case model.ObjectType_note:
return apimodel.TypeLayoutNote
default:
return apimodel.TypeLayoutBasic
}
}

View file

@ -36,7 +36,7 @@ func TestObjectService_ListTypes(t *testing.T) {
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// Mock GetPropertyMapFromStore
// Mock getPropertyMapFromStore
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
@ -54,6 +54,7 @@ func TestObjectService_ListTypes(t *testing.T) {
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyRelationKey.String(),
bundle.RelationKeyApiObjectKey.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyRelationFormat.String(),
},
@ -92,7 +93,7 @@ func TestObjectService_ListTypes(t *testing.T) {
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// Mock GetPropertyMapFromStore
// Mock getPropertyMapFromStore
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: "empty-space",
Filters: []*model.BlockContentDataviewFilter{
@ -110,6 +111,7 @@ func TestObjectService_ListTypes(t *testing.T) {
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyRelationKey.String(),
bundle.RelationKeyApiObjectKey.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyRelationFormat.String(),
},
@ -157,7 +159,7 @@ func TestObjectService_GetType(t *testing.T) {
},
}).Once()
// Mock GetPropertyMapFromStore
// Mock getPropertyMapFromStore
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
@ -175,6 +177,7 @@ func TestObjectService_GetType(t *testing.T) {
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyRelationKey.String(),
bundle.RelationKeyApiObjectKey.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyRelationFormat.String(),
},

View file

@ -0,0 +1,51 @@
package util
import (
"context"
"encoding/json"
"fmt"
)
type AnalyticsBroadcastEvent struct {
Type string `json:"type"`
Code string `json:"code"`
Param struct {
Route string `json:"route"`
ApiAppName string `json:"apiAppName"`
Status int `json:"status"`
} `json:"param"`
}
// ToJSON returns the event as a JSON string
func (e *AnalyticsBroadcastEvent) ToJSON() (string, error) {
eventJSON, err := json.Marshal(e)
if err != nil {
return "", fmt.Errorf("error marshalling analytics event: %w", err)
}
return string(eventJSON), nil
}
// NewAnalyticsEvent creates a new analytics event with the given code, route and apiAppName
func NewAnalyticsEvent(code, route, apiAppName string, status int) *AnalyticsBroadcastEvent {
return &AnalyticsBroadcastEvent{
Type: "analyticsEvent",
Code: code,
Param: struct {
Route string `json:"route"`
ApiAppName string `json:"apiAppName"`
Status int `json:"status"`
}{
Route: route,
ApiAppName: apiAppName,
Status: status,
},
}
}
// NewAnalyticsEventForApi creates a new analytics event for api with the app name from the context
func NewAnalyticsEventForApi(ctx context.Context, code string, status int) (string, error) {
// TODO: enable when apiAppName is available in context
// apiAppName := ctx.Value("apiAppName").(string)
apiAppName := "api-app"
return NewAnalyticsEvent(code, "api", apiAppName, status).ToJSON()
}

View file

@ -1,14 +1,15 @@
package util
import (
"regexp"
"strings"
"github.com/iancoleman/strcase"
)
// Internal -> API
// "rel-dueDate" -> "due_date"
// "rel-67b0d3e3cda913b84c1299b1" -> "67b0d3e3cda913b84c1299b1"
// "dueDate" -> "due_date"
// "67b0d3e3cda913b84c1299b1" -> "67b0d3e3cda913b84c1299b1"
// "ot-page" -> "page"
// "ot-67b0d3e3cda913b84c1299b1" -> "67b0d3e3cda913b84c1299b1"
// "opt-67b0d3e3cda913b84c1299b1" -> "67b0d3e3cda913b84c1299b1"
@ -17,61 +18,41 @@ const (
propPrefix = ""
typePrefix = ""
tagPrefix = ""
internalRelationPrefix = "rel-"
internalRelationPrefix = "" // internally, we're using rk instead of uk when working with relations from api, where no "rel-" prefix exists
internalObjectTypePrefix = "ot-"
internalRelationOptionPrefix = "opt-"
)
var (
hex24Pattern = regexp.MustCompile(`^[a-f\d]{24}$`)
digitPattern = regexp.MustCompile(`\d`)
)
func ToPropertyApiKey(internalKey string) string {
return toApiKey(propPrefix, internalRelationPrefix, internalKey)
}
func FromPropertyApiKey(apiKey string) string {
return fromApiKey(propPrefix, "", apiKey) // interally, we don't prefix relation keys
}
func ToTypeApiKey(internalKey string) string {
return toApiKey(typePrefix, internalObjectTypePrefix, internalKey)
}
func FromTypeApiKey(apiKey string) string {
return fromApiKey(typePrefix, internalObjectTypePrefix, apiKey)
}
func ToTagApiKey(internalKey string) string {
return toApiKey(tagPrefix, internalRelationOptionPrefix, internalKey)
}
func FromTagApiKey(apiKey string) string {
return fromApiKey(tagPrefix, internalRelationOptionPrefix, apiKey)
}
// IsCustomPropertyKey returns true if key is exactly 24 letters and contains at least a digit.
// Non-custom properties never contain a digit.
func IsCustomPropertyKey(key string) bool {
if len(key) != 24 && !strings.ContainsAny(key, "0123456789") {
return false
}
return true
// IsCustomKey returns true if key is exactly 24 letters and contains at least a digit.
func IsCustomKey(key string) bool {
return len(key) == 24 && hex24Pattern.MatchString(key) && digitPattern.MatchString(key)
}
// toApiKey converts an internal key into API format by stripping any existing internal prefixes and adding the API prefix.
func toApiKey(prefix, internalPrefix, internalKey string) string {
var k string
internalKey = strings.TrimPrefix(internalKey, internalPrefix)
if IsCustomPropertyKey(internalKey) {
if IsCustomKey(internalKey) {
k = internalKey
} else {
k = strcase.ToSnake(internalKey)
}
return prefix + k
}
// fromApiKey converts an API key back into internal format by stripping the API prefix and re-adding the internal prefix.
func fromApiKey(prefix, internalPrefix, apiKey string) string {
k := strings.TrimPrefix(apiKey, prefix)
if IsCustomPropertyKey(k) {
return internalPrefix + k
}
return internalPrefix + strcase.ToLowerCamel(k)
}

View file

@ -15,7 +15,6 @@ import (
)
var (
ErrFailedSearchType = errors.New("failed to search for type")
ErrFailedResolveToUniqueKey = errors.New("failed to resolve to unique key")
ErrFailedGetById = errors.New("failed to get object by id")
ErrFailedGetByIdNotFound = errors.New("failed to find object by id")
@ -23,33 +22,6 @@ var (
ErrRelationKeysNotFound = errors.New("failed to find relation keys")
)
// ResolveUniqueKeyToTypeId resolves the unique key to the type's ID
func ResolveUniqueKeyToTypeId(mw apicore.ClientCommands, spaceId string, uniqueKey string) (typeId string, err error) {
resp := mw.ObjectSearch(context.Background(), &pb.RpcObjectSearchRequest{
SpaceId: spaceId,
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeyUniqueKey.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(uniqueKey),
},
},
Keys: []string{bundle.RelationKeyId.String()},
})
if resp.Error != nil {
if resp.Error != nil && resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
return "", ErrFailedSearchType
}
if len(resp.Records) == 0 {
return "", ErrFailedSearchType
}
}
return resp.Records[0].Fields[bundle.RelationKeyId.String()].GetStringValue(), nil
}
// ResolveIdtoUniqueKeyAndRelationKey resolves the type's ID to the unique key
func ResolveIdtoUniqueKeyAndRelationKey(mw apicore.ClientCommands, spaceId string, objectId string) (uk string, rk string, err error) {
resp := mw.ObjectShow(context.Background(), &pb.RpcObjectShowRequest{

View file

@ -8,6 +8,7 @@ import (
"github.com/anyproto/any-sync/app"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/core/event"
"github.com/anyproto/anytype-heart/core/session"
"github.com/anyproto/anytype-heart/pkg/lib/logging"
@ -24,6 +25,7 @@ type Service struct {
// memoized private key derived from mnemonic, used for signing session tokens
sessionSigningKey []byte
sessionsByAppHash map[string]string
rootPath string
fulltextPrimaryLanguage string
@ -39,8 +41,9 @@ type Service struct {
func New() *Service {
s := &Service{
sessions: session.New(),
traceRecorder: &traceRecorder{},
sessions: session.New(),
traceRecorder: &traceRecorder{},
sessionsByAppHash: make(map[string]string),
}
m := newMigrationManager(s)
s.migrationManager = m
@ -70,6 +73,8 @@ func (s *Service) stop() error {
defer task.End()
if s != nil && s.app != nil {
log.Warnf("stopping app")
s.app.SetDeviceState(int(domain.CompStateAppClosingInitiated))
err := s.app.Close(ctx)
if err != nil {
log.Warnf("error while stop anytype: %v", err)

View file

@ -3,7 +3,6 @@ package application
import (
"errors"
"fmt"
"time"
"github.com/anyproto/anytype-heart/core/event"
"github.com/anyproto/anytype-heart/core/session"
@ -32,11 +31,14 @@ func (s *Service) CreateSession(req *pb.RpcWalletCreateSessionRequest) (token st
return "", "", err
}
log.Infof("appLink auth %s", appLink.AppName)
token, err := s.sessions.StartSession(s.sessionSigningKey, model.AccountAuthLocalApiScope(appLink.Scope)) // nolint:gosec
if err != nil {
return "", "", err
}
s.lock.Lock()
defer s.lock.Unlock()
s.sessionsByAppHash[appLink.AppHash] = token
return token, w.Account().SignKey.GetPublic().Account(), nil
}
@ -94,14 +96,21 @@ func (s *Service) LinkLocalSolveChallenge(req *pb.RpcAccountLocalLinkSolveChalle
if err != nil {
return "", "", err
}
wallet := s.app.Component(walletComp.CName).(walletComp.Wallet)
appKey, err = wallet.PersistAppLink(&walletComp.AppLinkPayload{
AppName: clientInfo.ProcessName,
AppPath: clientInfo.ProcessPath,
CreatedAt: time.Now().Unix(),
Scope: int(scope),
})
wallet := s.app.Component(walletComp.CName).(walletComp.Wallet)
name := clientInfo.Name
if name == "" {
name = clientInfo.ProcessName
}
appInfo, err := wallet.PersistAppLink(name, scope)
if err != nil {
return token, appKey, err
}
s.lock.Lock()
s.sessionsByAppHash[appInfo.AppHash] = token
s.lock.Unlock()
appKey = appInfo.AppKey
s.eventSender.Broadcast(event.NewEventSingleMessage("", &pb.EventMessageValueOfAccountLinkChallengeHide{
AccountLinkChallengeHide: &pb.EventAccountLinkChallengeHide{
Challenge: req.Answer,
@ -109,3 +118,70 @@ func (s *Service) LinkLocalSolveChallenge(req *pb.RpcAccountLocalLinkSolveChalle
}))
return
}
func (s *Service) LinkLocalCreateApp(req *pb.RpcAccountLocalLinkCreateAppRequest) (appKey string, err error) {
if s.app == nil {
return "", ErrApplicationIsNotRunning
}
wallet := s.app.Component(walletComp.CName).(walletComp.Wallet)
appInfo, err := wallet.PersistAppLink(req.App.AppName, req.App.Scope)
return appInfo.AppKey, err
}
func (s *Service) LinkLocalListApps() ([]*model.AccountAuthAppInfo, error) {
if s.app == nil {
return nil, ErrApplicationIsNotRunning
}
wallet := s.app.Component(walletComp.CName).(walletComp.Wallet)
links, err := wallet.ListAppLinks()
if err != nil {
return nil, err
}
appsList := make([]*model.AccountAuthAppInfo, len(links))
s.lock.RLock()
defer s.lock.RUnlock()
for i, app := range links {
if app.AppName == "" {
app.AppName = app.AppHash
}
_, isActive := s.sessionsByAppHash[app.AppHash]
appsList[i] = &model.AccountAuthAppInfo{
AppHash: app.AppHash,
AppName: app.AppName,
AppKey: app.AppKey,
CreatedAt: app.CreatedAt,
ExpireAt: app.ExpireAt,
Scope: model.AccountAuthLocalApiScope(app.Scope),
IsActive: isActive,
}
}
return appsList, nil
}
func (s *Service) LinkLocalRevokeApp(req *pb.RpcAccountLocalLinkRevokeAppRequest) error {
if s.app == nil {
return ErrApplicationIsNotRunning
}
wallet := s.app.Component(walletComp.CName).(walletComp.Wallet)
err := wallet.RevokeAppLink(req.AppHash)
if err != nil {
return err
}
s.lock.Lock()
defer s.lock.Unlock()
if token, ok := s.sessionsByAppHash[req.AppHash]; ok {
delete(s.sessionsByAppHash, req.AppHash)
closeErr := s.sessions.CloseSession(token)
if closeErr != nil {
log.Errorf("error while closing session: %v", err)
}
}
return err
}

View file

@ -1,4 +1,4 @@
package chatobject
package chatmodel
import (
"context"
@ -9,17 +9,40 @@ import (
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
type CounterType int
const (
creatorKey = "creator"
createdAtKey = "createdAt"
modifiedAtKey = "modifiedAt"
reactionsKey = "reactions"
contentKey = "content"
readKey = "read"
mentionReadKey = "mentionRead"
hasMentionKey = "hasMention"
stateIdKey = "stateId"
orderKey = "_o"
CounterTypeMessage = CounterType(iota)
CounterTypeMention
)
const (
DiffManagerMessages = "messages"
DiffManagerMentions = "mentions"
)
func (t CounterType) DiffManagerName() string {
switch t {
case CounterTypeMessage:
return DiffManagerMessages
case CounterTypeMention:
return DiffManagerMentions
default:
return "unknown"
}
}
const (
CreatorKey = "creator"
CreatedAtKey = "createdAt"
ModifiedAtKey = "modifiedAt"
ReactionsKey = "reactions"
ContentKey = "content"
ReadKey = "read"
MentionReadKey = "mentionRead"
HasMentionKey = "hasMention"
StateIdKey = "stateId"
OrderKey = "_o"
)
type Message struct {
@ -29,7 +52,11 @@ type Message struct {
CurrentUserMentioned bool
}
func (m *Message) IsCurrentUserMentioned(ctx context.Context, myParticipantId string, myIdentity string, repo *repository) (bool, error) {
type MessagesGetter interface {
GetMessagesByIds(ctx context.Context, messageIds []string) ([]*Message, error)
}
func (m *Message) IsCurrentUserMentioned(ctx context.Context, myParticipantId string, myIdentity string, repo MessagesGetter) (bool, error) {
for _, mark := range m.Message.Marks {
if mark.Type == model.BlockContentTextMark_Mention && mark.Param == myParticipantId {
return true, nil
@ -37,7 +64,7 @@ func (m *Message) IsCurrentUserMentioned(ctx context.Context, myParticipantId st
}
if m.ReplyToMessageId != "" {
msgs, err := repo.getMessagesByIds(ctx, []string{m.ReplyToMessageId})
msgs, err := repo.GetMessagesByIds(ctx, []string{m.ReplyToMessageId})
if err != nil {
return false, fmt.Errorf("get messages by id: %w", err)
}
@ -52,7 +79,7 @@ func (m *Message) IsCurrentUserMentioned(ctx context.Context, myParticipantId st
return false, nil
}
func unmarshalMessage(val *anyenc.Value) (*Message, error) {
func UnmarshalMessage(val *anyenc.Value) (*Message, error) {
return newMessageWrapper(val).toModel()
}
@ -137,16 +164,16 @@ func (m *Message) MarshalAnyenc(marshalTo *anyenc.Value, arena *anyenc.Arena) {
}
marshalTo.Set("id", arena.NewString(m.Id))
marshalTo.Set(creatorKey, arena.NewString(m.Creator))
marshalTo.Set(createdAtKey, arena.NewNumberInt(int(m.CreatedAt)))
marshalTo.Set(modifiedAtKey, arena.NewNumberInt(int(m.ModifiedAt)))
marshalTo.Set(CreatorKey, arena.NewString(m.Creator))
marshalTo.Set(CreatedAtKey, arena.NewNumberInt(int(m.CreatedAt)))
marshalTo.Set(ModifiedAtKey, arena.NewNumberInt(int(m.ModifiedAt)))
marshalTo.Set("replyToMessageId", arena.NewString(m.ReplyToMessageId))
marshalTo.Set(contentKey, content)
marshalTo.Set(readKey, arenaNewBool(arena, m.Read))
marshalTo.Set(mentionReadKey, arenaNewBool(arena, m.MentionRead))
marshalTo.Set(hasMentionKey, arenaNewBool(arena, m.CurrentUserMentioned))
marshalTo.Set(stateIdKey, arena.NewString(m.StateId))
marshalTo.Set(reactionsKey, reactions)
marshalTo.Set(ContentKey, content)
marshalTo.Set(ReadKey, arenaNewBool(arena, m.Read))
marshalTo.Set(MentionReadKey, arenaNewBool(arena, m.MentionRead))
marshalTo.Set(HasMentionKey, arenaNewBool(arena, m.CurrentUserMentioned))
marshalTo.Set(StateIdKey, arena.NewString(m.StateId))
marshalTo.Set(ReactionsKey, reactions)
}
func arenaNewBool(a *anyenc.Arena, value bool) *anyenc.Value {
@ -161,24 +188,24 @@ func (m *messageUnmarshaller) toModel() (*Message, error) {
return &Message{
ChatMessage: &model.ChatMessage{
Id: string(m.val.GetStringBytes("id")),
Creator: string(m.val.GetStringBytes(creatorKey)),
CreatedAt: int64(m.val.GetInt(createdAtKey)),
ModifiedAt: int64(m.val.GetInt(modifiedAtKey)),
StateId: m.val.GetString(stateIdKey),
Creator: string(m.val.GetStringBytes(CreatorKey)),
CreatedAt: int64(m.val.GetInt(CreatedAtKey)),
ModifiedAt: int64(m.val.GetInt(ModifiedAtKey)),
StateId: m.val.GetString(StateIdKey),
OrderId: string(m.val.GetStringBytes("_o", "id")),
ReplyToMessageId: string(m.val.GetStringBytes("replyToMessageId")),
Message: m.contentToModel(),
Read: m.val.GetBool(readKey),
MentionRead: m.val.GetBool(mentionReadKey),
Read: m.val.GetBool(ReadKey),
MentionRead: m.val.GetBool(MentionReadKey),
Attachments: m.attachmentsToModel(),
Reactions: m.reactionsToModel(),
},
CurrentUserMentioned: m.val.GetBool(hasMentionKey),
CurrentUserMentioned: m.val.GetBool(HasMentionKey),
}, nil
}
func (m *messageUnmarshaller) contentToModel() *model.ChatMessageMessageContent {
inMarks := m.val.GetArray(contentKey, "message", "marks")
inMarks := m.val.GetArray(ContentKey, "message", "marks")
marks := make([]*model.BlockContentTextMark, 0, len(inMarks))
for _, inMark := range inMarks {
mark := &model.BlockContentTextMark{
@ -192,14 +219,14 @@ func (m *messageUnmarshaller) contentToModel() *model.ChatMessageMessageContent
marks = append(marks, mark)
}
return &model.ChatMessageMessageContent{
Text: string(m.val.GetStringBytes(contentKey, "message", "text")),
Text: string(m.val.GetStringBytes(ContentKey, "message", "text")),
Style: model.BlockContentTextStyle(m.val.GetInt("content", "message", "style")),
Marks: marks,
}
}
func (m *messageUnmarshaller) attachmentsToModel() []*model.ChatMessageAttachment {
inAttachments := m.val.GetObject(contentKey, "attachments")
inAttachments := m.val.GetObject(ContentKey, "attachments")
var attachments []*model.ChatMessageAttachment
if inAttachments != nil {
attachments = make([]*model.ChatMessageAttachment, 0, inAttachments.Len())
@ -214,7 +241,7 @@ func (m *messageUnmarshaller) attachmentsToModel() []*model.ChatMessageAttachmen
}
func (m *messageUnmarshaller) reactionsToModel() *model.ChatMessageReactions {
inReactions := m.val.GetObject(reactionsKey)
inReactions := m.val.GetObject(ReactionsKey)
reactions := &model.ChatMessageReactions{
Reactions: map[string]*model.ChatMessageReactionsIdentityList{},
}

View file

@ -14,9 +14,10 @@ type Payload struct {
}
type NewMessagePayload struct {
ChatId string `json:"chatId"`
MsgId string `json:"msgId"`
SpaceName string `json:"spaceName"`
SenderName string `json:"senderName"`
Text string `json:"text"`
ChatId string `json:"chatId"`
MsgId string `json:"msgId"`
SpaceName string `json:"spaceName"`
SenderName string `json:"senderName"`
Text string `json:"text"`
HasAttachments bool `json:"hasAttachments"`
}

View file

@ -0,0 +1,92 @@
package chatrepository
import (
"github.com/anyproto/any-store/anyenc"
"github.com/anyproto/any-store/query"
"github.com/anyproto/anytype-heart/core/block/chats/chatmodel"
)
type readHandler interface {
getUnreadFilter() query.Filter
getMessagesFilter() query.Filter
getReadKey() string
readModifier(value bool) query.Modifier
}
type readMessagesHandler struct{}
func (h readMessagesHandler) getUnreadFilter() query.Filter {
return query.Not{
Filter: query.Key{Path: []string{chatmodel.ReadKey}, Filter: query.NewComp(query.CompOpEq, true)},
}
}
func (h readMessagesHandler) getMessagesFilter() query.Filter {
return nil
}
func (h readMessagesHandler) getReadKey() string {
return chatmodel.ReadKey
}
func (h readMessagesHandler) readModifier(value bool) query.Modifier {
return query.ModifyFunc(func(a *anyenc.Arena, v *anyenc.Value) (result *anyenc.Value, modified bool, err error) {
oldValue := v.GetBool(h.getReadKey())
if oldValue != value {
v.Set(h.getReadKey(), arenaNewBool(a, value))
return v, true, nil
}
return v, false, nil
})
}
type readMentionsHandler struct {
}
func (h readMentionsHandler) getUnreadFilter() query.Filter {
return query.And{
query.Key{Path: []string{chatmodel.HasMentionKey}, Filter: query.NewComp(query.CompOpEq, true)},
query.Key{Path: []string{chatmodel.MentionReadKey}, Filter: query.NewComp(query.CompOpEq, false)},
}
}
func (h readMentionsHandler) getMessagesFilter() query.Filter {
return query.Key{Path: []string{chatmodel.HasMentionKey}, Filter: query.NewComp(query.CompOpEq, true)}
}
func (h readMentionsHandler) getReadKey() string {
return chatmodel.MentionReadKey
}
func (h readMentionsHandler) readModifier(value bool) query.Modifier {
return query.ModifyFunc(func(a *anyenc.Arena, v *anyenc.Value) (result *anyenc.Value, modified bool, err error) {
if v.GetBool(chatmodel.HasMentionKey) {
oldValue := v.GetBool(h.getReadKey())
if oldValue != value {
v.Set(h.getReadKey(), arenaNewBool(a, value))
return v, true, nil
}
}
return v, false, nil
})
}
func newReadHandler(counterType chatmodel.CounterType) readHandler {
switch counterType {
case chatmodel.CounterTypeMessage:
return readMessagesHandler{}
case chatmodel.CounterTypeMention:
return readMentionsHandler{}
default:
panic("unknown counter type")
}
}
func arenaNewBool(a *anyenc.Arena, value bool) *anyenc.Value {
if value {
return a.NewTrue()
} else {
return a.NewFalse()
}
}

View file

@ -0,0 +1,474 @@
package chatrepository
import (
"context"
"errors"
"fmt"
"slices"
"sort"
anystore "github.com/anyproto/any-store"
"github.com/anyproto/any-store/anyenc"
"github.com/anyproto/any-store/query"
"github.com/anyproto/any-sync/app"
"go.uber.org/zap"
"github.com/anyproto/anytype-heart/core/block/chats/chatmodel"
"github.com/anyproto/anytype-heart/core/block/object/idresolver"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
"github.com/anyproto/anytype-heart/pkg/lib/logging"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
const CName = "chatrepository"
var log = logging.Logger(CName).Desugar()
const (
descOrder = "-_o.id"
ascOrder = "_o.id"
descStateId = "-stateId"
)
type Service interface {
app.ComponentRunnable
Repository(chatObjectId string) (Repository, error)
}
type service struct {
componentCtx context.Context
componentCtxCancel context.CancelFunc
objectStore objectstore.ObjectStore
spaceIdResolver idresolver.Resolver
arenaPool *anyenc.ArenaPool
}
func New() Service {
return &service{
arenaPool: &anyenc.ArenaPool{},
}
}
func (s *service) Run(ctx context.Context) error {
return nil
}
func (s *service) Close(ctx context.Context) error {
if s.componentCtxCancel != nil {
s.componentCtxCancel()
}
return nil
}
func (s *service) Init(a *app.App) (err error) {
s.componentCtx, s.componentCtxCancel = context.WithCancel(context.Background())
s.objectStore = app.MustComponent[objectstore.ObjectStore](a)
s.spaceIdResolver = app.MustComponent[idresolver.Resolver](a)
return nil
}
func (s *service) Name() (name string) {
return CName
}
func (s *service) Repository(chatObjectId string) (Repository, error) {
spaceId, err := s.spaceIdResolver.ResolveSpaceID(chatObjectId)
if err != nil {
return nil, fmt.Errorf("resolve space id: %w", err)
}
crdtDbGetter := s.objectStore.GetCrdtDb(spaceId)
crdtDb, err := crdtDbGetter.Wait()
if err != nil {
return nil, fmt.Errorf("get crdt db: %w", err)
}
collectionName := chatObjectId + "chats"
collection, err := crdtDb.OpenCollection(s.componentCtx, collectionName)
if errors.Is(err, anystore.ErrCollectionNotFound) {
collection, err = crdtDb.CreateCollection(s.componentCtx, collectionName)
if err != nil {
return nil, fmt.Errorf("create collection: %w", err)
}
}
if err != nil {
return nil, fmt.Errorf("get collection: %w", err)
}
return &repository{
collection: collection,
arenaPool: s.arenaPool,
}, nil
}
type Repository interface {
WriteTx(ctx context.Context) (anystore.WriteTx, error)
ReadTx(ctx context.Context) (anystore.ReadTx, error)
GetLastStateId(ctx context.Context) (string, error)
GetPrevOrderId(ctx context.Context, orderId string) (string, error)
LoadChatState(ctx context.Context) (*model.ChatState, error)
GetOldestOrderId(ctx context.Context, counterType chatmodel.CounterType) (string, error)
GetReadMessagesAfter(ctx context.Context, afterOrderId string, counterType chatmodel.CounterType) ([]string, error)
GetUnreadMessageIdsInRange(ctx context.Context, afterOrderId, beforeOrderId string, lastStateId string, counterType chatmodel.CounterType) ([]string, error)
GetAllUnreadMessages(ctx context.Context, counterType chatmodel.CounterType) ([]string, error)
SetReadFlag(ctx context.Context, chatObjectId string, msgIds []string, counterType chatmodel.CounterType, value bool) []string
GetMessages(ctx context.Context, req GetMessagesRequest) ([]*chatmodel.Message, error)
HasMyReaction(ctx context.Context, myIdentity string, messageId string, emoji string) (bool, error)
GetMessagesByIds(ctx context.Context, messageIds []string) ([]*chatmodel.Message, error)
GetLastMessages(ctx context.Context, limit uint) ([]*chatmodel.Message, error)
}
type repository struct {
collection anystore.Collection
arenaPool *anyenc.ArenaPool
}
func (s *repository) WriteTx(ctx context.Context) (anystore.WriteTx, error) {
return s.collection.WriteTx(ctx)
}
func (s *repository) ReadTx(ctx context.Context) (anystore.ReadTx, error) {
return s.collection.ReadTx(ctx)
}
func (s *repository) GetLastStateId(ctx context.Context) (string, error) {
lastAddedDate := s.collection.Find(nil).Sort(descStateId).Limit(1)
iter, err := lastAddedDate.Iter(ctx)
if err != nil {
return "", fmt.Errorf("find last added date: %w", err)
}
defer iter.Close()
for iter.Next() {
doc, err := iter.Doc()
if err != nil {
return "", fmt.Errorf("get doc: %w", err)
}
msg, err := chatmodel.UnmarshalMessage(doc.Value())
if err != nil {
return "", fmt.Errorf("unmarshal message: %w", err)
}
return msg.StateId, nil
}
return "", nil
}
func (s *repository) GetPrevOrderId(ctx context.Context, orderId string) (string, error) {
iter, err := s.collection.Find(query.Key{Path: []string{chatmodel.OrderKey, "id"}, Filter: query.NewComp(query.CompOpLt, orderId)}).
Sort(descOrder).
Limit(1).
Iter(ctx)
if err != nil {
return "", fmt.Errorf("init iterator: %w", err)
}
defer iter.Close()
if iter.Next() {
doc, err := iter.Doc()
if err != nil {
return "", fmt.Errorf("read doc: %w", err)
}
prevOrderId := doc.Value().GetString(chatmodel.OrderKey, "id")
return prevOrderId, nil
}
return "", nil
}
// initialChatState returns the initial chat state for the chat object from the DB
func (s *repository) LoadChatState(ctx context.Context) (*model.ChatState, error) {
txn, err := s.ReadTx(ctx)
if err != nil {
return nil, fmt.Errorf("start read tx: %w", err)
}
defer txn.Commit()
messagesState, err := s.loadChatStateByType(txn.Context(), chatmodel.CounterTypeMessage)
if err != nil {
return nil, fmt.Errorf("get messages state: %w", err)
}
mentionsState, err := s.loadChatStateByType(txn.Context(), chatmodel.CounterTypeMention)
if err != nil {
return nil, fmt.Errorf("get mentions state: %w", err)
}
lastStateId, err := s.GetLastStateId(txn.Context())
if err != nil {
return nil, fmt.Errorf("get last added date: %w", err)
}
return &model.ChatState{
Messages: messagesState,
Mentions: mentionsState,
LastStateId: lastStateId,
}, nil
}
func (s *repository) loadChatStateByType(ctx context.Context, counterType chatmodel.CounterType) (*model.ChatStateUnreadState, error) {
handler := newReadHandler(counterType)
oldestOrderId, err := s.GetOldestOrderId(ctx, counterType)
if err != nil {
return nil, fmt.Errorf("get oldest order id: %w", err)
}
count, err := s.countUnreadMessages(ctx, handler)
if err != nil {
return nil, fmt.Errorf("update messages: %w", err)
}
return &model.ChatStateUnreadState{
OldestOrderId: oldestOrderId,
Counter: int32(count),
}, nil
}
func (s *repository) GetOldestOrderId(ctx context.Context, counterType chatmodel.CounterType) (string, error) {
handler := newReadHandler(counterType)
unreadQuery := s.collection.Find(handler.getUnreadFilter()).Sort(ascOrder)
iter, err := unreadQuery.Limit(1).Iter(ctx)
if err != nil {
return "", fmt.Errorf("init iter: %w", err)
}
defer iter.Close()
for iter.Next() {
doc, err := iter.Doc()
if err != nil {
return "", fmt.Errorf("get doc: %w", err)
}
orders := doc.Value().GetObject(chatmodel.OrderKey)
if orders != nil {
return orders.Get("id").GetString(), nil
}
}
return "", nil
}
func (s *repository) countUnreadMessages(ctx context.Context, handler readHandler) (int, error) {
unreadQuery := s.collection.Find(handler.getUnreadFilter())
return unreadQuery.Count(ctx)
}
func (s *repository) GetReadMessagesAfter(ctx context.Context, afterOrderId string, counterType chatmodel.CounterType) ([]string, error) {
handler := newReadHandler(counterType)
filter := query.And{
query.Key{Path: []string{chatmodel.OrderKey, "id"}, Filter: query.NewComp(query.CompOpGte, afterOrderId)},
query.Key{Path: []string{handler.getReadKey()}, Filter: query.NewComp(query.CompOpEq, true)},
}
if handler.getMessagesFilter() != nil {
filter = append(filter, handler.getMessagesFilter())
}
iter, err := s.collection.Find(filter).Iter(ctx)
if err != nil {
return nil, fmt.Errorf("init iterator: %w", err)
}
defer iter.Close()
var msgIds []string
for iter.Next() {
doc, err := iter.Doc()
if err != nil {
return nil, fmt.Errorf("get doc: %w", err)
}
msgIds = append(msgIds, doc.Value().GetString("id"))
}
return msgIds, iter.Err()
}
func (s *repository) GetUnreadMessageIdsInRange(ctx context.Context, afterOrderId, beforeOrderId string, lastStateId string, counterType chatmodel.CounterType) ([]string, error) {
handler := newReadHandler(counterType)
qry := query.And{
query.Key{Path: []string{chatmodel.OrderKey, "id"}, Filter: query.NewComp(query.CompOpGte, afterOrderId)},
query.Key{Path: []string{chatmodel.OrderKey, "id"}, Filter: query.NewComp(query.CompOpLte, beforeOrderId)},
query.Or{
query.Not{Filter: query.Key{Path: []string{chatmodel.StateIdKey}, Filter: query.Exists{}}},
query.Key{Path: []string{chatmodel.StateIdKey}, Filter: query.NewComp(query.CompOpLte, lastStateId)},
},
handler.getUnreadFilter(),
}
iter, err := s.collection.Find(qry).Iter(ctx)
if err != nil {
return nil, fmt.Errorf("find id: %w", err)
}
defer iter.Close()
var msgIds []string
for iter.Next() {
doc, err := iter.Doc()
if err != nil {
return nil, fmt.Errorf("get doc: %w", err)
}
msgIds = append(msgIds, doc.Value().GetString("id"))
}
return msgIds, iter.Err()
}
func (s *repository) GetAllUnreadMessages(ctx context.Context, counterType chatmodel.CounterType) ([]string, error) {
handler := newReadHandler(counterType)
qry := query.And{
handler.getUnreadFilter(),
}
iter, err := s.collection.Find(qry).Iter(ctx)
if err != nil {
return nil, fmt.Errorf("find id: %w", err)
}
defer iter.Close()
var msgIds []string
for iter.Next() {
doc, err := iter.Doc()
if err != nil {
return nil, fmt.Errorf("get doc: %w", err)
}
msgIds = append(msgIds, doc.Value().GetString("id"))
}
return msgIds, iter.Err()
}
func (r *repository) SetReadFlag(ctx context.Context, chatObjectId string, msgIds []string, counterType chatmodel.CounterType, value bool) []string {
handler := newReadHandler(counterType)
var idsModified []string
for _, id := range msgIds {
if id == chatObjectId {
// skip tree root
continue
}
res, err := r.collection.UpdateId(ctx, id, handler.readModifier(value))
// Not all changes are messages, skip them
if errors.Is(err, anystore.ErrDocNotFound) {
continue
}
if err != nil {
log.Error("markReadMessages: update message", zap.Error(err), zap.String("changeId", id), zap.String("chatObjectId", chatObjectId))
continue
}
if res.Modified > 0 {
idsModified = append(idsModified, id)
}
}
return idsModified
}
type GetMessagesRequest struct {
AfterOrderId string
BeforeOrderId string
Limit int
IncludeBoundary bool
}
func (s *repository) GetMessages(ctx context.Context, req GetMessagesRequest) ([]*chatmodel.Message, error) {
var qry anystore.Query
if req.AfterOrderId != "" {
operator := query.CompOpGt
if req.IncludeBoundary {
operator = query.CompOpGte
}
qry = s.collection.Find(query.Key{Path: []string{chatmodel.OrderKey, "id"}, Filter: query.NewComp(operator, req.AfterOrderId)}).Sort(ascOrder).Limit(uint(req.Limit))
} else if req.BeforeOrderId != "" {
operator := query.CompOpLt
if req.IncludeBoundary {
operator = query.CompOpLte
}
qry = s.collection.Find(query.Key{Path: []string{chatmodel.OrderKey, "id"}, Filter: query.NewComp(operator, req.BeforeOrderId)}).Sort(descOrder).Limit(uint(req.Limit))
} else {
qry = s.collection.Find(nil).Sort(descOrder).Limit(uint(req.Limit))
}
msgs, err := s.queryMessages(ctx, qry)
if err != nil {
return nil, fmt.Errorf("query messages: %w", err)
}
return msgs, nil
}
func (s *repository) queryMessages(ctx context.Context, query anystore.Query) ([]*chatmodel.Message, error) {
arena := s.arenaPool.Get()
defer func() {
arena.Reset()
s.arenaPool.Put(arena)
}()
iter, err := query.Iter(ctx)
if err != nil {
return nil, fmt.Errorf("find iter: %w", err)
}
defer iter.Close()
var res []*chatmodel.Message
for iter.Next() {
doc, err := iter.Doc()
if err != nil {
return nil, fmt.Errorf("get doc: %w", err)
}
msg, err := chatmodel.UnmarshalMessage(doc.Value())
if err != nil {
return nil, fmt.Errorf("unmarshal message: %w", err)
}
res = append(res, msg)
}
// reverse
sort.Slice(res, func(i, j int) bool {
return res[i].OrderId < res[j].OrderId
})
return res, nil
}
func (s *repository) HasMyReaction(ctx context.Context, myIdentity string, messageId string, emoji string) (bool, error) {
doc, err := s.collection.FindId(ctx, messageId)
if err != nil {
return false, fmt.Errorf("find message: %w", err)
}
msg, err := chatmodel.UnmarshalMessage(doc.Value())
if err != nil {
return false, fmt.Errorf("unmarshal message: %w", err)
}
if v, ok := msg.GetReactions().GetReactions()[emoji]; ok {
if slices.Contains(v.GetIds(), myIdentity) {
return true, nil
}
}
return false, nil
}
func (s *repository) GetMessagesByIds(ctx context.Context, messageIds []string) ([]*chatmodel.Message, error) {
txn, err := s.ReadTx(ctx)
if err != nil {
return nil, fmt.Errorf("start read tx: %w", err)
}
defer txn.Commit()
messages := make([]*chatmodel.Message, 0, len(messageIds))
for _, messageId := range messageIds {
obj, err := s.collection.FindId(txn.Context(), messageId)
if errors.Is(err, anystore.ErrDocNotFound) {
continue
}
if err != nil {
return nil, errors.Join(txn.Commit(), fmt.Errorf("find id: %w", err))
}
msg, err := chatmodel.UnmarshalMessage(obj.Value())
if err != nil {
return nil, errors.Join(txn.Commit(), fmt.Errorf("unmarshal message: %w", err))
}
messages = append(messages, msg)
}
return messages, nil
}
func (s *repository) GetLastMessages(ctx context.Context, limit uint) ([]*chatmodel.Message, error) {
qry := s.collection.Find(nil).Sort(descOrder).Limit(limit)
return s.queryMessages(ctx, qry)
}

View file

@ -1,14 +1,16 @@
package chatobject
package chatsubscription
import (
"context"
"slices"
"sort"
"time"
"sync"
"github.com/hashicorp/golang-lru/v2/expirable"
"go.uber.org/zap"
"github.com/anyproto/anytype-heart/core/block/chats/chatmodel"
"github.com/anyproto/anytype-heart/core/block/chats/chatrepository"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/core/event"
"github.com/anyproto/anytype-heart/core/session"
@ -18,6 +20,8 @@ import (
)
type subscriptionManager struct {
lock sync.Mutex
componentCtx context.Context
spaceId string
@ -38,35 +42,31 @@ type subscriptionManager struct {
// Deps
spaceIndex spaceindex.Store
eventSender event.Sender
repository *repository
repository chatrepository.Repository
}
type subscription struct {
id string
withDependencies bool
onlyLastMessage bool
}
func (s *storeObject) newSubscriptionManager(fullId domain.FullID, myIdentity string, myParticipantId string) *subscriptionManager {
return &subscriptionManager{
componentCtx: s.componentCtx,
spaceId: fullId.SpaceID,
chatId: fullId.ObjectID,
eventSender: s.eventSender,
spaceIndex: s.spaceIndex,
myIdentity: myIdentity,
myParticipantId: myParticipantId,
identityCache: expirable.NewLRU[string, *domain.Details](50, nil, time.Minute),
repository: s.repository,
subscriptions: map[string]*subscription{},
}
func (s *subscriptionManager) Lock() {
s.lock.Lock()
}
func (s *subscriptionManager) Unlock() {
s.lock.Unlock()
}
// subscribe subscribes to messages. It returns true if there was no subscriptionManager with provided id
func (s *subscriptionManager) subscribe(subId string, withDependencies bool) bool {
func (s *subscriptionManager) subscribe(subId string, withDependencies bool, onlyLastMessage bool) bool {
if _, ok := s.subscriptions[subId]; !ok {
s.subscriptions[subId] = &subscription{
id: subId,
withDependencies: withDependencies,
onlyLastMessage: onlyLastMessage,
}
s.chatStateUpdated = false
return true
@ -78,7 +78,7 @@ func (s *subscriptionManager) unsubscribe(subId string) {
delete(s.subscriptions, subId)
}
func (s *subscriptionManager) isActive() bool {
func (s *subscriptionManager) IsActive() bool {
return len(s.subscriptions) > 0
}
@ -100,13 +100,13 @@ func (s *subscriptionManager) listSubIds() []string {
return subIds
}
// setSessionContext sets the session context for the current operation
func (s *subscriptionManager) setSessionContext(ctx session.Context) {
// SetSessionContext sets the session context for the current operation
func (s *subscriptionManager) SetSessionContext(ctx session.Context) {
s.sessionContext = ctx
}
func (s *subscriptionManager) loadChatState(ctx context.Context) error {
state, err := s.repository.loadChatState(ctx)
state, err := s.repository.LoadChatState(ctx)
if err != nil {
return err
}
@ -114,25 +114,25 @@ func (s *subscriptionManager) loadChatState(ctx context.Context) error {
return nil
}
func (s *subscriptionManager) getChatState() *model.ChatState {
func (s *subscriptionManager) GetChatState() *model.ChatState {
return copyChatState(s.chatState)
}
func (s *subscriptionManager) updateChatState(updater func(*model.ChatState) *model.ChatState) {
func (s *subscriptionManager) UpdateChatState(updater func(*model.ChatState) *model.ChatState) {
s.chatState = updater(s.chatState)
s.chatStateUpdated = true
}
// flush is called after commiting changes
func (s *subscriptionManager) flush() {
// Flush is called after committing changes
func (s *subscriptionManager) Flush() {
if !s.canSend() {
return
}
// Reload ChatState after commit
if s.needReloadState {
s.updateChatState(func(state *model.ChatState) *model.ChatState {
newState, err := s.repository.loadChatState(s.componentCtx)
s.UpdateChatState(func(state *model.ChatState) *model.ChatState {
newState, err := s.repository.LoadChatState(s.componentCtx)
if err != nil {
log.Error("failed to reload chat state", zap.Error(err))
return state
@ -145,9 +145,34 @@ func (s *subscriptionManager) flush() {
events := slices.Clone(s.eventsBuffer)
s.eventsBuffer = s.eventsBuffer[:0]
var subIdsOnlyLastMessage []string
subIdsAllMessages := make([]string, 0, len(s.subscriptions))
for _, sub := range s.subscriptions {
if sub.onlyLastMessage {
subIdsOnlyLastMessage = append(subIdsOnlyLastMessage, sub.id)
} else {
subIdsAllMessages = append(subIdsAllMessages, sub.id)
}
}
// Corner case when we are subscribed only for the last message
// The idea is to prevent sending a lot of events to message preview subscription on cold recovery or reindex.
if len(subIdsAllMessages) == 0 && len(subIdsOnlyLastMessage) > 0 {
events = s.getEventsOnlyForLastMessage(events, subIdsOnlyLastMessage)
} else {
for _, ev := range events {
if ev := ev.GetChatAdd(); ev != nil {
ev.SubIds = subIdsAllMessages
if s.withDeps() {
s.enrichWithDependencies(ev)
}
}
}
}
if s.chatStateUpdated {
events = append(events, event.NewMessage(s.spaceId, &pb.EventMessageValueOfChatStateUpdate{ChatStateUpdate: &pb.EventChatUpdateState{
State: s.getChatState(),
State: s.GetChatState(),
SubIds: s.listSubIds(),
}}))
s.chatStateUpdated = false
@ -158,14 +183,43 @@ func (s *subscriptionManager) flush() {
Messages: events,
}
if s.sessionContext != nil {
s.sessionContext.SetMessages(s.chatId, events)
s.sessionContext.SetMessages(s.chatId, append(s.sessionContext.GetMessages(), events...))
s.eventSender.BroadcastToOtherSessions(s.sessionContext.ID(), ev)
s.sessionContext = nil
} else if s.isActive() {
} else if s.IsActive() {
s.eventSender.Broadcast(ev)
}
}
func (s *subscriptionManager) getEventsOnlyForLastMessage(events []*pb.EventMessage, subIdsOnlyLastMessage []string) []*pb.EventMessage {
state := newMessagesState()
for _, ev := range events {
state.applyEvent(ev)
}
lastMessage, ok := state.getLastAddedMessage()
if ok {
addEvent := state.addEvents[lastMessage.Id]
addEvent.SubIds = subIdsOnlyLastMessage
if s.withDeps() {
s.enrichWithDependencies(addEvent)
}
// Just rewrite all events and leave only last message. This message already has all updates applied to it
events = []*pb.EventMessage{
event.NewMessage(s.spaceId, &pb.EventMessageValueOfChatAdd{
ChatAdd: addEvent,
}),
}
}
return events
}
func (s *subscriptionManager) enrichWithDependencies(ev *pb.EventChatAdd) {
deps := s.collectMessageDependencies(ev.Message)
for _, dep := range deps {
ev.Dependencies = append(ev.Dependencies, dep.ToProto())
}
}
func (s *subscriptionManager) getIdentityDetails(identity string) (*domain.Details, error) {
cached, ok := s.identityCache.Get(identity)
if ok {
@ -179,7 +233,7 @@ func (s *subscriptionManager) getIdentityDetails(identity string) (*domain.Detai
return details, nil
}
func (s *subscriptionManager) add(prevOrderId string, message *Message) {
func (s *subscriptionManager) Add(prevOrderId string, message *chatmodel.Message) {
if !s.canSend() {
return
}
@ -189,21 +243,13 @@ func (s *subscriptionManager) add(prevOrderId string, message *Message) {
Message: message.ChatMessage,
OrderId: message.OrderId,
AfterOrderId: prevOrderId,
SubIds: s.listSubIds(),
}
if s.withDeps() {
deps := s.collectMessageDependencies(message)
for _, dep := range deps {
ev.Dependencies = append(ev.Dependencies, dep.ToProto())
}
}
s.eventsBuffer = append(s.eventsBuffer, event.NewMessage(s.spaceId, &pb.EventMessageValueOfChatAdd{
ChatAdd: ev,
}))
}
func (s *subscriptionManager) collectMessageDependencies(message *Message) []*domain.Details {
func (s *subscriptionManager) collectMessageDependencies(message *model.ChatMessage) []*domain.Details {
var result []*domain.Details
identityDetails, err := s.getIdentityDetails(message.Creator)
@ -224,7 +270,7 @@ func (s *subscriptionManager) collectMessageDependencies(message *Message) []*do
return result
}
func (s *subscriptionManager) delete(messageId string) {
func (s *subscriptionManager) Delete(messageId string) {
ev := &pb.EventChatDelete{
Id: messageId,
SubIds: s.listSubIds(),
@ -237,7 +283,7 @@ func (s *subscriptionManager) delete(messageId string) {
s.needReloadState = true
}
func (s *subscriptionManager) updateFull(message *Message) {
func (s *subscriptionManager) UpdateFull(message *chatmodel.Message) {
if !s.canSend() {
return
}
@ -251,7 +297,7 @@ func (s *subscriptionManager) updateFull(message *Message) {
}))
}
func (s *subscriptionManager) updateReactions(message *Message) {
func (s *subscriptionManager) UpdateReactions(message *chatmodel.Message) {
if !s.canSend() {
return
}
@ -268,7 +314,7 @@ func (s *subscriptionManager) updateReactions(message *Message) {
// updateMessageRead updates the read status of the messages with the given ids
// read ids should ONLY contain ids if they were actually modified in the DB
func (s *subscriptionManager) updateMessageRead(ids []string, read bool) {
s.updateChatState(func(state *model.ChatState) *model.ChatState {
s.UpdateChatState(func(state *model.ChatState) *model.ChatState {
if read {
state.Messages.Counter -= int32(len(ids))
} else {
@ -290,7 +336,7 @@ func (s *subscriptionManager) updateMessageRead(ids []string, read bool) {
}
func (s *subscriptionManager) updateMentionRead(ids []string, read bool) {
s.updateChatState(func(state *model.ChatState) *model.ChatState {
s.UpdateChatState(func(state *model.ChatState) *model.ChatState {
if read {
state.Mentions.Counter -= int32(len(ids))
} else {
@ -315,12 +361,46 @@ func (s *subscriptionManager) canSend() bool {
if s.sessionContext != nil {
return true
}
if !s.isActive() {
if !s.IsActive() {
return false
}
return true
}
func (s *subscriptionManager) ReadMessages(newOldestOrderId string, idsModified []string, counterType chatmodel.CounterType) {
if counterType == chatmodel.CounterTypeMessage {
s.UpdateChatState(func(state *model.ChatState) *model.ChatState {
state.Messages.OldestOrderId = newOldestOrderId
return state
})
s.updateMessageRead(idsModified, true)
} else {
s.UpdateChatState(func(state *model.ChatState) *model.ChatState {
state.Mentions.OldestOrderId = newOldestOrderId
return state
})
s.updateMentionRead(idsModified, true)
}
}
func (s *subscriptionManager) UnreadMessages(newOldestOrderId string, lastStateId string, msgIds []string, counterType chatmodel.CounterType) {
if counterType == chatmodel.CounterTypeMessage {
s.UpdateChatState(func(state *model.ChatState) *model.ChatState {
state.Messages.OldestOrderId = newOldestOrderId
state.LastStateId = lastStateId
return state
})
s.updateMessageRead(msgIds, false)
} else {
s.UpdateChatState(func(state *model.ChatState) *model.ChatState {
state.Mentions.OldestOrderId = newOldestOrderId
state.LastStateId = lastStateId
return state
})
s.updateMentionRead(msgIds, false)
}
}
func copyChatState(state *model.ChatState) *model.ChatState {
if state == nil {
return nil

View file

@ -0,0 +1,386 @@
// Code generated by mockery. DO NOT EDIT.
package mock_chatsubscription
import (
context "context"
app "github.com/anyproto/any-sync/app"
chatsubscription "github.com/anyproto/anytype-heart/core/block/chats/chatsubscription"
mock "github.com/stretchr/testify/mock"
)
// MockService is an autogenerated mock type for the Service type
type MockService struct {
mock.Mock
}
type MockService_Expecter struct {
mock *mock.Mock
}
func (_m *MockService) EXPECT() *MockService_Expecter {
return &MockService_Expecter{mock: &_m.Mock}
}
// Close provides a mock function with given fields: ctx
func (_m *MockService) Close(ctx context.Context) error {
ret := _m.Called(ctx)
if len(ret) == 0 {
panic("no return value specified for Close")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
r0 = rf(ctx)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockService_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
type MockService_Close_Call struct {
*mock.Call
}
// Close is a helper method to define mock.On call
// - ctx context.Context
func (_e *MockService_Expecter) Close(ctx interface{}) *MockService_Close_Call {
return &MockService_Close_Call{Call: _e.mock.On("Close", ctx)}
}
func (_c *MockService_Close_Call) Run(run func(ctx context.Context)) *MockService_Close_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context))
})
return _c
}
func (_c *MockService_Close_Call) Return(err error) *MockService_Close_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockService_Close_Call) RunAndReturn(run func(context.Context) error) *MockService_Close_Call {
_c.Call.Return(run)
return _c
}
// GetManager provides a mock function with given fields: chatObjectId
func (_m *MockService) GetManager(chatObjectId string) (chatsubscription.Manager, error) {
ret := _m.Called(chatObjectId)
if len(ret) == 0 {
panic("no return value specified for GetManager")
}
var r0 chatsubscription.Manager
var r1 error
if rf, ok := ret.Get(0).(func(string) (chatsubscription.Manager, error)); ok {
return rf(chatObjectId)
}
if rf, ok := ret.Get(0).(func(string) chatsubscription.Manager); ok {
r0 = rf(chatObjectId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(chatsubscription.Manager)
}
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(chatObjectId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockService_GetManager_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetManager'
type MockService_GetManager_Call struct {
*mock.Call
}
// GetManager is a helper method to define mock.On call
// - chatObjectId string
func (_e *MockService_Expecter) GetManager(chatObjectId interface{}) *MockService_GetManager_Call {
return &MockService_GetManager_Call{Call: _e.mock.On("GetManager", chatObjectId)}
}
func (_c *MockService_GetManager_Call) Run(run func(chatObjectId string)) *MockService_GetManager_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockService_GetManager_Call) Return(_a0 chatsubscription.Manager, _a1 error) *MockService_GetManager_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockService_GetManager_Call) RunAndReturn(run func(string) (chatsubscription.Manager, error)) *MockService_GetManager_Call {
_c.Call.Return(run)
return _c
}
// Init provides a mock function with given fields: a
func (_m *MockService) Init(a *app.App) error {
ret := _m.Called(a)
if len(ret) == 0 {
panic("no return value specified for Init")
}
var r0 error
if rf, ok := ret.Get(0).(func(*app.App) error); ok {
r0 = rf(a)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockService_Init_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Init'
type MockService_Init_Call struct {
*mock.Call
}
// Init is a helper method to define mock.On call
// - a *app.App
func (_e *MockService_Expecter) Init(a interface{}) *MockService_Init_Call {
return &MockService_Init_Call{Call: _e.mock.On("Init", a)}
}
func (_c *MockService_Init_Call) Run(run func(a *app.App)) *MockService_Init_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(*app.App))
})
return _c
}
func (_c *MockService_Init_Call) Return(err error) *MockService_Init_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockService_Init_Call) RunAndReturn(run func(*app.App) error) *MockService_Init_Call {
_c.Call.Return(run)
return _c
}
// Name provides a mock function with given fields:
func (_m *MockService) Name() string {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Name")
}
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// MockService_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name'
type MockService_Name_Call struct {
*mock.Call
}
// Name is a helper method to define mock.On call
func (_e *MockService_Expecter) Name() *MockService_Name_Call {
return &MockService_Name_Call{Call: _e.mock.On("Name")}
}
func (_c *MockService_Name_Call) Run(run func()) *MockService_Name_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockService_Name_Call) Return(name string) *MockService_Name_Call {
_c.Call.Return(name)
return _c
}
func (_c *MockService_Name_Call) RunAndReturn(run func() string) *MockService_Name_Call {
_c.Call.Return(run)
return _c
}
// Run provides a mock function with given fields: ctx
func (_m *MockService) Run(ctx context.Context) error {
ret := _m.Called(ctx)
if len(ret) == 0 {
panic("no return value specified for Run")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
r0 = rf(ctx)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockService_Run_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Run'
type MockService_Run_Call struct {
*mock.Call
}
// Run is a helper method to define mock.On call
// - ctx context.Context
func (_e *MockService_Expecter) Run(ctx interface{}) *MockService_Run_Call {
return &MockService_Run_Call{Call: _e.mock.On("Run", ctx)}
}
func (_c *MockService_Run_Call) Run(run func(ctx context.Context)) *MockService_Run_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context))
})
return _c
}
func (_c *MockService_Run_Call) Return(err error) *MockService_Run_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockService_Run_Call) RunAndReturn(run func(context.Context) error) *MockService_Run_Call {
_c.Call.Return(run)
return _c
}
// SubscribeLastMessages provides a mock function with given fields: ctx, req
func (_m *MockService) SubscribeLastMessages(ctx context.Context, req chatsubscription.SubscribeLastMessagesRequest) (*chatsubscription.SubscribeLastMessagesResponse, error) {
ret := _m.Called(ctx, req)
if len(ret) == 0 {
panic("no return value specified for SubscribeLastMessages")
}
var r0 *chatsubscription.SubscribeLastMessagesResponse
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, chatsubscription.SubscribeLastMessagesRequest) (*chatsubscription.SubscribeLastMessagesResponse, error)); ok {
return rf(ctx, req)
}
if rf, ok := ret.Get(0).(func(context.Context, chatsubscription.SubscribeLastMessagesRequest) *chatsubscription.SubscribeLastMessagesResponse); ok {
r0 = rf(ctx, req)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*chatsubscription.SubscribeLastMessagesResponse)
}
}
if rf, ok := ret.Get(1).(func(context.Context, chatsubscription.SubscribeLastMessagesRequest) error); ok {
r1 = rf(ctx, req)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockService_SubscribeLastMessages_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SubscribeLastMessages'
type MockService_SubscribeLastMessages_Call struct {
*mock.Call
}
// SubscribeLastMessages is a helper method to define mock.On call
// - ctx context.Context
// - req chatsubscription.SubscribeLastMessagesRequest
func (_e *MockService_Expecter) SubscribeLastMessages(ctx interface{}, req interface{}) *MockService_SubscribeLastMessages_Call {
return &MockService_SubscribeLastMessages_Call{Call: _e.mock.On("SubscribeLastMessages", ctx, req)}
}
func (_c *MockService_SubscribeLastMessages_Call) Run(run func(ctx context.Context, req chatsubscription.SubscribeLastMessagesRequest)) *MockService_SubscribeLastMessages_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(chatsubscription.SubscribeLastMessagesRequest))
})
return _c
}
func (_c *MockService_SubscribeLastMessages_Call) Return(_a0 *chatsubscription.SubscribeLastMessagesResponse, _a1 error) *MockService_SubscribeLastMessages_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockService_SubscribeLastMessages_Call) RunAndReturn(run func(context.Context, chatsubscription.SubscribeLastMessagesRequest) (*chatsubscription.SubscribeLastMessagesResponse, error)) *MockService_SubscribeLastMessages_Call {
_c.Call.Return(run)
return _c
}
// Unsubscribe provides a mock function with given fields: chatObjectId, subId
func (_m *MockService) Unsubscribe(chatObjectId string, subId string) error {
ret := _m.Called(chatObjectId, subId)
if len(ret) == 0 {
panic("no return value specified for Unsubscribe")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(chatObjectId, subId)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockService_Unsubscribe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Unsubscribe'
type MockService_Unsubscribe_Call struct {
*mock.Call
}
// Unsubscribe is a helper method to define mock.On call
// - chatObjectId string
// - subId string
func (_e *MockService_Expecter) Unsubscribe(chatObjectId interface{}, subId interface{}) *MockService_Unsubscribe_Call {
return &MockService_Unsubscribe_Call{Call: _e.mock.On("Unsubscribe", chatObjectId, subId)}
}
func (_c *MockService_Unsubscribe_Call) Run(run func(chatObjectId string, subId string)) *MockService_Unsubscribe_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(string))
})
return _c
}
func (_c *MockService_Unsubscribe_Call) Return(_a0 error) *MockService_Unsubscribe_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockService_Unsubscribe_Call) RunAndReturn(run func(string, string) error) *MockService_Unsubscribe_Call {
_c.Call.Return(run)
return _c
}
// NewMockService creates a new instance of MockService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockService(t interface {
mock.TestingT
Cleanup(func())
}) *MockService {
mock := &MockService{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View file

@ -0,0 +1,251 @@
package chatsubscription
import (
"context"
"fmt"
"sync"
"time"
"github.com/anyproto/any-sync/app"
"github.com/hashicorp/golang-lru/v2/expirable"
"github.com/anyproto/anytype-heart/core/block/chats/chatmodel"
"github.com/anyproto/anytype-heart/core/block/chats/chatrepository"
"github.com/anyproto/anytype-heart/core/block/object/idresolver"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/core/event"
"github.com/anyproto/anytype-heart/core/session"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
"github.com/anyproto/anytype-heart/pkg/lib/logging"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/futures"
)
const CName = "chatsubscription"
var log = logging.Logger(CName).Desugar()
type Manager interface {
sync.Locker
IsActive() bool
GetChatState() *model.ChatState
SetSessionContext(ctx session.Context)
UpdateReactions(message *chatmodel.Message)
UpdateFull(message *chatmodel.Message)
UpdateChatState(updater func(*model.ChatState) *model.ChatState)
Add(prevOrderId string, message *chatmodel.Message)
Delete(messageId string)
Flush()
ReadMessages(newOldestOrderId string, idsModified []string, counterType chatmodel.CounterType)
UnreadMessages(newOldestOrderId string, lastStateId string, msgIds []string, counterType chatmodel.CounterType)
}
type Service interface {
app.ComponentRunnable
GetManager(chatObjectId string) (Manager, error)
SubscribeLastMessages(ctx context.Context, req SubscribeLastMessagesRequest) (*SubscribeLastMessagesResponse, error)
Unsubscribe(chatObjectId string, subId string) error
}
type AccountService interface {
AccountID() string
}
type service struct {
componentCtx context.Context
componentCtxCancel context.CancelFunc
spaceIdResolver idresolver.Resolver
objectStore objectstore.ObjectStore
eventSender event.Sender
repositoryService chatrepository.Service
accountService AccountService
identityCache *expirable.LRU[string, *domain.Details]
lock sync.Mutex
managers map[string]*futures.Future[*subscriptionManager]
}
func New() Service {
return &service{
managers: make(map[string]*futures.Future[*subscriptionManager]),
}
}
func (s *service) Init(a *app.App) (err error) {
s.componentCtx, s.componentCtxCancel = context.WithCancel(context.Background())
s.spaceIdResolver = app.MustComponent[idresolver.Resolver](a)
s.objectStore = app.MustComponent[objectstore.ObjectStore](a)
s.eventSender = app.MustComponent[event.Sender](a)
s.repositoryService = app.MustComponent[chatrepository.Service](a)
s.accountService = app.MustComponent[AccountService](a)
s.identityCache = expirable.NewLRU[string, *domain.Details](50, nil, time.Minute)
return nil
}
func (s *service) Name() (name string) {
return CName
}
func (s *service) Run(ctx context.Context) (err error) {
return nil
}
func (s *service) Close(ctx context.Context) (err error) {
if s.componentCtxCancel != nil {
s.componentCtxCancel()
}
return nil
}
func (s *service) GetManager(chatObjectId string) (Manager, error) {
return s.getManager(chatObjectId)
}
// getManagerFuture returns a future that should be resolved by the first who called this method.
// The idea behind using futures here is to initialize a manager once without blocking the whole service.
func (s *service) getManagerFuture(chatObjectId string) *futures.Future[*subscriptionManager] {
s.lock.Lock()
mngr, ok := s.managers[chatObjectId]
if ok {
s.lock.Unlock()
return mngr
}
mngr = futures.New[*subscriptionManager]()
s.managers[chatObjectId] = mngr
s.lock.Unlock()
mngr.Resolve(s.initManager(chatObjectId))
return mngr
}
func (s *service) getManager(chatObjectId string) (*subscriptionManager, error) {
return s.getManagerFuture(chatObjectId).Wait()
}
func (s *service) initManager(chatObjectId string) (*subscriptionManager, error) {
spaceId, err := s.spaceIdResolver.ResolveSpaceID(chatObjectId)
if err != nil {
return nil, fmt.Errorf("resolve space id: %w", err)
}
currentIdentity := s.accountService.AccountID()
currentParticipantId := domain.NewParticipantId(spaceId, currentIdentity)
repository, err := s.repositoryService.Repository(chatObjectId)
if err != nil {
return nil, fmt.Errorf("get repository: %w", err)
}
mngr := &subscriptionManager{
componentCtx: s.componentCtx,
spaceId: spaceId,
chatId: chatObjectId,
myIdentity: currentIdentity,
myParticipantId: currentParticipantId,
identityCache: s.identityCache,
subscriptions: make(map[string]*subscription),
spaceIndex: s.objectStore.SpaceIndex(spaceId),
eventSender: s.eventSender,
repository: repository,
}
err = mngr.loadChatState(s.componentCtx)
if err != nil {
err = fmt.Errorf("init chat state: %w", err)
return nil, err
}
return mngr, nil
}
type SubscribeLastMessagesRequest struct {
ChatObjectId string
SubId string
Limit int
// If AsyncInit is true, initial messages will be broadcast via events
AsyncInit bool
WithDependencies bool
OnlyLastMessage bool
}
type SubscribeLastMessagesResponse struct {
Messages []*chatmodel.Message
ChatState *model.ChatState
// Dependencies per message id
Dependencies map[string][]*domain.Details
}
func (s *service) SubscribeLastMessages(ctx context.Context, req SubscribeLastMessagesRequest) (*SubscribeLastMessagesResponse, error) {
if req.ChatObjectId == "" {
return nil, fmt.Errorf("empty chat object id")
}
mngr, err := s.getManager(req.ChatObjectId)
if err != nil {
return nil, fmt.Errorf("get manager: %w", err)
}
mngr.Lock()
defer mngr.Unlock()
txn, err := mngr.repository.ReadTx(ctx)
if err != nil {
return nil, fmt.Errorf("init read transaction: %w", err)
}
defer txn.Commit()
messages, err := mngr.repository.GetLastMessages(txn.Context(), uint(req.Limit))
if err != nil {
return nil, fmt.Errorf("query messages: %w", err)
}
mngr.subscribe(req.SubId, req.WithDependencies, req.OnlyLastMessage)
if req.AsyncInit {
var previousOrderId string
if len(messages) > 0 {
previousOrderId, err = mngr.repository.GetPrevOrderId(txn.Context(), messages[0].OrderId)
if err != nil {
return nil, fmt.Errorf("get previous order id: %w", err)
}
}
for _, message := range messages {
mngr.Add(previousOrderId, message)
previousOrderId = message.OrderId
}
// Force chatState to be sent
mngr.chatStateUpdated = true
mngr.Flush()
return nil, nil
} else {
depsPerMessage := map[string][]*domain.Details{}
if req.WithDependencies {
for _, message := range messages {
deps := mngr.collectMessageDependencies(message.ChatMessage)
depsPerMessage[message.Id] = deps
}
}
return &SubscribeLastMessagesResponse{
Messages: messages,
ChatState: mngr.GetChatState(),
Dependencies: depsPerMessage,
}, nil
}
}
func (s *service) Unsubscribe(chatObjectId string, subId string) error {
mngr, err := s.getManager(chatObjectId)
if err != nil {
return fmt.Errorf("get manager: %w", err)
}
mngr.lock.Lock()
defer mngr.lock.Unlock()
mngr.unsubscribe(subId)
return nil
}

View file

@ -0,0 +1,89 @@
package chatsubscription
import (
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
type messagesState struct {
messages map[string]*model.ChatMessage
addEvents map[string]*pb.EventChatAdd
}
func newMessagesState() *messagesState {
return &messagesState{
messages: map[string]*model.ChatMessage{},
addEvents: map[string]*pb.EventChatAdd{},
}
}
func (s *messagesState) getLastAddedMessage() (*model.ChatMessage, bool) {
var lastMessage *model.ChatMessage
for _, m := range s.messages {
if lastMessage == nil || lastMessage.OrderId < m.OrderId {
lastMessage = m
}
}
if lastMessage == nil {
return nil, false
}
_, ok := s.addEvents[lastMessage.Id]
return lastMessage, ok
}
func (s *messagesState) applyEvent(ev *pb.EventMessage) {
if v := ev.GetChatAdd(); v != nil {
s.applyAdd(v)
} else if v := ev.GetChatDelete(); v != nil {
s.applyDelete(v)
} else if v := ev.GetChatUpdate(); v != nil {
s.applyUpdate(v)
} else if v := ev.GetChatUpdateMentionReadStatus(); v != nil {
s.applyUpdateMentionReadStatus(v)
} else if v := ev.GetChatUpdateMessageReadStatus(); v != nil {
s.applyUpdateMessageReadStatus(v)
} else if v := ev.GetChatUpdateReactions(); v != nil {
s.applyUpdateReactions(v)
}
}
func (s *messagesState) applyAdd(ev *pb.EventChatAdd) {
s.messages[ev.Id] = ev.Message
s.addEvents[ev.Id] = ev
}
func (s *messagesState) applyDelete(ev *pb.EventChatDelete) {
delete(s.messages, ev.Id)
}
func (s *messagesState) applyUpdate(ev *pb.EventChatUpdate) {
s.messages[ev.Id] = ev.Message
}
func (s *messagesState) applyUpdateMentionReadStatus(ev *pb.EventChatUpdateMentionReadStatus) {
for _, id := range ev.Ids {
msg, ok := s.messages[id]
if ok {
msg.MentionRead = ev.IsRead
}
}
}
func (s *messagesState) applyUpdateMessageReadStatus(ev *pb.EventChatUpdateMessageReadStatus) {
for _, id := range ev.Ids {
msg, ok := s.messages[id]
if ok {
msg.Read = ev.IsRead
}
}
}
func (s *messagesState) applyUpdateReactions(ev *pb.EventChatUpdateReactions) {
msg, ok := s.messages[ev.Id]
if ok {
msg.Reactions = ev.Reactions
}
}

View file

@ -14,7 +14,10 @@ import (
"go.uber.org/zap"
"github.com/anyproto/anytype-heart/core/block/cache"
"github.com/anyproto/anytype-heart/core/block/chats/chatmodel"
"github.com/anyproto/anytype-heart/core/block/chats/chatpush"
"github.com/anyproto/anytype-heart/core/block/chats/chatrepository"
"github.com/anyproto/anytype-heart/core/block/chats/chatsubscription"
"github.com/anyproto/anytype-heart/core/block/editor/chatobject"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/core/session"
@ -33,20 +36,22 @@ const CName = "core.block.chats"
var log = logging.Logger(CName).Desugar()
type Service interface {
AddMessage(ctx context.Context, sessionCtx session.Context, chatObjectId string, message *chatobject.Message) (string, error)
EditMessage(ctx context.Context, chatObjectId string, messageId string, newMessage *chatobject.Message) error
AddMessage(ctx context.Context, sessionCtx session.Context, chatObjectId string, message *chatmodel.Message) (string, error)
EditMessage(ctx context.Context, chatObjectId string, messageId string, newMessage *chatmodel.Message) error
ToggleMessageReaction(ctx context.Context, chatObjectId string, messageId string, emoji string) error
DeleteMessage(ctx context.Context, chatObjectId string, messageId string) error
GetMessages(ctx context.Context, chatObjectId string, req chatobject.GetMessagesRequest) (*chatobject.GetMessagesResponse, error)
GetMessagesByIds(ctx context.Context, chatObjectId string, messageIds []string) ([]*chatobject.Message, error)
SubscribeLastMessages(ctx context.Context, chatObjectId string, limit int, subId string) (*chatobject.SubscribeLastMessagesResponse, error)
GetMessages(ctx context.Context, chatObjectId string, req chatrepository.GetMessagesRequest) (*chatobject.GetMessagesResponse, error)
GetMessagesByIds(ctx context.Context, chatObjectId string, messageIds []string) ([]*chatmodel.Message, error)
SubscribeLastMessages(ctx context.Context, chatObjectId string, limit int, subId string) (*chatsubscription.SubscribeLastMessagesResponse, error)
ReadMessages(ctx context.Context, req ReadMessagesRequest) error
UnreadMessages(ctx context.Context, chatObjectId string, afterOrderId string, counterType chatobject.CounterType) error
UnreadMessages(ctx context.Context, chatObjectId string, afterOrderId string, counterType chatmodel.CounterType) error
Unsubscribe(chatObjectId string, subId string) error
SubscribeToMessagePreviews(ctx context.Context, subId string) (*SubscribeToMessagePreviewsResponse, error)
UnsubscribeFromMessagePreviews(subId string) error
ReadAll(ctx context.Context) error
app.ComponentRunnable
}
@ -61,11 +66,12 @@ type accountService interface {
}
type service struct {
objectGetter cache.ObjectWaitGetter
crossSpaceSubService crossspacesub.Service
pushService pushService
accountService accountService
objectStore objectstore.ObjectStore
objectGetter cache.ObjectWaitGetter
crossSpaceSubService crossspacesub.Service
pushService pushService
accountService accountService
objectStore objectstore.ObjectStore
chatSubscriptionService chatsubscription.Service
componentCtx context.Context
componentCtxCancel context.CancelFunc
@ -99,6 +105,7 @@ func (s *service) Init(a *app.App) error {
s.accountService = app.MustComponent[accountService](a)
s.objectStore = app.MustComponent[objectstore.ObjectStore](a)
s.objectGetter = app.MustComponent[cache.ObjectWaitGetter](a)
s.chatSubscriptionService = app.MustComponent[chatsubscription.Service](a)
return nil
}
@ -111,7 +118,7 @@ type ChatPreview struct {
SpaceId string
ChatObjectId string
State *model.ChatState
Message *chatobject.Message
Message *chatmodel.Message
Dependencies []*domain.Details
}
@ -132,32 +139,43 @@ func (s *service) SubscribeToMessagePreviews(ctx context.Context, subId string)
s.subscriptionIds[subId] = struct{}{}
lock := &sync.Mutex{}
result := &SubscribeToMessagePreviewsResponse{
Previews: make([]*ChatPreview, 0, len(s.allChatObjectIds)),
}
var wg sync.WaitGroup
for chatObjectId, spaceId := range s.allChatObjectIds {
chatAddResp, err := s.onChatAdded(chatObjectId, subId, false)
if err != nil {
log.Error("init lastMessage subscription", zap.Error(err))
continue
}
var (
message *chatobject.Message
dependencies []*domain.Details
)
if len(chatAddResp.Messages) > 0 {
message = chatAddResp.Messages[0]
dependencies = chatAddResp.Dependencies[message.Id]
}
result.Previews = append(result.Previews, &ChatPreview{
SpaceId: spaceId,
ChatObjectId: chatObjectId,
State: chatAddResp.ChatState,
Message: message,
Dependencies: dependencies,
})
wg.Add(1)
go func() {
defer wg.Done()
chatAddResp, err := s.onChatAdded(chatObjectId, subId, false)
if err != nil {
log.Error("init lastMessage subscription", zap.Error(err))
return
}
var (
message *chatmodel.Message
dependencies []*domain.Details
)
if len(chatAddResp.Messages) > 0 {
message = chatAddResp.Messages[0]
dependencies = chatAddResp.Dependencies[message.Id]
}
lock.Lock()
defer lock.Unlock()
result.Previews = append(result.Previews, &ChatPreview{
SpaceId: spaceId,
ChatObjectId: chatObjectId,
State: chatAddResp.ChatState,
Message: message,
Dependencies: dependencies,
})
}()
}
wg.Wait()
return result, nil
}
@ -260,22 +278,15 @@ func (s *service) monitorMessagePreviews() {
}
}
func (s *service) onChatAdded(chatObjectId string, subId string, asyncInit bool) (*chatobject.SubscribeLastMessagesResponse, error) {
var resp *chatobject.SubscribeLastMessagesResponse
err := s.chatObjectDo(s.componentCtx, chatObjectId, func(sb chatobject.StoreObject) error {
var err error
resp, err = sb.SubscribeLastMessages(s.componentCtx, chatobject.SubscribeLastMessagesRequest{
SubId: subId,
Limit: 1,
AsyncInit: asyncInit,
WithDependencies: true,
})
if err != nil {
return err
}
return nil
func (s *service) onChatAdded(chatObjectId string, subId string, asyncInit bool) (*chatsubscription.SubscribeLastMessagesResponse, error) {
return s.chatSubscriptionService.SubscribeLastMessages(s.componentCtx, chatsubscription.SubscribeLastMessagesRequest{
ChatObjectId: chatObjectId,
SubId: subId,
Limit: 1,
AsyncInit: asyncInit,
WithDependencies: true,
OnlyLastMessage: true,
})
return resp, err
}
func (s *service) onChatRemoved(chatObjectId string, subId string) error {
@ -302,7 +313,7 @@ func (s *service) Close(ctx context.Context) error {
return err
}
func (s *service) AddMessage(ctx context.Context, sessionCtx session.Context, chatObjectId string, message *chatobject.Message) (string, error) {
func (s *service) AddMessage(ctx context.Context, sessionCtx session.Context, chatObjectId string, message *chatmodel.Message) (string, error) {
var messageId, spaceId string
err := s.chatObjectDo(ctx, chatObjectId, func(sb chatobject.StoreObject) error {
var err error
@ -311,18 +322,16 @@ func (s *service) AddMessage(ctx context.Context, sessionCtx session.Context, ch
return err
})
if err == nil {
go func() {
err := s.sendPushNotification(spaceId, chatObjectId, messageId, message.Message.Text)
if err != nil {
log.Error("sendPushNotification: ", zap.Error(err))
}
}()
pushErr := s.sendPushNotification(spaceId, chatObjectId, messageId, message.Message.Text, len(message.Attachments) != 0)
if pushErr != nil {
log.Error("sendPushNotification: ", zap.Error(pushErr))
}
}
return messageId, err
}
func (s *service) sendPushNotification(spaceId, chatObjectId string, messageId string, messageText string) (err error) {
func (s *service) sendPushNotification(spaceId, chatObjectId, messageId, messageText string, hasAttachments bool) (err error) {
accountId := s.accountService.AccountID()
spaceName := s.objectStore.GetSpaceName(spaceId)
details, err := s.objectStore.SpaceIndex(spaceId).GetDetails(domain.NewParticipantId(spaceId, accountId))
@ -338,11 +347,12 @@ func (s *service) sendPushNotification(spaceId, chatObjectId string, messageId s
SenderId: accountId,
Type: chatpush.ChatMessage,
NewMessagePayload: &chatpush.NewMessagePayload{
ChatId: chatObjectId,
MsgId: messageId,
SpaceName: spaceName,
SenderName: senderName,
Text: messageText,
ChatId: chatObjectId,
MsgId: messageId,
SpaceName: spaceName,
SenderName: senderName,
Text: messageText,
HasAttachments: hasAttachments,
},
}
@ -362,7 +372,7 @@ func (s *service) sendPushNotification(spaceId, chatObjectId string, messageId s
return
}
func (s *service) EditMessage(ctx context.Context, chatObjectId string, messageId string, newMessage *chatobject.Message) error {
func (s *service) EditMessage(ctx context.Context, chatObjectId string, messageId string, newMessage *chatmodel.Message) error {
return s.chatObjectDo(ctx, chatObjectId, func(sb chatobject.StoreObject) error {
return sb.EditMessage(ctx, messageId, newMessage)
})
@ -380,7 +390,7 @@ func (s *service) DeleteMessage(ctx context.Context, chatObjectId string, messag
})
}
func (s *service) GetMessages(ctx context.Context, chatObjectId string, req chatobject.GetMessagesRequest) (*chatobject.GetMessagesResponse, error) {
func (s *service) GetMessages(ctx context.Context, chatObjectId string, req chatrepository.GetMessagesRequest) (*chatobject.GetMessagesResponse, error) {
var resp *chatobject.GetMessagesResponse
err := s.chatObjectDo(ctx, chatObjectId, func(sb chatobject.StoreObject) error {
var err error
@ -393,8 +403,8 @@ func (s *service) GetMessages(ctx context.Context, chatObjectId string, req chat
return resp, err
}
func (s *service) GetMessagesByIds(ctx context.Context, chatObjectId string, messageIds []string) ([]*chatobject.Message, error) {
var res []*chatobject.Message
func (s *service) GetMessagesByIds(ctx context.Context, chatObjectId string, messageIds []string) ([]*chatmodel.Message, error) {
var res []*chatmodel.Message
err := s.chatObjectDo(ctx, chatObjectId, func(sb chatobject.StoreObject) error {
msg, err := sb.GetMessagesByIds(ctx, messageIds)
if err != nil {
@ -406,28 +416,18 @@ func (s *service) GetMessagesByIds(ctx context.Context, chatObjectId string, mes
return res, err
}
func (s *service) SubscribeLastMessages(ctx context.Context, chatObjectId string, limit int, subId string) (*chatobject.SubscribeLastMessagesResponse, error) {
var resp *chatobject.SubscribeLastMessagesResponse
err := s.chatObjectDo(ctx, chatObjectId, func(sb chatobject.StoreObject) error {
var err error
resp, err = sb.SubscribeLastMessages(ctx, chatobject.SubscribeLastMessagesRequest{
SubId: subId,
Limit: limit,
AsyncInit: false,
WithDependencies: false,
})
if err != nil {
return err
}
return nil
func (s *service) SubscribeLastMessages(ctx context.Context, chatObjectId string, limit int, subId string) (*chatsubscription.SubscribeLastMessagesResponse, error) {
return s.chatSubscriptionService.SubscribeLastMessages(s.componentCtx, chatsubscription.SubscribeLastMessagesRequest{
ChatObjectId: chatObjectId,
SubId: subId,
Limit: limit,
AsyncInit: false,
WithDependencies: false,
})
return resp, err
}
func (s *service) Unsubscribe(chatObjectId string, subId string) error {
return s.chatObjectDo(s.componentCtx, chatObjectId, func(sb chatobject.StoreObject) error {
return sb.Unsubscribe(subId)
})
return s.chatSubscriptionService.Unsubscribe(chatObjectId, subId)
}
type ReadMessagesRequest struct {
@ -435,16 +435,21 @@ type ReadMessagesRequest struct {
AfterOrderId string
BeforeOrderId string
LastStateId string
CounterType chatobject.CounterType
CounterType chatmodel.CounterType
}
func (s *service) ReadMessages(ctx context.Context, req ReadMessagesRequest) error {
return s.chatObjectDo(ctx, req.ChatObjectId, func(sb chatobject.StoreObject) error {
return sb.MarkReadMessages(ctx, req.AfterOrderId, req.BeforeOrderId, req.LastStateId, req.CounterType)
return sb.MarkReadMessages(ctx, chatobject.ReadMessagesRequest{
AfterOrderId: req.AfterOrderId,
BeforeOrderId: req.BeforeOrderId,
LastStateId: req.LastStateId,
CounterType: req.CounterType,
})
})
}
func (s *service) UnreadMessages(ctx context.Context, chatObjectId string, afterOrderId string, counterType chatobject.CounterType) error {
func (s *service) UnreadMessages(ctx context.Context, chatObjectId string, afterOrderId string, counterType chatmodel.CounterType) error {
return s.chatObjectDo(ctx, chatObjectId, func(sb chatobject.StoreObject) error {
return sb.MarkMessagesAsUnread(ctx, afterOrderId, counterType)
})
@ -455,3 +460,37 @@ func (s *service) chatObjectDo(ctx context.Context, chatObjectId string, proc fu
defer cancel()
return cache.DoWait(s.objectGetter, waitCtx, chatObjectId, proc)
}
func (s *service) ReadAll(ctx context.Context) error {
s.lock.Lock()
chatIds := make([]string, 0, len(s.allChatObjectIds))
for id := range s.allChatObjectIds {
chatIds = append(chatIds, id)
}
s.lock.Unlock()
for _, chatId := range chatIds {
err := s.chatObjectDo(ctx, chatId, func(sb chatobject.StoreObject) error {
err := sb.MarkReadMessages(ctx, chatobject.ReadMessagesRequest{
All: true,
CounterType: chatmodel.CounterTypeMessage,
})
if err != nil {
return fmt.Errorf("messages: %w", err)
}
err = sb.MarkReadMessages(ctx, chatobject.ReadMessagesRequest{
All: true,
CounterType: chatmodel.CounterTypeMention,
})
if err != nil {
return fmt.Errorf("mentions: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("read: %w", err)
}
}
return nil
}

View file

@ -13,9 +13,9 @@ import (
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/core/block/cache/mock_cache"
"github.com/anyproto/anytype-heart/core/block/editor/chatobject"
"github.com/anyproto/anytype-heart/core/block/editor/chatobject/mock_chatobject"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/block/chats/chatmodel"
"github.com/anyproto/anytype-heart/core/block/chats/chatsubscription"
"github.com/anyproto/anytype-heart/core/block/chats/chatsubscription/mock_chatsubscription"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/core/subscription"
"github.com/anyproto/anytype-heart/core/subscription/crossspacesub/mock_crossspacesub"
@ -70,6 +70,7 @@ type fixture struct {
*service
objectGetter *mock_cache.MockObjectWaitGetterComponent
subscriptionService *mock_chatsubscription.MockService
app *app.App
crossSpaceSubService *mock_crossspacesub.MockService
@ -109,10 +110,12 @@ func newFixture(t *testing.T) *fixture {
objectStore := objectstore.NewStoreFixture(t)
objectGetter := mock_cache.NewMockObjectWaitGetterComponent(t)
crossSpaceSubService := mock_crossspacesub.NewMockService(t)
subscriptionService := mock_chatsubscription.NewMockService(t)
fx := &fixture{
service: New().(*service),
crossSpaceSubService: crossSpaceSubService,
subscriptionService: subscriptionService,
objectGetter: objectGetter,
actions: map[string][]recordedAction{},
}
@ -122,12 +125,14 @@ func newFixture(t *testing.T) *fixture {
a.Register(objectStore)
a.Register(testutil.PrepareMock(ctx, a, objectGetter))
a.Register(testutil.PrepareMock(ctx, a, crossSpaceSubService))
a.Register(testutil.PrepareMock(ctx, a, subscriptionService))
a.Register(&pushServiceDummy{})
a.Register(&accountServiceDummy{})
a.Register(fx)
fx.app = a
fx.expectSubscribe(t)
return fx
}
@ -136,13 +141,8 @@ func (fx *fixture) start(t *testing.T) {
require.NoError(t, err)
}
type chatObjectWrapper struct {
smartblock.SmartBlock
chatobject.StoreObject
}
func givenLastMessages() []*chatobject.Message {
return []*chatobject.Message{
func givenLastMessages() []*chatmodel.Message {
return []*chatmodel.Message{
{
ChatMessage: &model.ChatMessage{
Id: "messageId1",
@ -172,34 +172,26 @@ func givenDependencies() map[string][]*domain.Details {
}
}
func (fx *fixture) expectChatObject(t *testing.T, chatObjectId string) {
fx.objectGetter.EXPECT().WaitAndGetObject(mock.Anything, chatObjectId).RunAndReturn(func(ctx context.Context, id string) (smartblock.SmartBlock, error) {
sb := mock_chatobject.NewMockStoreObject(t)
func (fx *fixture) expectSubscribe(t *testing.T) {
fx.subscriptionService.EXPECT().SubscribeLastMessages(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, req chatsubscription.SubscribeLastMessagesRequest) (*chatsubscription.SubscribeLastMessagesResponse, error) {
fx.recordAction(req.ChatObjectId, recordedAction{
actionType: actionTypeSubscribe,
subId: req.SubId,
})
return &chatsubscription.SubscribeLastMessagesResponse{
Messages: givenLastMessages(),
ChatState: givenLastState(),
Dependencies: givenDependencies(),
}, nil
}).Maybe()
sb.EXPECT().Lock().Return().Maybe()
sb.EXPECT().Unlock().Return().Maybe()
sb.EXPECT().SubscribeLastMessages(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, req chatobject.SubscribeLastMessagesRequest) (*chatobject.SubscribeLastMessagesResponse, error) {
fx.recordAction(chatObjectId, recordedAction{
actionType: actionTypeSubscribe,
subId: req.SubId,
})
return &chatobject.SubscribeLastMessagesResponse{
Messages: givenLastMessages(),
ChatState: givenLastState(),
Dependencies: givenDependencies(),
}, nil
}).Maybe()
sb.EXPECT().Unsubscribe(mock.Anything).RunAndReturn(func(subId string) error {
fx.recordAction(chatObjectId, recordedAction{
actionType: actionTypeUnsubscribe,
subId: subId,
})
return nil
}).Maybe()
return sb, nil
})
fx.subscriptionService.EXPECT().Unsubscribe(mock.Anything, mock.Anything).RunAndReturn(func(chatObjectId string, subId string) error {
fx.recordAction(chatObjectId, recordedAction{
actionType: actionTypeUnsubscribe,
subId: subId,
})
return nil
}).Maybe()
}
func TestSubscribeToMessagePreviews(t *testing.T) {
@ -220,9 +212,6 @@ func TestSubscribeToMessagePreviews(t *testing.T) {
},
}, nil).Maybe()
fx.expectChatObject(t, "chat1")
fx.expectChatObject(t, "chat2")
fx.start(t)
resp, err := fx.SubscribeToMessagePreviews(ctx, "previewSub1")
@ -270,9 +259,6 @@ func TestSubscribeToMessagePreviews(t *testing.T) {
Records: []*domain.Details{},
}, nil).Maybe()
fx.expectChatObject(t, "chat1")
fx.expectChatObject(t, "chat2")
fx.start(t)
fx.chatObjectsSubQueue.Add(ctx, &pb.EventMessage{
@ -329,9 +315,6 @@ func TestSubscribeToMessagePreviews(t *testing.T) {
},
}, nil).Maybe()
fx.expectChatObject(t, "chat1")
fx.expectChatObject(t, "chat2")
fx.start(t)
fx.chatObjectsSubQueue.Add(ctx, &pb.EventMessage{
@ -384,9 +367,6 @@ func TestSubscribeToMessagePreviews(t *testing.T) {
},
}, nil).Maybe()
fx.expectChatObject(t, "chat1")
fx.expectChatObject(t, "chat2")
fx.start(t)
resp, err := fx.SubscribeToMessagePreviews(ctx, "previewSub1")

View file

@ -136,7 +136,9 @@ func (a *accountObject) Init(ctx *smartblock.InitContext) error {
}
storeSource.SetPushChangeHook(a.OnPushChange)
a.storeSource = storeSource
err = storeSource.ReadStoreDoc(ctx.Ctx, stateStore, a.onUpdate)
err = storeSource.ReadStoreDoc(ctx.Ctx, stateStore, source.ReadStoreDocParams{
OnUpdateHook: a.onUpdate,
})
if err != nil {
return fmt.Errorf("read store doc: %w", err)
}

View file

@ -350,6 +350,7 @@ func (bs *basic) SetObjectTypesInState(s *state.State, objectTypeKeys []domain.T
s.SetObjectTypeKeys(objectTypeKeys)
removeInternalFlags(s)
s.Details().Delete(bundle.RelationKeyLayout)
toLayout, err := bs.getLayoutForType(objectTypeKeys[0])
if err != nil {

View file

@ -11,14 +11,17 @@ import (
"github.com/anyproto/any-store/query"
"github.com/globalsign/mgo/bson"
"github.com/anyproto/anytype-heart/core/block/chats/chatmodel"
"github.com/anyproto/anytype-heart/core/block/chats/chatrepository"
"github.com/anyproto/anytype-heart/core/block/chats/chatsubscription"
"github.com/anyproto/anytype-heart/core/block/editor/storestate"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
type ChatHandler struct {
repository *repository
subscription *subscriptionManager
repository chatrepository.Repository
subscription chatsubscription.Manager
currentIdentity string
myParticipantId string
// forceNotRead forces handler to mark all messages as not read. It's useful for unit testing
@ -44,7 +47,7 @@ func (d *ChatHandler) Init(ctx context.Context, s *storestate.StoreState) (err e
}
func (d *ChatHandler) BeforeCreate(ctx context.Context, ch storestate.ChangeOp) error {
msg, err := unmarshalMessage(ch.Value)
msg, err := chatmodel.UnmarshalMessage(ch.Value)
if err != nil {
return fmt.Errorf("unmarshal message: %w", err)
}
@ -72,12 +75,14 @@ func (d *ChatHandler) BeforeCreate(ctx context.Context, ch storestate.ChangeOp)
msg.CurrentUserMentioned = isMentioned
msg.OrderId = ch.Change.Order
prevOrderId, err := d.repository.getPrevOrderId(ctx, ch.Change.Order)
prevOrderId, err := d.repository.GetPrevOrderId(ctx, ch.Change.Order)
if err != nil {
return fmt.Errorf("get prev order id: %w", err)
}
d.subscription.updateChatState(func(state *model.ChatState) *model.ChatState {
d.subscription.Lock()
defer d.subscription.Unlock()
d.subscription.UpdateChatState(func(state *model.ChatState) *model.ChatState {
if !msg.Read {
if msg.OrderId < state.Messages.OldestOrderId || state.Messages.OldestOrderId == "" {
state.Messages.OldestOrderId = msg.OrderId
@ -98,7 +103,7 @@ func (d *ChatHandler) BeforeCreate(ctx context.Context, ch storestate.ChangeOp)
return state
})
d.subscription.add(prevOrderId, msg)
d.subscription.Add(prevOrderId, msg)
msg.MarshalAnyenc(ch.Value, ch.Arena)
@ -122,7 +127,7 @@ func (d *ChatHandler) BeforeDelete(ctx context.Context, ch storestate.ChangeOp)
return storestate.DeleteModeDelete, fmt.Errorf("get message: %w", err)
}
message, err := unmarshalMessage(doc.Value())
message, err := chatmodel.UnmarshalMessage(doc.Value())
if err != nil {
return storestate.DeleteModeDelete, fmt.Errorf("unmarshal message: %w", err)
}
@ -130,7 +135,9 @@ func (d *ChatHandler) BeforeDelete(ctx context.Context, ch storestate.ChangeOp)
return storestate.DeleteModeDelete, errors.New("can't delete not own message")
}
d.subscription.delete(messageId)
d.subscription.Lock()
defer d.subscription.Unlock()
d.subscription.Delete(messageId)
return storestate.DeleteModeDelete, nil
}
@ -149,13 +156,16 @@ func (d *ChatHandler) UpgradeKeyModifier(ch storestate.ChangeOp, key *pb.KeyModi
}
if modified {
msg, err := unmarshalMessage(result)
msg, err := chatmodel.UnmarshalMessage(result)
if err != nil {
return nil, false, fmt.Errorf("unmarshal message: %w", err)
}
d.subscription.Lock()
defer d.subscription.Unlock()
switch path {
case reactionsKey:
case chatmodel.ReactionsKey:
// Do not parse json, just trim "
identity := strings.Trim(key.ModifyValue, `"`)
if identity != ch.Change.Creator {
@ -163,15 +173,15 @@ func (d *ChatHandler) UpgradeKeyModifier(ch storestate.ChangeOp, key *pb.KeyModi
}
// TODO Count validation
d.subscription.updateReactions(msg)
case contentKey:
d.subscription.UpdateReactions(msg)
case chatmodel.ContentKey:
creator := msg.Creator
if creator != ch.Change.Creator {
return v, false, errors.Join(storestate.ErrValidation, fmt.Errorf("can't modify someone else's message"))
}
msg.ModifiedAt = ch.Change.Timestamp
msg.MarshalAnyenc(result, a)
d.subscription.updateFull(msg)
d.subscription.UpdateFull(msg)
default:
return nil, false, fmt.Errorf("invalid key path %s", key.KeyPath)
}

View file

@ -2,26 +2,27 @@ package chatobject
import (
"context"
"errors"
"fmt"
"time"
anystore "github.com/anyproto/any-store"
"github.com/anyproto/any-store/anyenc"
"github.com/anyproto/any-sync/commonspace/object/accountdata"
"github.com/anyproto/any-sync/commonspace/object/tree/objecttree"
"github.com/anyproto/any-sync/util/slice"
"go.uber.org/zap"
"golang.org/x/exp/slices"
"github.com/anyproto/anytype-heart/core/block/chats/chatmodel"
"github.com/anyproto/anytype-heart/core/block/chats/chatrepository"
"github.com/anyproto/anytype-heart/core/block/chats/chatsubscription"
"github.com/anyproto/anytype-heart/core/block/editor/anystoredebug"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/block/editor/storestate"
"github.com/anyproto/anytype-heart/core/block/source"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/core/event"
"github.com/anyproto/anytype-heart/core/session"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore/spaceindex"
"github.com/anyproto/anytype-heart/pkg/lib/logging"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
@ -41,27 +42,19 @@ type StoreObject interface {
smartblock.SmartBlock
anystoredebug.AnystoreDebug
AddMessage(ctx context.Context, sessionCtx session.Context, message *Message) (string, error)
GetMessages(ctx context.Context, req GetMessagesRequest) (*GetMessagesResponse, error)
GetMessagesByIds(ctx context.Context, messageIds []string) ([]*Message, error)
EditMessage(ctx context.Context, messageId string, newMessage *Message) error
AddMessage(ctx context.Context, sessionCtx session.Context, message *chatmodel.Message) (string, error)
GetMessages(ctx context.Context, req chatrepository.GetMessagesRequest) (*GetMessagesResponse, error)
GetMessagesByIds(ctx context.Context, messageIds []string) ([]*chatmodel.Message, error)
EditMessage(ctx context.Context, messageId string, newMessage *chatmodel.Message) error
ToggleMessageReaction(ctx context.Context, messageId string, emoji string) error
DeleteMessage(ctx context.Context, messageId string) error
SubscribeLastMessages(ctx context.Context, req SubscribeLastMessagesRequest) (*SubscribeLastMessagesResponse, error)
MarkReadMessages(ctx context.Context, afterOrderId string, beforeOrderId string, lastStateId string, counterType CounterType) error
MarkMessagesAsUnread(ctx context.Context, afterOrderId string, counterType CounterType) error
Unsubscribe(subId string) error
}
type GetMessagesRequest struct {
AfterOrderId string
BeforeOrderId string
Limit int
IncludeBoundary bool
MarkReadMessages(ctx context.Context, req ReadMessagesRequest) error
MarkMessagesAsUnread(ctx context.Context, afterOrderId string, counterType chatmodel.CounterType) error
}
type AccountService interface {
AccountID() string
Keys() *accountdata.AccountKeys
}
type seenHeadsCollector interface {
@ -73,34 +66,34 @@ type storeObject struct {
smartblock.SmartBlock
locker smartblock.Locker
seenHeadsCollector seenHeadsCollector
accountService AccountService
storeSource source.Store
store *storestate.StoreState
eventSender event.Sender
subscription *subscriptionManager
crdtDb anystore.DB
spaceIndex spaceindex.Store
chatHandler *ChatHandler
repository *repository
seenHeadsCollector seenHeadsCollector
accountService AccountService
storeSource source.Store
repositoryService chatrepository.Service
store *storestate.StoreState
chatSubscriptionService chatsubscription.Service
subscription chatsubscription.Manager
crdtDb anystore.DB
chatHandler *ChatHandler
repository chatrepository.Repository
arenaPool *anyenc.ArenaPool
componentCtx context.Context
componentCtxCancel context.CancelFunc
}
func New(sb smartblock.SmartBlock, accountService AccountService, eventSender event.Sender, crdtDb anystore.DB, spaceIndex spaceindex.Store) StoreObject {
func New(sb smartblock.SmartBlock, accountService AccountService, crdtDb anystore.DB, repositoryService chatrepository.Service, chatSubscriptionService chatsubscription.Service) StoreObject {
ctx, cancel := context.WithCancel(context.Background())
return &storeObject{
SmartBlock: sb,
locker: sb.(smartblock.Locker),
accountService: accountService,
arenaPool: &anyenc.ArenaPool{},
eventSender: eventSender,
crdtDb: crdtDb,
componentCtx: ctx,
componentCtxCancel: cancel,
spaceIndex: spaceIndex,
SmartBlock: sb,
locker: sb.(smartblock.Locker),
accountService: accountService,
arenaPool: &anyenc.ArenaPool{},
crdtDb: crdtDb,
repositoryService: repositoryService,
componentCtx: ctx,
componentCtxCancel: cancel,
chatSubscriptionService: chatSubscriptionService,
}
}
@ -110,42 +103,24 @@ func (s *storeObject) Init(ctx *smartblock.InitContext) error {
return fmt.Errorf("source is not a store")
}
collectionName := storeSource.Id() + CollectionName
collection, err := s.crdtDb.OpenCollection(ctx.Ctx, collectionName)
if errors.Is(err, anystore.ErrCollectionNotFound) {
collection, err = s.crdtDb.CreateCollection(ctx.Ctx, collectionName)
if err != nil {
return fmt.Errorf("create collection: %w", err)
}
}
var err error
s.repository, err = s.repositoryService.Repository(storeSource.Id())
if err != nil {
return fmt.Errorf("get collection: %w", err)
return fmt.Errorf("get repository: %w", err)
}
s.repository = &repository{
collection: collection,
arenaPool: s.arenaPool,
}
// Use Object and Space IDs from source, because object is not initialized yet
myParticipantId := domain.NewParticipantId(ctx.Source.SpaceID(), s.accountService.AccountID())
s.subscription = s.newSubscriptionManager(
domain.FullID{ObjectID: ctx.Source.Id(), SpaceID: ctx.Source.SpaceID()},
s.accountService.AccountID(),
myParticipantId,
)
messagesOpts := newReadHandler(CounterTypeMessage, s.subscription)
mentionsOpts := newReadHandler(CounterTypeMention, s.subscription)
// Diff managers should be added before SmartBlock.Init, because they have to be initialized in source.ReadStoreDoc
storeSource.RegisterDiffManager(diffManagerMessages, func(removed []string) {
markErr := s.markReadMessages(removed, messagesOpts)
markErr := s.markReadMessages(removed, chatmodel.CounterTypeMessage)
if markErr != nil {
log.Error("mark read messages", zap.Error(markErr))
}
})
storeSource.RegisterDiffManager(diffManagerMentions, func(removed []string) {
markErr := s.markReadMessages(removed, mentionsOpts)
markErr := s.markReadMessages(removed, chatmodel.CounterTypeMention)
if markErr != nil {
log.Error("mark read mentions", zap.Error(markErr))
}
@ -157,6 +132,11 @@ func (s *storeObject) Init(ctx *smartblock.InitContext) error {
}
s.storeSource = storeSource
s.subscription, err = s.chatSubscriptionService.GetManager(storeSource.Id())
if err != nil {
return fmt.Errorf("get subscription manager: %w", err)
}
s.chatHandler = &ChatHandler{
repository: s.repository,
subscription: s.subscription,
@ -170,12 +150,13 @@ func (s *storeObject) Init(ctx *smartblock.InitContext) error {
}
s.store = stateStore
err = s.subscription.loadChatState(s.componentCtx)
if err != nil {
return fmt.Errorf("init chat state: %w", err)
}
err = storeSource.ReadStoreDoc(ctx.Ctx, stateStore, s.onUpdate)
err = storeSource.ReadStoreDoc(ctx.Ctx, stateStore, source.ReadStoreDocParams{
OnUpdateHook: s.onUpdate,
ReadStoreTreeHook: &readStoreTreeHook{
currentIdentity: s.accountService.Keys().SignKey.GetPublic(),
source: s.storeSource,
},
})
if err != nil {
return fmt.Errorf("read store doc: %w", err)
}
@ -188,10 +169,12 @@ func (s *storeObject) Init(ctx *smartblock.InitContext) error {
}
func (s *storeObject) onUpdate() {
s.subscription.flush()
s.subscription.Lock()
defer s.subscription.Unlock()
s.subscription.Flush()
}
func (s *storeObject) GetMessageById(ctx context.Context, id string) (*Message, error) {
func (s *storeObject) GetMessageById(ctx context.Context, id string) (*chatmodel.Message, error) {
messages, err := s.GetMessagesByIds(ctx, []string{id})
if err != nil {
return nil, err
@ -202,27 +185,27 @@ func (s *storeObject) GetMessageById(ctx context.Context, id string) (*Message,
return messages[0], nil
}
func (s *storeObject) GetMessagesByIds(ctx context.Context, messageIds []string) ([]*Message, error) {
return s.repository.getMessagesByIds(ctx, messageIds)
func (s *storeObject) GetMessagesByIds(ctx context.Context, messageIds []string) ([]*chatmodel.Message, error) {
return s.repository.GetMessagesByIds(ctx, messageIds)
}
type GetMessagesResponse struct {
Messages []*Message
Messages []*chatmodel.Message
ChatState *model.ChatState
}
func (s *storeObject) GetMessages(ctx context.Context, req GetMessagesRequest) (*GetMessagesResponse, error) {
msgs, err := s.repository.getMessages(ctx, req)
func (s *storeObject) GetMessages(ctx context.Context, req chatrepository.GetMessagesRequest) (*GetMessagesResponse, error) {
msgs, err := s.repository.GetMessages(ctx, req)
if err != nil {
return nil, err
}
return &GetMessagesResponse{
Messages: msgs,
ChatState: s.subscription.getChatState(),
ChatState: s.subscription.GetChatState(),
}, nil
}
func (s *storeObject) AddMessage(ctx context.Context, sessionCtx session.Context, message *Message) (string, error) {
func (s *storeObject) AddMessage(ctx context.Context, sessionCtx session.Context, message *chatmodel.Message) (string, error) {
arena := s.arenaPool.Get()
defer func() {
arena.Reset()
@ -242,7 +225,14 @@ func (s *storeObject) AddMessage(ctx context.Context, sessionCtx session.Context
return "", fmt.Errorf("create chat: %w", err)
}
s.subscription.setSessionContext(sessionCtx)
s.subscription.Lock()
s.subscription.SetSessionContext(sessionCtx)
s.subscription.Unlock()
defer func() {
s.subscription.Lock()
s.subscription.SetSessionContext(nil)
s.subscription.Unlock()
}()
messageId, err := s.storeSource.PushStoreChange(ctx, source.PushStoreChangeParams{
Changes: builder.ChangeSet,
State: s.store,
@ -253,10 +243,8 @@ func (s *storeObject) AddMessage(ctx context.Context, sessionCtx session.Context
}
if !s.chatHandler.forceNotRead {
for _, counterType := range []CounterType{CounterTypeMessage, CounterTypeMention} {
handler := newReadHandler(counterType, s.subscription)
err = s.storeSource.MarkSeenHeads(ctx, handler.getDiffManagerName(), []string{messageId})
for _, counterType := range []chatmodel.CounterType{chatmodel.CounterTypeMessage, chatmodel.CounterTypeMention} {
err = s.storeSource.MarkSeenHeads(ctx, counterType.DiffManagerName(), []string{messageId})
if err != nil {
return "", fmt.Errorf("mark read: %w", err)
}
@ -280,7 +268,7 @@ func (s *storeObject) DeleteMessage(ctx context.Context, messageId string) error
return nil
}
func (s *storeObject) EditMessage(ctx context.Context, messageId string, newMessage *Message) error {
func (s *storeObject) EditMessage(ctx context.Context, messageId string, newMessage *chatmodel.Message) error {
arena := s.arenaPool.Get()
defer func() {
arena.Reset()
@ -291,7 +279,7 @@ func (s *storeObject) EditMessage(ctx context.Context, messageId string, newMess
newMessage.MarshalAnyenc(obj, arena)
builder := storestate.Builder{}
err := builder.Modify(CollectionName, messageId, []string{contentKey}, pb.ModifyOp_Set, obj.Get(contentKey))
err := builder.Modify(CollectionName, messageId, []string{chatmodel.ContentKey}, pb.ModifyOp_Set, obj.Get(chatmodel.ContentKey))
if err != nil {
return fmt.Errorf("modify content: %w", err)
}
@ -313,7 +301,7 @@ func (s *storeObject) ToggleMessageReaction(ctx context.Context, messageId strin
s.arenaPool.Put(arena)
}()
hasReaction, err := s.repository.hasMyReaction(ctx, s.accountService.AccountID(), messageId, emoji)
hasReaction, err := s.repository.HasMyReaction(ctx, s.accountService.AccountID(), messageId, emoji)
if err != nil {
return fmt.Errorf("check reaction: %w", err)
}
@ -321,12 +309,12 @@ func (s *storeObject) ToggleMessageReaction(ctx context.Context, messageId strin
builder := storestate.Builder{}
if hasReaction {
err = builder.Modify(CollectionName, messageId, []string{reactionsKey, emoji}, pb.ModifyOp_Pull, arena.NewString(s.accountService.AccountID()))
err = builder.Modify(CollectionName, messageId, []string{chatmodel.ReactionsKey, emoji}, pb.ModifyOp_Pull, arena.NewString(s.accountService.AccountID()))
if err != nil {
return fmt.Errorf("modify content: %w", err)
}
} else {
err = builder.Modify(CollectionName, messageId, []string{reactionsKey, emoji}, pb.ModifyOp_AddToSet, arena.NewString(s.accountService.AccountID()))
err = builder.Modify(CollectionName, messageId, []string{chatmodel.ReactionsKey, emoji}, pb.ModifyOp_AddToSet, arena.NewString(s.accountService.AccountID()))
if err != nil {
return fmt.Errorf("modify content: %w", err)
}
@ -343,78 +331,13 @@ func (s *storeObject) ToggleMessageReaction(ctx context.Context, messageId strin
return nil
}
type SubscribeLastMessagesRequest struct {
SubId string
Limit int
// If AsyncInit is true, initial messages will be broadcast via events
AsyncInit bool
WithDependencies bool
}
type SubscribeLastMessagesResponse struct {
Messages []*Message
ChatState *model.ChatState
// Dependencies per message id
Dependencies map[string][]*domain.Details
}
func (s *storeObject) SubscribeLastMessages(ctx context.Context, req SubscribeLastMessagesRequest) (*SubscribeLastMessagesResponse, error) {
txn, err := s.repository.readTx(ctx)
if err != nil {
return nil, fmt.Errorf("init read transaction: %w", err)
}
defer txn.Commit()
messages, err := s.repository.getLastMessages(txn.Context(), uint(req.Limit))
if err != nil {
return nil, fmt.Errorf("query messages: %w", err)
}
s.subscription.subscribe(req.SubId, req.WithDependencies)
if req.AsyncInit {
var previousOrderId string
if len(messages) > 0 {
previousOrderId, err = s.repository.getPrevOrderId(txn.Context(), messages[0].OrderId)
if err != nil {
return nil, fmt.Errorf("get previous order id: %w", err)
}
}
for _, message := range messages {
s.subscription.add(previousOrderId, message)
previousOrderId = message.OrderId
}
// Force chatState to be sent
s.subscription.chatStateUpdated = true
s.subscription.flush()
return nil, nil
} else {
depsPerMessage := map[string][]*domain.Details{}
if req.WithDependencies {
for _, message := range messages {
deps := s.subscription.collectMessageDependencies(message)
depsPerMessage[message.Id] = deps
}
}
return &SubscribeLastMessagesResponse{
Messages: messages,
ChatState: s.subscription.getChatState(),
Dependencies: depsPerMessage,
}, nil
}
}
func (s *storeObject) Unsubscribe(subId string) error {
s.subscription.unsubscribe(subId)
return nil
}
func (s *storeObject) TryClose(objectTTL time.Duration) (res bool, err error) {
if !s.locker.TryLock() {
return false, nil
}
isActive := s.subscription.isActive()
s.subscription.Lock()
defer s.subscription.Unlock()
isActive := s.subscription.IsActive()
s.Unlock()
if isActive {

View file

@ -4,26 +4,33 @@ import (
"context"
"errors"
"fmt"
"path/filepath"
"testing"
anystore "github.com/anyproto/any-store"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/any-sync/commonspace/object/accountdata"
"github.com/anyproto/any-sync/util/crypto"
"github.com/globalsign/mgo/bson"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/core/block/chats/chatmodel"
"github.com/anyproto/anytype-heart/core/block/chats/chatrepository"
"github.com/anyproto/anytype-heart/core/block/chats/chatsubscription"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock/smarttest"
"github.com/anyproto/anytype-heart/core/block/editor/storestate"
"github.com/anyproto/anytype-heart/core/block/object/idresolver/mock_idresolver"
"github.com/anyproto/anytype-heart/core/block/source"
"github.com/anyproto/anytype-heart/core/block/source/mock_source"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/core/event/mock_event"
"github.com/anyproto/anytype-heart/core/session"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore/spaceindex"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/tests/testutil"
)
const (
@ -32,12 +39,27 @@ const (
type accountServiceStub struct {
accountId string
signKey crypto.PrivKey
}
func (a *accountServiceStub) AccountID() string {
return a.accountId
}
func (a *accountServiceStub) Keys() *accountdata.AccountKeys {
return &accountdata.AccountKeys{
SignKey: a.signKey,
}
}
func (a *accountServiceStub) Name() string { return "accountServiceStub" }
func (a *accountServiceStub) Init(ap *app.App) error {
signKey, _, _ := crypto.GenerateRandomEd25519KeyPair()
a.signKey = signKey
return nil
}
type stubSeenHeadsCollector struct {
heads []string
}
@ -53,6 +75,7 @@ type fixture struct {
sourceCreator string
eventSender *mock_event.MockSender
events []*pb.EventMessage
spaceIndex spaceindex.Store
generateOrderIdFunc func(tx *storestate.StoreStateTx) string
}
@ -61,12 +84,11 @@ const testCreator = "accountId1"
func newFixture(t *testing.T) *fixture {
ctx := context.Background()
db, err := anystore.Open(ctx, filepath.Join(t.TempDir(), "crdt.db"), nil)
require.NoError(t, err)
t.Cleanup(func() {
err := db.Close()
require.NoError(t, err)
})
a := &app.App{}
idResolver := mock_idresolver.NewMockResolver(t)
idResolver.EXPECT().ResolveSpaceID(mock.Anything).Return(testSpaceId, nil).Maybe()
accountService := &accountServiceStub{accountId: testCreator}
@ -74,9 +96,25 @@ func newFixture(t *testing.T) *fixture {
sb := smarttest.New("chatId1")
spaceIndex := spaceindex.NewStoreFixture(t)
objectStore := objectstore.NewStoreFixture(t)
spaceIndex := objectStore.SpaceIndex(testSpaceId)
object := New(sb, accountService, eventSender, db, spaceIndex)
repo := chatrepository.New()
subscriptions := chatsubscription.New()
a.Register(accountService)
a.Register(testutil.PrepareMock(ctx, a, eventSender))
a.Register(testutil.PrepareMock(ctx, a, idResolver))
a.Register(objectStore)
a.Register(repo)
a.Register(subscriptions)
err := a.Start(ctx)
require.NoError(t, err)
db, err := objectStore.GetCrdtDb(testSpaceId).Wait()
require.NoError(t, err)
object := New(sb, accountService, db, repo, subscriptions)
rawObject := object.(*storeObject)
fx := &fixture{
@ -84,6 +122,7 @@ func newFixture(t *testing.T) *fixture {
accountServiceStub: accountService,
sourceCreator: testCreator,
eventSender: eventSender,
spaceIndex: spaceIndex,
}
eventSender.EXPECT().Broadcast(mock.Anything).Run(func(event *pb.Event) {
for _, msg := range event.Messages {
@ -107,7 +146,7 @@ func newFixture(t *testing.T) *fixture {
// Imitate diff manager
source.EXPECT().MarkSeenHeads(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, name string, seenHeads []string) error {
allMessagesResp, err := fx.GetMessages(ctx, GetMessagesRequest{
allMessagesResp, err := fx.GetMessages(ctx, chatrepository.GetMessagesRequest{
AfterOrderId: "",
IncludeBoundary: true,
})
@ -158,7 +197,7 @@ func TestAddMessage(t *testing.T) {
assert.NotEmpty(t, messageId)
assert.NotEmpty(t, sessionCtx.GetMessages())
messagesResp, err := fx.GetMessages(ctx, GetMessagesRequest{})
messagesResp, err := fx.GetMessages(ctx, chatrepository.GetMessagesRequest{})
require.NoError(t, err)
require.Len(t, messagesResp.Messages, 1)
@ -186,7 +225,7 @@ func TestAddMessage(t *testing.T) {
assert.NotEmpty(t, messageId)
assert.NotEmpty(t, sessionCtx.GetMessages())
messagesResp, err := fx.GetMessages(ctx, GetMessagesRequest{})
messagesResp, err := fx.GetMessages(ctx, chatrepository.GetMessagesRequest{})
require.NoError(t, err)
require.Len(t, messagesResp.Messages, 1)
assert.Equal(t, messagesResp.ChatState.LastStateId, messagesResp.Messages[0].StateId)
@ -214,7 +253,7 @@ func TestGetMessages(t *testing.T) {
assert.NotEmpty(t, messageId)
}
messagesResp, err := fx.GetMessages(ctx, GetMessagesRequest{Limit: 5})
messagesResp, err := fx.GetMessages(ctx, chatrepository.GetMessagesRequest{Limit: 5})
require.NoError(t, err)
lastMessage := messagesResp.Messages[4]
@ -227,7 +266,7 @@ func TestGetMessages(t *testing.T) {
t.Run("with requested BeforeOrderId", func(t *testing.T) {
lastOrderId := messagesResp.Messages[0].OrderId // text 6
gotMessages, err := fx.GetMessages(ctx, GetMessagesRequest{BeforeOrderId: lastOrderId, Limit: 5})
gotMessages, err := fx.GetMessages(ctx, chatrepository.GetMessagesRequest{BeforeOrderId: lastOrderId, Limit: 5})
require.NoError(t, err)
wantTexts = []string{"text 1", "text 2", "text 3", "text 4", "text 5"}
for i, msg := range gotMessages.Messages {
@ -237,7 +276,7 @@ func TestGetMessages(t *testing.T) {
t.Run("with requested AfterOrderId", func(t *testing.T) {
lastOrderId := messagesResp.Messages[0].OrderId // text 6
gotMessages, err := fx.GetMessages(ctx, GetMessagesRequest{AfterOrderId: lastOrderId, Limit: 2})
gotMessages, err := fx.GetMessages(ctx, chatrepository.GetMessagesRequest{AfterOrderId: lastOrderId, Limit: 2})
require.NoError(t, err)
wantTexts = []string{"text 7", "text 8"}
for i, msg := range gotMessages.Messages {
@ -287,7 +326,7 @@ func TestEditMessage(t *testing.T) {
err = fx.EditMessage(ctx, messageId, editedMessage)
require.NoError(t, err)
messagesResp, err := fx.GetMessages(ctx, GetMessagesRequest{})
messagesResp, err := fx.GetMessages(ctx, chatrepository.GetMessagesRequest{})
require.NoError(t, err)
require.Len(t, messagesResp.Messages, 1)
@ -322,7 +361,7 @@ func TestEditMessage(t *testing.T) {
require.Error(t, err)
// Check that nothing is changed
messagesResp, err := fx.GetMessages(ctx, GetMessagesRequest{})
messagesResp, err := fx.GetMessages(ctx, chatrepository.GetMessagesRequest{})
require.NoError(t, err)
require.Len(t, messagesResp.Messages, 1)
@ -378,7 +417,7 @@ func TestToggleReaction(t *testing.T) {
require.NoError(t, err)
})
messagesResp, err := fx.GetMessages(ctx, GetMessagesRequest{})
messagesResp, err := fx.GetMessages(ctx, chatrepository.GetMessagesRequest{})
require.NoError(t, err)
require.Len(t, messagesResp.Messages, 1)
@ -398,7 +437,7 @@ func TestToggleReaction(t *testing.T) {
}
func (fx *fixture) assertReadStatus(t *testing.T, ctx context.Context, afterOrderId string, beforeOrderId string, isRead bool, isMentionRead bool) *GetMessagesResponse {
messageResp, err := fx.GetMessages(ctx, GetMessagesRequest{
messageResp, err := fx.GetMessages(ctx, chatrepository.GetMessagesRequest{
AfterOrderId: afterOrderId,
BeforeOrderId: beforeOrderId,
IncludeBoundary: true,
@ -445,8 +484,8 @@ func (fx *fixture) applyToStore(ctx context.Context, params source.PushStoreChan
return changeId, nil
}
func givenSimpleMessage(text string) *Message {
return &Message{
func givenSimpleMessage(text string) *chatmodel.Message {
return &chatmodel.Message{
ChatMessage: &model.ChatMessage{
Id: "",
OrderId: "",
@ -461,8 +500,8 @@ func givenSimpleMessage(text string) *Message {
}
}
func givenMessageWithMention(text string) *Message {
return &Message{
func givenMessageWithMention(text string) *chatmodel.Message {
return &chatmodel.Message{
ChatMessage: &model.ChatMessage{
Id: "",
OrderId: "",
@ -484,8 +523,8 @@ func givenMessageWithMention(text string) *Message {
}
}
func givenComplexMessage() *Message {
return &Message{
func givenComplexMessage() *chatmodel.Message {
return &chatmodel.Message{
ChatMessage: &model.ChatMessage{
Id: "",
OrderId: "",
@ -538,7 +577,7 @@ func givenComplexMessage() *Message {
}
}
func assertMessagesEqual(t *testing.T, want, got *Message) {
func assertMessagesEqual(t *testing.T, want, got *chatmodel.Message) {
// Cleanup order id
assert.NotEmpty(t, got.OrderId)
got.OrderId = ""

View file

@ -3,9 +3,13 @@
package mock_chatobject
import (
chatmodel "github.com/anyproto/anytype-heart/core/block/chats/chatmodel"
anystoredebug "github.com/anyproto/anytype-heart/core/block/editor/anystoredebug"
chatobject "github.com/anyproto/anytype-heart/core/block/editor/chatobject"
chatrepository "github.com/anyproto/anytype-heart/core/block/chats/chatrepository"
context "context"
coresmartblock "github.com/anyproto/anytype-heart/pkg/lib/core/smartblock"
@ -150,7 +154,7 @@ func (_c *MockStoreObject_AddHookOnce_Call) RunAndReturn(run func(string, smartb
}
// AddMessage provides a mock function with given fields: ctx, sessionCtx, message
func (_m *MockStoreObject) AddMessage(ctx context.Context, sessionCtx session.Context, message *chatobject.Message) (string, error) {
func (_m *MockStoreObject) AddMessage(ctx context.Context, sessionCtx session.Context, message *chatmodel.Message) (string, error) {
ret := _m.Called(ctx, sessionCtx, message)
if len(ret) == 0 {
@ -159,16 +163,16 @@ func (_m *MockStoreObject) AddMessage(ctx context.Context, sessionCtx session.Co
var r0 string
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, session.Context, *chatobject.Message) (string, error)); ok {
if rf, ok := ret.Get(0).(func(context.Context, session.Context, *chatmodel.Message) (string, error)); ok {
return rf(ctx, sessionCtx, message)
}
if rf, ok := ret.Get(0).(func(context.Context, session.Context, *chatobject.Message) string); ok {
if rf, ok := ret.Get(0).(func(context.Context, session.Context, *chatmodel.Message) string); ok {
r0 = rf(ctx, sessionCtx, message)
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func(context.Context, session.Context, *chatobject.Message) error); ok {
if rf, ok := ret.Get(1).(func(context.Context, session.Context, *chatmodel.Message) error); ok {
r1 = rf(ctx, sessionCtx, message)
} else {
r1 = ret.Error(1)
@ -185,14 +189,14 @@ type MockStoreObject_AddMessage_Call struct {
// AddMessage is a helper method to define mock.On call
// - ctx context.Context
// - sessionCtx session.Context
// - message *chatobject.Message
// - message *chatmodel.Message
func (_e *MockStoreObject_Expecter) AddMessage(ctx interface{}, sessionCtx interface{}, message interface{}) *MockStoreObject_AddMessage_Call {
return &MockStoreObject_AddMessage_Call{Call: _e.mock.On("AddMessage", ctx, sessionCtx, message)}
}
func (_c *MockStoreObject_AddMessage_Call) Run(run func(ctx context.Context, sessionCtx session.Context, message *chatobject.Message)) *MockStoreObject_AddMessage_Call {
func (_c *MockStoreObject_AddMessage_Call) Run(run func(ctx context.Context, sessionCtx session.Context, message *chatmodel.Message)) *MockStoreObject_AddMessage_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(session.Context), args[2].(*chatobject.Message))
run(args[0].(context.Context), args[1].(session.Context), args[2].(*chatmodel.Message))
})
return _c
}
@ -202,7 +206,7 @@ func (_c *MockStoreObject_AddMessage_Call) Return(_a0 string, _a1 error) *MockSt
return _c
}
func (_c *MockStoreObject_AddMessage_Call) RunAndReturn(run func(context.Context, session.Context, *chatobject.Message) (string, error)) *MockStoreObject_AddMessage_Call {
func (_c *MockStoreObject_AddMessage_Call) RunAndReturn(run func(context.Context, session.Context, *chatmodel.Message) (string, error)) *MockStoreObject_AddMessage_Call {
_c.Call.Return(run)
return _c
}
@ -744,7 +748,7 @@ func (_c *MockStoreObject_Details_Call) RunAndReturn(run func() *domain.Details)
}
// EditMessage provides a mock function with given fields: ctx, messageId, newMessage
func (_m *MockStoreObject) EditMessage(ctx context.Context, messageId string, newMessage *chatobject.Message) error {
func (_m *MockStoreObject) EditMessage(ctx context.Context, messageId string, newMessage *chatmodel.Message) error {
ret := _m.Called(ctx, messageId, newMessage)
if len(ret) == 0 {
@ -752,7 +756,7 @@ func (_m *MockStoreObject) EditMessage(ctx context.Context, messageId string, ne
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, *chatobject.Message) error); ok {
if rf, ok := ret.Get(0).(func(context.Context, string, *chatmodel.Message) error); ok {
r0 = rf(ctx, messageId, newMessage)
} else {
r0 = ret.Error(0)
@ -769,14 +773,14 @@ type MockStoreObject_EditMessage_Call struct {
// EditMessage is a helper method to define mock.On call
// - ctx context.Context
// - messageId string
// - newMessage *chatobject.Message
// - newMessage *chatmodel.Message
func (_e *MockStoreObject_Expecter) EditMessage(ctx interface{}, messageId interface{}, newMessage interface{}) *MockStoreObject_EditMessage_Call {
return &MockStoreObject_EditMessage_Call{Call: _e.mock.On("EditMessage", ctx, messageId, newMessage)}
}
func (_c *MockStoreObject_EditMessage_Call) Run(run func(ctx context.Context, messageId string, newMessage *chatobject.Message)) *MockStoreObject_EditMessage_Call {
func (_c *MockStoreObject_EditMessage_Call) Run(run func(ctx context.Context, messageId string, newMessage *chatmodel.Message)) *MockStoreObject_EditMessage_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(*chatobject.Message))
run(args[0].(context.Context), args[1].(string), args[2].(*chatmodel.Message))
})
return _c
}
@ -786,7 +790,7 @@ func (_c *MockStoreObject_EditMessage_Call) Return(_a0 error) *MockStoreObject_E
return _c
}
func (_c *MockStoreObject_EditMessage_Call) RunAndReturn(run func(context.Context, string, *chatobject.Message) error) *MockStoreObject_EditMessage_Call {
func (_c *MockStoreObject_EditMessage_Call) RunAndReturn(run func(context.Context, string, *chatmodel.Message) error) *MockStoreObject_EditMessage_Call {
_c.Call.Return(run)
return _c
}
@ -948,7 +952,7 @@ func (_c *MockStoreObject_GetDocInfo_Call) RunAndReturn(run func() smartblock.Do
}
// GetMessages provides a mock function with given fields: ctx, req
func (_m *MockStoreObject) GetMessages(ctx context.Context, req chatobject.GetMessagesRequest) (*chatobject.GetMessagesResponse, error) {
func (_m *MockStoreObject) GetMessages(ctx context.Context, req chatrepository.GetMessagesRequest) (*chatobject.GetMessagesResponse, error) {
ret := _m.Called(ctx, req)
if len(ret) == 0 {
@ -957,10 +961,10 @@ func (_m *MockStoreObject) GetMessages(ctx context.Context, req chatobject.GetMe
var r0 *chatobject.GetMessagesResponse
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, chatobject.GetMessagesRequest) (*chatobject.GetMessagesResponse, error)); ok {
if rf, ok := ret.Get(0).(func(context.Context, chatrepository.GetMessagesRequest) (*chatobject.GetMessagesResponse, error)); ok {
return rf(ctx, req)
}
if rf, ok := ret.Get(0).(func(context.Context, chatobject.GetMessagesRequest) *chatobject.GetMessagesResponse); ok {
if rf, ok := ret.Get(0).(func(context.Context, chatrepository.GetMessagesRequest) *chatobject.GetMessagesResponse); ok {
r0 = rf(ctx, req)
} else {
if ret.Get(0) != nil {
@ -968,7 +972,7 @@ func (_m *MockStoreObject) GetMessages(ctx context.Context, req chatobject.GetMe
}
}
if rf, ok := ret.Get(1).(func(context.Context, chatobject.GetMessagesRequest) error); ok {
if rf, ok := ret.Get(1).(func(context.Context, chatrepository.GetMessagesRequest) error); ok {
r1 = rf(ctx, req)
} else {
r1 = ret.Error(1)
@ -984,14 +988,14 @@ type MockStoreObject_GetMessages_Call struct {
// GetMessages is a helper method to define mock.On call
// - ctx context.Context
// - req chatobject.GetMessagesRequest
// - req chatrepository.GetMessagesRequest
func (_e *MockStoreObject_Expecter) GetMessages(ctx interface{}, req interface{}) *MockStoreObject_GetMessages_Call {
return &MockStoreObject_GetMessages_Call{Call: _e.mock.On("GetMessages", ctx, req)}
}
func (_c *MockStoreObject_GetMessages_Call) Run(run func(ctx context.Context, req chatobject.GetMessagesRequest)) *MockStoreObject_GetMessages_Call {
func (_c *MockStoreObject_GetMessages_Call) Run(run func(ctx context.Context, req chatrepository.GetMessagesRequest)) *MockStoreObject_GetMessages_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(chatobject.GetMessagesRequest))
run(args[0].(context.Context), args[1].(chatrepository.GetMessagesRequest))
})
return _c
}
@ -1001,29 +1005,29 @@ func (_c *MockStoreObject_GetMessages_Call) Return(_a0 *chatobject.GetMessagesRe
return _c
}
func (_c *MockStoreObject_GetMessages_Call) RunAndReturn(run func(context.Context, chatobject.GetMessagesRequest) (*chatobject.GetMessagesResponse, error)) *MockStoreObject_GetMessages_Call {
func (_c *MockStoreObject_GetMessages_Call) RunAndReturn(run func(context.Context, chatrepository.GetMessagesRequest) (*chatobject.GetMessagesResponse, error)) *MockStoreObject_GetMessages_Call {
_c.Call.Return(run)
return _c
}
// GetMessagesByIds provides a mock function with given fields: ctx, messageIds
func (_m *MockStoreObject) GetMessagesByIds(ctx context.Context, messageIds []string) ([]*chatobject.Message, error) {
func (_m *MockStoreObject) GetMessagesByIds(ctx context.Context, messageIds []string) ([]*chatmodel.Message, error) {
ret := _m.Called(ctx, messageIds)
if len(ret) == 0 {
panic("no return value specified for GetMessagesByIds")
}
var r0 []*chatobject.Message
var r0 []*chatmodel.Message
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, []string) ([]*chatobject.Message, error)); ok {
if rf, ok := ret.Get(0).(func(context.Context, []string) ([]*chatmodel.Message, error)); ok {
return rf(ctx, messageIds)
}
if rf, ok := ret.Get(0).(func(context.Context, []string) []*chatobject.Message); ok {
if rf, ok := ret.Get(0).(func(context.Context, []string) []*chatmodel.Message); ok {
r0 = rf(ctx, messageIds)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*chatobject.Message)
r0 = ret.Get(0).([]*chatmodel.Message)
}
}
@ -1055,12 +1059,12 @@ func (_c *MockStoreObject_GetMessagesByIds_Call) Run(run func(ctx context.Contex
return _c
}
func (_c *MockStoreObject_GetMessagesByIds_Call) Return(_a0 []*chatobject.Message, _a1 error) *MockStoreObject_GetMessagesByIds_Call {
func (_c *MockStoreObject_GetMessagesByIds_Call) Return(_a0 []*chatmodel.Message, _a1 error) *MockStoreObject_GetMessagesByIds_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockStoreObject_GetMessagesByIds_Call) RunAndReturn(run func(context.Context, []string) ([]*chatobject.Message, error)) *MockStoreObject_GetMessagesByIds_Call {
func (_c *MockStoreObject_GetMessagesByIds_Call) RunAndReturn(run func(context.Context, []string) ([]*chatmodel.Message, error)) *MockStoreObject_GetMessagesByIds_Call {
_c.Call.Return(run)
return _c
}
@ -1568,7 +1572,7 @@ func (_c *MockStoreObject_Lock_Call) RunAndReturn(run func()) *MockStoreObject_L
}
// MarkMessagesAsUnread provides a mock function with given fields: ctx, afterOrderId, counterType
func (_m *MockStoreObject) MarkMessagesAsUnread(ctx context.Context, afterOrderId string, counterType chatobject.CounterType) error {
func (_m *MockStoreObject) MarkMessagesAsUnread(ctx context.Context, afterOrderId string, counterType chatmodel.CounterType) error {
ret := _m.Called(ctx, afterOrderId, counterType)
if len(ret) == 0 {
@ -1576,7 +1580,7 @@ func (_m *MockStoreObject) MarkMessagesAsUnread(ctx context.Context, afterOrderI
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, chatobject.CounterType) error); ok {
if rf, ok := ret.Get(0).(func(context.Context, string, chatmodel.CounterType) error); ok {
r0 = rf(ctx, afterOrderId, counterType)
} else {
r0 = ret.Error(0)
@ -1593,14 +1597,14 @@ type MockStoreObject_MarkMessagesAsUnread_Call struct {
// MarkMessagesAsUnread is a helper method to define mock.On call
// - ctx context.Context
// - afterOrderId string
// - counterType chatobject.CounterType
// - counterType chatmodel.CounterType
func (_e *MockStoreObject_Expecter) MarkMessagesAsUnread(ctx interface{}, afterOrderId interface{}, counterType interface{}) *MockStoreObject_MarkMessagesAsUnread_Call {
return &MockStoreObject_MarkMessagesAsUnread_Call{Call: _e.mock.On("MarkMessagesAsUnread", ctx, afterOrderId, counterType)}
}
func (_c *MockStoreObject_MarkMessagesAsUnread_Call) Run(run func(ctx context.Context, afterOrderId string, counterType chatobject.CounterType)) *MockStoreObject_MarkMessagesAsUnread_Call {
func (_c *MockStoreObject_MarkMessagesAsUnread_Call) Run(run func(ctx context.Context, afterOrderId string, counterType chatmodel.CounterType)) *MockStoreObject_MarkMessagesAsUnread_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(chatobject.CounterType))
run(args[0].(context.Context), args[1].(string), args[2].(chatmodel.CounterType))
})
return _c
}
@ -1610,13 +1614,13 @@ func (_c *MockStoreObject_MarkMessagesAsUnread_Call) Return(_a0 error) *MockStor
return _c
}
func (_c *MockStoreObject_MarkMessagesAsUnread_Call) RunAndReturn(run func(context.Context, string, chatobject.CounterType) error) *MockStoreObject_MarkMessagesAsUnread_Call {
func (_c *MockStoreObject_MarkMessagesAsUnread_Call) RunAndReturn(run func(context.Context, string, chatmodel.CounterType) error) *MockStoreObject_MarkMessagesAsUnread_Call {
_c.Call.Return(run)
return _c
}
// MarkReadMessages provides a mock function with given fields: ctx, afterOrderId, beforeOrderId, lastStateId, counterType
func (_m *MockStoreObject) MarkReadMessages(ctx context.Context, afterOrderId string, beforeOrderId string, lastStateId string, counterType chatobject.CounterType) error {
func (_m *MockStoreObject) MarkReadMessages(ctx context.Context, afterOrderId string, beforeOrderId string, lastStateId string, counterType chatmodel.CounterType) error {
ret := _m.Called(ctx, afterOrderId, beforeOrderId, lastStateId, counterType)
if len(ret) == 0 {
@ -1624,7 +1628,7 @@ func (_m *MockStoreObject) MarkReadMessages(ctx context.Context, afterOrderId st
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, chatobject.CounterType) error); ok {
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, chatmodel.CounterType) error); ok {
r0 = rf(ctx, afterOrderId, beforeOrderId, lastStateId, counterType)
} else {
r0 = ret.Error(0)
@ -1643,14 +1647,14 @@ type MockStoreObject_MarkReadMessages_Call struct {
// - afterOrderId string
// - beforeOrderId string
// - lastStateId string
// - counterType chatobject.CounterType
// - counterType chatmodel.CounterType
func (_e *MockStoreObject_Expecter) MarkReadMessages(ctx interface{}, afterOrderId interface{}, beforeOrderId interface{}, lastStateId interface{}, counterType interface{}) *MockStoreObject_MarkReadMessages_Call {
return &MockStoreObject_MarkReadMessages_Call{Call: _e.mock.On("MarkReadMessages", ctx, afterOrderId, beforeOrderId, lastStateId, counterType)}
}
func (_c *MockStoreObject_MarkReadMessages_Call) Run(run func(ctx context.Context, afterOrderId string, beforeOrderId string, lastStateId string, counterType chatobject.CounterType)) *MockStoreObject_MarkReadMessages_Call {
func (_c *MockStoreObject_MarkReadMessages_Call) Run(run func(ctx context.Context, afterOrderId string, beforeOrderId string, lastStateId string, counterType chatmodel.CounterType)) *MockStoreObject_MarkReadMessages_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(chatobject.CounterType))
run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(chatmodel.CounterType))
})
return _c
}
@ -1660,7 +1664,7 @@ func (_c *MockStoreObject_MarkReadMessages_Call) Return(_a0 error) *MockStoreObj
return _c
}
func (_c *MockStoreObject_MarkReadMessages_Call) RunAndReturn(run func(context.Context, string, string, string, chatobject.CounterType) error) *MockStoreObject_MarkReadMessages_Call {
func (_c *MockStoreObject_MarkReadMessages_Call) RunAndReturn(run func(context.Context, string, string, string, chatmodel.CounterType) error) *MockStoreObject_MarkReadMessages_Call {
_c.Call.Return(run)
return _c
}
@ -2628,65 +2632,6 @@ func (_c *MockStoreObject_SpaceID_Call) RunAndReturn(run func() string) *MockSto
return _c
}
// SubscribeLastMessages provides a mock function with given fields: ctx, req
func (_m *MockStoreObject) SubscribeLastMessages(ctx context.Context, req chatobject.SubscribeLastMessagesRequest) (*chatobject.SubscribeLastMessagesResponse, error) {
ret := _m.Called(ctx, req)
if len(ret) == 0 {
panic("no return value specified for SubscribeLastMessages")
}
var r0 *chatobject.SubscribeLastMessagesResponse
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, chatobject.SubscribeLastMessagesRequest) (*chatobject.SubscribeLastMessagesResponse, error)); ok {
return rf(ctx, req)
}
if rf, ok := ret.Get(0).(func(context.Context, chatobject.SubscribeLastMessagesRequest) *chatobject.SubscribeLastMessagesResponse); ok {
r0 = rf(ctx, req)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*chatobject.SubscribeLastMessagesResponse)
}
}
if rf, ok := ret.Get(1).(func(context.Context, chatobject.SubscribeLastMessagesRequest) error); ok {
r1 = rf(ctx, req)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockStoreObject_SubscribeLastMessages_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SubscribeLastMessages'
type MockStoreObject_SubscribeLastMessages_Call struct {
*mock.Call
}
// SubscribeLastMessages is a helper method to define mock.On call
// - ctx context.Context
// - req chatobject.SubscribeLastMessagesRequest
func (_e *MockStoreObject_Expecter) SubscribeLastMessages(ctx interface{}, req interface{}) *MockStoreObject_SubscribeLastMessages_Call {
return &MockStoreObject_SubscribeLastMessages_Call{Call: _e.mock.On("SubscribeLastMessages", ctx, req)}
}
func (_c *MockStoreObject_SubscribeLastMessages_Call) Run(run func(ctx context.Context, req chatobject.SubscribeLastMessagesRequest)) *MockStoreObject_SubscribeLastMessages_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(chatobject.SubscribeLastMessagesRequest))
})
return _c
}
func (_c *MockStoreObject_SubscribeLastMessages_Call) Return(_a0 *chatobject.SubscribeLastMessagesResponse, _a1 error) *MockStoreObject_SubscribeLastMessages_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockStoreObject_SubscribeLastMessages_Call) RunAndReturn(run func(context.Context, chatobject.SubscribeLastMessagesRequest) (*chatobject.SubscribeLastMessagesResponse, error)) *MockStoreObject_SubscribeLastMessages_Call {
_c.Call.Return(run)
return _c
}
// ToggleMessageReaction provides a mock function with given fields: ctx, messageId, emoji
func (_m *MockStoreObject) ToggleMessageReaction(ctx context.Context, messageId string, emoji string) error {
ret := _m.Called(ctx, messageId, emoji)
@ -3007,52 +2952,6 @@ func (_c *MockStoreObject_Unlock_Call) RunAndReturn(run func()) *MockStoreObject
return _c
}
// Unsubscribe provides a mock function with given fields: subId
func (_m *MockStoreObject) Unsubscribe(subId string) error {
ret := _m.Called(subId)
if len(ret) == 0 {
panic("no return value specified for Unsubscribe")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(subId)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockStoreObject_Unsubscribe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Unsubscribe'
type MockStoreObject_Unsubscribe_Call struct {
*mock.Call
}
// Unsubscribe is a helper method to define mock.On call
// - subId string
func (_e *MockStoreObject_Expecter) Unsubscribe(subId interface{}) *MockStoreObject_Unsubscribe_Call {
return &MockStoreObject_Unsubscribe_Call{Call: _e.mock.On("Unsubscribe", subId)}
}
func (_c *MockStoreObject_Unsubscribe_Call) Run(run func(subId string)) *MockStoreObject_Unsubscribe_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockStoreObject_Unsubscribe_Call) Return(_a0 error) *MockStoreObject_Unsubscribe_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockStoreObject_Unsubscribe_Call) RunAndReturn(run func(string) error) *MockStoreObject_Unsubscribe_Call {
_c.Call.Return(run)
return _c
}
// NewMockStoreObject creates a new instance of MockStoreObject. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockStoreObject(t interface {

View file

@ -4,162 +4,53 @@ import (
"context"
"fmt"
"github.com/anyproto/any-store/anyenc"
"github.com/anyproto/any-store/query"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/any-sync/commonspace/object/tree/objecttree"
"github.com/anyproto/any-sync/util/crypto"
"github.com/anyproto/any-sync/util/slice"
"github.com/anyproto/anytype-heart/core/block/chats/chatmodel"
"github.com/anyproto/anytype-heart/core/block/source"
"golang.org/x/exp/slices"
)
type CounterType int
type ReadMessagesRequest struct {
AfterOrderId string
BeforeOrderId string
LastStateId string
const (
CounterTypeMessage = CounterType(iota)
CounterTypeMention
)
CounterType chatmodel.CounterType
type readHandler interface {
getUnreadFilter() query.Filter
getMessagesFilter() query.Filter
getDiffManagerName() string
getReadKey() string
readModifier(value bool) query.Modifier
readMessages(newOldestOrderId string, idsModified []string)
unreadMessages(newOldestOrderId string, lastStateId string, msgIds []string)
// All forces to read all messages
All bool
}
type readMessagesHandler struct {
subscription *subscriptionManager
}
func (h *readMessagesHandler) getUnreadFilter() query.Filter {
return query.Not{
Filter: query.Key{Path: []string{readKey}, Filter: query.NewComp(query.CompOpEq, true)},
}
}
func (h *readMessagesHandler) getMessagesFilter() query.Filter {
return nil
}
func (h *readMessagesHandler) getDiffManagerName() string {
return diffManagerMessages
}
func (h *readMessagesHandler) getReadKey() string {
return readKey
}
func (h *readMessagesHandler) readMessages(newOldestOrderId string, idsModified []string) {
h.subscription.updateChatState(func(state *model.ChatState) *model.ChatState {
state.Messages.OldestOrderId = newOldestOrderId
return state
})
h.subscription.updateMessageRead(idsModified, true)
}
func (h *readMessagesHandler) unreadMessages(newOldestOrderId string, lastStateId string, msgIds []string) {
h.subscription.updateChatState(func(state *model.ChatState) *model.ChatState {
state.Messages.OldestOrderId = newOldestOrderId
state.LastStateId = lastStateId
return state
})
h.subscription.updateMessageRead(msgIds, false)
}
func (h *readMessagesHandler) readModifier(value bool) query.Modifier {
return query.ModifyFunc(func(a *anyenc.Arena, v *anyenc.Value) (result *anyenc.Value, modified bool, err error) {
oldValue := v.GetBool(h.getReadKey())
if oldValue != value {
v.Set(h.getReadKey(), arenaNewBool(a, value))
return v, true, nil
}
return v, false, nil
})
}
type readMentionsHandler struct {
subscription *subscriptionManager
}
func (h *readMentionsHandler) getUnreadFilter() query.Filter {
return query.And{
query.Key{Path: []string{hasMentionKey}, Filter: query.NewComp(query.CompOpEq, true)},
query.Key{Path: []string{mentionReadKey}, Filter: query.NewComp(query.CompOpEq, false)},
}
}
func (h *readMentionsHandler) getMessagesFilter() query.Filter {
return query.Key{Path: []string{hasMentionKey}, Filter: query.NewComp(query.CompOpEq, true)}
}
func (h *readMentionsHandler) getDiffManagerName() string {
return diffManagerMentions
}
func (h *readMentionsHandler) getReadKey() string {
return mentionReadKey
}
func (h *readMentionsHandler) readMessages(newOldestOrderId string, idsModified []string) {
h.subscription.updateChatState(func(state *model.ChatState) *model.ChatState {
state.Mentions.OldestOrderId = newOldestOrderId
return state
})
h.subscription.updateMentionRead(idsModified, true)
}
func (h *readMentionsHandler) unreadMessages(newOldestOrderId string, lastStateId string, msgIds []string) {
h.subscription.updateChatState(func(state *model.ChatState) *model.ChatState {
state.Mentions.OldestOrderId = newOldestOrderId
state.LastStateId = lastStateId
return state
})
h.subscription.updateMentionRead(msgIds, false)
}
func (h *readMentionsHandler) readModifier(value bool) query.Modifier {
return query.ModifyFunc(func(a *anyenc.Arena, v *anyenc.Value) (result *anyenc.Value, modified bool, err error) {
if v.GetBool(hasMentionKey) {
oldValue := v.GetBool(h.getReadKey())
if oldValue != value {
v.Set(h.getReadKey(), arenaNewBool(a, value))
return v, true, nil
}
}
return v, false, nil
})
}
func newReadHandler(counterType CounterType, subscription *subscriptionManager) readHandler {
switch counterType {
case CounterTypeMessage:
return &readMessagesHandler{subscription: subscription}
case CounterTypeMention:
return &readMentionsHandler{subscription: subscription}
default:
panic("unknown counter type")
}
}
func (s *storeObject) MarkReadMessages(ctx context.Context, afterOrderId, beforeOrderId string, lastStateId string, counterType CounterType) error {
handler := newReadHandler(counterType, s.subscription)
func (s *storeObject) MarkReadMessages(ctx context.Context, req ReadMessagesRequest) error {
// 1. select all messages with orderId < beforeOrderId and addedTime < lastDbState
// 2. use the last(by orderId) message id as lastHead
// 3. update the MarkSeenHeads
// 2. mark messages as read in the DB
msgs, err := s.repository.getUnreadMessageIdsInRange(ctx, afterOrderId, beforeOrderId, lastStateId, handler)
if err != nil {
return fmt.Errorf("get message: %w", err)
var msgs []string
if req.All {
var err error
msgs, err = s.repository.GetAllUnreadMessages(ctx, req.CounterType)
if err != nil {
return fmt.Errorf("get all messages: %w", err)
}
} else {
var err error
msgs, err = s.repository.GetUnreadMessageIdsInRange(ctx, req.AfterOrderId, req.BeforeOrderId, req.LastStateId, req.CounterType)
if err != nil {
return fmt.Errorf("get messages: %w", err)
}
}
// mark the whole tree as seen from the current message
return s.storeSource.MarkSeenHeads(ctx, handler.getDiffManagerName(), msgs)
return s.storeSource.MarkSeenHeads(ctx, req.CounterType.DiffManagerName(), msgs)
}
func (s *storeObject) MarkMessagesAsUnread(ctx context.Context, afterOrderId string, counterType CounterType) error {
txn, err := s.repository.writeTx(ctx)
func (s *storeObject) MarkMessagesAsUnread(ctx context.Context, afterOrderId string, counterType chatmodel.CounterType) error {
txn, err := s.repository.WriteTx(ctx)
if err != nil {
return fmt.Errorf("create tx: %w", err)
}
@ -169,8 +60,7 @@ func (s *storeObject) MarkMessagesAsUnread(ctx context.Context, afterOrderId str
_ = txn.Rollback()
}
}()
handler := newReadHandler(counterType, s.subscription)
messageIds, err := s.repository.getReadMessagesAfter(txn.Context(), afterOrderId, handler)
messageIds, err := s.repository.GetReadMessagesAfter(txn.Context(), afterOrderId, counterType)
if err != nil {
return fmt.Errorf("get read messages: %w", err)
}
@ -179,23 +69,25 @@ func (s *storeObject) MarkMessagesAsUnread(ctx context.Context, afterOrderId str
return nil
}
idsModified := s.repository.setReadFlag(txn.Context(), s.Id(), messageIds, handler, false)
idsModified := s.repository.SetReadFlag(txn.Context(), s.Id(), messageIds, counterType, false)
if len(idsModified) == 0 {
return nil
}
newOldestOrderId, err := s.repository.getOldestOrderId(txn.Context(), handler)
newOldestOrderId, err := s.repository.GetOldestOrderId(txn.Context(), counterType)
if err != nil {
return fmt.Errorf("get oldest order id: %w", err)
}
lastAdded, err := s.repository.getLastStateId(txn.Context())
lastAdded, err := s.repository.GetLastStateId(txn.Context())
if err != nil {
return fmt.Errorf("get last added date: %w", err)
}
handler.unreadMessages(newOldestOrderId, lastAdded, idsModified)
s.subscription.flush()
s.subscription.Lock()
defer s.subscription.Unlock()
s.subscription.UnreadMessages(newOldestOrderId, lastAdded, idsModified, counterType)
s.subscription.Flush()
seenHeads, err := s.seenHeadsCollector.collectSeenHeads(ctx, afterOrderId)
if err != nil {
@ -214,12 +106,12 @@ func (s *storeObject) MarkMessagesAsUnread(ctx context.Context, afterOrderId str
return txn.Commit()
}
func (s *storeObject) markReadMessages(changeIds []string, handler readHandler) error {
func (s *storeObject) markReadMessages(changeIds []string, counterType chatmodel.CounterType) error {
if len(changeIds) == 0 {
return nil
}
txn, err := s.repository.writeTx(s.componentCtx)
txn, err := s.repository.WriteTx(s.componentCtx)
if err != nil {
return fmt.Errorf("start write tx: %w", err)
}
@ -230,10 +122,10 @@ func (s *storeObject) markReadMessages(changeIds []string, handler readHandler)
}
}()
idsModified := s.repository.setReadFlag(txn.Context(), s.Id(), changeIds, handler, true)
idsModified := s.repository.SetReadFlag(txn.Context(), s.Id(), changeIds, counterType, true)
if len(idsModified) > 0 {
newOldestOrderId, err := s.repository.getOldestOrderId(txn.Context(), handler)
newOldestOrderId, err := s.repository.GetOldestOrderId(txn.Context(), counterType)
if err != nil {
return fmt.Errorf("get oldest order id: %w", err)
}
@ -244,8 +136,63 @@ func (s *storeObject) markReadMessages(changeIds []string, handler readHandler)
return fmt.Errorf("commit: %w", err)
}
handler.readMessages(newOldestOrderId, idsModified)
s.subscription.flush()
s.subscription.Lock()
defer s.subscription.Unlock()
s.subscription.ReadMessages(newOldestOrderId, idsModified, counterType)
s.subscription.Flush()
}
return nil
}
type readStoreTreeHook struct {
joinedAclRecordId string
headsBeforeJoin []string
currentIdentity crypto.PubKey
source source.Store
}
func (h *readStoreTreeHook) BeforeIteration(ot objecttree.ObjectTree) {
h.joinedAclRecordId = ot.AclList().Head().Id
for _, accState := range ot.AclList().AclState().CurrentAccounts() {
if !accState.PubKey.Equals(h.currentIdentity) {
continue
}
// Find the first record in which the user has got permissions since the last join
// Example:
// We have acl: [ 1:noPermissions, 2:reader, 3:noPermission, 4:reader, 5:writer ]
// Record with id=4 is one that we need
for i := len(accState.PermissionChanges) - 1; i >= 0; i-- {
permChange := accState.PermissionChanges[i]
if permChange.Permission.NoPermissions() {
break
} else {
h.joinedAclRecordId = permChange.RecordId
}
}
break
}
}
func (h *readStoreTreeHook) OnIteration(ot objecttree.ObjectTree, change *objecttree.Change) {
if ok, _ := ot.AclList().IsAfter(h.joinedAclRecordId, change.AclHeadId); ok {
h.headsBeforeJoin = slice.DiscardFromSlice(h.headsBeforeJoin, func(s string) bool {
return slices.Contains(change.PreviousIds, s)
})
if !slices.Contains(h.headsBeforeJoin, change.Id) {
h.headsBeforeJoin = append(h.headsBeforeJoin, change.Id)
}
}
}
func (h *readStoreTreeHook) AfterDiffManagersInit(ctx context.Context) error {
err := h.source.MarkSeenHeads(ctx, diffManagerMessages, h.headsBeforeJoin)
if err != nil {
return fmt.Errorf("mark read messages: %w", err)
}
err = h.source.MarkSeenHeads(ctx, diffManagerMentions, h.headsBeforeJoin)
if err != nil {
return fmt.Errorf("mark read mentions: %w", err)
}
return nil
}

View file

@ -8,6 +8,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/core/block/chats/chatmodel"
"github.com/anyproto/anytype-heart/core/block/chats/chatrepository"
"github.com/anyproto/anytype-heart/core/block/editor/storestate"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
@ -25,7 +27,12 @@ func TestReadMessages(t *testing.T) {
// All messages forced as not read
messagesResp := fx.assertReadStatus(t, ctx, "", "", false, false)
err := fx.MarkReadMessages(ctx, "", messagesResp.Messages[2].OrderId, messagesResp.ChatState.LastStateId, CounterTypeMessage)
err := fx.MarkReadMessages(ctx, ReadMessagesRequest{
AfterOrderId: "",
BeforeOrderId: messagesResp.Messages[2].OrderId,
LastStateId: messagesResp.ChatState.LastStateId,
CounterType: chatmodel.CounterTypeMessage,
})
require.NoError(t, err)
fx.assertReadStatus(t, ctx, "", messagesResp.Messages[2].OrderId, true, false)
@ -56,14 +63,18 @@ func TestReadMessagesLoadedInBackground(t *testing.T) {
secondMessage, err := fx.GetMessageById(ctx, secondMessageId)
require.NoError(t, err)
err = fx.MarkReadMessages(ctx, "", firstMessage.OrderId, firstMessage.StateId, CounterTypeMessage)
require.NoError(t, err)
err = fx.MarkReadMessages(ctx, ReadMessagesRequest{
AfterOrderId: "",
BeforeOrderId: firstMessage.OrderId,
LastStateId: firstMessage.StateId,
CounterType: chatmodel.CounterTypeMessage,
})
gotResponse, err := fx.GetMessages(ctx, GetMessagesRequest{})
gotResponse, err := fx.GetMessages(ctx, chatrepository.GetMessagesRequest{})
require.NoError(t, err)
firstMessage.Read = true
wantMessages := []*Message{
wantMessages := []*chatmodel.Message{
secondMessage,
firstMessage,
}
@ -95,7 +106,12 @@ func TestReadMentions(t *testing.T) {
// All messages forced as not read
messagesResp := fx.assertReadStatus(t, ctx, "", "", false, false)
err := fx.MarkReadMessages(ctx, "", messagesResp.Messages[2].OrderId, messagesResp.ChatState.LastStateId, CounterTypeMention)
err := fx.MarkReadMessages(ctx, ReadMessagesRequest{
AfterOrderId: "",
BeforeOrderId: messagesResp.Messages[2].OrderId,
LastStateId: messagesResp.ChatState.LastStateId,
CounterType: chatmodel.CounterTypeMention,
})
require.NoError(t, err)
fx.assertReadStatus(t, ctx, "", messagesResp.Messages[2].OrderId, false, true)
@ -122,7 +138,12 @@ func TestReadMentions(t *testing.T) {
// All messages forced as not read
messagesResp := fx.assertReadStatus(t, ctx, "", "", false, false)
err = fx.MarkReadMessages(ctx, "", secondMessage.OrderId, messagesResp.ChatState.LastStateId, CounterTypeMention)
err = fx.MarkReadMessages(ctx, ReadMessagesRequest{
AfterOrderId: "",
BeforeOrderId: secondMessage.OrderId,
LastStateId: messagesResp.ChatState.LastStateId,
CounterType: chatmodel.CounterTypeMention,
})
require.NoError(t, err)
fx.assertReadStatus(t, ctx, secondMessage.OrderId, secondMessage.OrderId, false, true)
@ -141,7 +162,7 @@ func TestMarkMessagesAsNotRead(t *testing.T) {
// All messages added by myself are read
fx.assertReadStatus(t, ctx, "", "", true, true)
err := fx.MarkMessagesAsUnread(ctx, "", CounterTypeMessage)
err := fx.MarkMessagesAsUnread(ctx, "", chatmodel.CounterTypeMessage)
require.NoError(t, err)
fx.assertReadStatus(t, ctx, "", "", false, true)
@ -159,7 +180,7 @@ func TestMarkMentionsAsNotRead(t *testing.T) {
// All messages added by myself are read
fx.assertReadStatus(t, ctx, "", "", true, true)
err := fx.MarkMessagesAsUnread(ctx, "", CounterTypeMention)
err := fx.MarkMessagesAsUnread(ctx, "", chatmodel.CounterTypeMention)
require.NoError(t, err)
fx.assertReadStatus(t, ctx, "", "", true, false)

View file

@ -1,331 +0,0 @@
package chatobject
import (
"context"
"errors"
"fmt"
"slices"
"sort"
anystore "github.com/anyproto/any-store"
"github.com/anyproto/any-store/anyenc"
"github.com/anyproto/any-store/query"
"go.uber.org/zap"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
type repository struct {
collection anystore.Collection
arenaPool *anyenc.ArenaPool
}
func (s *repository) writeTx(ctx context.Context) (anystore.WriteTx, error) {
return s.collection.WriteTx(ctx)
}
func (s *repository) readTx(ctx context.Context) (anystore.ReadTx, error) {
return s.collection.ReadTx(ctx)
}
func (s *repository) getLastStateId(ctx context.Context) (string, error) {
lastAddedDate := s.collection.Find(nil).Sort(descStateId).Limit(1)
iter, err := lastAddedDate.Iter(ctx)
if err != nil {
return "", fmt.Errorf("find last added date: %w", err)
}
defer iter.Close()
for iter.Next() {
doc, err := iter.Doc()
if err != nil {
return "", fmt.Errorf("get doc: %w", err)
}
msg, err := unmarshalMessage(doc.Value())
if err != nil {
return "", fmt.Errorf("unmarshal message: %w", err)
}
return msg.StateId, nil
}
return "", nil
}
func (s *repository) getPrevOrderId(ctx context.Context, orderId string) (string, error) {
iter, err := s.collection.Find(query.Key{Path: []string{orderKey, "id"}, Filter: query.NewComp(query.CompOpLt, orderId)}).
Sort(descOrder).
Limit(1).
Iter(ctx)
if err != nil {
return "", fmt.Errorf("init iterator: %w", err)
}
defer iter.Close()
if iter.Next() {
doc, err := iter.Doc()
if err != nil {
return "", fmt.Errorf("read doc: %w", err)
}
prevOrderId := doc.Value().GetString(orderKey, "id")
return prevOrderId, nil
}
return "", nil
}
// initialChatState returns the initial chat state for the chat object from the DB
func (s *repository) loadChatState(ctx context.Context) (*model.ChatState, error) {
txn, err := s.readTx(ctx)
if err != nil {
return nil, fmt.Errorf("start read tx: %w", err)
}
defer txn.Commit()
messagesState, err := s.loadChatStateByType(txn.Context(), CounterTypeMessage)
if err != nil {
return nil, fmt.Errorf("get messages state: %w", err)
}
mentionsState, err := s.loadChatStateByType(txn.Context(), CounterTypeMention)
if err != nil {
return nil, fmt.Errorf("get mentions state: %w", err)
}
lastStateId, err := s.getLastStateId(txn.Context())
if err != nil {
return nil, fmt.Errorf("get last added date: %w", err)
}
return &model.ChatState{
Messages: messagesState,
Mentions: mentionsState,
LastStateId: lastStateId,
}, nil
}
func (s *repository) loadChatStateByType(ctx context.Context, counterType CounterType) (*model.ChatStateUnreadState, error) {
opts := newReadHandler(counterType, nil)
oldestOrderId, err := s.getOldestOrderId(ctx, opts)
if err != nil {
return nil, fmt.Errorf("get oldest order id: %w", err)
}
count, err := s.countUnreadMessages(ctx, opts)
if err != nil {
return nil, fmt.Errorf("update messages: %w", err)
}
return &model.ChatStateUnreadState{
OldestOrderId: oldestOrderId,
Counter: int32(count),
}, nil
}
func (s *repository) getOldestOrderId(ctx context.Context, handler readHandler) (string, error) {
unreadQuery := s.collection.Find(handler.getUnreadFilter()).Sort(ascOrder)
iter, err := unreadQuery.Limit(1).Iter(ctx)
if err != nil {
return "", fmt.Errorf("init iter: %w", err)
}
defer iter.Close()
for iter.Next() {
doc, err := iter.Doc()
if err != nil {
return "", fmt.Errorf("get doc: %w", err)
}
orders := doc.Value().GetObject(orderKey)
if orders != nil {
return orders.Get("id").GetString(), nil
}
}
return "", nil
}
func (s *repository) countUnreadMessages(ctx context.Context, handler readHandler) (int, error) {
unreadQuery := s.collection.Find(handler.getUnreadFilter())
return unreadQuery.Count(ctx)
}
func (s *repository) getReadMessagesAfter(ctx context.Context, afterOrderId string, handler readHandler) ([]string, error) {
filter := query.And{
query.Key{Path: []string{orderKey, "id"}, Filter: query.NewComp(query.CompOpGte, afterOrderId)},
query.Key{Path: []string{handler.getReadKey()}, Filter: query.NewComp(query.CompOpEq, true)},
}
if handler.getMessagesFilter() != nil {
filter = append(filter, handler.getMessagesFilter())
}
iter, err := s.collection.Find(filter).Iter(ctx)
if err != nil {
return nil, fmt.Errorf("init iterator: %w", err)
}
defer iter.Close()
var msgIds []string
for iter.Next() {
doc, err := iter.Doc()
if err != nil {
return nil, fmt.Errorf("get doc: %w", err)
}
msgIds = append(msgIds, doc.Value().GetString("id"))
}
return msgIds, iter.Err()
}
func (s *repository) getUnreadMessageIdsInRange(ctx context.Context, afterOrderId, beforeOrderId string, lastStateId string, handler readHandler) ([]string, error) {
qry := query.And{
query.Key{Path: []string{orderKey, "id"}, Filter: query.NewComp(query.CompOpGte, afterOrderId)},
query.Key{Path: []string{orderKey, "id"}, Filter: query.NewComp(query.CompOpLte, beforeOrderId)},
query.Or{
query.Not{query.Key{Path: []string{stateIdKey}, Filter: query.Exists{}}},
query.Key{Path: []string{stateIdKey}, Filter: query.NewComp(query.CompOpLte, lastStateId)},
},
handler.getUnreadFilter(),
}
iter, err := s.collection.Find(qry).Iter(ctx)
if err != nil {
return nil, fmt.Errorf("find id: %w", err)
}
defer iter.Close()
var msgIds []string
for iter.Next() {
doc, err := iter.Doc()
if err != nil {
return nil, fmt.Errorf("get doc: %w", err)
}
msgIds = append(msgIds, doc.Value().GetString("id"))
}
return msgIds, iter.Err()
}
func (r *repository) setReadFlag(ctx context.Context, chatObjectId string, msgIds []string, handler readHandler, value bool) []string {
var idsModified []string
for _, id := range msgIds {
if id == chatObjectId {
// skip tree root
continue
}
res, err := r.collection.UpdateId(ctx, id, handler.readModifier(value))
// Not all changes are messages, skip them
if errors.Is(err, anystore.ErrDocNotFound) {
continue
}
if err != nil {
log.Error("markReadMessages: update message", zap.Error(err), zap.String("changeId", id), zap.String("chatObjectId", chatObjectId))
continue
}
if res.Modified > 0 {
idsModified = append(idsModified, id)
}
}
return idsModified
}
func (s *repository) getMessages(ctx context.Context, req GetMessagesRequest) ([]*Message, error) {
var qry anystore.Query
if req.AfterOrderId != "" {
operator := query.CompOpGt
if req.IncludeBoundary {
operator = query.CompOpGte
}
qry = s.collection.Find(query.Key{Path: []string{orderKey, "id"}, Filter: query.NewComp(operator, req.AfterOrderId)}).Sort(ascOrder).Limit(uint(req.Limit))
} else if req.BeforeOrderId != "" {
operator := query.CompOpLt
if req.IncludeBoundary {
operator = query.CompOpLte
}
qry = s.collection.Find(query.Key{Path: []string{orderKey, "id"}, Filter: query.NewComp(operator, req.BeforeOrderId)}).Sort(descOrder).Limit(uint(req.Limit))
} else {
qry = s.collection.Find(nil).Sort(descOrder).Limit(uint(req.Limit))
}
msgs, err := s.queryMessages(ctx, qry)
if err != nil {
return nil, fmt.Errorf("query messages: %w", err)
}
return msgs, nil
}
func (s *repository) queryMessages(ctx context.Context, query anystore.Query) ([]*Message, error) {
arena := s.arenaPool.Get()
defer func() {
arena.Reset()
s.arenaPool.Put(arena)
}()
iter, err := query.Iter(ctx)
if err != nil {
return nil, fmt.Errorf("find iter: %w", err)
}
defer iter.Close()
var res []*Message
for iter.Next() {
doc, err := iter.Doc()
if err != nil {
return nil, fmt.Errorf("get doc: %w", err)
}
msg, err := unmarshalMessage(doc.Value())
if err != nil {
return nil, fmt.Errorf("unmarshal message: %w", err)
}
res = append(res, msg)
}
// reverse
sort.Slice(res, func(i, j int) bool {
return res[i].OrderId < res[j].OrderId
})
return res, nil
}
func (s *repository) hasMyReaction(ctx context.Context, myIdentity string, messageId string, emoji string) (bool, error) {
doc, err := s.collection.FindId(ctx, messageId)
if err != nil {
return false, fmt.Errorf("find message: %w", err)
}
msg, err := unmarshalMessage(doc.Value())
if err != nil {
return false, fmt.Errorf("unmarshal message: %w", err)
}
if v, ok := msg.GetReactions().GetReactions()[emoji]; ok {
if slices.Contains(v.GetIds(), myIdentity) {
return true, nil
}
}
return false, nil
}
func (s *repository) getMessagesByIds(ctx context.Context, messageIds []string) ([]*Message, error) {
txn, err := s.readTx(ctx)
if err != nil {
return nil, fmt.Errorf("start read tx: %w", err)
}
defer txn.Commit()
messages := make([]*Message, 0, len(messageIds))
for _, messageId := range messageIds {
obj, err := s.collection.FindId(txn.Context(), messageId)
if errors.Is(err, anystore.ErrDocNotFound) {
continue
}
if err != nil {
return nil, errors.Join(txn.Commit(), fmt.Errorf("find id: %w", err))
}
msg, err := unmarshalMessage(obj.Value())
if err != nil {
return nil, errors.Join(txn.Commit(), fmt.Errorf("unmarshal message: %w", err))
}
messages = append(messages, msg)
}
return messages, nil
}
func (s *repository) getLastMessages(ctx context.Context, limit uint) ([]*Message, error) {
qry := s.collection.Find(nil).Sort(descOrder).Limit(limit)
return s.queryMessages(ctx, qry)
}

View file

@ -9,6 +9,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/core/block/chats/chatmodel"
"github.com/anyproto/anytype-heart/core/block/chats/chatsubscription"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
@ -27,7 +29,9 @@ func TestSubscription(t *testing.T) {
assert.NotEmpty(t, messageId)
}
resp, err := fx.SubscribeLastMessages(ctx, SubscribeLastMessagesRequest{SubId: "subId", Limit: 5, AsyncInit: false})
resp, err := fx.chatSubscriptionService.SubscribeLastMessages(ctx, chatsubscription.SubscribeLastMessagesRequest{
ChatObjectId: fx.Id(), SubId: "subId", Limit: 5, AsyncInit: false,
})
require.NoError(t, err)
wantTexts := []string{"text 6", "text 7", "text 8", "text 9", "text 10"}
for i, msg := range resp.Messages {
@ -181,7 +185,9 @@ func TestSubscriptionMessageCounters(t *testing.T) {
fx := newFixture(t)
fx.chatHandler.forceNotRead = true
subscribeResp, err := fx.SubscribeLastMessages(ctx, SubscribeLastMessagesRequest{SubId: "subId", Limit: 10, AsyncInit: false})
subscribeResp, err := fx.chatSubscriptionService.SubscribeLastMessages(ctx, chatsubscription.SubscribeLastMessagesRequest{
ChatObjectId: fx.Id(), SubId: "subId", Limit: 10, AsyncInit: false,
})
require.NoError(t, err)
assert.Empty(t, subscribeResp.Messages)
@ -275,7 +281,12 @@ func TestSubscriptionMessageCounters(t *testing.T) {
fx.events = nil
err = fx.MarkReadMessages(ctx, "", firstMessage.OrderId, secondMessage.StateId, CounterTypeMessage)
err = fx.MarkReadMessages(ctx, ReadMessagesRequest{
AfterOrderId: "",
BeforeOrderId: firstMessage.OrderId,
LastStateId: secondMessage.StateId,
CounterType: chatmodel.CounterTypeMessage,
})
require.NoError(t, err)
wantEvents = []*pb.EventMessage{
@ -315,7 +326,12 @@ func TestSubscriptionMentionCounters(t *testing.T) {
fx := newFixture(t)
fx.chatHandler.forceNotRead = true
subscribeResp, err := fx.SubscribeLastMessages(ctx, SubscribeLastMessagesRequest{SubId: "subId", Limit: 10, AsyncInit: false})
subscribeResp, err := fx.chatSubscriptionService.SubscribeLastMessages(ctx, chatsubscription.SubscribeLastMessagesRequest{
ChatObjectId: fx.Id(),
SubId: "subId",
Limit: 10,
AsyncInit: false,
})
require.NoError(t, err)
assert.Empty(t, subscribeResp.Messages)
@ -415,7 +431,12 @@ func TestSubscriptionMentionCounters(t *testing.T) {
fx.events = nil
err = fx.MarkReadMessages(ctx, "", firstMessage.OrderId, secondMessage.StateId, CounterTypeMention)
err = fx.MarkReadMessages(ctx, ReadMessagesRequest{
AfterOrderId: "",
BeforeOrderId: firstMessage.OrderId,
LastStateId: secondMessage.StateId,
CounterType: chatmodel.CounterTypeMention,
})
require.NoError(t, err)
wantEvents = []*pb.EventMessage{
@ -457,7 +478,13 @@ func TestSubscriptionWithDeps(t *testing.T) {
ctx := context.Background()
fx := newFixture(t)
_, err := fx.SubscribeLastMessages(ctx, SubscribeLastMessagesRequest{SubId: "subId", Limit: 10, AsyncInit: false, WithDependencies: true})
_, err := fx.chatSubscriptionService.SubscribeLastMessages(ctx, chatsubscription.SubscribeLastMessagesRequest{
ChatObjectId: fx.Id(),
SubId: "subId",
Limit: 10,
AsyncInit: false,
WithDependencies: true,
})
require.NoError(t, err)
myParticipantId := domain.NewParticipantId(testSpaceId, testCreator)

View file

@ -10,6 +10,8 @@ import (
"github.com/anyproto/anytype-heart/core/anytype/config"
"github.com/anyproto/anytype-heart/core/block/cache"
"github.com/anyproto/anytype-heart/core/block/chats/chatrepository"
"github.com/anyproto/anytype-heart/core/block/chats/chatsubscription"
"github.com/anyproto/anytype-heart/core/block/editor/accountobject"
"github.com/anyproto/anytype-heart/core/block/editor/bookmark"
"github.com/anyproto/anytype-heart/core/block/editor/chatobject"
@ -52,27 +54,29 @@ type deviceService interface {
}
type ObjectFactory struct {
bookmarkService bookmark.BookmarkService
fileBlockService file.BlockService
layoutConverter converter.LayoutConverter
objectStore objectstore.ObjectStore
sourceService source.Service
tempDirProvider core.TempDirProvider
fileStore filestore.FileStore
fileService files.Service
config *config.Config
picker cache.ObjectGetter
eventSender event.Sender
indexer smartblock.Indexer
spaceService spaceService
accountService accountService
fileObjectService fileobject.Service
processService process.Service
fileUploaderService fileuploader.Service
fileReconciler reconciler.Reconciler
objectDeleter ObjectDeleter
deviceService deviceService
spaceIdResolver idresolver.Resolver
bookmarkService bookmark.BookmarkService
fileBlockService file.BlockService
layoutConverter converter.LayoutConverter
objectStore objectstore.ObjectStore
sourceService source.Service
tempDirProvider core.TempDirProvider
fileStore filestore.FileStore
fileService files.Service
config *config.Config
picker cache.ObjectGetter
eventSender event.Sender
indexer smartblock.Indexer
spaceService spaceService
accountService accountService
fileObjectService fileobject.Service
processService process.Service
fileUploaderService fileuploader.Service
fileReconciler reconciler.Reconciler
objectDeleter ObjectDeleter
deviceService deviceService
spaceIdResolver idresolver.Resolver
chatRepositoryService chatrepository.Service
chatSubscriptionService chatsubscription.Service
}
func NewObjectFactory() *ObjectFactory {
@ -104,6 +108,8 @@ func (f *ObjectFactory) Init(a *app.App) (err error) {
f.fileReconciler = app.MustComponent[reconciler.Reconciler](a)
f.deviceService = app.MustComponent[deviceService](a)
f.spaceIdResolver = app.MustComponent[idresolver.Resolver](a)
f.chatRepositoryService = app.MustComponent[chatrepository.Service](a)
f.chatSubscriptionService = app.MustComponent[chatsubscription.Service](a)
return nil
}
@ -217,9 +223,17 @@ func (f *ObjectFactory) New(space smartblock.Space, sbType coresb.SmartBlockType
case coresb.SmartBlockTypeDevicesObject:
return NewDevicesObject(sb, f.deviceService), nil
case coresb.SmartBlockTypeChatDerivedObject:
return chatobject.New(sb, f.accountService, f.eventSender, f.objectStore.GetCrdtDb(space.Id()), spaceIndex), nil
crdtDb, err := f.objectStore.GetCrdtDb(space.Id()).Wait()
if err != nil {
return nil, fmt.Errorf("get crdt db: %w", err)
}
return chatobject.New(sb, f.accountService, crdtDb, f.chatRepositoryService, f.chatSubscriptionService), nil
case coresb.SmartBlockTypeAccountObject:
return accountobject.New(sb, f.accountService.Keys(), spaceIndex, f.layoutConverter, f.fileObjectService, f.objectStore.GetCrdtDb(space.Id()), f.config), nil
crdtDb, err := f.objectStore.GetCrdtDb(space.Id()).Wait()
if err != nil {
return nil, fmt.Errorf("get crdt db: %w", err)
}
return accountobject.New(sb, f.accountService.Keys(), spaceIndex, f.layoutConverter, f.fileObjectService, crdtDb, f.config), nil
default:
return nil, fmt.Errorf("unexpected smartblock type: %v", sbType)
}

View file

@ -34,6 +34,7 @@ var typeRequiredRelations = append(typeAndRelationRequiredRelations,
bundle.RelationKeyIconOption,
bundle.RelationKeyIconName,
bundle.RelationKeyPluralName,
bundle.RelationKeyHeaderRelationsLayout,
)
type ObjectType struct {
@ -99,6 +100,7 @@ func (ot *ObjectType) CreationStateMigration(ctx *smartblock.InitContext) migrat
template.WithObjectTypes(ctx.State.ObjectTypeKeys()),
template.WithTitle,
template.WithLayout(model.ObjectType_objectType),
template.WithDetail(bundle.RelationKeyRecommendedLayout, domain.Int64(model.ObjectType_basic)),
}
templates = append(templates, ot.dataviewTemplates()...)

View file

@ -42,6 +42,7 @@ var typeAndRelationRequiredRelations = []domain.RelationKey{
bundle.RelationKeyLastUsedDate,
bundle.RelationKeyRevision,
bundle.RelationKeyIsHidden,
bundle.RelationKeyApiObjectKey,
}
var relationRequiredRelations = append(typeAndRelationRequiredRelations,
@ -50,6 +51,10 @@ var relationRequiredRelations = append(typeAndRelationRequiredRelations,
bundle.RelationKeyRelationKey,
)
var relationOptionRequiredRelations = []domain.RelationKey{
bundle.RelationKeyApiObjectKey,
}
type Page struct {
smartblock.SmartBlock
basic.AllOperations
@ -136,6 +141,8 @@ func appendRequiredInternalRelations(ctx *smartblock.InitContext) {
ctx.RequiredInternalRelationKeys = append(ctx.RequiredInternalRelationKeys, typeRequiredRelations...)
case bundle.TypeKeyRelation:
ctx.RequiredInternalRelationKeys = append(ctx.RequiredInternalRelationKeys, relationRequiredRelations...)
case bundle.TypeKeyRelationOption:
ctx.RequiredInternalRelationKeys = append(ctx.RequiredInternalRelationKeys, relationOptionRequiredRelations...)
}
}

View file

@ -360,6 +360,9 @@ func (sb *smartBlock) Init(ctx *InitContext) (err error) {
}
}
ctx.State.AddBundledRelationLinks(relKeys...)
if ctx.IsNewObject && ctx.State != nil {
source.NewSubObjectsAndProfileLinksMigration(sb.Type(), sb.space, sb.currentParticipantId, sb.spaceIndex).Migrate(ctx.State)
}
if err = sb.injectLocalDetails(ctx.State); err != nil {
return
@ -848,6 +851,7 @@ func (sb *smartBlock) Apply(s *state.State, flags ...ApplyFlag) (err error) {
}
func (sb *smartBlock) ResetToVersion(s *state.State) (err error) {
source.NewSubObjectsAndProfileLinksMigration(sb.Type(), sb.space, sb.currentParticipantId, sb.spaceIndex).Migrate(s)
s.SetParent(sb.Doc.(*state.State))
sb.storeFileKeys(s)
sb.injectLocalDetails(s)

View file

@ -32,8 +32,6 @@ var spaceViewRequiredRelations = []domain.RelationKey{
bundle.RelationKeySpaceLocalStatus,
bundle.RelationKeySpaceRemoteStatus,
bundle.RelationKeyTargetSpaceId,
bundle.RelationKeySpaceInviteFileCid,
bundle.RelationKeySpaceInviteFileKey,
bundle.RelationKeyIsAclShared,
bundle.RelationKeySharedSpacesLimit,
bundle.RelationKeySpaceAccountStatus,
@ -121,28 +119,6 @@ func (s *SpaceView) initTemplate(st *state.State) {
)
}
func (s *SpaceView) GetExistingInviteInfo() (fileCid string, fileKey string) {
details := s.CombinedDetails()
fileCid = details.GetString(bundle.RelationKeySpaceInviteFileCid)
fileKey = details.GetString(bundle.RelationKeySpaceInviteFileKey)
return
}
func (s *SpaceView) RemoveExistingInviteInfo() (fileCid string, err error) {
details := s.Details()
fileCid = details.GetString(bundle.RelationKeySpaceInviteFileCid)
newState := s.NewState()
newState.RemoveDetail(bundle.RelationKeySpaceInviteFileCid, bundle.RelationKeySpaceInviteFileKey)
return fileCid, s.Apply(newState)
}
func (s *SpaceView) GetGuestUserInviteInfo() (fileCid string, fileKey string) {
details := s.CombinedDetails()
fileCid = details.GetString(bundle.RelationKeySpaceInviteGuestFileCid)
fileKey = details.GetString(bundle.RelationKeySpaceInviteGuestFileKey)
return
}
func (s *SpaceView) TryClose(objectTTL time.Duration) (res bool, err error) {
return false, nil
}
@ -210,13 +186,6 @@ func (s *SpaceView) GetSharedSpacesLimit() (limit int) {
return int(s.CombinedDetails().GetInt64(bundle.RelationKeySharedSpacesLimit))
}
func (s *SpaceView) SetInviteFileInfo(fileCid string, fileKey string) (err error) {
st := s.NewState()
st.SetDetailAndBundledRelation(bundle.RelationKeySpaceInviteFileCid, domain.String(fileCid))
st.SetDetailAndBundledRelation(bundle.RelationKeySpaceInviteFileKey, domain.String(fileKey))
return s.Apply(st)
}
func (s *SpaceView) afterApply(info smartblock.ApplyInfo) (err error) {
s.spaceService.OnViewUpdated(s.getSpacePersistentInfo(info.State))
return nil

View file

@ -1,6 +1,8 @@
package editor
import (
"github.com/anyproto/any-sync/commonspace/object/acl/list"
"github.com/anyproto/anytype-heart/core/anytype/config"
"github.com/anyproto/anytype-heart/core/block/editor/basic"
"github.com/anyproto/anytype-heart/core/block/editor/dataview"
@ -95,26 +97,37 @@ func (w *Workspaces) CreationStateMigration(ctx *smartblock.InitContext) migrati
}
}
func (w *Workspaces) SetInviteFileInfo(fileCid string, fileKey string) (err error) {
func (w *Workspaces) SetInviteFileInfo(info domain.InviteInfo) (err error) {
st := w.NewState()
st.SetDetailAndBundledRelation(bundle.RelationKeySpaceInviteFileCid, domain.String(fileCid))
st.SetDetailAndBundledRelation(bundle.RelationKeySpaceInviteFileKey, domain.String(fileKey))
st.SetDetailAndBundledRelation(bundle.RelationKeySpaceInvitePermissions, domain.Int64(domain.ConvertAclPermissions(info.Permissions)))
st.SetDetailAndBundledRelation(bundle.RelationKeySpaceInviteType, domain.Int64(info.InviteType))
st.SetDetailAndBundledRelation(bundle.RelationKeySpaceInviteFileCid, domain.String(info.InviteFileCid))
st.SetDetailAndBundledRelation(bundle.RelationKeySpaceInviteFileKey, domain.String(info.InviteFileKey))
return w.Apply(st)
}
func (w *Workspaces) GetExistingInviteInfo() (fileCid string, fileKey string) {
func (w *Workspaces) GetExistingInviteInfo() (inviteInfo domain.InviteInfo) {
details := w.CombinedDetails()
fileCid = details.GetString(bundle.RelationKeySpaceInviteFileCid)
fileKey = details.GetString(bundle.RelationKeySpaceInviteFileKey)
inviteInfo.InviteType = domain.InviteType(details.GetInt64(bundle.RelationKeySpaceInviteType))
// nolint: gosec
inviteInfo.Permissions = domain.ConvertParticipantPermissions(model.ParticipantPermissions(details.GetInt64(bundle.RelationKeySpaceInvitePermissions)))
inviteInfo.InviteFileCid = details.GetString(bundle.RelationKeySpaceInviteFileCid)
inviteInfo.InviteFileKey = details.GetString(bundle.RelationKeySpaceInviteFileKey)
if inviteInfo.InviteType == domain.InviteTypeDefault {
inviteInfo.Permissions = list.AclPermissionsNone
}
return
}
func (w *Workspaces) RemoveExistingInviteInfo() (fileCid string, err error) {
details := w.Details()
fileCid = details.GetString(bundle.RelationKeySpaceInviteFileCid)
func (w *Workspaces) RemoveExistingInviteInfo() (info domain.InviteInfo, err error) {
info = w.GetExistingInviteInfo()
newState := w.NewState()
newState.RemoveDetail(bundle.RelationKeySpaceInviteFileCid, bundle.RelationKeySpaceInviteFileKey)
return fileCid, w.Apply(newState)
newState.RemoveDetail(
bundle.RelationKeySpaceInviteFileCid,
bundle.RelationKeySpaceInviteFileKey,
bundle.RelationKeySpaceInvitePermissions,
bundle.RelationKeySpaceInviteType)
return info, w.Apply(newState)
}
func (w *Workspaces) SetGuestInviteFileInfo(fileCid string, fileKey string) (err error) {

View file

@ -3,6 +3,7 @@ package editor
import (
"testing"
"github.com/anyproto/any-sync/commonspace/object/acl/list"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
@ -11,23 +12,29 @@ import (
"github.com/anyproto/anytype-heart/core/block/editor/smartblock/smarttest"
"github.com/anyproto/anytype-heart/core/block/editor/state"
"github.com/anyproto/anytype-heart/core/block/migration"
"github.com/anyproto/anytype-heart/core/domain"
)
func TestWorkspaces_FileInfo(t *testing.T) {
t.Run("file info add remove", func(t *testing.T) {
fx := newWorkspacesFixture(t)
defer fx.finish()
err := fx.SetInviteFileInfo("fileId", "fileKey")
info := domain.InviteInfo{
InviteFileCid: "fileId",
InviteFileKey: "fileKey",
InviteType: domain.InviteTypeAnyone,
Permissions: list.AclPermissionsWriter,
}
err := fx.SetInviteFileInfo(info)
require.NoError(t, err)
fileId, fileKey := fx.GetExistingInviteInfo()
require.Equal(t, "fileId", fileId)
require.Equal(t, "fileKey", fileKey)
fileId, err = fx.RemoveExistingInviteInfo()
returnedInfo := fx.GetExistingInviteInfo()
require.Equal(t, info, returnedInfo)
returnedInfo, err = fx.RemoveExistingInviteInfo()
require.NoError(t, err)
require.Equal(t, "fileId", fileId)
fileId, err = fx.RemoveExistingInviteInfo()
require.Equal(t, info, returnedInfo)
returnedInfo, err = fx.RemoveExistingInviteInfo()
require.NoError(t, err)
require.Empty(t, fileId)
require.Empty(t, returnedInfo)
})
t.Run("file info empty", func(t *testing.T) {
fx := newWorkspacesFixture(t)

View file

@ -273,7 +273,9 @@ func (e *exportContext) exportObject(ctx context.Context, objectId string) (stri
if err != nil {
return "", err
}
// do not allow file export for in-memory writer
// nolint: gosec
switch model.ObjectTypeLayout(details.GetInt64(bundle.RelationKeyLayout)) {
case model.ObjectType_file, model.ObjectType_image, model.ObjectType_video, model.ObjectType_audio, model.ObjectType_pdf:
return "", fmt.Errorf("file export is not allowed for in-memory writer")

View file

@ -374,6 +374,9 @@ func (oc *ObjectCreator) resetState(newID string, st *state.State) *domain.Detai
// we use revision for bundled objects like relations and object types
return nil
}
if st.ObjectTypeKey() == bundle.TypeKeyObjectType {
template.InitTemplate(st, template.WithDetail(bundle.RelationKeyRecommendedLayout, domain.Int64(model.ObjectType_basic)))
}
err := history.ResetToVersion(b, st)
if err != nil {
log.With(zap.String("object id", newID)).Errorf("failed to set state %s: %s", newID, err)

View file

@ -20,12 +20,18 @@ func (s *service) createObjectType(ctx context.Context, space clientspace.Space,
return "", nil, fmt.Errorf("create object type: no data")
}
uniqueKey, err := getUniqueKeyOrGenerate(coresb.SmartBlockTypeObjectType, details)
uniqueKey, wasGenerated, err := getUniqueKeyOrGenerate(coresb.SmartBlockTypeObjectType, details)
if err != nil {
return "", nil, fmt.Errorf("getUniqueKeyOrGenerate: %w", err)
}
object := details.Copy()
var objectKey string
if !wasGenerated {
objectKey = uniqueKey.InternalKey()
}
injectApiObjectKey(object, objectKey)
if !object.Has(bundle.RelationKeyRecommendedLayout) {
object.SetInt64(bundle.RelationKeyRecommendedLayout, int64(model.ObjectType_basic))
}

View file

@ -32,12 +32,17 @@ func (s *service) createRelation(ctx context.Context, space clientspace.Space, d
if details.GetString(bundle.RelationKeyName) == "" {
return "", nil, fmt.Errorf("missing relation name")
}
if !details.Has(bundle.RelationKeyCreatedDate) {
details.SetInt64(bundle.RelationKeyCreatedDate, time.Now().Unix())
}
object = details.Copy()
key := domain.RelationKey(details.GetString(bundle.RelationKeyRelationKey))
injectApiObjectKey(object, key.String())
if key == "" {
key = domain.RelationKey(bson.NewObjectId().Hex())
} else if bundle.HasRelation(key) {
@ -50,6 +55,7 @@ func (s *service) createRelation(ctx context.Context, space clientspace.Space, d
object.SetString(bundle.RelationKeyUniqueKey, uniqueKey.Marshal())
object.SetString(bundle.RelationKeyId, id)
object.SetString(bundle.RelationKeyRelationKey, string(key))
if details.GetInt64(bundle.RelationKeyRelationFormat) == int64(model.RelationFormat_status) {
object.SetInt64(bundle.RelationKeyRelationMaxCount, 1)
}

View file

@ -3,10 +3,9 @@ package objectcreator
import (
"context"
"fmt"
"strings"
"time"
"github.com/globalsign/mgo/bson"
"github.com/anyproto/anytype-heart/core/block/editor/state"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
@ -29,7 +28,7 @@ func (s *service) createRelationOption(ctx context.Context, space clientspace.Sp
if !details.Has(bundle.RelationKeyCreatedDate) {
details.SetInt64(bundle.RelationKeyCreatedDate, time.Now().Unix())
}
uniqueKey, err := getUniqueKeyOrGenerate(coresb.SmartBlockTypeRelationOption, details)
uniqueKey, wasGenerated, err := getUniqueKeyOrGenerate(coresb.SmartBlockTypeRelationOption, details)
if err != nil {
return "", nil, fmt.Errorf("getUniqueKeyOrGenerate: %w", err)
}
@ -38,21 +37,18 @@ func (s *service) createRelationOption(ctx context.Context, space clientspace.Sp
object.SetString(bundle.RelationKeyUniqueKey, uniqueKey.Marshal())
object.SetInt64(bundle.RelationKeyLayout, int64(model.ObjectType_relationOption))
var objectKey string
if !wasGenerated {
objectKey = uniqueKey.InternalKey()
}
injectApiObjectKey(object, objectKey)
if strings.TrimSpace(object.GetString(bundle.RelationKeyApiObjectKey)) == "" {
object.SetString(bundle.RelationKeyApiObjectKey, transliterate(object.GetString(bundle.RelationKeyName)))
}
createState := state.NewDocWithUniqueKey("", nil, uniqueKey).(*state.State)
createState.SetDetails(object)
setOriginalCreatedTimestamp(createState, details)
return s.CreateSmartBlockFromStateInSpace(ctx, space, []domain.TypeKey{bundle.TypeKeyRelationOption}, createState)
}
func getUniqueKeyOrGenerate(sbType coresb.SmartBlockType, details *domain.Details) (domain.UniqueKey, error) {
uniqueKey := details.GetString(bundle.RelationKeyUniqueKey)
if uniqueKey == "" {
newUniqueKey, err := domain.NewUniqueKey(sbType, bson.NewObjectId().Hex())
if err != nil {
return nil, err
}
details.SetString(bundle.RelationKeyUniqueKey, newUniqueKey.Marshal())
return newUniqueKey, err
}
return domain.UnmarshalUniqueKey(uniqueKey)
}

View file

@ -0,0 +1,44 @@
package objectcreator
import (
"strings"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
coresb "github.com/anyproto/anytype-heart/pkg/lib/core/smartblock"
"github.com/globalsign/mgo/bson"
"github.com/gosimple/unidecode"
"github.com/iancoleman/strcase"
)
// injectApiObjectKey sets a value for ApiObjectKey relation in priority:
// - User-provided ApiObjectKey
// - Key from relationKey/uniqueKey
// - Transliterated Name relation
func injectApiObjectKey(object *domain.Details, key string) {
if strings.TrimSpace(object.GetString(bundle.RelationKeyApiObjectKey)) == "" {
if key == "" {
key = transliterate(object.GetString(bundle.RelationKeyName))
}
key = strcase.ToSnake(key)
object.SetString(bundle.RelationKeyApiObjectKey, key)
}
}
func transliterate(in string) string {
return unidecode.Unidecode(strings.TrimSpace(in))
}
func getUniqueKeyOrGenerate(sbType coresb.SmartBlockType, details *domain.Details) (uk domain.UniqueKey, wasGenerated bool, err error) {
uniqueKey := details.GetString(bundle.RelationKeyUniqueKey)
if uniqueKey == "" {
newUniqueKey, err := domain.NewUniqueKey(sbType, bson.NewObjectId().Hex())
if err != nil {
return nil, false, err
}
details.SetString(bundle.RelationKeyUniqueKey, newUniqueKey.Marshal())
return newUniqueKey, true, err
}
uk, err = domain.UnmarshalUniqueKey(uniqueKey)
return uk, false, err
}

View file

@ -187,6 +187,7 @@ func (s *Dataview) SetView(viewID string, view model.BlockContentDataviewView) e
v.PageLimit = view.PageLimit
v.DefaultTemplateId = view.DefaultTemplateId
v.DefaultObjectTypeId = view.DefaultObjectTypeId
v.EndRelationKey = view.EndRelationKey
return nil
}
@ -209,6 +210,7 @@ func (d *Dataview) SetViewFields(viewID string, view *model.BlockContentDataview
v.PageLimit = view.PageLimit
v.DefaultTemplateId = view.DefaultTemplateId
v.DefaultObjectTypeId = view.DefaultObjectTypeId
v.EndRelationKey = view.EndRelationKey
return nil
}

Some files were not shown because too many files have changed in this diff Show more